From 4200c7832700f6f07ea334722c4ae02e2d6b6ba4 Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:13:44 +0800 Subject: [PATCH] feat(ext): style; code split --- .../src/entrypoints/sidepanel/App.tsx | 356 +----------------- .../sidepanel/components/ConfigPanel.tsx | 82 ++++ .../sidepanel/components/cards/index.tsx | 255 +++++++++++++ .../sidepanel/components/index.tsx | 45 +++ 4 files changed, 387 insertions(+), 351 deletions(-) create mode 100644 packages/extension/src/entrypoints/sidepanel/components/ConfigPanel.tsx create mode 100644 packages/extension/src/entrypoints/sidepanel/components/cards/index.tsx create mode 100644 packages/extension/src/entrypoints/sidepanel/components/index.tsx diff --git a/packages/extension/src/entrypoints/sidepanel/App.tsx b/packages/extension/src/entrypoints/sidepanel/App.tsx index ed50053..7e459a8 100644 --- a/packages/extension/src/entrypoints/sidepanel/App.tsx +++ b/packages/extension/src/entrypoints/sidepanel/App.tsx @@ -1,367 +1,21 @@ -import { - ArrowRight, - CheckCircle, - ChevronDown, - ChevronRight, - Loader2, - MessageSquare, - Send, - Settings, - Sparkles, - Square, - XCircle, -} from 'lucide-react' -import { Fragment, useCallback, useEffect, useRef, useState } from 'react' +import { Send, Settings, Square } from 'lucide-react' +import { useCallback, useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea, } from '@/components/ui/input-group' -import { cn } from '@/lib/utils' import { subscribeToEvents } from '@/messaging/events' import { agentCommands } from '@/messaging/protocol' import type { AgentActivity, AgentState, AgentStatus, HistoricalEvent } from '@/messaging/protocol' import { DEMO_API_KEY, DEMO_BASE_URL, DEMO_MODEL } from '@/utils/constants' -// Configuration panel component -function ConfigPanel({ onClose }: { onClose: () => void }) { - const [apiKey, setApiKey] = useState(DEMO_API_KEY) - const [baseURL, setBaseURL] = useState(DEMO_BASE_URL) - const [model, setModel] = useState(DEMO_MODEL) - const [saving, setSaving] = useState(false) - - useEffect(() => { - chrome.storage.local.get('llmConfig').then((result) => { - const config = result.llmConfig as - | { apiKey?: string; baseURL?: string; model?: string } - | undefined - if (config) { - setApiKey(config.apiKey || DEMO_API_KEY) - setBaseURL(config.baseURL || DEMO_BASE_URL) - setModel(config.model || DEMO_MODEL) - } - }) - }, []) - - const handleSave = async () => { - setSaving(true) - try { - await agentCommands.sendMessage('agent:configure', { apiKey, baseURL, model }) - onClose() - } finally { - setSaving(false) - } - } - - return ( -
-

Settings

- -
- - setApiKey(e.target.value)} - className="text-xs h-8" - /> -
- -
- - setBaseURL(e.target.value)} - className="text-xs h-8" - /> -
- -
- - setModel(e.target.value)} - className="text-xs h-8" - /> -
- -
- - -
-
- ) -} - -// Result card for done action -function ResultCard({ - success, - text, - children, -}: { - success: boolean - text: string - children?: React.ReactNode -}) { - return ( -
-
- {success ? ( - - ) : ( - - )} - - Result: {success ? 'Success' : 'Failed'} - -
-

{text}

