refactor(agent): decouple ui and agent; disable EventBus for now
This commit is contained in:
20
AGENTS.md
20
AGENTS.md
@@ -107,19 +107,6 @@ DOM element references and internal state (selectorMap, elementTextMap) are enca
|
|||||||
3. **LLM Processing**: AI model returns action plans (in page-agent)
|
3. **LLM Processing**: AI model returns action plans (in page-agent)
|
||||||
4. **Indexed Operations**: PageAgent calls PageController methods by element index
|
4. **Indexed Operations**: PageAgent calls PageController methods by element index
|
||||||
|
|
||||||
### Event Bus Communication
|
|
||||||
|
|
||||||
Use `src/utils/bus.ts` for decoupled PageAgent ↔ UI communication:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Emit from PageAgent
|
|
||||||
getEventBus().emit('panel:show')
|
|
||||||
getEventBus().emit('panel:update', { status: 'thinking' })
|
|
||||||
|
|
||||||
// Listen in UI components
|
|
||||||
getEventBus().on('panel:show', () => panel.show())
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hash Routing Requirement
|
### Hash Routing Requirement
|
||||||
|
|
||||||
Uses wouter with `useHashLocation` for static hosting:
|
Uses wouter with `useHashLocation` for static hosting:
|
||||||
@@ -147,7 +134,6 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
|||||||
| `src/PageAgent.ts` | ⭐ Main AI agent class orchestrating tools and LLM |
|
| `src/PageAgent.ts` | ⭐ Main AI agent class orchestrating tools and LLM |
|
||||||
| `src/entry.ts` | CDN/UMD entry point with auto-initialization |
|
| `src/entry.ts` | CDN/UMD entry point with auto-initialization |
|
||||||
| `src/tools/` | Tool definitions that call PageController methods |
|
| `src/tools/` | Tool definitions that call PageController methods |
|
||||||
| `src/utils/bus.ts` | Type-safe event bus for decoupled communication |
|
|
||||||
| `src/ui/` | UI components (Panel, SimulatorMask) with CSS modules |
|
| `src/ui/` | UI components (Panel, SimulatorMask) with CSS modules |
|
||||||
| `src/llms/` | LLM integration and communication layer |
|
| `src/llms/` | LLM integration and communication layer |
|
||||||
| `vite.config.js` | Library build configuration (ES + UMD) |
|
| `vite.config.js` | Library build configuration (ES + UMD) |
|
||||||
@@ -194,11 +180,6 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
|||||||
2. Expose via async method in `PageController.ts`
|
2. Expose via async method in `PageController.ts`
|
||||||
3. Export from `packages/page-controller/src/index.ts`
|
3. Export from `packages/page-controller/src/index.ts`
|
||||||
|
|
||||||
### New UI Component
|
|
||||||
|
|
||||||
1. Create in `packages/page-agent/src/ui/` with colocated CSS modules
|
|
||||||
2. Use event bus for PageAgent communication
|
|
||||||
|
|
||||||
## Code Standards
|
## Code Standards
|
||||||
|
|
||||||
### TypeScript
|
### TypeScript
|
||||||
@@ -233,5 +214,4 @@ Query params configure `PageAgentConfig` automatically in `src/entry.ts`.
|
|||||||
|
|
||||||
1. Check `packages/page-agent/dist/lib/page-agent.umd.js` builds correctly
|
1. Check `packages/page-agent/dist/lib/page-agent.umd.js` builds correctly
|
||||||
2. Test CDN injection with query params
|
2. Test CDN injection with query params
|
||||||
3. Verify event bus communications are properly typed
|
|
||||||
4. Use `packages/website/src/test-pages/` for isolated testing
|
4. Use `packages/website/src/test-pages/` for isolated testing
|
||||||
|
|||||||
@@ -8,15 +8,13 @@ import zod from 'zod'
|
|||||||
|
|
||||||
import type { PageAgentConfig } from './config'
|
import type { PageAgentConfig } from './config'
|
||||||
import { MAX_STEPS } from './config/constants'
|
import { MAX_STEPS } from './config/constants'
|
||||||
import { I18n } from './i18n'
|
|
||||||
import { LLM, type Tool } from './llms'
|
import { LLM, type Tool } from './llms'
|
||||||
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
import SYSTEM_PROMPT from './prompts/system_prompt.md?raw'
|
||||||
import { tools } from './tools'
|
import { tools } from './tools'
|
||||||
import { Panel, getToolCompletedText, getToolExecutingText } from './ui/Panel'
|
import { Panel } from './ui/Panel'
|
||||||
import { SimulatorMask } from './ui/SimulatorMask'
|
import { SimulatorMask } from './ui/SimulatorMask'
|
||||||
import { trimLines, uid, waitUntil } from './utils'
|
import { trimLines, uid, waitUntil } from './utils'
|
||||||
import { assert } from './utils/assert'
|
import { assert } from './utils/assert'
|
||||||
import { getEventBus } from './utils/bus'
|
|
||||||
|
|
||||||
export type { PageAgentConfig }
|
export type { PageAgentConfig }
|
||||||
export { tool, type PageAgentTool } from './tools'
|
export { tool, type PageAgentTool } from './tools'
|
||||||
@@ -71,8 +69,6 @@ export interface ExecutionResult {
|
|||||||
export class PageAgent extends EventTarget {
|
export class PageAgent extends EventTarget {
|
||||||
config: PageAgentConfig
|
config: PageAgentConfig
|
||||||
id = uid()
|
id = uid()
|
||||||
bus = getEventBus(this.id)
|
|
||||||
i18n: I18n
|
|
||||||
panel: Panel
|
panel: Panel
|
||||||
tools: typeof tools
|
tools: typeof tools
|
||||||
paused = false
|
paused = false
|
||||||
@@ -96,14 +92,32 @@ export class PageAgent extends EventTarget {
|
|||||||
super()
|
super()
|
||||||
|
|
||||||
this.config = config
|
this.config = config
|
||||||
this.#llm = new LLM(this.config, this.id)
|
this.#llm = new LLM(this.config)
|
||||||
this.i18n = new I18n(this.config.language)
|
this.panel = new Panel({
|
||||||
this.panel = new Panel(this)
|
language: this.config.language,
|
||||||
|
onExecuteTask: (task) => this.execute(task),
|
||||||
|
onStop: () => this.dispose(),
|
||||||
|
onPauseToggle: () => {
|
||||||
|
this.paused = !this.paused
|
||||||
|
return this.paused
|
||||||
|
},
|
||||||
|
getPaused: () => this.paused,
|
||||||
|
})
|
||||||
this.tools = new Map(tools)
|
this.tools = new Map(tools)
|
||||||
|
|
||||||
// Initialize PageController with config
|
// Initialize PageController with config
|
||||||
this.pageController = new PageController(this.config)
|
this.pageController = new PageController(this.config)
|
||||||
|
|
||||||
|
// Listen to LLM events
|
||||||
|
this.#llm.addEventListener('retry', (e) => {
|
||||||
|
const { current, max } = (e as CustomEvent).detail
|
||||||
|
this.panel.update({ type: 'retry', current, max })
|
||||||
|
})
|
||||||
|
this.#llm.addEventListener('error', (e) => {
|
||||||
|
const { error } = (e as CustomEvent).detail
|
||||||
|
this.panel.update({ type: 'error', message: `step failed: ${error.message}` })
|
||||||
|
})
|
||||||
|
|
||||||
if (this.config.customTools) {
|
if (this.config.customTools) {
|
||||||
for (const [name, tool] of Object.entries(this.config.customTools)) {
|
for (const [name, tool] of Object.entries(this.config.customTools)) {
|
||||||
if (tool === null) {
|
if (tool === null) {
|
||||||
@@ -141,13 +155,10 @@ export class PageAgent extends EventTarget {
|
|||||||
// Show mask and panel
|
// Show mask and panel
|
||||||
this.mask.show()
|
this.mask.show()
|
||||||
|
|
||||||
this.bus.emit('panel:show')
|
this.panel.show()
|
||||||
this.bus.emit('panel:reset')
|
this.panel.reset()
|
||||||
|
|
||||||
this.bus.emit('panel:update', {
|
this.panel.update({ type: 'input', task: this.task })
|
||||||
type: 'input',
|
|
||||||
displayText: this.task,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.#abortController) {
|
if (this.#abortController) {
|
||||||
this.#abortController.abort()
|
this.#abortController.abort()
|
||||||
@@ -171,10 +182,7 @@ export class PageAgent extends EventTarget {
|
|||||||
|
|
||||||
// Update status to thinking
|
// Update status to thinking
|
||||||
console.log(chalk.blue('Thinking...'))
|
console.log(chalk.blue('Thinking...'))
|
||||||
this.bus.emit('panel:update', {
|
this.panel.update({ type: 'thinking' })
|
||||||
type: 'thinking',
|
|
||||||
displayText: this.i18n.t('ui.panel.thinking'),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await this.#llm.invoke(
|
const result = await this.#llm.invoke(
|
||||||
[
|
[
|
||||||
@@ -304,22 +312,14 @@ export class PageAgent extends EventTarget {
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
console.log(brain)
|
console.log(brain)
|
||||||
this.bus.emit('panel:update', {
|
this.panel.update({ type: 'thinking', text: brain })
|
||||||
type: 'thinking',
|
|
||||||
displayText: brain,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Find the corresponding tool
|
// Find the corresponding tool
|
||||||
const tool = tools.get(toolName)
|
const tool = tools.get(toolName)
|
||||||
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
|
assert(tool, `Tool ${toolName} not found. (@note should have been caught before this!!!)`)
|
||||||
|
|
||||||
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
|
console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput)
|
||||||
this.bus.emit('panel:update', {
|
this.panel.update({ type: 'toolExecuting', toolName, args: toolInput })
|
||||||
type: 'tool_executing',
|
|
||||||
toolName,
|
|
||||||
toolArgs: toolInput,
|
|
||||||
displayText: getToolExecutingText(toolName, toolInput, this.i18n),
|
|
||||||
})
|
|
||||||
|
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
@@ -341,14 +341,11 @@ export class PageAgent extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Briefly display execution result
|
// Briefly display execution result
|
||||||
const displayResult = getToolCompletedText(toolName, toolInput, this.i18n)
|
this.panel.update({
|
||||||
if (displayResult)
|
type: 'toolCompleted',
|
||||||
this.bus.emit('panel:update', {
|
|
||||||
type: 'tool_executing',
|
|
||||||
toolName,
|
toolName,
|
||||||
toolArgs: toolInput,
|
args: toolInput,
|
||||||
toolResult: result,
|
result,
|
||||||
displayText: displayResult,
|
|
||||||
duration,
|
duration,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -426,16 +423,14 @@ export class PageAgent extends EventTarget {
|
|||||||
this.pageController.cleanUpHighlights()
|
this.pageController.cleanUpHighlights()
|
||||||
|
|
||||||
// Update panel status
|
// Update panel status
|
||||||
this.bus.emit('panel:update', {
|
if (success) {
|
||||||
type: success ? 'output' : 'error',
|
this.panel.update({ type: 'output', text })
|
||||||
displayText: text,
|
} else {
|
||||||
})
|
this.panel.update({ type: 'error', message: text })
|
||||||
|
}
|
||||||
|
|
||||||
// Task completed
|
// Task completed
|
||||||
this.bus.emit('panel:update', {
|
this.panel.update({ type: 'completed' })
|
||||||
type: 'completed',
|
|
||||||
displayText: this.i18n.t('ui.panel.taskCompleted'),
|
|
||||||
})
|
|
||||||
|
|
||||||
this.mask.hide()
|
this.mask.hide()
|
||||||
|
|
||||||
|
|||||||
@@ -33,24 +33,19 @@
|
|||||||
*/
|
*/
|
||||||
import type { LLMConfig } from '../config'
|
import type { LLMConfig } from '../config'
|
||||||
import { parseLLMConfig } from '../config'
|
import { parseLLMConfig } from '../config'
|
||||||
import { EventBus, getEventBus } from '../utils/bus'
|
|
||||||
import { OpenAIClient } from './OpenAILenientClient'
|
import { OpenAIClient } from './OpenAILenientClient'
|
||||||
import { InvokeError } from './errors'
|
import { InvokeError } from './errors'
|
||||||
import type { InvokeResult, LLMClient, Message, Tool } from './types'
|
import type { InvokeResult, LLMClient, Message, Tool } from './types'
|
||||||
|
|
||||||
export type { Message, Tool, InvokeResult, LLMClient }
|
export type { Message, Tool, InvokeResult, LLMClient }
|
||||||
|
|
||||||
export class LLM {
|
export class LLM extends EventTarget {
|
||||||
config: Required<LLMConfig>
|
config: Required<LLMConfig>
|
||||||
id: string
|
|
||||||
client: LLMClient
|
client: LLMClient
|
||||||
#bus: EventBus
|
|
||||||
|
|
||||||
constructor(config: LLMConfig, id: string) {
|
constructor(config: LLMConfig) {
|
||||||
|
super()
|
||||||
this.config = parseLLMConfig(config)
|
this.config = parseLLMConfig(config)
|
||||||
this.id = id
|
|
||||||
|
|
||||||
this.#bus = getEventBus(id)
|
|
||||||
|
|
||||||
// Default to OpenAI client
|
// Default to OpenAI client
|
||||||
this.client = new OpenAIClient({
|
this.client = new OpenAIClient({
|
||||||
@@ -81,17 +76,13 @@ export class LLM {
|
|||||||
// retry settings
|
// retry settings
|
||||||
{
|
{
|
||||||
maxRetries: this.config.maxRetries,
|
maxRetries: this.config.maxRetries,
|
||||||
onRetry: (retries: number) => {
|
onRetry: (current: number) => {
|
||||||
this.#bus.emit('panel:update', {
|
this.dispatchEvent(
|
||||||
type: 'retry',
|
new CustomEvent('retry', { detail: { current, max: this.config.maxRetries } })
|
||||||
displayText: `retry-ing (${retries} / ${this.config.maxRetries})`,
|
)
|
||||||
})
|
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
this.#bus.emit('panel:update', {
|
this.dispatchEvent(new CustomEvent('error', { detail: { error } }))
|
||||||
type: 'error',
|
|
||||||
displayText: `step failed: ${(error as Error).message}`,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,35 @@
|
|||||||
import type { PageAgent } from '../PageAgent'
|
import { I18n, type SupportedLanguage } from '../i18n'
|
||||||
import type { I18n } from '../i18n'
|
|
||||||
import { truncate } from '../utils'
|
import { truncate } from '../utils'
|
||||||
import type { EventBus } from '../utils/bus'
|
|
||||||
import { type Step, UIState } from './UIState'
|
import { type Step, UIState } from './UIState'
|
||||||
|
|
||||||
import styles from './Panel.module.css'
|
import styles from './Panel.module.css'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel configuration
|
||||||
|
*/
|
||||||
|
export interface PanelConfig {
|
||||||
|
language?: SupportedLanguage
|
||||||
|
onExecuteTask: (task: string) => void
|
||||||
|
onStop: () => void
|
||||||
|
onPauseToggle: () => boolean // returns new paused state
|
||||||
|
getPaused: () => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semantic update types - Panel handles i18n internally
|
||||||
|
*/
|
||||||
|
export type PanelUpdate =
|
||||||
|
| { type: 'thinking'; text?: string } // text is optional, defaults to i18n thinking text
|
||||||
|
| { type: 'input'; task: string }
|
||||||
|
| { type: 'question'; question: string }
|
||||||
|
| { type: 'userAnswer'; input: string }
|
||||||
|
| { type: 'retry'; current: number; max: number }
|
||||||
|
| { type: 'error'; message: string }
|
||||||
|
| { type: 'output'; text: string }
|
||||||
|
| { type: 'completed' }
|
||||||
|
| { type: 'toolExecuting'; toolName: string; args: any }
|
||||||
|
| { type: 'toolCompleted'; toolName: string; args: any; result?: string; duration?: number }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent control panel
|
* Agent control panel
|
||||||
*/
|
*/
|
||||||
@@ -19,11 +43,11 @@ export class Panel {
|
|||||||
#stopButton: HTMLElement
|
#stopButton: HTMLElement
|
||||||
#inputSection: HTMLElement
|
#inputSection: HTMLElement
|
||||||
#taskInput: HTMLInputElement
|
#taskInput: HTMLInputElement
|
||||||
#bus: EventBus
|
|
||||||
|
|
||||||
#state = new UIState()
|
#state = new UIState()
|
||||||
#isExpanded = false
|
#isExpanded = false
|
||||||
#pageAgent: PageAgent
|
#config: PanelConfig
|
||||||
|
#i18n: I18n
|
||||||
#userAnswerResolver: ((input: string) => void) | null = null
|
#userAnswerResolver: ((input: string) => void) | null = null
|
||||||
#isWaitingForUserAnswer: boolean = false
|
#isWaitingForUserAnswer: boolean = false
|
||||||
#headerUpdateTimer: ReturnType<typeof setInterval> | null = null
|
#headerUpdateTimer: ReturnType<typeof setInterval> | null = null
|
||||||
@@ -34,9 +58,9 @@ export class Panel {
|
|||||||
return this.#wrapper
|
return this.#wrapper
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(pageAgent: PageAgent) {
|
constructor(config: PanelConfig) {
|
||||||
this.#pageAgent = pageAgent
|
this.#config = config
|
||||||
this.#bus = pageAgent.bus
|
this.#i18n = new I18n(config.language ?? 'en-US')
|
||||||
this.#wrapper = this.#createWrapper()
|
this.#wrapper = this.#createWrapper()
|
||||||
this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)!
|
this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`)!
|
||||||
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
|
this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`)!
|
||||||
@@ -49,16 +73,8 @@ export class Panel {
|
|||||||
|
|
||||||
this.#setupEventListeners()
|
this.#setupEventListeners()
|
||||||
this.#startHeaderUpdateLoop()
|
this.#startHeaderUpdateLoop()
|
||||||
// this.#expand() // debug
|
|
||||||
|
|
||||||
this.#showInputArea()
|
this.#showInputArea()
|
||||||
|
|
||||||
this.#bus.on('panel:show', () => this.#show())
|
|
||||||
this.#bus.on('panel:hide', () => this.#hide())
|
|
||||||
this.#bus.on('panel:reset', () => this.#reset())
|
|
||||||
this.#bus.on('panel:update', (stepData) => this.#update(stepData))
|
|
||||||
this.#bus.on('panel:expand', () => this.#expand())
|
|
||||||
this.#bus.on('panel:collapse', () => this.#collapse())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,18 +87,67 @@ export class Panel {
|
|||||||
this.#userAnswerResolver = resolve
|
this.#userAnswerResolver = resolve
|
||||||
|
|
||||||
// Update state to `running`
|
// Update state to `running`
|
||||||
this.#update({
|
this.#updateInternal({
|
||||||
type: 'output',
|
type: 'output',
|
||||||
displayText: this.#pageAgent.i18n.t('ui.panel.question', { question }),
|
displayText: this.#i18n.t('ui.panel.question', { question }),
|
||||||
}) // Expand history panel
|
}) // Expand history panel
|
||||||
if (!this.#isExpanded) {
|
if (!this.#isExpanded) {
|
||||||
this.#expand()
|
this.#expand()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#showInputArea(this.#pageAgent.i18n.t('ui.panel.userAnswerPrompt'))
|
this.#showInputArea(this.#i18n.t('ui.panel.userAnswerPrompt'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Public control methods ==========
|
||||||
|
|
||||||
|
show(): void {
|
||||||
|
this.wrapper.style.display = 'block'
|
||||||
|
void this.wrapper.offsetHeight
|
||||||
|
this.wrapper.style.opacity = '1'
|
||||||
|
this.wrapper.style.transform = 'translateX(-50%) translateY(0)'
|
||||||
|
}
|
||||||
|
|
||||||
|
hide(): void {
|
||||||
|
this.wrapper.style.opacity = '0'
|
||||||
|
this.wrapper.style.transform = 'translateX(-50%) translateY(20px)'
|
||||||
|
this.wrapper.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.#state.reset()
|
||||||
|
this.#statusText.textContent = this.#i18n.t('ui.panel.ready')
|
||||||
|
this.#updateStatusIndicator('thinking')
|
||||||
|
this.#updateHistory()
|
||||||
|
this.#collapse()
|
||||||
|
// Reset pause state via callback
|
||||||
|
if (this.#config.getPaused()) {
|
||||||
|
this.#config.onPauseToggle()
|
||||||
|
}
|
||||||
|
this.#updatePauseButton()
|
||||||
|
// Reset user input state
|
||||||
|
this.#isWaitingForUserAnswer = false
|
||||||
|
this.#userAnswerResolver = null
|
||||||
|
// Show input area
|
||||||
|
this.#showInputArea()
|
||||||
|
}
|
||||||
|
|
||||||
|
expand(): void {
|
||||||
|
this.#expand()
|
||||||
|
}
|
||||||
|
|
||||||
|
collapse(): void {
|
||||||
|
this.#collapse()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update panel with semantic data - i18n handled internally
|
||||||
|
*/
|
||||||
|
update(data: PanelUpdate): void {
|
||||||
|
const stepData = this.#toStepData(data)
|
||||||
|
this.#updateInternal(stepData)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose panel
|
* Dispose panel
|
||||||
*/
|
*/
|
||||||
@@ -92,10 +157,102 @@ export class Panel {
|
|||||||
this.wrapper.remove()
|
this.wrapper.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Private methods ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update status
|
* Convert semantic update to step data with i18n
|
||||||
*/
|
*/
|
||||||
#update(stepData: Omit<Step, 'id' | 'stepNumber' | 'timestamp'>): void {
|
#toStepData(data: PanelUpdate): Omit<Step, 'id' | 'stepNumber' | 'timestamp'> {
|
||||||
|
switch (data.type) {
|
||||||
|
case 'thinking':
|
||||||
|
return { type: 'thinking', displayText: data.text ?? this.#i18n.t('ui.panel.thinking') }
|
||||||
|
case 'input':
|
||||||
|
return { type: 'input', displayText: data.task }
|
||||||
|
case 'question':
|
||||||
|
return {
|
||||||
|
type: 'output',
|
||||||
|
displayText: this.#i18n.t('ui.panel.question', { question: data.question }),
|
||||||
|
}
|
||||||
|
case 'userAnswer':
|
||||||
|
return {
|
||||||
|
type: 'input',
|
||||||
|
displayText: this.#i18n.t('ui.panel.userAnswer', { input: data.input }),
|
||||||
|
}
|
||||||
|
case 'retry':
|
||||||
|
return { type: 'retry', displayText: `retry-ing (${data.current} / ${data.max})` }
|
||||||
|
case 'error':
|
||||||
|
return { type: 'error', displayText: data.message }
|
||||||
|
case 'output':
|
||||||
|
return { type: 'output', displayText: data.text }
|
||||||
|
case 'completed':
|
||||||
|
return { type: 'completed', displayText: this.#i18n.t('ui.panel.taskCompleted') }
|
||||||
|
case 'toolExecuting':
|
||||||
|
return {
|
||||||
|
type: 'tool_executing',
|
||||||
|
toolName: data.toolName,
|
||||||
|
toolArgs: data.args,
|
||||||
|
displayText: this.#getToolExecutingText(data.toolName, data.args),
|
||||||
|
}
|
||||||
|
case 'toolCompleted': {
|
||||||
|
const displayText = this.#getToolCompletedText(data.toolName, data.args)
|
||||||
|
if (!displayText) return { type: 'tool_executing', displayText: '' } // will be filtered
|
||||||
|
return {
|
||||||
|
type: 'tool_executing',
|
||||||
|
toolName: data.toolName,
|
||||||
|
toolArgs: data.args,
|
||||||
|
toolResult: data.result,
|
||||||
|
displayText,
|
||||||
|
duration: data.duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#getToolExecutingText(toolName: string, args: any): string {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'click_element_by_index':
|
||||||
|
return this.#i18n.t('ui.tools.clicking', { index: args.index })
|
||||||
|
case 'input_text':
|
||||||
|
return this.#i18n.t('ui.tools.inputting', { index: args.index })
|
||||||
|
case 'select_dropdown_option':
|
||||||
|
return this.#i18n.t('ui.tools.selecting', { text: args.text })
|
||||||
|
case 'scroll':
|
||||||
|
return this.#i18n.t('ui.tools.scrolling')
|
||||||
|
case 'wait':
|
||||||
|
return this.#i18n.t('ui.tools.waiting', { seconds: args.seconds })
|
||||||
|
case 'done':
|
||||||
|
return this.#i18n.t('ui.tools.done')
|
||||||
|
default:
|
||||||
|
return this.#i18n.t('ui.tools.executing', { toolName })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#getToolCompletedText(toolName: string, args: any): string | null {
|
||||||
|
switch (toolName) {
|
||||||
|
case 'click_element_by_index':
|
||||||
|
return this.#i18n.t('ui.tools.clicked', { index: args.index })
|
||||||
|
case 'input_text':
|
||||||
|
return this.#i18n.t('ui.tools.inputted', { text: args.text })
|
||||||
|
case 'select_dropdown_option':
|
||||||
|
return this.#i18n.t('ui.tools.selected', { text: args.text })
|
||||||
|
case 'scroll':
|
||||||
|
return this.#i18n.t('ui.tools.scrolled')
|
||||||
|
case 'wait':
|
||||||
|
return this.#i18n.t('ui.tools.waited')
|
||||||
|
case 'done':
|
||||||
|
return null
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update status (internal)
|
||||||
|
*/
|
||||||
|
#updateInternal(stepData: Omit<Step, 'id' | 'stepNumber' | 'timestamp'>): void {
|
||||||
|
// Skip empty displayText (filtered toolCompleted for 'done')
|
||||||
|
if (!stepData.displayText) return
|
||||||
|
|
||||||
const step = this.#state.addStep(stepData)
|
const step = this.#state.addStep(stepData)
|
||||||
|
|
||||||
// Queue header text update (will be processed by periodic check)
|
// Queue header text update (will be processed by periodic check)
|
||||||
@@ -120,59 +277,20 @@ export class Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show panel
|
|
||||||
*/
|
|
||||||
#show(): void {
|
|
||||||
this.wrapper.style.display = 'block'
|
|
||||||
// Force reflow to trigger animation
|
|
||||||
void this.wrapper.offsetHeight
|
|
||||||
this.wrapper.style.opacity = '1'
|
|
||||||
this.wrapper.style.transform = 'translateX(-50%) translateY(0)'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide panel
|
|
||||||
*/
|
|
||||||
#hide(): void {
|
|
||||||
this.wrapper.style.opacity = '0'
|
|
||||||
this.wrapper.style.transform = 'translateX(-50%) translateY(20px)'
|
|
||||||
this.wrapper.style.display = 'none'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset state
|
|
||||||
*/
|
|
||||||
#reset(): void {
|
|
||||||
this.#state.reset()
|
|
||||||
this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.ready')
|
|
||||||
this.#updateStatusIndicator('thinking')
|
|
||||||
this.#updateHistory()
|
|
||||||
this.#collapse()
|
|
||||||
// Reset pause state
|
|
||||||
this.#pageAgent.paused = false
|
|
||||||
this.#updatePauseButton()
|
|
||||||
// Reset user input state
|
|
||||||
this.#isWaitingForUserAnswer = false
|
|
||||||
this.#userAnswerResolver = null
|
|
||||||
// Show input area
|
|
||||||
this.#showInputArea()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle pause state
|
* Toggle pause state
|
||||||
*/
|
*/
|
||||||
#togglePause(): void {
|
#togglePause(): void {
|
||||||
this.#pageAgent.paused = !this.#pageAgent.paused
|
const paused = this.#config.onPauseToggle()
|
||||||
this.#updatePauseButton()
|
this.#updatePauseButton()
|
||||||
|
|
||||||
// Update status display
|
// Update status display
|
||||||
if (this.#pageAgent.paused) {
|
if (paused) {
|
||||||
this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.paused')
|
this.#statusText.textContent = this.#i18n.t('ui.panel.paused')
|
||||||
this.#updateStatusIndicator('thinking') // Use existing thinking state
|
this.#updateStatusIndicator('thinking')
|
||||||
} else {
|
} else {
|
||||||
this.#statusText.textContent = this.#pageAgent.i18n.t('ui.panel.continueExecution')
|
this.#statusText.textContent = this.#i18n.t('ui.panel.continueExecution')
|
||||||
this.#updateStatusIndicator('tool_executing') // Restore to execution state
|
this.#updateStatusIndicator('tool_executing')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,13 +298,14 @@ export class Panel {
|
|||||||
* Update pause button state
|
* Update pause button state
|
||||||
*/
|
*/
|
||||||
#updatePauseButton(): void {
|
#updatePauseButton(): void {
|
||||||
if (this.#pageAgent.paused) {
|
const paused = this.#config.getPaused()
|
||||||
|
if (paused) {
|
||||||
this.#pauseButton.textContent = '▶'
|
this.#pauseButton.textContent = '▶'
|
||||||
this.#pauseButton.title = this.#pageAgent.i18n.t('ui.panel.continue')
|
this.#pauseButton.title = this.#i18n.t('ui.panel.continue')
|
||||||
this.#pauseButton.classList.add(styles.paused)
|
this.#pauseButton.classList.add(styles.paused)
|
||||||
} else {
|
} else {
|
||||||
this.#pauseButton.textContent = '⏸︎'
|
this.#pauseButton.textContent = '⏸︎'
|
||||||
this.#pauseButton.title = this.#pageAgent.i18n.t('ui.panel.pause')
|
this.#pauseButton.title = this.#i18n.t('ui.panel.pause')
|
||||||
this.#pauseButton.classList.remove(styles.paused)
|
this.#pauseButton.classList.remove(styles.paused)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,12 +315,12 @@ export class Panel {
|
|||||||
*/
|
*/
|
||||||
#stopAgent(): void {
|
#stopAgent(): void {
|
||||||
// Update status display
|
// Update status display
|
||||||
this.#update({
|
this.#updateInternal({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
displayText: this.#pageAgent.i18n.t('ui.panel.taskTerminated'),
|
displayText: this.#i18n.t('ui.panel.taskTerminated'),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.#pageAgent.dispose()
|
this.#config.onStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,7 +337,7 @@ export class Panel {
|
|||||||
// Handle user input mode
|
// Handle user input mode
|
||||||
this.#handleUserAnswer(input)
|
this.#handleUserAnswer(input)
|
||||||
} else {
|
} else {
|
||||||
this.#pageAgent.execute(input)
|
this.#config.onExecuteTask(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,9 +346,9 @@ export class Panel {
|
|||||||
*/
|
*/
|
||||||
#handleUserAnswer(input: string): void {
|
#handleUserAnswer(input: string): void {
|
||||||
// Add user input to history
|
// Add user input to history
|
||||||
this.#update({
|
this.#updateInternal({
|
||||||
type: 'input',
|
type: 'input',
|
||||||
displayText: this.#pageAgent.i18n.t('ui.panel.userAnswer', { input }),
|
displayText: this.#i18n.t('ui.panel.userAnswer', { input }),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
@@ -248,7 +367,7 @@ export class Panel {
|
|||||||
#showInputArea(placeholder?: string): void {
|
#showInputArea(placeholder?: string): void {
|
||||||
// Clear input field
|
// Clear input field
|
||||||
this.#taskInput.value = ''
|
this.#taskInput.value = ''
|
||||||
this.#taskInput.placeholder = placeholder || this.#pageAgent.i18n.t('ui.panel.taskInput')
|
this.#taskInput.placeholder = placeholder || this.#i18n.t('ui.panel.taskInput')
|
||||||
this.#inputSection.classList.remove(styles.hidden)
|
this.#inputSection.classList.remove(styles.hidden)
|
||||||
// Focus on input field
|
// Focus on input field
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -294,23 +413,23 @@ export class Panel {
|
|||||||
stepNumber: 0,
|
stepNumber: 0,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
type: 'thinking',
|
type: 'thinking',
|
||||||
displayText: this.#pageAgent.i18n.t('ui.panel.waitingPlaceholder'),
|
displayText: this.#i18n.t('ui.panel.waitingPlaceholder'),
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="${styles.header}">
|
<div class="${styles.header}">
|
||||||
<div class="${styles.statusSection}">
|
<div class="${styles.statusSection}">
|
||||||
<div class="${styles.indicator} ${styles.thinking}"></div>
|
<div class="${styles.indicator} ${styles.thinking}"></div>
|
||||||
<div class="${styles.statusText}">${this.#pageAgent.i18n.t('ui.panel.ready')}</div>
|
<div class="${styles.statusText}">${this.#i18n.t('ui.panel.ready')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="${styles.controls}">
|
<div class="${styles.controls}">
|
||||||
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#pageAgent.i18n.t('ui.panel.expand')}">
|
<button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t('ui.panel.expand')}">
|
||||||
▼
|
▼
|
||||||
</button>
|
</button>
|
||||||
<button class="${styles.controlButton} ${styles.pauseButton}" title="${this.#pageAgent.i18n.t('ui.panel.pause')}">
|
<button class="${styles.controlButton} ${styles.pauseButton}" title="${this.#i18n.t('ui.panel.pause')}">
|
||||||
⏸︎
|
⏸︎
|
||||||
</button>
|
</button>
|
||||||
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#pageAgent.i18n.t('ui.panel.stop')}">
|
<button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t('ui.panel.stop')}">
|
||||||
X
|
X
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -501,8 +620,8 @@ export class Panel {
|
|||||||
// Check if this is a result from done tool
|
// Check if this is a result from done tool
|
||||||
if (step.toolName === 'done') {
|
if (step.toolName === 'done') {
|
||||||
// Judge success or failure based on result
|
// Judge success or failure based on result
|
||||||
const failureKeyword = this.#pageAgent.i18n.t('ui.tools.resultFailure')
|
const failureKeyword = this.#i18n.t('ui.tools.resultFailure')
|
||||||
const errorKeyword = this.#pageAgent.i18n.t('ui.tools.resultError')
|
const errorKeyword = this.#i18n.t('ui.tools.resultError')
|
||||||
const isSuccess =
|
const isSuccess =
|
||||||
!step.toolResult ||
|
!step.toolResult ||
|
||||||
(!step.toolResult.includes(failureKeyword) && !step.toolResult.includes(errorKeyword))
|
(!step.toolResult.includes(failureKeyword) && !step.toolResult.includes(errorKeyword))
|
||||||
@@ -531,7 +650,7 @@ export class Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const durationText = step.duration ? ` · ${step.duration}ms` : ''
|
const durationText = step.duration ? ` · ${step.duration}ms` : ''
|
||||||
const stepLabel = this.#pageAgent.i18n.t('ui.panel.step', {
|
const stepLabel = this.#i18n.t('ui.panel.step', {
|
||||||
number: step.stepNumber.toString(),
|
number: step.stepNumber.toString(),
|
||||||
time,
|
time,
|
||||||
duration: durationText || '', // Explicitly pass empty string to replace template
|
duration: durationText || '', // Explicitly pass empty string to replace template
|
||||||
@@ -550,47 +669,3 @@ export class Panel {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get display text for tool execution
|
|
||||||
*/
|
|
||||||
export function getToolExecutingText(toolName: string, args: any, i18n: I18n): string {
|
|
||||||
switch (toolName) {
|
|
||||||
case 'click_element_by_index':
|
|
||||||
return i18n.t('ui.tools.clicking', { index: args.index })
|
|
||||||
case 'input_text':
|
|
||||||
return i18n.t('ui.tools.inputting', { index: args.index })
|
|
||||||
case 'select_dropdown_option':
|
|
||||||
return i18n.t('ui.tools.selecting', { text: args.text })
|
|
||||||
case 'scroll':
|
|
||||||
return i18n.t('ui.tools.scrolling')
|
|
||||||
case 'wait':
|
|
||||||
return i18n.t('ui.tools.waiting', { seconds: args.seconds })
|
|
||||||
case 'done':
|
|
||||||
return i18n.t('ui.tools.done')
|
|
||||||
default:
|
|
||||||
return i18n.t('ui.tools.executing', { toolName })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get display text for tool completion
|
|
||||||
*/
|
|
||||||
export function getToolCompletedText(toolName: string, args: any, i18n: I18n): string | null {
|
|
||||||
switch (toolName) {
|
|
||||||
case 'click_element_by_index':
|
|
||||||
return i18n.t('ui.tools.clicked', { index: args.index })
|
|
||||||
case 'input_text':
|
|
||||||
return i18n.t('ui.tools.inputted', { text: args.text })
|
|
||||||
case 'select_dropdown_option':
|
|
||||||
return i18n.t('ui.tools.selected', { text: args.text })
|
|
||||||
case 'scroll':
|
|
||||||
return i18n.t('ui.tools.scrolled')
|
|
||||||
case 'wait':
|
|
||||||
return i18n.t('ui.tools.waited')
|
|
||||||
case 'done':
|
|
||||||
return null
|
|
||||||
default:
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,4 +37,4 @@ if (currentScript) {
|
|||||||
|
|
||||||
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
||||||
|
|
||||||
window.pageAgent.bus.emit('panel:show') // Show panel
|
window.pageAgent.panel.show() // Show panel
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Type-safe event bus for decoupling PageAgent and Panel
|
* Type-safe event bus for decoupling PageAgent and Panel
|
||||||
*/
|
*/
|
||||||
import type { Step } from '../ui/UIState'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event mapping definitions
|
* Event mapping definitions
|
||||||
@@ -12,15 +11,15 @@ export interface PageAgentEventMap {
|
|||||||
// call panel.show()
|
// call panel.show()
|
||||||
'panel:show': { params: undefined }
|
'panel:show': { params: undefined }
|
||||||
// call panel.hide()
|
// call panel.hide()
|
||||||
'panel:hide': { params: undefined }
|
// 'panel:hide': { params: undefined }
|
||||||
// call panel.reset()
|
// call panel.reset()
|
||||||
'panel:reset': { params: undefined }
|
// 'panel:reset': { params: undefined }
|
||||||
// call panel.update()
|
// call panel.update()
|
||||||
'panel:update': { params: Omit<Step, 'id' | 'stepNumber' | 'timestamp'> }
|
// 'panel:update': { params: Omit<Step, 'id' | 'stepNumber' | 'timestamp'> }
|
||||||
// call panel.expand()
|
// call panel.expand()
|
||||||
'panel:expand': { params: undefined }
|
// 'panel:expand': { params: undefined }
|
||||||
// call panel.collapse()
|
// call panel.collapse()
|
||||||
'panel:collapse': { params: undefined }
|
// 'panel:collapse': { params: undefined }
|
||||||
|
|
||||||
// PageAgent status events
|
// PageAgent status events
|
||||||
// 'agent:execute': { params: { task: string } }
|
// 'agent:execute': { params: { task: string } }
|
||||||
|
|||||||
Reference in New Issue
Block a user