diff --git a/frontends/nextjs/src/components/misc/demos/DBALDemo.tsx b/frontends/nextjs/src/components/misc/demos/DBALDemo.tsx index 0292b4142..373463a95 100644 --- a/frontends/nextjs/src/components/misc/demos/DBALDemo.tsx +++ b/frontends/nextjs/src/components/misc/demos/DBALDemo.tsx @@ -5,10 +5,15 @@ * with the MetaBuilder application. */ +import { useMemo, useState } from 'react' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui' +import { useDBAL } from '@/hooks/use-dbal/use-dbal' import { BlobStorageDemo } from './dbal/BlobStorageDemo' import { CachedDataDemo } from './dbal/CachedDataDemo' +import { ConnectionForm } from './dbal/ConnectionForm' import { KVStoreDemo } from './dbal/KVStoreDemo' +import { LogsPanel } from './dbal/LogsPanel' +import { ResultPanel } from './dbal/ResultPanel' import { DBALTabConfig, DBAL_CONTAINER_CLASS, DBAL_TAB_GRID_CLASS } from './dbal/dbal-demo.utils' const tabs: DBALTabConfig[] = [ @@ -18,6 +23,25 @@ const tabs: DBALTabConfig[] = [ ] export function DBALDemo() { + const { isReady, error } = useDBAL() + const [logs, setLogs] = useState([]) + const [latestResult, setLatestResult] = useState(null) + + const statusMessage = useMemo(() => { + if (error) return `Error: ${error}` + return isReady ? 'Connected' : 'Initializing...' + }, [error, isReady]) + + const handleConnect = (config: { endpoint: string; apiKey: string }) => { + const timestamp = new Date().toLocaleTimeString() + setLogs((current) => [...current, `${timestamp}: Connected to ${config.endpoint}`]) + setLatestResult({ + endpoint: config.endpoint, + apiKey: config.apiKey ? '***' : 'Not provided', + ready: isReady, + }) + } + return (
@@ -27,6 +51,20 @@ export function DBALDemo() {

+
+ + +
+ +
+ +
+ {tabs.map((tab) => ( diff --git a/frontends/nextjs/src/components/misc/demos/IRCWebchat.tsx b/frontends/nextjs/src/components/misc/demos/IRCWebchat.tsx index 746af5e67..068de8118 100644 --- a/frontends/nextjs/src/components/misc/demos/IRCWebchat.tsx +++ b/frontends/nextjs/src/components/misc/demos/IRCWebchat.tsx @@ -1,21 +1,9 @@ -import { useState, useEffect, useRef } from 'react' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui' -import { Input } from '@/components/ui' -import { Button } from '@/components/ui' -import { ScrollArea } from '@/components/ui' -import { Badge } from '@/components/ui' -import { PaperPlaneTilt, Users, SignOut, Gear } from '@phosphor-icons/react' +import { useState, useEffect } from 'react' import { useKV } from '@github/spark/hooks' import type { User } from '@/lib/level-types' - -interface ChatMessage { - id: string - username: string - userId: string - message: string - timestamp: number - type: 'message' | 'system' | 'join' | 'leave' -} +import { ChatWindow } from './irc/ChatWindow' +import { useChatInput, useFormattedTimes } from './irc/hooks' +import type { ChatMessage } from './irc/types' interface IRCWebchatProps { user: User @@ -26,10 +14,9 @@ interface IRCWebchatProps { export function IRCWebchat({ user, channelName = 'general', onClose }: IRCWebchatProps) { const [messages, setMessages] = useKV(`chat_${channelName}`, []) const [onlineUsers, setOnlineUsers] = useKV(`chat_${channelName}_users`, []) - const [inputMessage, setInputMessage] = useState('') const [showSettings, setShowSettings] = useState(false) - const scrollRef = useRef(null) - const messagesEndRef = useRef(null) + const { inputMessage, setInputMessage, handleKeyPress } = useChatInput(handleSendMessage) + const formattedTimes = useFormattedTimes(messages || [], formatTime) useEffect(() => { addUserToChannel() @@ -38,14 +25,6 @@ export function IRCWebchat({ user, channelName = 'general', onClose }: IRCWebcha } }, []) - useEffect(() => { - scrollToBottom() - }, [messages]) - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) - } - const addUserToChannel = () => { setOnlineUsers((current) => { if (!current) return [user.username] @@ -89,7 +68,7 @@ export function IRCWebchat({ user, channelName = 'general', onClose }: IRCWebcha }) } - const handleSendMessage = () => { + function handleSendMessage() { const trimmed = inputMessage.trim() if (!trimmed) return @@ -151,121 +130,24 @@ export function IRCWebchat({ user, channelName = 'general', onClose }: IRCWebcha } } - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() - handleSendMessage() - } - } - - const formatTime = (timestamp: number) => { + function formatTime(timestamp: number) { const date = new Date(timestamp) return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) } - const getMessageStyle = (msg: ChatMessage) => { - if (msg.type === 'system' || msg.type === 'join' || msg.type === 'leave') { - return 'text-muted-foreground italic text-sm' - } - return '' - } - return ( - - -
- - # - {channelName} - -
- - - {onlineUsers?.length || 0} - - - {onClose && ( - - )} -
-
-
- -
- -
- {(messages || []).map((msg) => ( -
- {msg.type === 'message' && ( -
- {formatTime(msg.timestamp)} - <{msg.username}> - {msg.message} -
- )} - {msg.type === 'system' && msg.username === 'System' && ( -
- {formatTime(msg.timestamp)} - *** {msg.message} -
- )} - {msg.type === 'system' && msg.username !== 'System' && ( -
- {formatTime(msg.timestamp)} - * {msg.username} {msg.message} -
- )} - {(msg.type === 'join' || msg.type === 'leave') && ( -
- {formatTime(msg.timestamp)} - - --> {msg.message} - -
- )} -
- ))} -
-
- - - {showSettings && ( -
-

Online Users

-
- {(onlineUsers || []).map((username) => ( -
-
- {username} -
- ))} -
-
- )} -
- -
-
- setInputMessage(e.target.value)} - onKeyPress={handleKeyPress} - placeholder="Type a message... (/help for commands)" - className="flex-1 font-mono" - /> - -
-

- Press Enter to send. Type /help for commands. -

-
- - + setShowSettings(!showSettings)} + showSettings={showSettings} + onClose={onClose} + onInputKeyPress={handleKeyPress} + /> ) } diff --git a/frontends/nextjs/src/components/misc/demos/IRCWebchatDeclarative.tsx b/frontends/nextjs/src/components/misc/demos/IRCWebchatDeclarative.tsx index 1919b9524..8d77f26dc 100644 --- a/frontends/nextjs/src/components/misc/demos/IRCWebchatDeclarative.tsx +++ b/frontends/nextjs/src/components/misc/demos/IRCWebchatDeclarative.tsx @@ -1,22 +1,10 @@ -import { useState, useEffect, useRef } from 'react' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui' -import { Input } from '@/components/ui' -import { Button } from '@/components/ui' -import { ScrollArea } from '@/components/ui' -import { Badge } from '@/components/ui' -import { PaperPlaneTilt, Users, SignOut, Gear } from '@phosphor-icons/react' +import { useState, useEffect } from 'react' import { useKV } from '@github/spark/hooks' import type { User } from '@/lib/level-types' import { getDeclarativeRenderer } from '@/lib/rendering-lib/declarative-component-renderer' - -interface ChatMessage { - id: string - username: string - userId: string - message: string - timestamp: number - type: 'message' | 'system' | 'join' | 'leave' | 'command' -} +import { ChatWindow } from './irc/ChatWindow' +import { useChatInput, useFormattedTimes } from './irc/hooks' +import type { ChatMessage } from './irc/types' interface IRCWebchatDeclarativeProps { user: User @@ -27,11 +15,10 @@ interface IRCWebchatDeclarativeProps { export function IRCWebchatDeclarative({ user, channelName = 'general', onClose }: IRCWebchatDeclarativeProps) { const [messages, setMessages] = useKV(`chat_${channelName}`, []) const [onlineUsers, setOnlineUsers] = useKV(`chat_${channelName}_users`, []) - const [inputMessage, setInputMessage] = useState('') const [showSettings, setShowSettings] = useState(false) - const scrollRef = useRef(null) - const messagesEndRef = useRef(null) + const { inputMessage, setInputMessage, handleKeyPress } = useChatInput(handleSendMessage) const renderer = getDeclarativeRenderer() + const formattedTimes = useFormattedTimes(messages, formatTime) useEffect(() => { addUserToChannel() @@ -40,14 +27,6 @@ export function IRCWebchatDeclarative({ user, channelName = 'general', onClose } } }, []) - useEffect(() => { - scrollToBottom() - }, [messages]) - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) - } - const addUserToChannel = async () => { setOnlineUsers((current) => { if (!current) return [user.username] @@ -113,7 +92,7 @@ export function IRCWebchatDeclarative({ user, channelName = 'general', onClose } } } - const handleSendMessage = async () => { + async function handleSendMessage() { const trimmed = inputMessage.trim() if (!trimmed) return @@ -182,14 +161,9 @@ export function IRCWebchatDeclarative({ user, channelName = 'general', onClose } } } - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() - handleSendMessage() - } } - const formatTime = async (timestamp: number): Promise => { + async function formatTime(timestamp: number): Promise { try { const formatted = await renderer.executeLuaScript('lua_irc_format_time', [timestamp]) return formatted || new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) @@ -198,122 +172,19 @@ export function IRCWebchatDeclarative({ user, channelName = 'general', onClose } } } - const [formattedTimes, setFormattedTimes] = useState>({}) - - useEffect(() => { - const updateTimes = async () => { - const times: Record = {} - for (const msg of messages || []) { - times[msg.id] = await formatTime(msg.timestamp) - } - setFormattedTimes(times) - } - updateTimes() - }, [messages]) - - const getMessageStyle = (msg: ChatMessage) => { - if (msg.type === 'system' || msg.type === 'join' || msg.type === 'leave') { - return 'text-muted-foreground italic text-sm' - } - return '' - } - return ( - - -
- - # - {channelName} - -
- - - {onlineUsers?.length || 0} - - - {onClose && ( - - )} -
-
-
- -
- -
- {(messages || []).map((msg) => ( -
- {msg.type === 'message' && ( -
- {formattedTimes[msg.id] || ''} - <{msg.username}> - {msg.message} -
- )} - {msg.type === 'system' && msg.username === 'System' && ( -
- {formattedTimes[msg.id] || ''} - *** {msg.message} -
- )} - {msg.type === 'system' && msg.username !== 'System' && ( -
- {formattedTimes[msg.id] || ''} - * {msg.username} {msg.message} -
- )} - {(msg.type === 'join' || msg.type === 'leave') && ( -
- {formattedTimes[msg.id] || ''} - - --> {msg.message} - -
- )} -
- ))} -
-
- - - {showSettings && ( -
-

Online Users

-
- {(onlineUsers || []).map((username) => ( -
-
- {username} -
- ))} -
-
- )} -
- -
-
- setInputMessage(e.target.value)} - onKeyPress={handleKeyPress} - placeholder="Type a message... (/help for commands)" - className="flex-1 font-mono" - /> - -
-