- {children} -
- ) -} - -// Reflection section in step card -function ReflectionSection({ - reflection, -}: { - reflection: { - evaluation_previous_goal?: string - memory?: string - next_goal?: string - } -}) { - const items = [ - { icon: '✅', label: 'eval', value: reflection.evaluation_previous_goal }, - { icon: '💾', label: 'memory', value: reflection.memory }, - { icon: '🎯', label: 'goal', value: reflection.next_goal }, - ].filter((item) => item.value) - - if (items.length === 0) return null - - return ( -
-
- Reflection -
-
- {items.map((item) => ( - - {item.icon} - {item.value} - - ))} -
-
- ) -} - -// Raw response section (collapsible, for debugging) -function RawResponseSection({ rawResponse }: { rawResponse: unknown }) { - const [expanded, setExpanded] = useState(false) - - return ( -
- - {expanded && ( -
-					{JSON.stringify(rawResponse, null, 4)}
-				
- )} -
- ) -} - -// History event card component -function EventCard({ event }: { event: HistoricalEvent }) { - // Done action - show as result card - if (event.type === 'step' && event.action?.name === 'done') { - const input = event.action.input as { text?: string; success?: boolean } - return ( -
- - {!event.rawResponse || } - -
- ) - } - - if (event.type === 'step') { - return ( -
- {/* Reflection */} - {event.reflection && } - - {/* Action */} - {event.action && ( -
-
- {event.action.name} -
-
- -
-

- {JSON.stringify(event.action.input)} -

-

→ {event.action.output}

-
-
-
- )} - - {/* Raw Response */} - {!event.rawResponse || } -
- ) - } - - if (event.type === 'observation') { - return ( -
- - {event.content} -
- ) - } - - if (event.type === 'error') { - return ( -
-
- - {event.message} -
- {!event.rawResponse || } -
- ) - } - - return null -} - -// Activity card with animation -function ActivityCard({ activity }: { activity: AgentActivity }) { - const getActivityInfo = () => { - switch (activity.type) { - case 'thinking': - return { text: 'Thinking...', color: 'text-blue-500' } - case 'executing': - return { text: `Executing ${activity.tool}...`, color: 'text-amber-500' } - case 'executed': - return { text: `Done: ${activity.tool}`, color: 'text-green-500' } - case 'retrying': - return { - text: `Retrying (${activity.attempt}/${activity.maxAttempts})...`, - color: 'text-amber-500', - } - case 'error': - return { text: activity.message, color: 'text-destructive' } - } - } - - const info = getActivityInfo() - - return ( -
-
- - -
- {info.text} -
- ) -} - -// Status dot indicator -function StatusDot({ status }: { status: AgentStatus }) { - const colorClass = { - idle: 'bg-muted-foreground', - running: 'bg-blue-500', - completed: 'bg-green-500', - error: 'bg-destructive', - }[status] - - const label = { - idle: 'Ready', - running: 'Running', - completed: 'Done', - error: 'Error', - }[status] - - return ( -
- - {label} -
- ) -} - -function Logo({ className }: { className?: string }) { - return Page Agent -} - -// Empty state with logo -function EmptyState() { - return ( -
- -
-

Page Agent Ext

-

Enter a task to automate this page

-
-
- ) -} +import { EmptyState, Logo, StatusDot } from './components' +import { ConfigPanel } from './components/ConfigPanel' +import { ActivityCard, EventCard } from './components/cards' export default function App() { const [showConfig, setShowConfig] = useState(false) diff --git a/packages/extension/src/entrypoints/sidepanel/components/ConfigPanel.tsx b/packages/extension/src/entrypoints/sidepanel/components/ConfigPanel.tsx new file mode 100644 index 0000000..451dfee --- /dev/null +++ b/packages/extension/src/entrypoints/sidepanel/components/ConfigPanel.tsx @@ -0,0 +1,82 @@ +import { Loader2 } from 'lucide-react' + +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { agentCommands } from '@/messaging' + +// Configuration panel component +export function ConfigPanel({ onClose }: { onClose: () => void }) { + const [apiKey, setApiKey] = useState(DEMO_API_KEY) + const [baseURL, setBaseURL] = useState(DEMO_BASE_URL) + const [model, setModel] = useState(DEMO_MODEL) + const [saving, setSaving] = useState(false) + + useEffect(() => { + chrome.storage.local.get('llmConfig').then((result) => { + const config = result.llmConfig as + | { apiKey?: string; baseURL?: string; model?: string } + | undefined + if (config) { + setApiKey(config.apiKey || DEMO_API_KEY) + setBaseURL(config.baseURL || DEMO_BASE_URL) + setModel(config.model || DEMO_MODEL) + } + }) + }, []) + + const handleSave = async () => { + setSaving(true) + try { + await agentCommands.sendMessage('agent:configure', { apiKey, baseURL, model }) + onClose() + } finally { + setSaving(false) + } + } + + return ( +
+

Settings

+ +
+ + setApiKey(e.target.value)} + className="text-xs h-8" + /> +
+ +
+ + setBaseURL(e.target.value)} + className="text-xs h-8" + /> +
+ +
+ + setModel(e.target.value)} + className="text-xs h-8" + /> +
+ +
+ + +
+
+ ) +} diff --git a/packages/extension/src/entrypoints/sidepanel/components/cards/index.tsx b/packages/extension/src/entrypoints/sidepanel/components/cards/index.tsx new file mode 100644 index 0000000..569120c --- /dev/null +++ b/packages/extension/src/entrypoints/sidepanel/components/cards/index.tsx @@ -0,0 +1,255 @@ +import { + CheckCircle, + ChevronDown, + ChevronRight, + Eye, + Globe, + Keyboard, + Mouse, + MoveVertical, + Sparkles, + XCircle, + Zap, +} from 'lucide-react' +import { Fragment } from 'react/jsx-runtime' + +import { cn } from '@/lib/utils' +import { AgentActivity, HistoricalEvent } from '@/messaging' + +// Result card for done action +function ResultCard({ + success, + text, + children, +}: { + success: boolean + text: string + children?: React.ReactNode +}) { + return ( +
+
+ {success ? ( + + ) : ( + + )} + + Result: {success ? 'Success' : 'Failed'} + +
+

