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,442 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Wrench,
|
|
4
|
+
Server,
|
|
5
|
+
Plug,
|
|
6
|
+
PlugZap,
|
|
7
|
+
RefreshCw,
|
|
8
|
+
ChevronDown,
|
|
9
|
+
ChevronRight,
|
|
10
|
+
Check,
|
|
11
|
+
X,
|
|
12
|
+
AlertCircle,
|
|
13
|
+
Loader2,
|
|
14
|
+
Plus,
|
|
15
|
+
} from 'lucide-react'
|
|
16
|
+
import { Button } from '@/components/ui/button'
|
|
17
|
+
import { Input } from '@/components/ui/input'
|
|
18
|
+
import { cn } from '@/lib/utils'
|
|
19
|
+
import {
|
|
20
|
+
listMcpServers,
|
|
21
|
+
listMcpTools,
|
|
22
|
+
connectMcpServer,
|
|
23
|
+
disconnectMcpServer,
|
|
24
|
+
discoverMcpTools,
|
|
25
|
+
addMcpServer,
|
|
26
|
+
deleteMcpServer,
|
|
27
|
+
BUILTIN_SERVER_ID,
|
|
28
|
+
type McpServer,
|
|
29
|
+
type McpTool,
|
|
30
|
+
} from '@/api/client'
|
|
31
|
+
|
|
32
|
+
interface ToolPickerProps {
|
|
33
|
+
selectedTools: Set<string>
|
|
34
|
+
onToolsChange: (tools: Set<string>) => void
|
|
35
|
+
className?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ServerWithTools extends McpServer {
|
|
39
|
+
tools: McpTool[]
|
|
40
|
+
expanded: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ToolPicker({ selectedTools, onToolsChange, className }: ToolPickerProps) {
|
|
44
|
+
const [servers, setServers] = useState<ServerWithTools[]>([])
|
|
45
|
+
const [allTools, setAllTools] = useState<McpTool[]>([])
|
|
46
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
47
|
+
const [isConnecting, setIsConnecting] = useState<string | null>(null)
|
|
48
|
+
const [showAddServer, setShowAddServer] = useState(false)
|
|
49
|
+
const [newServerName, setNewServerName] = useState('')
|
|
50
|
+
const [newServerUrl, setNewServerUrl] = useState('')
|
|
51
|
+
const [addError, setAddError] = useState<string | null>(null)
|
|
52
|
+
|
|
53
|
+
// Load servers and tools
|
|
54
|
+
const loadData = useCallback(async () => {
|
|
55
|
+
setIsLoading(true)
|
|
56
|
+
try {
|
|
57
|
+
const [serversRes, toolsRes] = await Promise.all([
|
|
58
|
+
listMcpServers(),
|
|
59
|
+
listMcpTools(),
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
let tools = toolsRes.data
|
|
63
|
+
|
|
64
|
+
// Auto-connect the built-in server if it's present but not yet connected
|
|
65
|
+
const builtinServer = serversRes.data.find((s) => s.id === BUILTIN_SERVER_ID)
|
|
66
|
+
if (builtinServer && !builtinServer.connected) {
|
|
67
|
+
try {
|
|
68
|
+
await connectMcpServer(BUILTIN_SERVER_ID)
|
|
69
|
+
// Re-fetch tools now that built-in is connected
|
|
70
|
+
const refreshed = await listMcpTools()
|
|
71
|
+
tools = refreshed.data
|
|
72
|
+
} catch {
|
|
73
|
+
// Non-fatal — built-in may not be ready yet
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setAllTools(tools)
|
|
78
|
+
|
|
79
|
+
// Group tools by server
|
|
80
|
+
const serverMap = new Map<string, McpTool[]>()
|
|
81
|
+
for (const tool of tools) {
|
|
82
|
+
const existing = serverMap.get(tool.serverId) || []
|
|
83
|
+
serverMap.set(tool.serverId, [...existing, tool])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setServers(
|
|
87
|
+
serversRes.data.map((s) => ({
|
|
88
|
+
...s,
|
|
89
|
+
tools: serverMap.get(s.id) || [],
|
|
90
|
+
expanded: true,
|
|
91
|
+
}))
|
|
92
|
+
)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Failed to load MCP data:', error)
|
|
95
|
+
} finally {
|
|
96
|
+
setIsLoading(false)
|
|
97
|
+
}
|
|
98
|
+
}, [])
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
loadData()
|
|
102
|
+
}, [loadData])
|
|
103
|
+
|
|
104
|
+
// Connect to a server
|
|
105
|
+
const handleConnect = async (serverId: string) => {
|
|
106
|
+
setIsConnecting(serverId)
|
|
107
|
+
try {
|
|
108
|
+
await connectMcpServer(serverId)
|
|
109
|
+
await loadData() // Reload to get new tools
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Failed to connect:', error)
|
|
112
|
+
} finally {
|
|
113
|
+
setIsConnecting(null)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Disconnect from a server
|
|
118
|
+
const handleDisconnect = async (serverId: string) => {
|
|
119
|
+
setIsConnecting(serverId)
|
|
120
|
+
try {
|
|
121
|
+
await disconnectMcpServer(serverId)
|
|
122
|
+
// Remove tools from selection
|
|
123
|
+
const serverTools = allTools.filter((t) => t.serverId === serverId)
|
|
124
|
+
const newSelected = new Set(selectedTools)
|
|
125
|
+
serverTools.forEach((t) => newSelected.delete(t.name))
|
|
126
|
+
onToolsChange(newSelected)
|
|
127
|
+
await loadData()
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('Failed to disconnect:', error)
|
|
130
|
+
} finally {
|
|
131
|
+
setIsConnecting(null)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Discover all tools
|
|
136
|
+
const handleDiscoverAll = async () => {
|
|
137
|
+
setIsLoading(true)
|
|
138
|
+
try {
|
|
139
|
+
await discoverMcpTools()
|
|
140
|
+
await loadData()
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Failed to discover tools:', error)
|
|
143
|
+
} finally {
|
|
144
|
+
setIsLoading(false)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add new server
|
|
149
|
+
const handleAddServer = async () => {
|
|
150
|
+
if (!newServerName.trim() || !newServerUrl.trim()) {
|
|
151
|
+
setAddError('Name and URL are required')
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setAddError(null)
|
|
156
|
+
try {
|
|
157
|
+
await addMcpServer(newServerName.trim(), newServerUrl.trim(), true)
|
|
158
|
+
setNewServerName('')
|
|
159
|
+
setNewServerUrl('')
|
|
160
|
+
setShowAddServer(false)
|
|
161
|
+
await loadData()
|
|
162
|
+
} catch (error) {
|
|
163
|
+
setAddError((error as Error).message || 'Failed to add server')
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Delete server
|
|
168
|
+
const handleDeleteServer = async (serverId: string) => {
|
|
169
|
+
try {
|
|
170
|
+
await deleteMcpServer(serverId)
|
|
171
|
+
await loadData()
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('Failed to delete server:', error)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Toggle tool selection
|
|
178
|
+
const toggleTool = (toolName: string) => {
|
|
179
|
+
const newSelected = new Set(selectedTools)
|
|
180
|
+
if (newSelected.has(toolName)) {
|
|
181
|
+
newSelected.delete(toolName)
|
|
182
|
+
} else {
|
|
183
|
+
newSelected.add(toolName)
|
|
184
|
+
}
|
|
185
|
+
onToolsChange(newSelected)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Toggle all tools from a server
|
|
189
|
+
const toggleServerTools = (server: ServerWithTools) => {
|
|
190
|
+
const serverToolNames = server.tools.map((t) => t.name)
|
|
191
|
+
const allSelected = serverToolNames.every((name) => selectedTools.has(name))
|
|
192
|
+
|
|
193
|
+
const newSelected = new Set(selectedTools)
|
|
194
|
+
if (allSelected) {
|
|
195
|
+
serverToolNames.forEach((name) => newSelected.delete(name))
|
|
196
|
+
} else {
|
|
197
|
+
serverToolNames.forEach((name) => newSelected.add(name))
|
|
198
|
+
}
|
|
199
|
+
onToolsChange(newSelected)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Toggle server expanded state
|
|
203
|
+
const toggleServerExpanded = (serverId: string) => {
|
|
204
|
+
setServers((prev) =>
|
|
205
|
+
prev.map((s) =>
|
|
206
|
+
s.id === serverId ? { ...s, expanded: !s.expanded } : s
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div className={cn('flex flex-col', className)}>
|
|
213
|
+
{/* Header */}
|
|
214
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-border">
|
|
215
|
+
<div className="flex items-center gap-2">
|
|
216
|
+
<Wrench className="w-4 h-4 text-primary" />
|
|
217
|
+
<span className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
|
|
218
|
+
Tools
|
|
219
|
+
</span>
|
|
220
|
+
{selectedTools.size > 0 && (
|
|
221
|
+
<span className="px-1.5 py-0.5 bg-primary/20 text-primary text-2xs rounded font-mono">
|
|
222
|
+
{selectedTools.size}
|
|
223
|
+
</span>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
<div className="flex items-center gap-1">
|
|
227
|
+
<Button
|
|
228
|
+
variant="ghost"
|
|
229
|
+
size="icon"
|
|
230
|
+
className="h-6 w-6"
|
|
231
|
+
onClick={() => setShowAddServer(!showAddServer)}
|
|
232
|
+
title="Add server"
|
|
233
|
+
>
|
|
234
|
+
<Plus className="w-3.5 h-3.5" />
|
|
235
|
+
</Button>
|
|
236
|
+
<Button
|
|
237
|
+
variant="ghost"
|
|
238
|
+
size="icon"
|
|
239
|
+
className="h-6 w-6"
|
|
240
|
+
onClick={handleDiscoverAll}
|
|
241
|
+
disabled={isLoading}
|
|
242
|
+
title="Discover all tools"
|
|
243
|
+
>
|
|
244
|
+
<RefreshCw className={cn('w-3.5 h-3.5', isLoading && 'animate-spin')} />
|
|
245
|
+
</Button>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{/* Add Server Form */}
|
|
250
|
+
{showAddServer && (
|
|
251
|
+
<div className="p-3 border-b border-border bg-secondary/30 space-y-2 animate-slide-in-bottom">
|
|
252
|
+
<Input
|
|
253
|
+
placeholder="Server name"
|
|
254
|
+
value={newServerName}
|
|
255
|
+
onChange={(e) => setNewServerName(e.target.value)}
|
|
256
|
+
className="h-8 text-sm"
|
|
257
|
+
/>
|
|
258
|
+
<Input
|
|
259
|
+
placeholder="http://localhost:3000/mcp"
|
|
260
|
+
value={newServerUrl}
|
|
261
|
+
onChange={(e) => setNewServerUrl(e.target.value)}
|
|
262
|
+
className="h-8 text-sm font-mono"
|
|
263
|
+
/>
|
|
264
|
+
{addError && (
|
|
265
|
+
<p className="text-destructive text-2xs flex items-center gap-1">
|
|
266
|
+
<AlertCircle className="w-3 h-3" />
|
|
267
|
+
{addError}
|
|
268
|
+
</p>
|
|
269
|
+
)}
|
|
270
|
+
<div className="flex gap-2">
|
|
271
|
+
<Button size="sm" className="flex-1 h-7 text-xs" onClick={handleAddServer}>
|
|
272
|
+
Add
|
|
273
|
+
</Button>
|
|
274
|
+
<Button
|
|
275
|
+
variant="outline"
|
|
276
|
+
size="sm"
|
|
277
|
+
className="h-7 text-xs"
|
|
278
|
+
onClick={() => {
|
|
279
|
+
setShowAddServer(false)
|
|
280
|
+
setAddError(null)
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
Cancel
|
|
284
|
+
</Button>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{/* Server and Tools List */}
|
|
290
|
+
<div className="flex-1 overflow-y-auto">
|
|
291
|
+
{servers.length === 0 && !isLoading && (
|
|
292
|
+
<div className="p-4 text-center">
|
|
293
|
+
<Server className="w-8 h-8 text-muted-foreground/50 mx-auto mb-2" />
|
|
294
|
+
<p className="text-sm text-muted-foreground">No MCP servers</p>
|
|
295
|
+
<p className="text-2xs text-muted-foreground/70 mt-1">
|
|
296
|
+
Add a server to discover tools
|
|
297
|
+
</p>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
{servers.map((server) => (
|
|
302
|
+
<div key={server.id} className="border-b border-border/50 last:border-0">
|
|
303
|
+
{/* Server Header */}
|
|
304
|
+
<div
|
|
305
|
+
className={cn(
|
|
306
|
+
'flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-secondary/50 transition-colors',
|
|
307
|
+
server.connected && 'bg-emerald-500/5'
|
|
308
|
+
)}
|
|
309
|
+
onClick={() => toggleServerExpanded(server.id)}
|
|
310
|
+
>
|
|
311
|
+
{server.expanded ? (
|
|
312
|
+
<ChevronDown className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
|
|
313
|
+
) : (
|
|
314
|
+
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
<div
|
|
318
|
+
className={cn(
|
|
319
|
+
'w-2 h-2 rounded-full shrink-0',
|
|
320
|
+
server.connected
|
|
321
|
+
? 'bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.5)]'
|
|
322
|
+
: server.status === 'error'
|
|
323
|
+
? 'bg-red-500'
|
|
324
|
+
: 'bg-muted-foreground/30'
|
|
325
|
+
)}
|
|
326
|
+
/>
|
|
327
|
+
|
|
328
|
+
<span className="text-sm font-medium truncate flex-1">{server.name}</span>
|
|
329
|
+
|
|
330
|
+
{server.id === BUILTIN_SERVER_ID && (
|
|
331
|
+
<span className="text-2xs font-mono text-muted-foreground/60 border border-border/50 rounded px-1 py-0.5 shrink-0">
|
|
332
|
+
built-in
|
|
333
|
+
</span>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
{server.tools.length > 0 && (
|
|
337
|
+
<button
|
|
338
|
+
onClick={(e) => {
|
|
339
|
+
e.stopPropagation()
|
|
340
|
+
toggleServerTools(server)
|
|
341
|
+
}}
|
|
342
|
+
className={cn(
|
|
343
|
+
'w-5 h-5 rounded flex items-center justify-center transition-colors',
|
|
344
|
+
server.tools.every((t) => selectedTools.has(t.name))
|
|
345
|
+
? 'bg-primary text-primary-foreground'
|
|
346
|
+
: server.tools.some((t) => selectedTools.has(t.name))
|
|
347
|
+
? 'bg-primary/50 text-primary-foreground'
|
|
348
|
+
: 'bg-secondary hover:bg-secondary/80'
|
|
349
|
+
)}
|
|
350
|
+
>
|
|
351
|
+
<Check className="w-3 h-3" />
|
|
352
|
+
</button>
|
|
353
|
+
)}
|
|
354
|
+
|
|
355
|
+
{isConnecting === server.id ? (
|
|
356
|
+
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
|
357
|
+
) : server.id === BUILTIN_SERVER_ID ? null : server.connected ? (
|
|
358
|
+
<button
|
|
359
|
+
onClick={(e) => {
|
|
360
|
+
e.stopPropagation()
|
|
361
|
+
handleDisconnect(server.id)
|
|
362
|
+
}}
|
|
363
|
+
className="p-1 hover:bg-destructive/20 rounded transition-colors"
|
|
364
|
+
title="Disconnect"
|
|
365
|
+
>
|
|
366
|
+
<PlugZap className="w-3.5 h-3.5 text-emerald-500" />
|
|
367
|
+
</button>
|
|
368
|
+
) : (
|
|
369
|
+
<button
|
|
370
|
+
onClick={(e) => {
|
|
371
|
+
e.stopPropagation()
|
|
372
|
+
handleConnect(server.id)
|
|
373
|
+
}}
|
|
374
|
+
className="p-1 hover:bg-primary/20 rounded transition-colors"
|
|
375
|
+
title="Connect"
|
|
376
|
+
>
|
|
377
|
+
<Plug className="w-3.5 h-3.5 text-muted-foreground" />
|
|
378
|
+
</button>
|
|
379
|
+
)}
|
|
380
|
+
|
|
381
|
+
{server.id !== BUILTIN_SERVER_ID && (
|
|
382
|
+
<button
|
|
383
|
+
onClick={(e) => {
|
|
384
|
+
e.stopPropagation()
|
|
385
|
+
handleDeleteServer(server.id)
|
|
386
|
+
}}
|
|
387
|
+
className="p-1 hover:bg-destructive/20 rounded transition-colors opacity-50 hover:opacity-100"
|
|
388
|
+
title="Delete server"
|
|
389
|
+
>
|
|
390
|
+
<X className="w-3 h-3 text-destructive" />
|
|
391
|
+
</button>
|
|
392
|
+
)}
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
{/* Tools List */}
|
|
396
|
+
{server.expanded && (
|
|
397
|
+
<div className="pb-1">
|
|
398
|
+
{server.tools.length === 0 ? (
|
|
399
|
+
<div className="px-8 py-2 text-2xs text-muted-foreground/70">
|
|
400
|
+
{server.connected ? 'No tools available' : 'Connect to discover tools'}
|
|
401
|
+
</div>
|
|
402
|
+
) : (
|
|
403
|
+
server.tools.map((tool) => (
|
|
404
|
+
<button
|
|
405
|
+
key={tool.name}
|
|
406
|
+
onClick={() => toggleTool(tool.name)}
|
|
407
|
+
className={cn(
|
|
408
|
+
'w-full flex items-start gap-2 px-8 py-1.5 text-left hover:bg-secondary/50 transition-colors',
|
|
409
|
+
selectedTools.has(tool.name) && 'bg-primary/5'
|
|
410
|
+
)}
|
|
411
|
+
>
|
|
412
|
+
<div
|
|
413
|
+
className={cn(
|
|
414
|
+
'w-4 h-4 rounded border flex items-center justify-center shrink-0 mt-0.5 transition-colors',
|
|
415
|
+
selectedTools.has(tool.name)
|
|
416
|
+
? 'bg-primary border-primary'
|
|
417
|
+
: 'border-border'
|
|
418
|
+
)}
|
|
419
|
+
>
|
|
420
|
+
{selectedTools.has(tool.name) && (
|
|
421
|
+
<Check className="w-2.5 h-2.5 text-primary-foreground" />
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
<div className="min-w-0">
|
|
425
|
+
<p className="text-sm font-mono truncate">{tool.name}</p>
|
|
426
|
+
{tool.description && (
|
|
427
|
+
<p className="text-2xs text-muted-foreground/70 line-clamp-2">
|
|
428
|
+
{tool.description}
|
|
429
|
+
</p>
|
|
430
|
+
)}
|
|
431
|
+
</div>
|
|
432
|
+
</button>
|
|
433
|
+
))
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
))}
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
)
|
|
442
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { parseMessageContent } from './messageContentParser'
|
|
4
|
+
|
|
5
|
+
test('parses standard think block', () => {
|
|
6
|
+
const parts = parseMessageContent('<think>analyze</think>\n\nanswer')
|
|
7
|
+
assert.deepEqual(parts, [
|
|
8
|
+
{ type: 'thinking', content: 'analyze' },
|
|
9
|
+
{ type: 'text', content: 'answer' },
|
|
10
|
+
])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('parses missing opening tag before closing think tag', () => {
|
|
14
|
+
const parts = parseMessageContent('analysis text</think>\n\nanswer')
|
|
15
|
+
assert.deepEqual(parts, [
|
|
16
|
+
{ type: 'thinking', content: 'analysis text' },
|
|
17
|
+
{ type: 'text', content: 'answer' },
|
|
18
|
+
])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('parses unclosed think tag during streaming', () => {
|
|
22
|
+
const parts = parseMessageContent('<think>still thinking')
|
|
23
|
+
assert.deepEqual(parts, [
|
|
24
|
+
{ type: 'thinking', content: 'still thinking' },
|
|
25
|
+
])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('parses whitespace-tolerant think tags', () => {
|
|
29
|
+
const parts = parseMessageContent('<think > spaced </ think >\n\nok')
|
|
30
|
+
assert.deepEqual(parts, [
|
|
31
|
+
{ type: 'thinking', content: 'spaced' },
|
|
32
|
+
{ type: 'text', content: 'ok' },
|
|
33
|
+
])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('plain text remains text segment', () => {
|
|
37
|
+
const parts = parseMessageContent('just answer')
|
|
38
|
+
assert.deepEqual(parts, [
|
|
39
|
+
{ type: 'text', content: 'just answer' },
|
|
40
|
+
])
|
|
41
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface ParsedMessagePart {
|
|
2
|
+
type: 'text' | 'thinking'
|
|
3
|
+
content: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Parse content to extract thinking blocks.
|
|
7
|
+
// Handles:
|
|
8
|
+
// 1) <think>...</think>
|
|
9
|
+
// 2) content...</think> (missing opening tag)
|
|
10
|
+
// 3) <think>... (unclosed tag during streaming)
|
|
11
|
+
// 4) tags with extra whitespace, e.g. <think >, </ think >
|
|
12
|
+
export function parseMessageContent(content: string): ParsedMessagePart[] {
|
|
13
|
+
const parts: ParsedMessagePart[] = []
|
|
14
|
+
const openTag = /<\s*think\s*>/i
|
|
15
|
+
const closeTag = /<\s*\/\s*think\s*>/i
|
|
16
|
+
|
|
17
|
+
const startsWithOpeningTag = openTag.test(content.trimStart())
|
|
18
|
+
const hasClosingWithoutOpening = !startsWithOpeningTag && closeTag.test(content)
|
|
19
|
+
|
|
20
|
+
let processedContent = content
|
|
21
|
+
if (hasClosingWithoutOpening) {
|
|
22
|
+
const closeMatch = closeTag.exec(content)
|
|
23
|
+
if (closeMatch) {
|
|
24
|
+
const closeIndex = closeMatch.index
|
|
25
|
+
const thinkingContent = content.slice(0, closeIndex).trim()
|
|
26
|
+
if (thinkingContent) {
|
|
27
|
+
parts.push({ type: 'thinking', content: thinkingContent })
|
|
28
|
+
}
|
|
29
|
+
processedContent = content.slice(closeIndex + closeMatch[0].length)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const closedThinkRegex = /<\s*think\s*>([\s\S]*?)<\s*\/\s*think\s*>/gi
|
|
34
|
+
let lastIndex = 0
|
|
35
|
+
let match: RegExpExecArray | null
|
|
36
|
+
|
|
37
|
+
while ((match = closedThinkRegex.exec(processedContent)) !== null) {
|
|
38
|
+
if (match.index > lastIndex) {
|
|
39
|
+
const text = processedContent.slice(lastIndex, match.index).trim()
|
|
40
|
+
if (text) {
|
|
41
|
+
parts.push({ type: 'text', content: text })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
parts.push({ type: 'thinking', content: (match[1] ?? '').trim() })
|
|
45
|
+
lastIndex = match.index + match[0].length
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (lastIndex < processedContent.length) {
|
|
49
|
+
const tail = processedContent.slice(lastIndex)
|
|
50
|
+
const unclosedMatch = tail.match(/([\s\S]*?)<\s*think\s*>([\s\S]*)$/i)
|
|
51
|
+
if (unclosedMatch) {
|
|
52
|
+
const textBefore = (unclosedMatch[1] ?? '').trim()
|
|
53
|
+
const thinkingTail = (unclosedMatch[2] ?? '').trim()
|
|
54
|
+
if (textBefore) {
|
|
55
|
+
parts.push({ type: 'text', content: textBefore })
|
|
56
|
+
}
|
|
57
|
+
if (thinkingTail) {
|
|
58
|
+
parts.push({ type: 'thinking', content: thinkingTail })
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
const text = tail.trim()
|
|
62
|
+
if (text) {
|
|
63
|
+
parts.push({ type: 'text', content: text })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (parts.length === 0 && content.trim()) {
|
|
69
|
+
parts.push({ type: 'text', content: content.trim() })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return parts
|
|
73
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { getLatestThinkingLines, hasUnclosedThinkingTag } from './thinkingPreview'
|
|
4
|
+
|
|
5
|
+
test('latest lines append chars within current trailing line', () => {
|
|
6
|
+
const before = getLatestThinkingLines('one\ntwo\npar')
|
|
7
|
+
const after = getLatestThinkingLines('one\ntwo\npart')
|
|
8
|
+
|
|
9
|
+
assert.deepEqual(before, ['one', 'two', 'par'])
|
|
10
|
+
assert.deepEqual(after, ['one', 'two', 'part'])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('latest lines create a new line once newline arrives', () => {
|
|
14
|
+
const lines = getLatestThinkingLines('one\ntwo\npart\nnext')
|
|
15
|
+
assert.deepEqual(lines, ['one', 'two', 'part', 'next'])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('latest lines drop oldest full line when window exceeds five lines', () => {
|
|
19
|
+
const lines = getLatestThinkingLines('1\n2\n3\n4\n5\n6')
|
|
20
|
+
assert.deepEqual(lines, ['2', '3', '4', '5', '6'])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('unclosed think tags are considered live, closed are not', () => {
|
|
24
|
+
assert.equal(hasUnclosedThinkingTag('<think>still thinking'), true)
|
|
25
|
+
assert.equal(hasUnclosedThinkingTag('<think>done</think>final'), false)
|
|
26
|
+
})
|
|
27
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const LIVE_PREVIEW_MAX_LINES = 5
|
|
2
|
+
|
|
3
|
+
export function hasUnclosedThinkingTag(content: string): boolean {
|
|
4
|
+
const openTagMatches = content.match(/<\s*think\s*>/gi)?.length ?? 0
|
|
5
|
+
const closeTagMatches = content.match(/<\s*\/\s*think\s*>/gi)?.length ?? 0
|
|
6
|
+
return openTagMatches > closeTagMatches
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getLatestThinkingLines(content: string, maxLines = LIVE_PREVIEW_MAX_LINES): string[] {
|
|
10
|
+
if (!content) return []
|
|
11
|
+
const normalized = content.replace(/\r\n?/g, '\n')
|
|
12
|
+
const lines = normalized.split('\n')
|
|
13
|
+
return lines.slice(-maxLines)
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { toMermaidSankey } from './toMermaidSankey'
|
|
4
|
+
|
|
5
|
+
test('toMermaidSankey emits valid sankey-beta csv rows', () => {
|
|
6
|
+
const code = toMermaidSankey({
|
|
7
|
+
eligible: true,
|
|
8
|
+
method: 'exact_totals_estimated_categories',
|
|
9
|
+
totals: {
|
|
10
|
+
inputTokens: 1000,
|
|
11
|
+
outputTokens: 400,
|
|
12
|
+
totalTokens: 1400,
|
|
13
|
+
},
|
|
14
|
+
input: [
|
|
15
|
+
{ key: 'system', label: 'System Prompt', tokens: 300 },
|
|
16
|
+
{ key: 'user', label: 'User Prompt', tokens: 700 },
|
|
17
|
+
],
|
|
18
|
+
output: [
|
|
19
|
+
{ key: 'completion', label: 'Final Answer', tokens: 400 },
|
|
20
|
+
],
|
|
21
|
+
notes: [],
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const lines = code.split('\n')
|
|
25
|
+
assert.equal(lines[0], 'sankey-beta')
|
|
26
|
+
assert.ok(lines.includes('Context Window,Input Tokens,1000'))
|
|
27
|
+
assert.ok(lines.includes('Context Window,Output Tokens,400'))
|
|
28
|
+
assert.ok(lines.includes('Input Tokens,System Prompt [input:system],300'))
|
|
29
|
+
assert.ok(lines.includes('Output Tokens,Final Answer [output:completion],400'))
|
|
30
|
+
assert.ok(!code.includes('-->'))
|
|
31
|
+
assert.ok(!code.includes('[') || code.includes('[input:') || code.includes('[output:'))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('toMermaidSankey compacts known category labels while retaining keys', () => {
|
|
35
|
+
const code = toMermaidSankey({
|
|
36
|
+
eligible: true,
|
|
37
|
+
method: 'exact_totals_estimated_categories',
|
|
38
|
+
totals: {
|
|
39
|
+
inputTokens: 100,
|
|
40
|
+
outputTokens: 50,
|
|
41
|
+
totalTokens: 150,
|
|
42
|
+
},
|
|
43
|
+
input: [
|
|
44
|
+
{ key: 'assistant_history', label: 'Assistant History', tokens: 60 },
|
|
45
|
+
{ key: 'input_media', label: 'Input Media', tokens: 40 },
|
|
46
|
+
],
|
|
47
|
+
output: [
|
|
48
|
+
{ key: 'assistant_text', label: 'Assistant Text', tokens: 50 },
|
|
49
|
+
],
|
|
50
|
+
notes: [],
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
assert.ok(code.includes('Input Tokens,Asst Hist [input:assistant_history],60'))
|
|
54
|
+
assert.ok(code.includes('Input Tokens,Media In [input:input_media],40'))
|
|
55
|
+
assert.ok(code.includes('Output Tokens,Asst Text [output:assistant_text],50'))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('toMermaidSankey escapes csv cells for commas and quotes', () => {
|
|
59
|
+
const code = toMermaidSankey({
|
|
60
|
+
eligible: true,
|
|
61
|
+
method: 'estimated_only',
|
|
62
|
+
totals: {
|
|
63
|
+
inputTokens: 10,
|
|
64
|
+
outputTokens: 5,
|
|
65
|
+
totalTokens: 15,
|
|
66
|
+
},
|
|
67
|
+
input: [
|
|
68
|
+
{ key: 'quoted', label: 'System "Rules", Prompt', tokens: 10 },
|
|
69
|
+
],
|
|
70
|
+
output: [
|
|
71
|
+
{ key: 'final', label: 'Final, "Answer"', tokens: 5 },
|
|
72
|
+
],
|
|
73
|
+
notes: [],
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
assert.match(code, /"System ""Rules"", Prompt \[input:quoted\]"/)
|
|
77
|
+
assert.match(code, /"Final, ""Answer"" \[output:final\]"/)
|
|
78
|
+
})
|