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.
- package/.github/instructions/ui.instructions.md +42 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +71 -0
- package/.github/workflows/release.yml +48 -0
- package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
- package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
- package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
- package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
- package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
- package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +131 -0
- package/README.md +552 -0
- package/assets/agent-mode.png +0 -0
- package/assets/categorize.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/endpoint-proxy.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/mcp-generate-image.png +0 -0
- package/assets/mcp-understand-image.png +0 -0
- package/assets/peek-token-flow.png +0 -0
- package/assets/playground.png +0 -0
- package/assets/sankey.png +0 -0
- package/cli/index.ts +2805 -0
- package/cli/legacyRewrite.ts +108 -0
- package/cli/modelRef.ts +24 -0
- package/dist/cli/index.js +2536 -0
- package/dist/cli/legacyRewrite.js +92 -0
- package/dist/cli/modelRef.js +20 -0
- package/dist/src/benchmark/artifacts.js +131 -0
- package/dist/src/benchmark/capabilityClassifier.js +81 -0
- package/dist/src/benchmark/capabilityStore.js +144 -0
- package/dist/src/benchmark/config.js +238 -0
- package/dist/src/benchmark/gates.js +118 -0
- package/dist/src/benchmark/jobs.js +252 -0
- package/dist/src/benchmark/runner.js +1847 -0
- package/dist/src/benchmark/schema.js +353 -0
- package/dist/src/benchmark/suites.js +314 -0
- package/dist/src/benchmark/tinyQaDataset.js +422 -0
- package/dist/src/benchmark/types.js +25 -0
- package/dist/src/config.js +47 -0
- package/dist/src/index.js +178 -0
- package/dist/src/mcp/client.js +215 -0
- package/dist/src/mcp/discovery.js +226 -0
- package/dist/src/mcp/policy.js +65 -0
- package/dist/src/mcp/registry.js +129 -0
- package/dist/src/mcp/service.js +460 -0
- package/dist/src/middleware/auth.js +179 -0
- package/dist/src/middleware/requestCapture.js +192 -0
- package/dist/src/middleware/requestStats.js +118 -0
- package/dist/src/pools/builder.js +132 -0
- package/dist/src/pools/repository.js +69 -0
- package/dist/src/pools/scheduler.js +360 -0
- package/dist/src/pools/types.js +2 -0
- package/dist/src/protocols/adapters/dashscope.js +267 -0
- package/dist/src/protocols/adapters/inferenceV2.js +346 -0
- package/dist/src/protocols/adapters/openai.js +27 -0
- package/dist/src/protocols/registry.js +99 -0
- package/dist/src/protocols/types.js +2 -0
- package/dist/src/providers/health.js +153 -0
- package/dist/src/providers/importer.js +289 -0
- package/dist/src/providers/modelRegistry.js +313 -0
- package/dist/src/providers/repository.js +361 -0
- package/dist/src/providers/types.js +2 -0
- package/dist/src/routes/admin.js +531 -0
- package/dist/src/routes/audio.js +295 -0
- package/dist/src/routes/chat.js +240 -0
- package/dist/src/routes/embeddings.js +157 -0
- package/dist/src/routes/images.js +288 -0
- package/dist/src/routes/mcp.js +256 -0
- package/dist/src/routes/mcpService.js +100 -0
- package/dist/src/routes/models.js +48 -0
- package/dist/src/routes/responses.js +711 -0
- package/dist/src/routes/sessions.js +450 -0
- package/dist/src/routes/stats.js +270 -0
- package/dist/src/routes/ui.js +97 -0
- package/dist/src/routes/videos.js +107 -0
- package/dist/src/routing/router.js +338 -0
- package/dist/src/services/imageGeneration.js +280 -0
- package/dist/src/services/imageUnderstanding.js +352 -0
- package/dist/src/services/videoGeneration.js +79 -0
- package/dist/src/storage/captureRepository.js +1591 -0
- package/dist/src/storage/files.js +157 -0
- package/dist/src/storage/imageCache.js +346 -0
- package/dist/src/storage/repositories.js +388 -0
- package/dist/src/storage/sessionRepository.js +370 -0
- package/dist/src/storage/statsRepository.js +204 -0
- package/dist/src/transport/httpClient.js +126 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/messageMedia.js +285 -0
- package/dist/src/utils/modelCapabilities.js +108 -0
- package/dist/src/utils/modelDiscovery.js +170 -0
- package/dist/src/version.js +5 -0
- package/dist/src/workers/captureRetention.js +25 -0
- package/dist/src/workers/configWatcher.js +91 -0
- package/dist/src/workers/healthChecker.js +21 -0
- package/dist/src/workers/statsRotation.js +41 -0
- package/docs/LLM/output_schema.md +312 -0
- package/docs/benchmark.md +208 -0
- package/docs/mcp-guidelines.md +125 -0
- package/docs/mcp-service.md +178 -0
- package/docs/opencode.md +86 -0
- package/docs/providers.md +79 -0
- package/examples/benchmark.config.yaml +28 -0
- package/examples/providers/alibaba-dashscope.yaml +88 -0
- package/examples/providers/alibaba-llm.yaml +64 -0
- package/examples/providers/alibaba-registry.yaml +7 -0
- package/examples/providers/inference-v2-ray.yaml +29 -0
- package/examples/scenarios/assets/omni-call-sample.wav +0 -0
- package/examples/scenarios/custom.jsonl +5 -0
- package/examples/scenarios/custom.yaml +40 -0
- package/model-form-v2.png +0 -0
- package/package.json +66 -0
- package/provider-form-v2.png +0 -0
- package/provider-form.png +0 -0
- package/scripts/manual-test.sh +11 -0
- package/scripts/version-from-git.js +23 -0
- package/src/benchmark/artifacts.ts +149 -0
- package/src/benchmark/capabilityClassifier.ts +99 -0
- package/src/benchmark/capabilityStore.ts +174 -0
- package/src/benchmark/config.ts +337 -0
- package/src/benchmark/gates.ts +164 -0
- package/src/benchmark/jobs.ts +312 -0
- package/src/benchmark/runner.ts +2519 -0
- package/src/benchmark/schema.ts +443 -0
- package/src/benchmark/suites.ts +323 -0
- package/src/benchmark/tinyQaDataset.ts +428 -0
- package/src/benchmark/types.ts +442 -0
- package/src/config.ts +44 -0
- package/src/index.ts +195 -0
- package/src/mcp/client.ts +305 -0
- package/src/mcp/discovery.ts +266 -0
- package/src/mcp/policy.ts +105 -0
- package/src/mcp/registry.ts +164 -0
- package/src/mcp/service.ts +611 -0
- package/src/middleware/auth.ts +251 -0
- package/src/middleware/requestCapture.ts +245 -0
- package/src/middleware/requestStats.ts +163 -0
- package/src/pools/builder.ts +159 -0
- package/src/pools/repository.ts +71 -0
- package/src/pools/scheduler.ts +425 -0
- package/src/pools/types.ts +117 -0
- package/src/protocols/adapters/dashscope.ts +335 -0
- package/src/protocols/adapters/inferenceV2.ts +428 -0
- package/src/protocols/adapters/openai.ts +32 -0
- package/src/protocols/registry.ts +117 -0
- package/src/protocols/types.ts +81 -0
- package/src/providers/health.ts +207 -0
- package/src/providers/importer.ts +402 -0
- package/src/providers/modelRegistry.ts +415 -0
- package/src/providers/repository.ts +439 -0
- package/src/providers/types.ts +113 -0
- package/src/routes/admin.ts +666 -0
- package/src/routes/audio.ts +372 -0
- package/src/routes/chat.ts +301 -0
- package/src/routes/embeddings.ts +197 -0
- package/src/routes/images.ts +356 -0
- package/src/routes/mcp.ts +320 -0
- package/src/routes/mcpService.ts +114 -0
- package/src/routes/models.ts +50 -0
- package/src/routes/responses.ts +872 -0
- package/src/routes/sessions.ts +558 -0
- package/src/routes/stats.ts +312 -0
- package/src/routes/ui.ts +96 -0
- package/src/routes/videos.ts +132 -0
- package/src/routing/router.ts +501 -0
- package/src/services/imageGeneration.ts +396 -0
- package/src/services/imageUnderstanding.ts +449 -0
- package/src/services/videoGeneration.ts +127 -0
- package/src/storage/captureRepository.ts +1835 -0
- package/src/storage/files.ts +178 -0
- package/src/storage/imageCache.ts +405 -0
- package/src/storage/repositories.ts +494 -0
- package/src/storage/sessionRepository.ts +419 -0
- package/src/storage/statsRepository.ts +238 -0
- package/src/transport/httpClient.ts +145 -0
- package/src/types.ts +322 -0
- package/src/utils/messageMedia.ts +293 -0
- package/src/utils/modelCapabilities.ts +161 -0
- package/src/utils/modelDiscovery.ts +203 -0
- package/src/workers/captureRetention.ts +25 -0
- package/src/workers/configWatcher.ts +115 -0
- package/src/workers/healthChecker.ts +22 -0
- package/src/workers/statsRotation.ts +49 -0
- package/tests/benchmarkAdminRoutes.test.ts +82 -0
- package/tests/benchmarkBasics.test.ts +116 -0
- package/tests/captureAdminRoutes.test.ts +420 -0
- package/tests/captureRepository.test.ts +797 -0
- package/tests/cliLegacyRewrite.test.ts +45 -0
- package/tests/imageGeneration.service.test.ts +107 -0
- package/tests/imageUnderstanding.service.test.ts +123 -0
- package/tests/mcpPolicy.test.ts +105 -0
- package/tests/mcpService.test.ts +1245 -0
- package/tests/modelRef.test.ts +23 -0
- package/tests/modelsRoutes.test.ts +154 -0
- package/tests/sessionMediaCache.test.ts +167 -0
- package/tests/statsRoutes.test.ts +323 -0
- package/tsconfig.json +15 -0
- package/ui/index.html +16 -0
- package/ui/package-lock.json +8521 -0
- package/ui/package.json +52 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/assets/apple-touch-icon.png +0 -0
- package/ui/public/assets/favicon-16.png +0 -0
- package/ui/public/assets/favicon-32.png +0 -0
- package/ui/public/assets/icon-192.png +0 -0
- package/ui/public/assets/icon-512.png +0 -0
- package/ui/src/App.tsx +27 -0
- package/ui/src/api/client.ts +1503 -0
- package/ui/src/components/EndpointUsageGuide.tsx +361 -0
- package/ui/src/components/Layout.tsx +124 -0
- package/ui/src/components/MessageContent.tsx +365 -0
- package/ui/src/components/ToolCallMessage.tsx +179 -0
- package/ui/src/components/ToolPicker.tsx +442 -0
- package/ui/src/components/messageContentParser.test.ts +41 -0
- package/ui/src/components/messageContentParser.ts +73 -0
- package/ui/src/components/thinkingPreview.test.ts +27 -0
- package/ui/src/components/thinkingPreview.ts +15 -0
- package/ui/src/components/toMermaidSankey.test.ts +78 -0
- package/ui/src/components/toMermaidSankey.ts +56 -0
- package/ui/src/components/ui/button.tsx +58 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/textarea.tsx +21 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +9 -0
- package/ui/src/pages/AgentPlayground.tsx +2010 -0
- package/ui/src/pages/Benchmark.tsx +988 -0
- package/ui/src/pages/Dashboard.tsx +581 -0
- package/ui/src/pages/Peek.tsx +962 -0
- package/ui/src/pages/Settings.tsx +2013 -0
- package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
- package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
- package/ui/src/pages/agentThinkingContent.test.ts +50 -0
- package/ui/src/pages/agentThinkingContent.ts +57 -0
- package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
- package/ui/src/pages/dashboardTokenUsage.ts +36 -0
- package/ui/src/pages/imageUpload.test.ts +39 -0
- package/ui/src/pages/imageUpload.ts +71 -0
- package/ui/src/pages/peekFilters.test.ts +29 -0
- package/ui/src/pages/peekFilters.ts +13 -0
- package/ui/src/pages/peekMedia.test.ts +58 -0
- package/ui/src/pages/peekMedia.ts +148 -0
- package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
- package/ui/src/pages/sessionAutoTitle.ts +106 -0
- package/ui/src/stores/settings.ts +58 -0
- package/ui/src/styles/globals.css +223 -0
- package/ui/src/vite-env.d.ts +8 -0
- package/ui/tailwind.config.js +106 -0
- package/ui/tsconfig.json +32 -0
- 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 }
|