{text}

+ {children} +
+ ) +} + +// Reflection section in step card +function ReflectionSection({ + reflection, +}: { + reflection: { + evaluation_previous_goal?: string + memory?: string + next_goal?: string + } +}) { + const items = [ + { icon: '☑️', label: 'eval', value: reflection.evaluation_previous_goal }, + { icon: '🧠', label: 'memory', value: reflection.memory }, + { icon: '🎯', label: 'goal', value: reflection.next_goal }, + ].filter((item) => item.value) + + if (items.length === 0) return null + + return ( +
+
+ Reflection +
+
+ {items.map((item) => ( + + {item.icon} + {item.value} + + ))} +
+
+ ) +} + +// Get icon for action type +function ActionIcon({ name, className }: { name: string; className?: string }) { + const icons: Record = { + click_element_by_index: , + input: , + scroll: , + go_to_url: , + } + return icons[name] || +} + +// Raw response section (collapsible, for debugging) +function RawResponseSection({ rawResponse }: { rawResponse: unknown }) { + const [expanded, setExpanded] = useState(false) + + return ( +
+ + {expanded && ( +
+					{JSON.stringify(rawResponse, null, 4)}
+				
+ )} +
+ ) +} + +// History event card component +export function EventCard({ event }: { event: HistoricalEvent }) { + // Done action - show as result card + if (event.type === 'step' && event.action?.name === 'done') { + const input = event.action.input as { text?: string; success?: boolean } + return ( +
+ + {!event.rawResponse || } + +
+ ) + } + + if (event.type === 'step') { + return ( +
+ {/* Reflection */} + {event.reflection && } + + {/* Action */} + {event.action && ( +
+
+ Action +
+
+ +
+

+ {event.action.name} + + {JSON.stringify(event.action.input)} + +

+

→ {event.action.output}

+
+
+
+ )} + + {/* Raw Response */} + {!event.rawResponse || } +
+ ) + } + + if (event.type === 'observation') { + return ( +
+
+ Observation +
+
+ + {event.content} +
+
+ ) + } + + if (event.type === 'error') { + return ( +
+
+ + {event.message} +
+ {!event.rawResponse || } +
+ ) + } + + return null +} + +// Activity card with animation +export function ActivityCard({ activity }: { activity: AgentActivity }) { + const getActivityInfo = () => { + switch (activity.type) { + case 'thinking': + return { text: 'Thinking...', color: 'text-blue-500' } + case 'executing': + return { text: `Executing ${activity.tool}...`, color: 'text-amber-500' } + case 'executed': + return { text: `Done: ${activity.tool}`, color: 'text-green-500' } + case 'retrying': + return { + text: `Retrying (${activity.attempt}/${activity.maxAttempts})...`, + color: 'text-amber-500', + } + case 'error': + return { text: activity.message, color: 'text-destructive' } + } + } + + const info = getActivityInfo() + + return ( +
+
+ + +
+ {info.text} +
+ ) +} diff --git a/packages/extension/src/entrypoints/sidepanel/components/index.tsx b/packages/extension/src/entrypoints/sidepanel/components/index.tsx new file mode 100644 index 0000000..a50e5c4 --- /dev/null +++ b/packages/extension/src/entrypoints/sidepanel/components/index.tsx @@ -0,0 +1,45 @@ +import { cn } from '@/lib/utils' +import { AgentStatus } from '@/messaging' + +// Status dot indicator +export function StatusDot({ status }: { status: AgentStatus }) { + const colorClass = { + idle: 'bg-muted-foreground', + running: 'bg-blue-500', + completed: 'bg-green-500', + error: 'bg-destructive', + }[status] + + const label = { + idle: 'Ready', + running: 'Running', + completed: 'Done', + error: 'Error', + }[status] + + return ( +
+ + {label} +
+ ) +} + +export function Logo({ className }: { className?: string }) { + return Page Agent +} + +// Empty state with logo +export function EmptyState() { + return ( +
+ +
+

Page Agent Ext

+

Enter a task to automate this page

+
+
+ ) +}