waypoi 0.0.0

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.
Files changed (260) hide show
  1. package/.github/instructions/ui.instructions.md +42 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +71 -0
  4. package/.github/workflows/release.yml +48 -0
  5. package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
  6. package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
  7. package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
  8. package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
  10. package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
  11. package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
  12. package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
  13. package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
  14. package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
  15. package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
  16. package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
  17. package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
  18. package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
  19. package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
  20. package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
  21. package/AGENTS.md +48 -0
  22. package/CHANGELOG.md +131 -0
  23. package/README.md +552 -0
  24. package/assets/agent-mode.png +0 -0
  25. package/assets/categorize.png +0 -0
  26. package/assets/dashboard.png +0 -0
  27. package/assets/endpoint-proxy.png +0 -0
  28. package/assets/icon.png +0 -0
  29. package/assets/mcp-generate-image.png +0 -0
  30. package/assets/mcp-understand-image.png +0 -0
  31. package/assets/peek-token-flow.png +0 -0
  32. package/assets/playground.png +0 -0
  33. package/assets/sankey.png +0 -0
  34. package/cli/index.ts +2805 -0
  35. package/cli/legacyRewrite.ts +108 -0
  36. package/cli/modelRef.ts +24 -0
  37. package/dist/cli/index.js +2536 -0
  38. package/dist/cli/legacyRewrite.js +92 -0
  39. package/dist/cli/modelRef.js +20 -0
  40. package/dist/src/benchmark/artifacts.js +131 -0
  41. package/dist/src/benchmark/capabilityClassifier.js +81 -0
  42. package/dist/src/benchmark/capabilityStore.js +144 -0
  43. package/dist/src/benchmark/config.js +238 -0
  44. package/dist/src/benchmark/gates.js +118 -0
  45. package/dist/src/benchmark/jobs.js +252 -0
  46. package/dist/src/benchmark/runner.js +1847 -0
  47. package/dist/src/benchmark/schema.js +353 -0
  48. package/dist/src/benchmark/suites.js +314 -0
  49. package/dist/src/benchmark/tinyQaDataset.js +422 -0
  50. package/dist/src/benchmark/types.js +25 -0
  51. package/dist/src/config.js +47 -0
  52. package/dist/src/index.js +178 -0
  53. package/dist/src/mcp/client.js +215 -0
  54. package/dist/src/mcp/discovery.js +226 -0
  55. package/dist/src/mcp/policy.js +65 -0
  56. package/dist/src/mcp/registry.js +129 -0
  57. package/dist/src/mcp/service.js +460 -0
  58. package/dist/src/middleware/auth.js +179 -0
  59. package/dist/src/middleware/requestCapture.js +192 -0
  60. package/dist/src/middleware/requestStats.js +118 -0
  61. package/dist/src/pools/builder.js +132 -0
  62. package/dist/src/pools/repository.js +69 -0
  63. package/dist/src/pools/scheduler.js +360 -0
  64. package/dist/src/pools/types.js +2 -0
  65. package/dist/src/protocols/adapters/dashscope.js +267 -0
  66. package/dist/src/protocols/adapters/inferenceV2.js +346 -0
  67. package/dist/src/protocols/adapters/openai.js +27 -0
  68. package/dist/src/protocols/registry.js +99 -0
  69. package/dist/src/protocols/types.js +2 -0
  70. package/dist/src/providers/health.js +153 -0
  71. package/dist/src/providers/importer.js +289 -0
  72. package/dist/src/providers/modelRegistry.js +313 -0
  73. package/dist/src/providers/repository.js +361 -0
  74. package/dist/src/providers/types.js +2 -0
  75. package/dist/src/routes/admin.js +531 -0
  76. package/dist/src/routes/audio.js +295 -0
  77. package/dist/src/routes/chat.js +240 -0
  78. package/dist/src/routes/embeddings.js +157 -0
  79. package/dist/src/routes/images.js +288 -0
  80. package/dist/src/routes/mcp.js +256 -0
  81. package/dist/src/routes/mcpService.js +100 -0
  82. package/dist/src/routes/models.js +48 -0
  83. package/dist/src/routes/responses.js +711 -0
  84. package/dist/src/routes/sessions.js +450 -0
  85. package/dist/src/routes/stats.js +270 -0
  86. package/dist/src/routes/ui.js +97 -0
  87. package/dist/src/routes/videos.js +107 -0
  88. package/dist/src/routing/router.js +338 -0
  89. package/dist/src/services/imageGeneration.js +280 -0
  90. package/dist/src/services/imageUnderstanding.js +352 -0
  91. package/dist/src/services/videoGeneration.js +79 -0
  92. package/dist/src/storage/captureRepository.js +1591 -0
  93. package/dist/src/storage/files.js +157 -0
  94. package/dist/src/storage/imageCache.js +346 -0
  95. package/dist/src/storage/repositories.js +388 -0
  96. package/dist/src/storage/sessionRepository.js +370 -0
  97. package/dist/src/storage/statsRepository.js +204 -0
  98. package/dist/src/transport/httpClient.js +126 -0
  99. package/dist/src/types.js +2 -0
  100. package/dist/src/utils/messageMedia.js +285 -0
  101. package/dist/src/utils/modelCapabilities.js +108 -0
  102. package/dist/src/utils/modelDiscovery.js +170 -0
  103. package/dist/src/version.js +5 -0
  104. package/dist/src/workers/captureRetention.js +25 -0
  105. package/dist/src/workers/configWatcher.js +91 -0
  106. package/dist/src/workers/healthChecker.js +21 -0
  107. package/dist/src/workers/statsRotation.js +41 -0
  108. package/docs/LLM/output_schema.md +312 -0
  109. package/docs/benchmark.md +208 -0
  110. package/docs/mcp-guidelines.md +125 -0
  111. package/docs/mcp-service.md +178 -0
  112. package/docs/opencode.md +86 -0
  113. package/docs/providers.md +79 -0
  114. package/examples/benchmark.config.yaml +28 -0
  115. package/examples/providers/alibaba-dashscope.yaml +88 -0
  116. package/examples/providers/alibaba-llm.yaml +64 -0
  117. package/examples/providers/alibaba-registry.yaml +7 -0
  118. package/examples/providers/inference-v2-ray.yaml +29 -0
  119. package/examples/scenarios/assets/omni-call-sample.wav +0 -0
  120. package/examples/scenarios/custom.jsonl +5 -0
  121. package/examples/scenarios/custom.yaml +40 -0
  122. package/model-form-v2.png +0 -0
  123. package/package.json +66 -0
  124. package/provider-form-v2.png +0 -0
  125. package/provider-form.png +0 -0
  126. package/scripts/manual-test.sh +11 -0
  127. package/scripts/version-from-git.js +23 -0
  128. package/src/benchmark/artifacts.ts +149 -0
  129. package/src/benchmark/capabilityClassifier.ts +99 -0
  130. package/src/benchmark/capabilityStore.ts +174 -0
  131. package/src/benchmark/config.ts +337 -0
  132. package/src/benchmark/gates.ts +164 -0
  133. package/src/benchmark/jobs.ts +312 -0
  134. package/src/benchmark/runner.ts +2519 -0
  135. package/src/benchmark/schema.ts +443 -0
  136. package/src/benchmark/suites.ts +323 -0
  137. package/src/benchmark/tinyQaDataset.ts +428 -0
  138. package/src/benchmark/types.ts +442 -0
  139. package/src/config.ts +44 -0
  140. package/src/index.ts +195 -0
  141. package/src/mcp/client.ts +305 -0
  142. package/src/mcp/discovery.ts +266 -0
  143. package/src/mcp/policy.ts +105 -0
  144. package/src/mcp/registry.ts +164 -0
  145. package/src/mcp/service.ts +611 -0
  146. package/src/middleware/auth.ts +251 -0
  147. package/src/middleware/requestCapture.ts +245 -0
  148. package/src/middleware/requestStats.ts +163 -0
  149. package/src/pools/builder.ts +159 -0
  150. package/src/pools/repository.ts +71 -0
  151. package/src/pools/scheduler.ts +425 -0
  152. package/src/pools/types.ts +117 -0
  153. package/src/protocols/adapters/dashscope.ts +335 -0
  154. package/src/protocols/adapters/inferenceV2.ts +428 -0
  155. package/src/protocols/adapters/openai.ts +32 -0
  156. package/src/protocols/registry.ts +117 -0
  157. package/src/protocols/types.ts +81 -0
  158. package/src/providers/health.ts +207 -0
  159. package/src/providers/importer.ts +402 -0
  160. package/src/providers/modelRegistry.ts +415 -0
  161. package/src/providers/repository.ts +439 -0
  162. package/src/providers/types.ts +113 -0
  163. package/src/routes/admin.ts +666 -0
  164. package/src/routes/audio.ts +372 -0
  165. package/src/routes/chat.ts +301 -0
  166. package/src/routes/embeddings.ts +197 -0
  167. package/src/routes/images.ts +356 -0
  168. package/src/routes/mcp.ts +320 -0
  169. package/src/routes/mcpService.ts +114 -0
  170. package/src/routes/models.ts +50 -0
  171. package/src/routes/responses.ts +872 -0
  172. package/src/routes/sessions.ts +558 -0
  173. package/src/routes/stats.ts +312 -0
  174. package/src/routes/ui.ts +96 -0
  175. package/src/routes/videos.ts +132 -0
  176. package/src/routing/router.ts +501 -0
  177. package/src/services/imageGeneration.ts +396 -0
  178. package/src/services/imageUnderstanding.ts +449 -0
  179. package/src/services/videoGeneration.ts +127 -0
  180. package/src/storage/captureRepository.ts +1835 -0
  181. package/src/storage/files.ts +178 -0
  182. package/src/storage/imageCache.ts +405 -0
  183. package/src/storage/repositories.ts +494 -0
  184. package/src/storage/sessionRepository.ts +419 -0
  185. package/src/storage/statsRepository.ts +238 -0
  186. package/src/transport/httpClient.ts +145 -0
  187. package/src/types.ts +322 -0
  188. package/src/utils/messageMedia.ts +293 -0
  189. package/src/utils/modelCapabilities.ts +161 -0
  190. package/src/utils/modelDiscovery.ts +203 -0
  191. package/src/workers/captureRetention.ts +25 -0
  192. package/src/workers/configWatcher.ts +115 -0
  193. package/src/workers/healthChecker.ts +22 -0
  194. package/src/workers/statsRotation.ts +49 -0
  195. package/tests/benchmarkAdminRoutes.test.ts +82 -0
  196. package/tests/benchmarkBasics.test.ts +116 -0
  197. package/tests/captureAdminRoutes.test.ts +420 -0
  198. package/tests/captureRepository.test.ts +797 -0
  199. package/tests/cliLegacyRewrite.test.ts +45 -0
  200. package/tests/imageGeneration.service.test.ts +107 -0
  201. package/tests/imageUnderstanding.service.test.ts +123 -0
  202. package/tests/mcpPolicy.test.ts +105 -0
  203. package/tests/mcpService.test.ts +1245 -0
  204. package/tests/modelRef.test.ts +23 -0
  205. package/tests/modelsRoutes.test.ts +154 -0
  206. package/tests/sessionMediaCache.test.ts +167 -0
  207. package/tests/statsRoutes.test.ts +323 -0
  208. package/tsconfig.json +15 -0
  209. package/ui/index.html +16 -0
  210. package/ui/package-lock.json +8521 -0
  211. package/ui/package.json +52 -0
  212. package/ui/postcss.config.js +6 -0
  213. package/ui/public/assets/apple-touch-icon.png +0 -0
  214. package/ui/public/assets/favicon-16.png +0 -0
  215. package/ui/public/assets/favicon-32.png +0 -0
  216. package/ui/public/assets/icon-192.png +0 -0
  217. package/ui/public/assets/icon-512.png +0 -0
  218. package/ui/src/App.tsx +27 -0
  219. package/ui/src/api/client.ts +1503 -0
  220. package/ui/src/components/EndpointUsageGuide.tsx +361 -0
  221. package/ui/src/components/Layout.tsx +124 -0
  222. package/ui/src/components/MessageContent.tsx +365 -0
  223. package/ui/src/components/ToolCallMessage.tsx +179 -0
  224. package/ui/src/components/ToolPicker.tsx +442 -0
  225. package/ui/src/components/messageContentParser.test.ts +41 -0
  226. package/ui/src/components/messageContentParser.ts +73 -0
  227. package/ui/src/components/thinkingPreview.test.ts +27 -0
  228. package/ui/src/components/thinkingPreview.ts +15 -0
  229. package/ui/src/components/toMermaidSankey.test.ts +78 -0
  230. package/ui/src/components/toMermaidSankey.ts +56 -0
  231. package/ui/src/components/ui/button.tsx +58 -0
  232. package/ui/src/components/ui/input.tsx +21 -0
  233. package/ui/src/components/ui/textarea.tsx +21 -0
  234. package/ui/src/lib/utils.ts +6 -0
  235. package/ui/src/main.tsx +9 -0
  236. package/ui/src/pages/AgentPlayground.tsx +2010 -0
  237. package/ui/src/pages/Benchmark.tsx +988 -0
  238. package/ui/src/pages/Dashboard.tsx +581 -0
  239. package/ui/src/pages/Peek.tsx +962 -0
  240. package/ui/src/pages/Settings.tsx +2013 -0
  241. package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
  242. package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
  243. package/ui/src/pages/agentThinkingContent.test.ts +50 -0
  244. package/ui/src/pages/agentThinkingContent.ts +57 -0
  245. package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
  246. package/ui/src/pages/dashboardTokenUsage.ts +36 -0
  247. package/ui/src/pages/imageUpload.test.ts +39 -0
  248. package/ui/src/pages/imageUpload.ts +71 -0
  249. package/ui/src/pages/peekFilters.test.ts +29 -0
  250. package/ui/src/pages/peekFilters.ts +13 -0
  251. package/ui/src/pages/peekMedia.test.ts +58 -0
  252. package/ui/src/pages/peekMedia.ts +148 -0
  253. package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
  254. package/ui/src/pages/sessionAutoTitle.ts +106 -0
  255. package/ui/src/stores/settings.ts +58 -0
  256. package/ui/src/styles/globals.css +223 -0
  257. package/ui/src/vite-env.d.ts +8 -0
  258. package/ui/tailwind.config.js +106 -0
  259. package/ui/tsconfig.json +32 -0
  260. package/ui/vite.config.ts +37 -0