- Press Enter to send. Type /help for commands. -

-
- - + setShowSettings(!showSettings)} + showSettings={showSettings} + onClose={onClose} + onInputKeyPress={handleKeyPress} + /> ) } diff --git a/frontends/nextjs/src/components/misc/demos/ScreenshotAnalyzer.tsx b/frontends/nextjs/src/components/misc/demos/ScreenshotAnalyzer.tsx index a8ab25a73..a919b591c 100644 --- a/frontends/nextjs/src/components/misc/demos/ScreenshotAnalyzer.tsx +++ b/frontends/nextjs/src/components/misc/demos/ScreenshotAnalyzer.tsx @@ -1,26 +1,11 @@ import { useState } from 'react' -import { - Box, - Button, - Card, - CardContent, - CardHeader, - Chip, - CircularProgress, - Stack, - Typography, - Grid, -} from '@mui/material' -import { - CameraAlt as CameraIcon, - Visibility as EyeIcon, - Download as DownloadIcon, - Refresh as RefreshIcon, -} from '@mui/icons-material' +import { Box, Card, CardContent, CardHeader, Chip, Grid, Typography } from '@mui/material' import { toast } from 'sonner' import { captureDomSnapshot } from '@/lib/screenshot/capture-dom-snapshot' import { requestScreenshotAnalysis } from '@/lib/screenshot/request-screenshot-analysis' import type { ScreenshotAnalysisResult } from '@/lib/screenshot/types' +import { UploadSection } from './screenshot-analyzer/UploadSection' +import { ResultPanel } from './screenshot-analyzer/ResultPanel' export function ScreenshotAnalyzer() { const [isCapturing, setIsCapturing] = useState(false) @@ -96,108 +81,16 @@ export function ScreenshotAnalyzer() { - - - - - + - {screenshotData && ( - <> - - - - - )} - - - {isAnalyzing && ( - - - Analyzing with heuristics... - - )} - - {analysisReport && !isAnalyzing && ( - - } - title="Heuristic Analysis" - titleTypographyProps={{ variant: 'subtitle1' }} - /> - - {analysisResult && ( - - - - - - - - - )} - - {analysisResult?.warnings.length ? ( - - Warnings - - {analysisResult.warnings.map((warning) => ( -
  • - {warning} -
  • - ))} -
    -
    - ) : null} - - - {analysisReport} - -
    -
    - )} - - {screenshotData && ( - - Screenshot Preview - - - - - )} -
    -
    + diff --git a/frontends/nextjs/src/components/misc/demos/dbal/ConnectionForm.tsx b/frontends/nextjs/src/components/misc/demos/dbal/ConnectionForm.tsx new file mode 100644 index 000000000..0e8d2f960 --- /dev/null +++ b/frontends/nextjs/src/components/misc/demos/dbal/ConnectionForm.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react' +import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label } from '@/components/ui' + +interface ConnectionFormProps { + defaultUrl?: string + defaultApiKey?: string + isConnecting?: boolean + statusMessage?: string + onConnect?: (config: { endpoint: string; apiKey: string }) => void +} + +export function ConnectionForm({ + defaultUrl = '', + defaultApiKey = '', + isConnecting = false, + statusMessage, + onConnect, +}: ConnectionFormProps) { + const [endpoint, setEndpoint] = useState(defaultUrl) + const [apiKey, setApiKey] = useState(defaultApiKey) + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault() + onConnect?.({ endpoint, apiKey }) + } + + return ( + + + DBAL Connection + Configure the DBAL endpoint used by the demos + + +
    +
    + + setEndpoint(event.target.value)} + /> +
    + +
    + + setApiKey(event.target.value)} + /> +
    + +
    + + {statusMessage ?

    {statusMessage}

    : null} +
    +
    +
    +
    + ) +} diff --git a/frontends/nextjs/src/components/misc/demos/dbal/LogsPanel.tsx b/frontends/nextjs/src/components/misc/demos/dbal/LogsPanel.tsx new file mode 100644 index 000000000..331915b6f --- /dev/null +++ b/frontends/nextjs/src/components/misc/demos/dbal/LogsPanel.tsx @@ -0,0 +1,31 @@ +import { Card, CardContent, CardHeader, CardTitle, ScrollArea } from '@/components/ui' + +interface LogsPanelProps { + logs: string[] + title?: string +} + +export function LogsPanel({ logs, title = 'Activity' }: LogsPanelProps) { + return ( + + + {title} + + + +
    + {logs.length === 0 ? ( +

    No events yet

    + ) : ( + logs.map((entry, index) => ( +
    + {entry} +
    + )) + )} +
    +
    +
    +
    + ) +} diff --git a/frontends/nextjs/src/components/misc/demos/dbal/ResultPanel.tsx b/frontends/nextjs/src/components/misc/demos/dbal/ResultPanel.tsx new file mode 100644 index 000000000..0c7f27d36 --- /dev/null +++ b/frontends/nextjs/src/components/misc/demos/dbal/ResultPanel.tsx @@ -0,0 +1,26 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui' + +interface ResultPanelProps { + title?: string + result: unknown + emptyLabel?: string +} + +export function ResultPanel({ title = 'Latest Result', result, emptyLabel = 'No result yet' }: ResultPanelProps) { + return ( + + + {title} + + + {result ? ( +
    +            {JSON.stringify(result, null, 2)}
    +          
    + ) : ( +

    {emptyLabel}

    + )} +
    +
    + ) +} diff --git a/frontends/nextjs/src/components/misc/demos/irc/ChatWindow.tsx b/frontends/nextjs/src/components/misc/demos/irc/ChatWindow.tsx new file mode 100644 index 000000000..2bf7af42f --- /dev/null +++ b/frontends/nextjs/src/components/misc/demos/irc/ChatWindow.tsx @@ -0,0 +1,138 @@ +import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input, ScrollArea } from '@/components/ui' +import { Gear, PaperPlaneTilt, SignOut, Users } from '@phosphor-icons/react' +import { UserList } from './UserList' +import type { ChatMessage } from './types' + +interface ChatWindowProps { + channelName: string + messages: ChatMessage[] + formattedTimes: Record + onlineUsers: string[] + inputMessage: string + onInputChange: (value: string) => void + onSendMessage: () => void + onToggleSettings: () => void + showSettings: boolean + onClose?: () => void + onInputKeyPress?: (event: React.KeyboardEvent) => void +} + +export function ChatWindow({ + channelName, + messages, + formattedTimes, + onlineUsers, + inputMessage, + onInputChange, + onSendMessage, + onToggleSettings, + showSettings, + onClose, + onInputKeyPress, +}: ChatWindowProps) { + const getMessageStyle = (message: ChatMessage) => { + if (message.type === 'system' || message.type === 'join' || message.type === 'leave' || message.type === 'command') { + return 'text-muted-foreground italic text-sm' + } + return '' + } + + return ( + + +
    + + # + {channelName} + +
    + + + {onlineUsers.length} + + + {onClose && ( + + )} +
    +
    +
    + +
    + +
    + {messages.map((message) => ( +
    + {message.type === 'message' && ( +
    + {formattedTimes[message.id] || ''} + <{message.username}> + {message.message} +
    + )} + + {message.type === 'system' && message.username === 'System' && ( +
    + {formattedTimes[message.id] || ''} + *** {message.message} +
    + )} + + {message.type === 'system' && message.username !== 'System' && ( +
    + {formattedTimes[message.id] || ''} + * {message.username} {message.message} +
    + )} + + {(message.type === 'join' || message.type === 'leave') && ( +
    + {formattedTimes[message.id] || ''} + + --> {message.message} + +
    + )} + + {message.type === 'command' && ( +
    + {formattedTimes[message.id] || ''} + {message.message} +
    + )} +
    + ))} +
    +
    + + {showSettings && ( +
    +

    Online Users

    + +
    + )} +
    + +
    +
    + onInputChange(event.target.value)} + onKeyPress={onInputKeyPress} + placeholder="Type a message... (/help for commands)" + className="flex-1 font-mono" + /> + +
    +

    Press Enter to send. Type /help for commands.

    +
    +
    +
    + ) +} diff --git a/frontends/nextjs/src/components/misc/demos/irc/UserList.tsx b/frontends/nextjs/src/components/misc/demos/irc/UserList.tsx new file mode 100644 index 000000000..c6cf0a932 --- /dev/null +++ b/frontends/nextjs/src/components/misc/demos/irc/UserList.tsx @@ -0,0 +1,20 @@ +interface UserListProps { + users: string[] +} + +export function UserList({ users }: UserListProps) { + if (users.length === 0) { + return

    No users online

    + } + + return ( +
    + {users.map((username) => ( +
    +
    + {username} +
    + ))} +
    + ) +} diff --git a/frontends/nextjs/src/components/misc/demos/irc/hooks.ts b/frontends/nextjs/src/components/misc/demos/irc/hooks.ts new file mode 100644 index 000000000..86c588024 --- /dev/null +++ b/frontends/nextjs/src/components/misc/demos/irc/hooks.ts @@ -0,0 +1,55 @@ +import { useEffect, useState } from 'react' +import type { ChatMessage } from './types' + +type TimestampFormatter = (timestamp: number) => Promise | string + +export function useChatInput(onSubmit: () => void) { + const [inputMessage, setInputMessage] = useState('') + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault() + onSubmit() + } + } + + return { + inputMessage, + setInputMessage, + handleKeyPress, + } +} + +export function useFormattedTimes(messages: ChatMessage[] | undefined, formatTime: TimestampFormatter) { + const [formattedTimes, setFormattedTimes] = useState>({}) + + useEffect(() => { + let isMounted = true + + const formatAllTimes = async () => { + if (!messages) { + setFormattedTimes({}) + return + } + + const entries = await Promise.all( + messages.map(async (message) => { + const formatted = await formatTime(message.timestamp) + return [message.id, formatted] as const + }), + ) + + if (isMounted) { + setFormattedTimes(Object.fromEntries(entries)) + } + } + + formatAllTimes() + + return () => { + isMounted = false + } + }, [messages, formatTime]) + + return formattedTimes +} diff --git a/frontends/nextjs/src/components/misc/demos/irc/types.ts b/frontends/nextjs/src/components/misc/demos/irc/types.ts new file mode 100644 index 000000000..c46671dc7 --- /dev/null +++ b/frontends/nextjs/src/components/misc/demos/irc/types.ts @@ -0,0 +1,10 @@ +export type ChatMessageType = 'message' | 'system' | 'join' | 'leave' | 'command' + +export interface ChatMessage { + id: string + username: string + userId: string + message: string + timestamp: number + type: ChatMessageType +} diff --git a/frontends/nextjs/src/components/misc/demos/screenshot-analyzer/ResultPanel.tsx b/frontends/nextjs/src/components/misc/demos/screenshot-analyzer/ResultPanel.tsx new file mode 100644 index 000000000..8090f442f --- /dev/null +++ b/frontends/nextjs/src/components/misc/demos/screenshot-analyzer/ResultPanel.tsx @@ -0,0 +1,56 @@ +import { Box, Card, CardContent, CardHeader, Chip, Stack, Typography } from '@mui/material' +import { Visibility as EyeIcon } from '@mui/icons-material' +import type { ScreenshotAnalysisResult } from '@/lib/screenshot/types' + +interface ResultPanelProps { + analysisReport: string + analysisResult: ScreenshotAnalysisResult | null +} + +export function ResultPanel({ analysisReport, analysisResult }: ResultPanelProps) { + if (!analysisReport) return null + + return ( + + } title="Heuristic Analysis" titleTypographyProps={{ variant: 'subtitle1' }} /> + + {analysisResult && ( + + + + + + + + + )} + + {analysisResult?.warnings.length ? ( + + + Warnings + + + {analysisResult.warnings.map((warning) => ( +
  • + {warning} +
  • + ))} +
    +
    + ) : null} + + + {analysisReport} + +
    +
    + ) +} diff --git a/frontends/nextjs/src/components/misc/demos/screenshot-analyzer/UploadSection.tsx b/frontends/nextjs/src/components/misc/demos/screenshot-analyzer/UploadSection.tsx new file mode 100644 index 000000000..c768becdc --- /dev/null +++ b/frontends/nextjs/src/components/misc/demos/screenshot-analyzer/UploadSection.tsx @@ -0,0 +1,71 @@ +import { Box, Button, Card, CardContent, CardHeader, CircularProgress, Typography } from '@mui/material' +import { CameraAlt as CameraIcon, Download as DownloadIcon, Refresh as RefreshIcon } from '@mui/icons-material' + +interface UploadSectionProps { + isCapturing: boolean + isAnalyzing: boolean + screenshotData: string | null + onCapture: () => void + onDownload: () => void + onReanalyze: () => void + previewTitle?: string +} + +export function UploadSection({ + isCapturing, + isAnalyzing, + screenshotData, + onCapture, + onDownload, + onReanalyze, + previewTitle = 'Screenshot Preview', +}: UploadSectionProps) { + return ( + + + + + + + {screenshotData && ( + <> + + + + + )} + + + {isAnalyzing && ( + + + Analyzing with heuristics... + + )} + + {screenshotData && ( + + + {previewTitle} + + + + + + )} + + + ) +}