@electric-ax/agents 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entrypoint.js +5 -3
- package/dist/index.cjs +5 -3
- package/dist/index.js +5 -3
- package/docs/entities/agents/horton.md +89 -0
- package/docs/entities/agents/worker.md +102 -0
- package/docs/entities/patterns/blackboard.md +111 -0
- package/docs/entities/patterns/dispatcher.md +77 -0
- package/docs/entities/patterns/manager-worker.md +127 -0
- package/docs/entities/patterns/map-reduce.md +81 -0
- package/docs/entities/patterns/pipeline.md +101 -0
- package/docs/entities/patterns/reactive-observers.md +125 -0
- package/docs/examples/mega-draw.md +106 -0
- package/docs/examples/playground.md +46 -0
- package/docs/index.md +208 -0
- package/docs/quickstart.md +201 -0
- package/docs/reference/agent-config.md +82 -0
- package/docs/reference/agent-tool.md +58 -0
- package/docs/reference/built-in-collections.md +334 -0
- package/docs/reference/cli.md +238 -0
- package/docs/reference/entity-definition.md +57 -0
- package/docs/reference/entity-handle.md +63 -0
- package/docs/reference/entity-registry.md +73 -0
- package/docs/reference/handler-context.md +108 -0
- package/docs/reference/runtime-handler.md +136 -0
- package/docs/reference/shared-state-handle.md +74 -0
- package/docs/reference/state-collection-proxy.md +41 -0
- package/docs/reference/wake-event.md +132 -0
- package/docs/usage/app-setup.md +165 -0
- package/docs/usage/clients-and-react.md +191 -0
- package/docs/usage/configuring-the-agent.md +136 -0
- package/docs/usage/context-composition.md +204 -0
- package/docs/usage/defining-entities.md +181 -0
- package/docs/usage/defining-tools.md +229 -0
- package/docs/usage/embedded-builtins.md +180 -0
- package/docs/usage/managing-state.md +93 -0
- package/docs/usage/overview.md +284 -0
- package/docs/usage/programmatic-runtime-client.md +216 -0
- package/docs/usage/shared-state.md +169 -0
- package/docs/usage/spawning-and-coordinating.md +165 -0
- package/docs/usage/testing.md +76 -0
- package/docs/usage/waking-entities.md +148 -0
- package/docs/usage/writing-handlers.md +267 -0
- package/package.json +3 -2
- package/skills/quickstart/scaffold/package.json +17 -3
- package/skills/quickstart/scaffold/tsconfig.json +8 -3
- package/skills/quickstart/scaffold/vite.config.ts +21 -0
- package/skills/quickstart/scaffold-ui/index.html +12 -0
- package/skills/quickstart/scaffold-ui/main.tsx +440 -0
- package/skills/quickstart.md +209 -344
|
@@ -4,14 +4,28 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"start": "tsx server.ts",
|
|
7
|
-
"dev": "tsx --watch server.ts"
|
|
7
|
+
"dev": "tsx --watch server.ts",
|
|
8
|
+
"dev:server": "tsx --watch server.ts",
|
|
9
|
+
"dev:ui": "vite",
|
|
10
|
+
"dev:all": "npm run dev:server & npm run dev:ui"
|
|
8
11
|
},
|
|
9
12
|
"dependencies": {
|
|
10
13
|
"@electric-ax/agents-runtime": "latest",
|
|
11
|
-
"@
|
|
14
|
+
"@radix-ui/react-icons": "^1.3.0",
|
|
15
|
+
"@radix-ui/themes": "^3.3.0",
|
|
16
|
+
"@sinclair/typebox": "^0.34.49",
|
|
17
|
+
"@tanstack/db": "^0.6.0",
|
|
18
|
+
"@tanstack/react-db": "^0.1.78",
|
|
19
|
+
"react": "^19.2.4",
|
|
20
|
+
"react-dom": "^19.2.4",
|
|
21
|
+
"streamdown": "^2.5.0"
|
|
12
22
|
},
|
|
13
23
|
"devDependencies": {
|
|
24
|
+
"@types/react": "^19.2.14",
|
|
25
|
+
"@types/react-dom": "^19.2.3",
|
|
26
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
14
27
|
"tsx": "^4.19.0",
|
|
15
|
-
"typescript": "^5.7.0"
|
|
28
|
+
"typescript": "^5.7.0",
|
|
29
|
+
"vite": "^7.2.4"
|
|
16
30
|
}
|
|
17
31
|
}
|
|
@@ -3,13 +3,18 @@
|
|
|
3
3
|
"target": "ES2022",
|
|
4
4
|
"module": "ESNext",
|
|
5
5
|
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
6
7
|
"strict": true,
|
|
7
8
|
"esModuleInterop": true,
|
|
8
9
|
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
11
|
"resolveJsonModule": true,
|
|
10
|
-
"
|
|
11
|
-
"noEmit": true
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"paths": {
|
|
15
|
+
"@tanstack/db": ["./node_modules/@tanstack/db"]
|
|
16
|
+
}
|
|
12
17
|
},
|
|
13
|
-
"include": ["**/*.ts"],
|
|
18
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
14
19
|
"exclude": ["node_modules"]
|
|
15
20
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { defineConfig } from 'vite'
|
|
3
|
+
import react from '@vitejs/plugin-react'
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
root: `ui`,
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
'@tanstack/db': path.resolve(
|
|
11
|
+
import.meta.dirname,
|
|
12
|
+
`node_modules/@tanstack/db`
|
|
13
|
+
),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
server: {
|
|
17
|
+
port: 5175,
|
|
18
|
+
proxy: { '/api': `http://localhost:3000` },
|
|
19
|
+
},
|
|
20
|
+
build: { outDir: `../dist`, emptyOutDir: true },
|
|
21
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Perspectives Analyzer</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="./main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react'
|
|
10
|
+
import { createRoot } from 'react-dom/client'
|
|
11
|
+
import { createAgentsClient, entity } from '@electric-ax/agents-runtime'
|
|
12
|
+
import { useChat } from '@electric-ax/agents-runtime/react'
|
|
13
|
+
import {
|
|
14
|
+
Theme,
|
|
15
|
+
Box,
|
|
16
|
+
Flex,
|
|
17
|
+
Heading,
|
|
18
|
+
Text,
|
|
19
|
+
TextField,
|
|
20
|
+
Button,
|
|
21
|
+
Card,
|
|
22
|
+
IconButton,
|
|
23
|
+
} from '@radix-ui/themes'
|
|
24
|
+
import { SunIcon, MoonIcon, DesktopIcon } from '@radix-ui/react-icons'
|
|
25
|
+
import { Streamdown } from 'streamdown'
|
|
26
|
+
import '@radix-ui/themes/styles.css'
|
|
27
|
+
import 'streamdown/styles.css'
|
|
28
|
+
import type { EntityStreamDB } from '@electric-ax/agents-runtime'
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Dark-mode hook (inlined – single-file scaffold)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const DARK_MODE_STORAGE_KEY = `electric-perspectives.dark-mode`
|
|
35
|
+
|
|
36
|
+
type ThemePreference = `light` | `dark` | `system`
|
|
37
|
+
|
|
38
|
+
type DarkModeContextValue = {
|
|
39
|
+
darkMode: boolean
|
|
40
|
+
preference: ThemePreference
|
|
41
|
+
cyclePreference: () => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const DarkModeContext = createContext<DarkModeContextValue | null>(null)
|
|
45
|
+
|
|
46
|
+
function readInitialPreference(): ThemePreference {
|
|
47
|
+
if (typeof window === `undefined`) return `system`
|
|
48
|
+
const stored = window.localStorage.getItem(DARK_MODE_STORAGE_KEY)
|
|
49
|
+
if (stored === `true` || stored === `dark`) return `dark`
|
|
50
|
+
if (stored === `false` || stored === `light`) return `light`
|
|
51
|
+
return `system`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function systemPrefersDark(): boolean {
|
|
55
|
+
if (typeof window === `undefined`) return false
|
|
56
|
+
return window.matchMedia?.(`(prefers-color-scheme: dark)`).matches ?? false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const cycleOrder: Record<ThemePreference, ThemePreference> = {
|
|
60
|
+
light: `dark`,
|
|
61
|
+
dark: `system`,
|
|
62
|
+
system: `light`,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function DarkModeProvider({
|
|
66
|
+
children,
|
|
67
|
+
}: {
|
|
68
|
+
children: React.ReactNode
|
|
69
|
+
}): React.ReactElement {
|
|
70
|
+
const [preference, setPreference] = useState<ThemePreference>(
|
|
71
|
+
readInitialPreference
|
|
72
|
+
)
|
|
73
|
+
const [systemDark, setSystemDark] = useState<boolean>(systemPrefersDark)
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (typeof window === `undefined`) return
|
|
77
|
+
const mql = window.matchMedia(`(prefers-color-scheme: dark)`)
|
|
78
|
+
const onChange = (e: MediaQueryListEvent): void => setSystemDark(e.matches)
|
|
79
|
+
mql.addEventListener(`change`, onChange)
|
|
80
|
+
return () => mql.removeEventListener(`change`, onChange)
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
const darkMode = preference === `system` ? systemDark : preference === `dark`
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
document.documentElement.classList.toggle(`dark`, darkMode)
|
|
87
|
+
}, [darkMode])
|
|
88
|
+
|
|
89
|
+
const cyclePreference = useCallback(() => {
|
|
90
|
+
setPreference((current) => {
|
|
91
|
+
const next = cycleOrder[current]
|
|
92
|
+
window.localStorage.setItem(DARK_MODE_STORAGE_KEY, next)
|
|
93
|
+
return next
|
|
94
|
+
})
|
|
95
|
+
}, [])
|
|
96
|
+
|
|
97
|
+
const value = useMemo(
|
|
98
|
+
() => ({ darkMode, preference, cyclePreference }),
|
|
99
|
+
[darkMode, preference, cyclePreference]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<DarkModeContext.Provider value={value}>
|
|
104
|
+
{children}
|
|
105
|
+
</DarkModeContext.Provider>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function useDarkModeContext(): DarkModeContextValue {
|
|
110
|
+
const value = useContext(DarkModeContext)
|
|
111
|
+
if (!value) {
|
|
112
|
+
throw new Error(`useDarkModeContext must be used inside DarkModeProvider`)
|
|
113
|
+
}
|
|
114
|
+
return value
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Theme toggle button
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
function themeButtonIcon(preference: ThemePreference) {
|
|
122
|
+
if (preference === `light`) return <SunIcon />
|
|
123
|
+
if (preference === `dark`) return <MoonIcon />
|
|
124
|
+
return <DesktopIcon />
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function themeButtonAriaLabel(preference: ThemePreference): string {
|
|
128
|
+
if (preference === `light`) return `Switch to dark mode`
|
|
129
|
+
if (preference === `dark`) return `Switch to system theme`
|
|
130
|
+
return `Switch to light mode`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ThemeToggle() {
|
|
134
|
+
const { preference, cyclePreference } = useDarkModeContext()
|
|
135
|
+
return (
|
|
136
|
+
<IconButton
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="2"
|
|
139
|
+
color="gray"
|
|
140
|
+
onClick={cyclePreference}
|
|
141
|
+
aria-label={themeButtonAriaLabel(preference)}
|
|
142
|
+
>
|
|
143
|
+
{themeButtonIcon(preference)}
|
|
144
|
+
</IconButton>
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const AGENTS_URL = `http://localhost:4437`
|
|
149
|
+
|
|
150
|
+
const AGENT_COLORS: Record<string, { bg: string; border: string }> = {
|
|
151
|
+
analyser: { bg: `var(--blue-3)`, border: `var(--blue-6)` },
|
|
152
|
+
optimist: { bg: `var(--green-3)`, border: `var(--green-6)` },
|
|
153
|
+
critic: { bg: `var(--red-3)`, border: `var(--red-6)` },
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function useEntityDb(url: string | null, retryMs = 0) {
|
|
157
|
+
const [db, setDb] = useState<EntityStreamDB | null>(null)
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!url) {
|
|
161
|
+
setDb(null)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
let cancelled = false
|
|
165
|
+
let observedDb: EntityStreamDB | null = null
|
|
166
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
167
|
+
|
|
168
|
+
const connect = () => {
|
|
169
|
+
const client = createAgentsClient({ baseUrl: AGENTS_URL })
|
|
170
|
+
client.observe(entity(url)).then(
|
|
171
|
+
(observed) => {
|
|
172
|
+
observedDb = observed as EntityStreamDB
|
|
173
|
+
if (cancelled) {
|
|
174
|
+
observedDb.close()
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
setDb(observedDb)
|
|
178
|
+
},
|
|
179
|
+
() => {
|
|
180
|
+
if (!cancelled && retryMs > 0) {
|
|
181
|
+
timer = setTimeout(connect, retryMs)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
connect()
|
|
187
|
+
|
|
188
|
+
return () => {
|
|
189
|
+
cancelled = true
|
|
190
|
+
if (timer) clearTimeout(timer)
|
|
191
|
+
observedDb?.close()
|
|
192
|
+
}
|
|
193
|
+
}, [url, retryMs])
|
|
194
|
+
|
|
195
|
+
return db
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface AgentMessage {
|
|
199
|
+
agent: string
|
|
200
|
+
text: string
|
|
201
|
+
isStreaming: boolean
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function useAgentMessages(
|
|
205
|
+
url: string | null,
|
|
206
|
+
agent: string,
|
|
207
|
+
retryMs = 0
|
|
208
|
+
): AgentMessage[] {
|
|
209
|
+
const db = useEntityDb(url, retryMs)
|
|
210
|
+
const chat = useChat(db)
|
|
211
|
+
|
|
212
|
+
return chat.runs.flatMap((r, ri) =>
|
|
213
|
+
r.texts
|
|
214
|
+
.filter((t) => t.text.trim().length > 0)
|
|
215
|
+
.map((t, ti) => ({
|
|
216
|
+
agent,
|
|
217
|
+
text: t.text,
|
|
218
|
+
isStreaming:
|
|
219
|
+
chat.state === `working` &&
|
|
220
|
+
ri === chat.runs.length - 1 &&
|
|
221
|
+
ti === r.texts.length - 1,
|
|
222
|
+
}))
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function MessageBubble({ msg }: { msg: AgentMessage }) {
|
|
227
|
+
const colors = AGENT_COLORS[msg.agent] ?? {
|
|
228
|
+
bg: `var(--gray-3)`,
|
|
229
|
+
border: `var(--gray-6)`,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<Card
|
|
234
|
+
size="1"
|
|
235
|
+
style={{
|
|
236
|
+
background: colors.bg,
|
|
237
|
+
borderLeft: `3px solid ${colors.border}`,
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
<Text size="1" weight="bold" style={{ textTransform: `capitalize` }}>
|
|
241
|
+
{msg.agent}
|
|
242
|
+
</Text>
|
|
243
|
+
<Box mt="1" style={{ fontSize: `var(--font-size-2)` }}>
|
|
244
|
+
<Streamdown isAnimating={msg.isStreaming} controls={false}>
|
|
245
|
+
{msg.text}
|
|
246
|
+
</Streamdown>
|
|
247
|
+
</Box>
|
|
248
|
+
</Card>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function urlsFromId(id: string) {
|
|
253
|
+
return {
|
|
254
|
+
entityUrl: `/perspectives/${id}`,
|
|
255
|
+
optimistUrl: `/worker/${id}-optimist`,
|
|
256
|
+
criticUrl: `/worker/${id}-critic`,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function getIdFromHash(): string | null {
|
|
261
|
+
const hash = window.location.hash.slice(1)
|
|
262
|
+
return hash || null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function App() {
|
|
266
|
+
const [question, setQuestion] = useState(``)
|
|
267
|
+
const [urls, setUrls] = useState<{
|
|
268
|
+
entityUrl: string
|
|
269
|
+
optimistUrl: string
|
|
270
|
+
criticUrl: string
|
|
271
|
+
} | null>(() => {
|
|
272
|
+
const id = getIdFromHash()
|
|
273
|
+
return id ? urlsFromId(id) : null
|
|
274
|
+
})
|
|
275
|
+
const [loading, setLoading] = useState(false)
|
|
276
|
+
const bottomRef = useRef<HTMLDivElement>(null)
|
|
277
|
+
|
|
278
|
+
const analyserMessages = useAgentMessages(urls?.entityUrl ?? null, `analyser`)
|
|
279
|
+
const optimistMessages = useAgentMessages(
|
|
280
|
+
urls?.optimistUrl ?? null,
|
|
281
|
+
`optimist`,
|
|
282
|
+
2000
|
|
283
|
+
)
|
|
284
|
+
const criticMessages = useAgentMessages(
|
|
285
|
+
urls?.criticUrl ?? null,
|
|
286
|
+
`critic`,
|
|
287
|
+
2000
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
const hasOptimist = optimistMessages.length > 0
|
|
291
|
+
const hasCritic = criticMessages.length > 0
|
|
292
|
+
const hasWorkerMessages = hasOptimist || hasCritic
|
|
293
|
+
const hasBothWorkers = hasOptimist && hasCritic
|
|
294
|
+
// First analyser message is the intro; the rest is synthesis after workers
|
|
295
|
+
const analyserIntro = analyserMessages.slice(0, 1)
|
|
296
|
+
const analyserSynthesis = hasWorkerMessages
|
|
297
|
+
? analyserMessages.slice(1).filter((m) => m.text.trim() !== `waiting`)
|
|
298
|
+
: []
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
const el = bottomRef.current?.parentElement
|
|
302
|
+
if (!el) return
|
|
303
|
+
const observer = new MutationObserver(() => {
|
|
304
|
+
window.scrollTo({ top: document.body.scrollHeight, behavior: `smooth` })
|
|
305
|
+
})
|
|
306
|
+
observer.observe(el, {
|
|
307
|
+
childList: true,
|
|
308
|
+
subtree: true,
|
|
309
|
+
characterData: true,
|
|
310
|
+
})
|
|
311
|
+
return () => observer.disconnect()
|
|
312
|
+
}, [urls])
|
|
313
|
+
|
|
314
|
+
const handleAnalyze = async () => {
|
|
315
|
+
if (!question.trim()) return
|
|
316
|
+
setLoading(true)
|
|
317
|
+
try {
|
|
318
|
+
const res = await fetch(`/api/analyze`, {
|
|
319
|
+
method: `POST`,
|
|
320
|
+
headers: { 'Content-Type': `application/json` },
|
|
321
|
+
body: JSON.stringify({ question }),
|
|
322
|
+
})
|
|
323
|
+
const data = (await res.json()) as {
|
|
324
|
+
entityUrl: string
|
|
325
|
+
optimistUrl: string
|
|
326
|
+
criticUrl: string
|
|
327
|
+
}
|
|
328
|
+
const id = data.entityUrl.split(`/`).pop()!
|
|
329
|
+
window.location.hash = id
|
|
330
|
+
setUrls(data)
|
|
331
|
+
} catch (err) {
|
|
332
|
+
console.error(`Analyze failed:`, err)
|
|
333
|
+
} finally {
|
|
334
|
+
setLoading(false)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<>
|
|
340
|
+
<style>{`@keyframes blink { 50% { opacity: 0; } }`}</style>
|
|
341
|
+
<Box maxWidth="900px" mx="auto" p="5">
|
|
342
|
+
<Flex justify="between" align="center" mb="1">
|
|
343
|
+
<Heading size="6">Perspectives Analyzer</Heading>
|
|
344
|
+
<ThemeToggle />
|
|
345
|
+
</Flex>
|
|
346
|
+
<Text size="2" color="gray" mb="5" as="p">
|
|
347
|
+
Ask a question and get two perspectives — an optimist and a critic —
|
|
348
|
+
then a balanced analysis.
|
|
349
|
+
</Text>
|
|
350
|
+
|
|
351
|
+
<Flex gap="2" mb="5">
|
|
352
|
+
<Box flexGrow="1">
|
|
353
|
+
<TextField.Root
|
|
354
|
+
size="3"
|
|
355
|
+
placeholder="Enter a question to analyze..."
|
|
356
|
+
value={question}
|
|
357
|
+
onChange={(e) => setQuestion(e.target.value)}
|
|
358
|
+
onKeyDown={(e) => e.key === `Enter` && handleAnalyze()}
|
|
359
|
+
/>
|
|
360
|
+
</Box>
|
|
361
|
+
<Button
|
|
362
|
+
size="3"
|
|
363
|
+
onClick={handleAnalyze}
|
|
364
|
+
disabled={loading || !question.trim()}
|
|
365
|
+
>
|
|
366
|
+
{loading ? `Analyzing...` : `Analyze`}
|
|
367
|
+
</Button>
|
|
368
|
+
</Flex>
|
|
369
|
+
|
|
370
|
+
<Flex direction="column" gap="3">
|
|
371
|
+
{urls && analyserIntro.length === 0 && (
|
|
372
|
+
<Text color="gray" size="2">
|
|
373
|
+
Waiting for agents to respond...
|
|
374
|
+
</Text>
|
|
375
|
+
)}
|
|
376
|
+
|
|
377
|
+
{/* Analyser intro — full width */}
|
|
378
|
+
{analyserIntro.map((msg, i) => (
|
|
379
|
+
<MessageBubble
|
|
380
|
+
key={`analyser-intro-${i}`}
|
|
381
|
+
msg={hasWorkerMessages ? { ...msg, isStreaming: false } : msg}
|
|
382
|
+
/>
|
|
383
|
+
))}
|
|
384
|
+
|
|
385
|
+
{/* Workers — single column until both respond, then side-by-side */}
|
|
386
|
+
{hasWorkerMessages && !hasBothWorkers && (
|
|
387
|
+
<Flex direction="column" gap="3">
|
|
388
|
+
{optimistMessages.map((msg, i) => (
|
|
389
|
+
<MessageBubble key={`optimist-${i}`} msg={msg} />
|
|
390
|
+
))}
|
|
391
|
+
{criticMessages.map((msg, i) => (
|
|
392
|
+
<MessageBubble key={`critic-${i}`} msg={msg} />
|
|
393
|
+
))}
|
|
394
|
+
</Flex>
|
|
395
|
+
)}
|
|
396
|
+
{hasBothWorkers && (
|
|
397
|
+
<Flex gap="3">
|
|
398
|
+
<Flex direction="column" gap="3" style={{ flex: 1, minWidth: 0 }}>
|
|
399
|
+
{optimistMessages.map((msg, i) => (
|
|
400
|
+
<MessageBubble key={`optimist-${i}`} msg={msg} />
|
|
401
|
+
))}
|
|
402
|
+
</Flex>
|
|
403
|
+
<Flex direction="column" gap="3" style={{ flex: 1, minWidth: 0 }}>
|
|
404
|
+
{criticMessages.map((msg, i) => (
|
|
405
|
+
<MessageBubble key={`critic-${i}`} msg={msg} />
|
|
406
|
+
))}
|
|
407
|
+
</Flex>
|
|
408
|
+
</Flex>
|
|
409
|
+
)}
|
|
410
|
+
|
|
411
|
+
{/* Analyser synthesis — full width */}
|
|
412
|
+
{analyserSynthesis.map((msg, i) => (
|
|
413
|
+
<MessageBubble key={`analyser-synth-${i}`} msg={msg} />
|
|
414
|
+
))}
|
|
415
|
+
|
|
416
|
+
<div ref={bottomRef} />
|
|
417
|
+
</Flex>
|
|
418
|
+
</Box>
|
|
419
|
+
</>
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function ThemedApp() {
|
|
424
|
+
const { darkMode } = useDarkModeContext()
|
|
425
|
+
return (
|
|
426
|
+
<Theme
|
|
427
|
+
appearance={darkMode ? `dark` : `light`}
|
|
428
|
+
accentColor="blue"
|
|
429
|
+
radius="medium"
|
|
430
|
+
>
|
|
431
|
+
<App />
|
|
432
|
+
</Theme>
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
createRoot(document.getElementById(`root`)!).render(
|
|
437
|
+
<DarkModeProvider>
|
|
438
|
+
<ThemedApp />
|
|
439
|
+
</DarkModeProvider>
|
|
440
|
+
)
|