@@ -0,0 +1,365 @@
1
+ import { useState, useEffect, useRef, memo } from 'react'
2
+ import ReactMarkdown from 'react-markdown'
3
+ import remarkGfm from 'remark-gfm'
4
+ import mermaid from 'mermaid'
5
+ import { Copy, Check, ChevronDown, ChevronUp, Brain } from 'lucide-react'
6
+ import { cn } from '@/lib/utils'
7
+ import { parseMessageContent } from './messageContentParser'
8
+ import { getLatestThinkingLines, hasUnclosedThinkingTag } from './thinkingPreview'
9
+
10
+ // Initialize mermaid with dark theme
11
+ mermaid.initialize({
12
+ startOnLoad: false,
13
+ theme: 'dark',
14
+ securityLevel: 'loose',
15
+ fontFamily: 'JetBrains Mono, monospace',
16
+ })
17
+
18
+ // Clean up any mermaid error elements that might have been appended to body
19
+ function cleanupMermaidErrors() {
20
+ // Remove orphaned mermaid error SVGs from body
21
+ document.querySelectorAll('body > svg[id^="mermaid-"]').forEach(el => el.remove())
22
+ document.querySelectorAll('body > #d').forEach(el => el.remove())
23
+ }
24
+
25
+ interface MessageContentProps {
26
+ content: string
27
+ className?: string
28
+ }
29
+
30
+ // Mermaid diagram component with debounced rendering
31
+ const MermaidDiagram = memo(({ code }: { code: string }) => {
32
+ const containerRef = useRef<HTMLDivElement>(null)
33
+ const [svg, setSvg] = useState<string>('')
34
+ const [error, setError] = useState<string | null>(null)
35
+ const [isRendering, setIsRendering] = useState(true)
36
+ const renderTimeoutRef = useRef<NodeJS.Timeout | null>(null)
37
+
38
+ useEffect(() => {
39
+ // Clear any pending render
40
+ if (renderTimeoutRef.current) {
41
+ clearTimeout(renderTimeoutRef.current)
42
+ }
43
+
44
+ setIsRendering(true)
45
+
46
+ // Debounce rendering to avoid rendering during streaming
47
+ renderTimeoutRef.current = setTimeout(async () => {
48
+ try {
49
+ // Clean up any orphaned error elements first
50
+ cleanupMermaidErrors()
51
+
52
+ const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`
53
+ const { svg } = await mermaid.render(id, code)
54
+ setSvg(svg)
55
+ setError(null)
56
+ } catch (err) {
57
+ // Don't show error during streaming - might be incomplete code
58
+ setError((err as Error).message)
59
+ setSvg('')
60
+ } finally {
61
+ setIsRendering(false)
62
+ // Clean up again after render attempt
63
+ cleanupMermaidErrors()
64
+ }
65
+ }, 300) // 300ms debounce
66
+
67
+ return () => {
68
+ if (renderTimeoutRef.current) {
69
+ clearTimeout(renderTimeoutRef.current)
70
+ }
71
+ cleanupMermaidErrors()
72
+ }
73
+ }, [code])
74
+
75
+ if (isRendering) {
76
+ return (
77
+ <div className="my-4 p-4 bg-secondary/50 rounded-lg flex items-center gap-2 text-muted-foreground">
78
+ <div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
79
+ <span className="text-sm font-mono">Rendering diagram...</span>
80
+ </div>
81
+ )
82
+ }
83
+
84
+ if (error) {
85
+ return (
86
+ <div className="bg-destructive/10 border border-destructive/30 rounded-lg p-4 my-2">
87
+ <p className="text-destructive text-sm font-mono">Mermaid Error: {error}</p>
88
+ <pre className="mt-2 text-xs text-muted-foreground overflow-x-auto">{code}</pre>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ return (
94
+ <div
95
+ ref={containerRef}
96
+ className="my-4 p-4 bg-secondary/50 rounded-lg overflow-x-auto"
97
+ dangerouslySetInnerHTML={{ __html: svg }}
98
+ />
99
+ )
100
+ })
101
+ MermaidDiagram.displayName = 'MermaidDiagram'
102
+
103
+ // Thinking block component
104
+ const ThinkingBlock = memo(({ content, isLive }: { content: string; isLive: boolean }) => {
105
+ const PREVIEW_LINES = 5
106
+ const PREVIEW_LINE_HEIGHT_PX = 20
107
+ const [expanded, setExpanded] = useState(false)
108
+ const latestLines = getLatestThinkingLines(content)
109
+ const livePreview = latestLines.join('\n')
110
+ const displayContent = expanded || !isLive ? content.trim() : livePreview
111
+ const title = 'Thinking process'
112
+
113
+ return (
114
+ <div className="my-3 border border-border/50 rounded-lg bg-secondary/30 overflow-hidden">
115
+ <button
116
+ onClick={() => setExpanded(!expanded)}
117
+ className="w-full px-3 py-2 flex items-center gap-2 text-left hover:bg-secondary/50 transition-colors"
118
+ >
119
+ <Brain className="w-4 h-4 text-muted-foreground" />
120
+ <span className="text-xs font-mono text-muted-foreground flex-1 truncate" title={title}>
121
+ {title}
122
+ </span>
123
+ {isLive && (
124
+ <span className="inline-block h-2 w-2 rounded-full bg-primary animate-pulse" aria-hidden="true" />
125
+ )}
126
+ {expanded ? (
127
+ <ChevronUp className="w-4 h-4 text-muted-foreground" />
128
+ ) : (
129
+ <ChevronDown className="w-4 h-4 text-muted-foreground" />
130
+ )}
131
+ </button>
132
+ <div
133
+ className={cn(
134
+ 'overflow-hidden transition-all duration-300 ease-in-out border-t border-border/30',
135
+ expanded ? 'max-h-[420px]' : ''
136
+ )}
137
+ style={!expanded ? { height: `${PREVIEW_LINES * PREVIEW_LINE_HEIGHT_PX + 20}px` } : undefined}
138
+ >
139
+ <div className={cn(
140
+ 'px-3 pb-3 pt-2 overflow-x-auto',
141
+ expanded ? 'max-h-96 overflow-y-auto' : 'overflow-y-hidden'
142
+ )}>
143
+ <pre className={cn(
144
+ 'text-xs text-muted-foreground font-mono leading-5',
145
+ expanded ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'
146
+ )}>
147
+ {displayContent || 'Waiting for reasoning details...'}
148
+ </pre>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ )
153
+ })
154
+ ThinkingBlock.displayName = 'ThinkingBlock'
155
+
156
+ // Copy button component
157
+ const CopyButton = ({ text, className }: { text: string; className?: string }) => {
158
+ const [copied, setCopied] = useState(false)
159
+
160
+ const handleCopy = async () => {
161
+ try {
162
+ await navigator.clipboard.writeText(text)
163
+ setCopied(true)
164
+ setTimeout(() => setCopied(false), 2000)
165
+ } catch (err) {
166
+ console.error('Failed to copy:', err)
167
+ }
168
+ }
169
+
170
+ return (
171
+ <button
172
+ onClick={handleCopy}
173
+ className={cn(
174
+ 'p-1.5 rounded hover:bg-secondary transition-colors',
175
+ className
176
+ )}
177
+ title="Copy raw content"
178
+ >
179
+ {copied ? (
180
+ <Check className="w-3.5 h-3.5 text-green-500" />
181
+ ) : (
182
+ <Copy className="w-3.5 h-3.5 text-muted-foreground" />
183
+ )}
184
+ </button>
185
+ )
186
+ }
187
+
188
+ // Code block component with copy and mermaid support
189
+ const CodeBlock = ({
190
+ className,
191
+ children,
192
+ ...props
193
+ }: React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }) => {
194
+ const [copied, setCopied] = useState(false)
195
+ const code = String(children).replace(/\n$/, '')
196
+ const match = /language-(\w+)/.exec(className || '')
197
+ const language = match ? match[1] : ''
198
+
199
+ const handleCopy = async () => {
200
+ try {
201
+ await navigator.clipboard.writeText(code)
202
+ setCopied(true)
203
+ setTimeout(() => setCopied(false), 2000)
204
+ } catch (err) {
205
+ console.error('Failed to copy:', err)
206
+ }
207
+ }
208
+
209
+ // Render mermaid diagrams
210
+ if (language === 'mermaid') {
211
+ return <MermaidDiagram code={code} />
212
+ }
213
+
214
+ // Render SVG code blocks as actual SVG
215
+ if (language === 'svg') {
216
+ // Validate it looks like SVG before rendering
217
+ const trimmedCode = code.trim()
218
+ if (trimmedCode.startsWith('<svg') || trimmedCode.startsWith('<?xml')) {
219
+ return (
220
+ <div className="relative my-4 group">
221
+ <div className="absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity">
222
+ <button
223
+ onClick={handleCopy}
224
+ className="p-1.5 rounded bg-secondary/80 hover:bg-secondary transition-colors"
225
+ title="Copy SVG"
226
+ >
227
+ {copied ? (
228
+ <Check className="w-3.5 h-3.5 text-green-500" />
229
+ ) : (
230
+ <Copy className="w-3.5 h-3.5 text-muted-foreground" />
231
+ )}
232
+ </button>
233
+ </div>
234
+ <div
235
+ className="p-4 bg-secondary/50 rounded-lg overflow-x-auto flex items-center justify-center"
236
+ dangerouslySetInnerHTML={{ __html: code }}
237
+ />
238
+ </div>
239
+ )
240
+ }
241
+ }
242
+
243
+ // Inline code
244
+ if (!match) {
245
+ return (
246
+ <code className="bg-secondary px-1.5 py-0.5 rounded text-sm font-mono" {...props}>
247
+ {children}
248
+ </code>
249
+ )
250
+ }
251
+
252
+ // Code block with copy button
253
+ return (
254
+ <div className="relative my-3 group">
255
+ <div className="absolute top-2 right-2 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
256
+ {language && (
257
+ <span className="text-xs text-muted-foreground font-mono">{language}</span>
258
+ )}
259
+ <button
260
+ onClick={handleCopy}
261
+ className="p-1.5 rounded bg-secondary/80 hover:bg-secondary transition-colors"
262
+ title="Copy code"
263
+ >
264
+ {copied ? (
265
+ <Check className="w-3.5 h-3.5 text-green-500" />
266
+ ) : (
267
+ <Copy className="w-3.5 h-3.5 text-muted-foreground" />
268
+ )}
269
+ </button>
270
+ </div>
271
+ <pre className="bg-secondary/50 border border-border rounded-lg p-4 overflow-x-auto">
272
+ <code className={cn('text-sm font-mono', className)} {...props}>
273
+ {children}
274
+ </code>
275
+ </pre>
276
+ </div>
277
+ )
278
+ }
279
+
280
+ export const MessageContent = memo(function MessageContent({ content, className }: MessageContentProps) {
281
+ const parts = parseMessageContent(content)
282
+ const hasLiveThinking = hasUnclosedThinkingTag(content)
283
+ const thinkingParts = parts.filter((part) => part.type === 'thinking').length
284
+ let seenThinkingParts = 0
285
+
286
+ return (
287
+ <div className={cn('relative group', className)}>
288
+ {/* Copy raw button */}
289
+ <div className="absolute -top-1 -right-1 opacity-0 group-hover:opacity-100 transition-opacity">
290
+ <CopyButton text={content} />
291
+ </div>
292
+
293
+ {/* Render content parts */}
294
+ <div className="prose prose-sm prose-invert max-w-none">
295
+ {parts.map((part, index) => {
296
+ if (part.type === 'thinking') {
297
+ const thinkingPartIndex = seenThinkingParts
298
+ seenThinkingParts += 1
299
+ return (
300
+ <ThinkingBlock
301
+ key={index}
302
+ content={part.content}
303
+ isLive={hasLiveThinking && thinkingPartIndex === thinkingParts - 1}
304
+ />
305
+ )
306
+ }
307
+
308
+ return (
309
+ <ReactMarkdown
310
+ key={index}
311
+ remarkPlugins={[remarkGfm]}
312
+ components={{
313
+ code: CodeBlock,
314
+ // Style other elements
315
+ p: ({ children }) => <p className="mb-3 last:mb-0">{children}</p>,
316
+ ul: ({ children }) => <ul className="list-disc list-inside mb-3 space-y-1">{children}</ul>,
317
+ ol: ({ children }) => <ol className="list-decimal list-inside mb-3 space-y-1">{children}</ol>,
318
+ li: ({ children }) => <li className="text-sm">{children}</li>,
319
+ h1: ({ children }) => <h1 className="text-xl font-bold mb-3 mt-4">{children}</h1>,
320
+ h2: ({ children }) => <h2 className="text-lg font-bold mb-2 mt-3">{children}</h2>,
321
+ h3: ({ children }) => <h3 className="text-base font-bold mb-2 mt-3">{children}</h3>,
322
+ blockquote: ({ children }) => (
323
+ <blockquote className="border-l-2 border-primary/50 pl-4 my-3 italic text-muted-foreground">
324
+ {children}
325
+ </blockquote>
326
+ ),
327
+ a: ({ href, children }) => (
328
+ <a
329
+ href={href}
330
+ target="_blank"
331
+ rel="noopener noreferrer"
332
+ className="text-primary hover:underline"
333
+ >
334
+ {children}
335
+ </a>
336
+ ),
337
+ table: ({ children }) => (
338
+ <div className="overflow-x-auto my-3">
339
+ <table className="min-w-full border border-border rounded">
340
+ {children}
341
+ </table>
342
+ </div>
343
+ ),
344
+ th: ({ children }) => (
345
+ <th className="bg-secondary px-3 py-2 text-left text-sm font-semibold border-b border-border">
346
+ {children}
347
+ </th>
348
+ ),
349
+ td: ({ children }) => (
350
+ <td className="px-3 py-2 text-sm border-b border-border/50">
351
+ {children}
352
+ </td>
353
+ ),
354
+ hr: () => <hr className="my-4 border-border" />,
355
+ }}
356
+ >
357
+ {part.content}
358
+ </ReactMarkdown>
359
+ )
360
+ })}
361
+ </div>
362
+ </div>
363
+ )
364
+ })
365
+ MessageContent.displayName = 'MessageContent'
@@ -0,0 +1,179 @@
1
+ import { useState } from 'react'
2
+ import { Wrench, ChevronDown, ChevronRight, Check, X, Loader2, Code2 } from 'lucide-react'
3
+ import { cn } from '@/lib/utils'
4
+
5
+ interface ToolCall {
6
+ id: string
7
+ name: string
8
+ arguments: string
9
+ status: 'pending' | 'executing' | 'success' | 'error'
10
+ result?: string
11
+ error?: string
12
+ }
13
+
14
+ interface ToolCallMessageProps {
15
+ toolCalls: ToolCall[]
16
+ className?: string
17
+ }
18
+
19
+ export function ToolCallMessage({ toolCalls, className }: ToolCallMessageProps) {
20
+ return (
21
+ <div className={cn('space-y-2', className)}>
22
+ {toolCalls.map((call) => (
23
+ <ToolCallCard key={call.id} call={call} />
24
+ ))}
25
+ </div>
26
+ )
27
+ }
28
+
29
+ function ToolCallCard({ call }: { call: ToolCall }) {
30
+ const [expanded, setExpanded] = useState(call.status === 'error')
31
+ const [showArgs, setShowArgs] = useState(false)
32
+
33
+ // Parse arguments for display
34
+ let parsedArgs: Record<string, unknown> = {}
35
+ try {
36
+ parsedArgs = JSON.parse(call.arguments)
37
+ } catch {
38
+ // Keep empty object
39
+ }
40
+
41
+ const argEntries = Object.entries(parsedArgs)
42
+
43
+ return (
44
+ <div
45
+ className={cn(
46
+ 'rounded-lg border overflow-hidden transition-all duration-200',
47
+ call.status === 'executing'
48
+ ? 'border-amber-500/50 bg-amber-500/5'
49
+ : call.status === 'success'
50
+ ? 'border-emerald-500/30 bg-emerald-500/5'
51
+ : call.status === 'error'
52
+ ? 'border-red-500/30 bg-red-500/5'
53
+ : 'border-border bg-secondary/30'
54
+ )}
55
+ >
56
+ {/* Header */}
57
+ <button
58
+ onClick={() => setExpanded(!expanded)}
59
+ className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-secondary/50 transition-colors"
60
+ >
61
+ {/* Status Icon */}
62
+ <div
63
+ className={cn(
64
+ 'w-6 h-6 rounded flex items-center justify-center shrink-0',
65
+ call.status === 'executing'
66
+ ? 'bg-amber-500/20'
67
+ : call.status === 'success'
68
+ ? 'bg-emerald-500/20'
69
+ : call.status === 'error'
70
+ ? 'bg-red-500/20'
71
+ : 'bg-secondary'
72
+ )}
73
+ >
74
+ {call.status === 'executing' ? (
75
+ <Loader2 className="w-3.5 h-3.5 text-amber-500 animate-spin" />
76
+ ) : call.status === 'success' ? (
77
+ <Check className="w-3.5 h-3.5 text-emerald-500" />
78
+ ) : call.status === 'error' ? (
79
+ <X className="w-3.5 h-3.5 text-red-500" />
80
+ ) : (
81
+ <Wrench className="w-3.5 h-3.5 text-muted-foreground" />
82
+ )}
83
+ </div>
84
+
85
+ {/* Tool Name */}
86
+ <div className="flex-1 min-w-0">
87
+ <div className="flex items-center gap-2">
88
+ <span className="font-mono text-sm font-medium truncate">{call.name}</span>
89
+ {argEntries.length > 0 && (
90
+ <span className="text-2xs text-muted-foreground/70">
91
+ ({argEntries.length} arg{argEntries.length !== 1 ? 's' : ''})
92
+ </span>
93
+ )}
94
+ </div>
95
+ {call.status === 'executing' && (
96
+ <p className="text-2xs text-amber-500/80 font-mono">Executing...</p>
97
+ )}
98
+ </div>
99
+
100
+ {/* Expand Icon */}
101
+ {expanded ? (
102
+ <ChevronDown className="w-4 h-4 text-muted-foreground shrink-0" />
103
+ ) : (
104
+ <ChevronRight className="w-4 h-4 text-muted-foreground shrink-0" />
105
+ )}
106
+ </button>
107
+
108
+ {/* Expanded Content */}
109
+ {expanded && (
110
+ <div className="border-t border-border/50 animate-slide-in-bottom">
111
+ {/* Arguments */}
112
+ {argEntries.length > 0 && (
113
+ <div className="px-3 py-2 border-b border-border/30">
114
+ <button
115
+ onClick={() => setShowArgs(!showArgs)}
116
+ className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground transition-colors"
117
+ >
118
+ <Code2 className="w-3 h-3" />
119
+ <span>Arguments</span>
120
+ {showArgs ? (
121
+ <ChevronDown className="w-3 h-3" />
122
+ ) : (
123
+ <ChevronRight className="w-3 h-3" />
124
+ )}
125
+ </button>
126
+ {showArgs && (
127
+ <pre className="mt-2 text-2xs font-mono bg-background/50 rounded p-2 overflow-x-auto">
128
+ {JSON.stringify(parsedArgs, null, 2)}
129
+ </pre>
130
+ )}
131
+ {!showArgs && (
132
+ <div className="mt-1 flex flex-wrap gap-1">
133
+ {argEntries.slice(0, 3).map(([key, value]) => (
134
+ <span
135
+ key={key}
136
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-background/50 rounded text-2xs font-mono"
137
+ >
138
+ <span className="text-muted-foreground">{key}:</span>
139
+ <span className="truncate max-w-20">
140
+ {typeof value === 'string' ? value : JSON.stringify(value)}
141
+ </span>
142
+ </span>
143
+ ))}
144
+ {argEntries.length > 3 && (
145
+ <span className="text-2xs text-muted-foreground/70">
146
+ +{argEntries.length - 3} more
147
+ </span>
148
+ )}
149
+ </div>
150
+ )}
151
+ </div>
152
+ )}
153
+
154
+ {/* Result or Error */}
155
+ {(call.result || call.error) && (
156
+ <div className="px-3 py-2">
157
+ <p className="text-2xs text-muted-foreground mb-1">
158
+ {call.error ? 'Error' : 'Result'}
159
+ </p>
160
+ <div
161
+ className={cn(
162
+ 'text-xs font-mono whitespace-pre-wrap break-words max-h-40 overflow-y-auto rounded p-2',
163
+ call.error
164
+ ? 'bg-red-500/10 text-red-400'
165
+ : 'bg-background/50 text-foreground'
166
+ )}
167
+ >
168
+ {call.error || call.result}
169
+ </div>
170
+ </div>
171
+ )}
172
+ </div>
173
+ )}
174
+ </div>
175
+ )
176
+ }
177
+
178
+ // Export the ToolCall type for use in other components
179
+ export type { ToolCall }