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,148 @@
|
|
|
1
|
+
export type PeekEmbeddedMedia = {
|
|
2
|
+
path: string
|
|
3
|
+
source: 'request' | 'response'
|
|
4
|
+
mime: string
|
|
5
|
+
kind: 'image' | 'audio' | 'binary'
|
|
6
|
+
url: string
|
|
7
|
+
origin: 'b64_json' | 'data_url'
|
|
8
|
+
sizeHint: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type MediaSource = PeekEmbeddedMedia['source']
|
|
12
|
+
|
|
13
|
+
const DATA_URL_RE = /^data:([^;,]+)(?:;[^,]*)?,(.*)$/i
|
|
14
|
+
|
|
15
|
+
export function extractEmbeddedMedia(source: MediaSource, value: unknown): PeekEmbeddedMedia[] {
|
|
16
|
+
const results: PeekEmbeddedMedia[] = []
|
|
17
|
+
const seen = new Set<string>()
|
|
18
|
+
walkValue(value, source, '$', results, seen)
|
|
19
|
+
return results
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function redactEmbeddedMedia(value: unknown): unknown {
|
|
23
|
+
return redactValue(value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function walkValue(
|
|
27
|
+
value: unknown,
|
|
28
|
+
source: MediaSource,
|
|
29
|
+
path: string,
|
|
30
|
+
results: PeekEmbeddedMedia[],
|
|
31
|
+
seen: Set<string>,
|
|
32
|
+
): void {
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
const parsed = parseDataUrl(value)
|
|
35
|
+
if (parsed) {
|
|
36
|
+
pushMedia(results, seen, {
|
|
37
|
+
path,
|
|
38
|
+
source,
|
|
39
|
+
mime: parsed.mime,
|
|
40
|
+
kind: kindFromMime(parsed.mime),
|
|
41
|
+
url: value,
|
|
42
|
+
origin: 'data_url',
|
|
43
|
+
sizeHint: parsed.payload.length,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
value.forEach((item, index) => walkValue(item, source, `${path}[${index}]`, results, seen))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!value || typeof value !== 'object') {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const record = value as Record<string, unknown>
|
|
59
|
+
const b64Json = typeof record.b64_json === 'string' ? record.b64_json : null
|
|
60
|
+
const siblingDataUrl = findSiblingDataUrl(record)
|
|
61
|
+
if (b64Json) {
|
|
62
|
+
const mime = siblingDataUrl?.mime ?? inferMime(record) ?? 'image/png'
|
|
63
|
+
pushMedia(results, seen, {
|
|
64
|
+
path: `${path}.b64_json`,
|
|
65
|
+
source,
|
|
66
|
+
mime,
|
|
67
|
+
kind: kindFromMime(mime),
|
|
68
|
+
url: siblingDataUrl?.url ?? `data:${mime};base64,${b64Json}`,
|
|
69
|
+
origin: 'b64_json',
|
|
70
|
+
sizeHint: b64Json.length,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const [key, child] of Object.entries(record)) {
|
|
75
|
+
if (b64Json && siblingDataUrl?.url === child && (key === 'url' || key === 'image_url' || key === 'audio_url')) {
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
walkValue(child, source, `${path}.${key}`, results, seen)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function redactValue(value: unknown): unknown {
|
|
83
|
+
if (typeof value === 'string') {
|
|
84
|
+
const parsed = parseDataUrl(value)
|
|
85
|
+
if (!parsed) return value
|
|
86
|
+
return `[data URL omitted: ${parsed.mime}, ${parsed.payload.length} chars]`
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
return value.map(redactValue)
|
|
90
|
+
}
|
|
91
|
+
if (!value || typeof value !== 'object') {
|
|
92
|
+
return value
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const record = value as Record<string, unknown>
|
|
96
|
+
const next: Record<string, unknown> = {}
|
|
97
|
+
const inferredMime = inferMime(record) ?? findSiblingDataUrl(record)?.mime ?? 'image/png'
|
|
98
|
+
for (const [key, child] of Object.entries(record)) {
|
|
99
|
+
if (key === 'b64_json' && typeof child === 'string') {
|
|
100
|
+
next[key] = `[base64 media omitted: ${inferredMime}, ${child.length} chars]`
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
next[key] = redactValue(child)
|
|
104
|
+
}
|
|
105
|
+
return next
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function pushMedia(results: PeekEmbeddedMedia[], seen: Set<string>, media: PeekEmbeddedMedia) {
|
|
109
|
+
const key = `${media.source}:${media.path}:${media.url}`
|
|
110
|
+
if (seen.has(key)) return
|
|
111
|
+
seen.add(key)
|
|
112
|
+
results.push(media)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function findSiblingDataUrl(record: Record<string, unknown>): { mime: string; payload: string; url: string } | null {
|
|
116
|
+
for (const key of ['url', 'image_url', 'audio_url']) {
|
|
117
|
+
const value = record[key]
|
|
118
|
+
if (typeof value !== 'string') continue
|
|
119
|
+
const parsed = parseDataUrl(value)
|
|
120
|
+
if (parsed) return { ...parsed, url: value }
|
|
121
|
+
}
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function inferMime(record: Record<string, unknown>): string | null {
|
|
126
|
+
for (const key of ['mime', 'mime_type', 'content_type', 'media_type']) {
|
|
127
|
+
const value = record[key]
|
|
128
|
+
if (typeof value === 'string' && value.includes('/')) {
|
|
129
|
+
return value
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseDataUrl(value: string): { mime: string; payload: string } | null {
|
|
136
|
+
const match = value.match(DATA_URL_RE)
|
|
137
|
+
if (!match) return null
|
|
138
|
+
return {
|
|
139
|
+
mime: match[1].trim().toLowerCase(),
|
|
140
|
+
payload: match[2],
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function kindFromMime(mime: string): PeekEmbeddedMedia['kind'] {
|
|
145
|
+
if (mime.startsWith('image/')) return 'image'
|
|
146
|
+
if (mime.startsWith('audio/')) return 'audio'
|
|
147
|
+
return 'binary'
|
|
148
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import {
|
|
4
|
+
applyAutoTitleToSessions,
|
|
5
|
+
createDeferredAutoTitleCandidate,
|
|
6
|
+
flushDeferredAutoTitle,
|
|
7
|
+
isDefaultSessionName,
|
|
8
|
+
} from './sessionAutoTitle'
|
|
9
|
+
|
|
10
|
+
test('recognizes default session names', () => {
|
|
11
|
+
assert.equal(isDefaultSessionName('Session 3/17/2026'), true)
|
|
12
|
+
assert.equal(isDefaultSessionName('Trip planning'), false)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('creates deferred title candidate only for default sessions with text', () => {
|
|
16
|
+
assert.deepEqual(
|
|
17
|
+
createDeferredAutoTitleCandidate('session-1', 'Session 3/17/2026', ' hello world '),
|
|
18
|
+
{ sessionId: 'session-1', seedText: 'hello world' }
|
|
19
|
+
)
|
|
20
|
+
assert.equal(createDeferredAutoTitleCandidate('session-1', 'Trip planning', 'hello world'), null)
|
|
21
|
+
assert.equal(createDeferredAutoTitleCandidate('session-1', 'Session 3/17/2026', ' '), null)
|
|
22
|
+
assert.equal(createDeferredAutoTitleCandidate(null, 'Session 3/17/2026', 'hello world'), null)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('flushDeferredAutoTitle runs once and updates status lifecycle', async () => {
|
|
26
|
+
const generationStates: Array<string | null> = []
|
|
27
|
+
const resolved: Array<{ name: string }> = []
|
|
28
|
+
let cleared = 0
|
|
29
|
+
const calls: Array<{ sessionId: string; model?: string; seedText?: string }> = []
|
|
30
|
+
|
|
31
|
+
const didRun = await flushDeferredAutoTitle({
|
|
32
|
+
sessionId: 'session-1',
|
|
33
|
+
sessionName: 'Session 3/17/2026',
|
|
34
|
+
model: 'gpt-test',
|
|
35
|
+
queuedCandidate: { sessionId: 'session-1', seedText: 'hello world' },
|
|
36
|
+
generatingSessionId: null,
|
|
37
|
+
autoTitleSession: async (sessionId, payload) => {
|
|
38
|
+
calls.push({ sessionId, ...payload })
|
|
39
|
+
return { name: 'Hello world summary', titleStatus: 'generated' }
|
|
40
|
+
},
|
|
41
|
+
onGenerationChange: (sessionId) => generationStates.push(sessionId),
|
|
42
|
+
onResolved: (response) => resolved.push({ name: response.name }),
|
|
43
|
+
clearQueuedCandidate: () => {
|
|
44
|
+
cleared += 1
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
assert.equal(didRun, true)
|
|
49
|
+
assert.deepEqual(calls, [
|
|
50
|
+
{ sessionId: 'session-1', model: 'gpt-test', seedText: 'hello world' },
|
|
51
|
+
])
|
|
52
|
+
assert.deepEqual(generationStates, ['session-1', null])
|
|
53
|
+
assert.deepEqual(resolved, [{ name: 'Hello world summary' }])
|
|
54
|
+
assert.equal(cleared, 1)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('flushDeferredAutoTitle skips duplicate or stale candidates', async () => {
|
|
58
|
+
let called = false
|
|
59
|
+
|
|
60
|
+
const staleRun = await flushDeferredAutoTitle({
|
|
61
|
+
sessionId: 'session-1',
|
|
62
|
+
sessionName: 'Session 3/17/2026',
|
|
63
|
+
queuedCandidate: { sessionId: 'session-2', seedText: 'hello world' },
|
|
64
|
+
generatingSessionId: null,
|
|
65
|
+
autoTitleSession: async () => {
|
|
66
|
+
called = true
|
|
67
|
+
return { name: 'unused' }
|
|
68
|
+
},
|
|
69
|
+
onGenerationChange: () => undefined,
|
|
70
|
+
onResolved: () => undefined,
|
|
71
|
+
clearQueuedCandidate: () => undefined,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const duplicateRun = await flushDeferredAutoTitle({
|
|
75
|
+
sessionId: 'session-1',
|
|
76
|
+
sessionName: 'Session 3/17/2026',
|
|
77
|
+
queuedCandidate: { sessionId: 'session-1', seedText: 'hello world' },
|
|
78
|
+
generatingSessionId: 'session-1',
|
|
79
|
+
autoTitleSession: async () => {
|
|
80
|
+
called = true
|
|
81
|
+
return { name: 'unused' }
|
|
82
|
+
},
|
|
83
|
+
onGenerationChange: () => undefined,
|
|
84
|
+
onResolved: () => undefined,
|
|
85
|
+
clearQueuedCandidate: () => undefined,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
assert.equal(staleRun, false)
|
|
89
|
+
assert.equal(duplicateRun, false)
|
|
90
|
+
assert.equal(called, false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('flushDeferredAutoTitle clears status on failure and non-default names do not retry', async () => {
|
|
94
|
+
const generationStates: Array<string | null> = []
|
|
95
|
+
let cleared = 0
|
|
96
|
+
const errors: unknown[] = []
|
|
97
|
+
|
|
98
|
+
const failedRun = await flushDeferredAutoTitle({
|
|
99
|
+
sessionId: 'session-1',
|
|
100
|
+
sessionName: 'Session 3/17/2026',
|
|
101
|
+
queuedCandidate: { sessionId: 'session-1', seedText: 'hello world' },
|
|
102
|
+
generatingSessionId: null,
|
|
103
|
+
autoTitleSession: async () => {
|
|
104
|
+
throw new Error('boom')
|
|
105
|
+
},
|
|
106
|
+
onGenerationChange: (sessionId) => generationStates.push(sessionId),
|
|
107
|
+
onResolved: () => undefined,
|
|
108
|
+
clearQueuedCandidate: () => {
|
|
109
|
+
cleared += 1
|
|
110
|
+
},
|
|
111
|
+
onError: (error) => errors.push(error),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
assert.equal(failedRun, false)
|
|
115
|
+
assert.deepEqual(generationStates, ['session-1', null])
|
|
116
|
+
assert.equal(cleared, 1)
|
|
117
|
+
assert.equal(errors.length, 1)
|
|
118
|
+
|
|
119
|
+
const sessions = applyAutoTitleToSessions(
|
|
120
|
+
[{ id: 'session-1', name: 'Session 3/17/2026', messageCount: 1, createdAt: '', updatedAt: '' }],
|
|
121
|
+
'session-1',
|
|
122
|
+
{ name: 'Trip planning', titleStatus: 'generated', titleUpdatedAt: 'now' }
|
|
123
|
+
)
|
|
124
|
+
assert.equal(
|
|
125
|
+
createDeferredAutoTitleCandidate('session-1', sessions[0].name, 'another message'),
|
|
126
|
+
null
|
|
127
|
+
)
|
|
128
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { SessionListItem } from '../api/client'
|
|
2
|
+
|
|
3
|
+
export interface DeferredAutoTitleCandidate {
|
|
4
|
+
sessionId: string
|
|
5
|
+
seedText: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AutoTitleSessionResponse {
|
|
9
|
+
name: string
|
|
10
|
+
titleStatus?: 'pending' | 'generated' | 'manual' | 'failed'
|
|
11
|
+
titleUpdatedAt?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_SESSION_NAME_PATTERN = /^Session\s+\d{1,2}\/\d{1,2}\/\d{2,4}$/
|
|
15
|
+
|
|
16
|
+
export function isDefaultSessionName(sessionName: string): boolean {
|
|
17
|
+
return DEFAULT_SESSION_NAME_PATTERN.test(sessionName)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createDeferredAutoTitleCandidate(
|
|
21
|
+
sessionId: string | null,
|
|
22
|
+
sessionName: string,
|
|
23
|
+
seedText: string
|
|
24
|
+
): DeferredAutoTitleCandidate | null {
|
|
25
|
+
const trimmed = seedText.trim()
|
|
26
|
+
if (!sessionId || !trimmed || !isDefaultSessionName(sessionName)) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
sessionId,
|
|
31
|
+
seedText: trimmed,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function applyAutoTitleToSessions(
|
|
36
|
+
sessions: SessionListItem[],
|
|
37
|
+
sessionId: string,
|
|
38
|
+
response: AutoTitleSessionResponse
|
|
39
|
+
): SessionListItem[] {
|
|
40
|
+
return sessions.map((item) =>
|
|
41
|
+
item.id === sessionId
|
|
42
|
+
? {
|
|
43
|
+
...item,
|
|
44
|
+
name: response.name,
|
|
45
|
+
titleStatus: response.titleStatus,
|
|
46
|
+
titleUpdatedAt: response.titleUpdatedAt,
|
|
47
|
+
}
|
|
48
|
+
: item
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface FlushDeferredAutoTitleArgs {
|
|
53
|
+
sessionId: string
|
|
54
|
+
sessionName: string
|
|
55
|
+
model?: string
|
|
56
|
+
queuedCandidate: DeferredAutoTitleCandidate | null
|
|
57
|
+
generatingSessionId: string | null
|
|
58
|
+
autoTitleSession: (
|
|
59
|
+
sessionId: string,
|
|
60
|
+
payload: { model?: string; seedText?: string }
|
|
61
|
+
) => Promise<AutoTitleSessionResponse>
|
|
62
|
+
onGenerationChange: (sessionId: string | null) => void
|
|
63
|
+
onResolved: (response: AutoTitleSessionResponse) => void
|
|
64
|
+
clearQueuedCandidate: () => void
|
|
65
|
+
onError?: (error: unknown) => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function flushDeferredAutoTitle({
|
|
69
|
+
sessionId,
|
|
70
|
+
sessionName,
|
|
71
|
+
model,
|
|
72
|
+
queuedCandidate,
|
|
73
|
+
generatingSessionId,
|
|
74
|
+
autoTitleSession,
|
|
75
|
+
onGenerationChange,
|
|
76
|
+
onResolved,
|
|
77
|
+
clearQueuedCandidate,
|
|
78
|
+
onError,
|
|
79
|
+
}: FlushDeferredAutoTitleArgs): Promise<boolean> {
|
|
80
|
+
if (!queuedCandidate || queuedCandidate.sessionId !== sessionId) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
if (generatingSessionId === sessionId) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
if (!isDefaultSessionName(sessionName)) {
|
|
87
|
+
clearQueuedCandidate()
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
clearQueuedCandidate()
|
|
92
|
+
onGenerationChange(sessionId)
|
|
93
|
+
try {
|
|
94
|
+
const response = await autoTitleSession(sessionId, {
|
|
95
|
+
model,
|
|
96
|
+
seedText: queuedCandidate.seedText,
|
|
97
|
+
})
|
|
98
|
+
onResolved(response)
|
|
99
|
+
return true
|
|
100
|
+
} catch (error) {
|
|
101
|
+
onError?.(error)
|
|
102
|
+
return false
|
|
103
|
+
} finally {
|
|
104
|
+
onGenerationChange(null)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Store
|
|
3
|
+
*
|
|
4
|
+
* Manages user preferences with localStorage persistence.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type ImageSize = '256x256' | '512x512' | '1024x1024' | '1024x1792' | '1792x1024'
|
|
8
|
+
|
|
9
|
+
export interface UserSettings {
|
|
10
|
+
defaultImageSize: ImageSize
|
|
11
|
+
// Future settings can be added here
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const STORAGE_KEY = 'waypoi-settings'
|
|
15
|
+
|
|
16
|
+
const DEFAULT_SETTINGS: UserSettings = {
|
|
17
|
+
defaultImageSize: '1024x1024',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Available image size options
|
|
21
|
+
export const IMAGE_SIZE_OPTIONS: { value: ImageSize; label: string; aspect: string }[] = [
|
|
22
|
+
{ value: '256x256', label: '256×256', aspect: '1:1' },
|
|
23
|
+
{ value: '512x512', label: '512×512', aspect: '1:1' },
|
|
24
|
+
{ value: '1024x1024', label: '1024×1024', aspect: '1:1' },
|
|
25
|
+
{ value: '1024x1792', label: '1024×1792', aspect: '9:16 (Portrait)' },
|
|
26
|
+
{ value: '1792x1024', label: '1792×1024', aspect: '16:9 (Landscape)' },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
export function loadSettings(): UserSettings {
|
|
30
|
+
try {
|
|
31
|
+
const stored = localStorage.getItem(STORAGE_KEY)
|
|
32
|
+
if (stored) {
|
|
33
|
+
const parsed = JSON.parse(stored)
|
|
34
|
+
return { ...DEFAULT_SETTINGS, ...parsed }
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to load settings:', error)
|
|
38
|
+
}
|
|
39
|
+
return DEFAULT_SETTINGS
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function saveSettings(settings: UserSettings): void {
|
|
43
|
+
try {
|
|
44
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Failed to save settings:', error)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function updateSetting<K extends keyof UserSettings>(
|
|
51
|
+
key: K,
|
|
52
|
+
value: UserSettings[K]
|
|
53
|
+
): UserSettings {
|
|
54
|
+
const settings = loadSettings()
|
|
55
|
+
settings[key] = value
|
|
56
|
+
saveSettings(settings)
|
|
57
|
+
return settings
|
|
58
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
|
2
|
+
|
|
3
|
+
@tailwind base;
|
|
4
|
+
@tailwind components;
|
|
5
|
+
@tailwind utilities;
|
|
6
|
+
|
|
7
|
+
@layer base {
|
|
8
|
+
:root {
|
|
9
|
+
/* Waypoi Industrial Dark Theme
|
|
10
|
+
Inspired by control room interfaces and technical dashboards
|
|
11
|
+
Deep charcoal base with amber/gold accents */
|
|
12
|
+
|
|
13
|
+
--background: 220 15% 6%;
|
|
14
|
+
--foreground: 40 15% 92%;
|
|
15
|
+
|
|
16
|
+
--card: 220 15% 8%;
|
|
17
|
+
--card-foreground: 40 15% 92%;
|
|
18
|
+
|
|
19
|
+
--popover: 220 15% 10%;
|
|
20
|
+
--popover-foreground: 40 15% 92%;
|
|
21
|
+
|
|
22
|
+
/* Amber/Gold primary - warm industrial accent */
|
|
23
|
+
--primary: 38 92% 50%;
|
|
24
|
+
--primary-foreground: 220 15% 6%;
|
|
25
|
+
|
|
26
|
+
--secondary: 220 10% 15%;
|
|
27
|
+
--secondary-foreground: 40 10% 75%;
|
|
28
|
+
|
|
29
|
+
--muted: 220 10% 12%;
|
|
30
|
+
--muted-foreground: 220 10% 50%;
|
|
31
|
+
|
|
32
|
+
--accent: 38 70% 45%;
|
|
33
|
+
--accent-foreground: 220 15% 6%;
|
|
34
|
+
|
|
35
|
+
--destructive: 0 72% 51%;
|
|
36
|
+
--destructive-foreground: 0 0% 100%;
|
|
37
|
+
|
|
38
|
+
--success: 142 71% 45%;
|
|
39
|
+
--success-foreground: 142 71% 10%;
|
|
40
|
+
|
|
41
|
+
--warning: 38 92% 50%;
|
|
42
|
+
--warning-foreground: 38 92% 10%;
|
|
43
|
+
|
|
44
|
+
--border: 220 10% 18%;
|
|
45
|
+
--input: 220 10% 14%;
|
|
46
|
+
--ring: 38 92% 50%;
|
|
47
|
+
|
|
48
|
+
--radius: 0.375rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
* {
|
|
52
|
+
@apply border-border;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
body {
|
|
56
|
+
@apply bg-background text-foreground font-sans antialiased;
|
|
57
|
+
font-feature-settings: "rlig" 1, "calt" 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Custom scrollbar - minimal and dark */
|
|
61
|
+
::-webkit-scrollbar {
|
|
62
|
+
width: 6px;
|
|
63
|
+
height: 6px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
::-webkit-scrollbar-track {
|
|
67
|
+
@apply bg-transparent;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
::-webkit-scrollbar-thumb {
|
|
71
|
+
@apply bg-border rounded-full;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
::-webkit-scrollbar-thumb:hover {
|
|
75
|
+
@apply bg-muted-foreground/50;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@layer components {
|
|
80
|
+
/* Industrial panel styling */
|
|
81
|
+
.panel {
|
|
82
|
+
@apply bg-card border border-border rounded-md;
|
|
83
|
+
box-shadow:
|
|
84
|
+
inset 0 1px 0 0 hsl(220 10% 20% / 0.5),
|
|
85
|
+
0 4px 6px -1px hsl(0 0% 0% / 0.3);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.panel-header {
|
|
89
|
+
@apply px-4 py-3 border-b border-border flex items-center gap-3;
|
|
90
|
+
background: linear-gradient(
|
|
91
|
+
180deg,
|
|
92
|
+
hsl(220 15% 10%) 0%,
|
|
93
|
+
hsl(220 15% 8%) 100%
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.panel-title {
|
|
98
|
+
@apply text-sm font-mono font-medium uppercase tracking-wider text-muted-foreground;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Status indicators */
|
|
102
|
+
.status-dot {
|
|
103
|
+
@apply w-2 h-2 rounded-full;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.status-dot-live {
|
|
107
|
+
@apply bg-success animate-pulse-glow;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.status-dot-degraded {
|
|
111
|
+
@apply bg-warning;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.status-dot-down {
|
|
115
|
+
@apply bg-destructive;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Terminal-style input */
|
|
119
|
+
.terminal-input {
|
|
120
|
+
@apply font-mono bg-input border-0 rounded-sm px-3 py-2 text-sm;
|
|
121
|
+
@apply focus:ring-1 focus:ring-primary focus:outline-none;
|
|
122
|
+
@apply placeholder:text-muted-foreground/50;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Metric card */
|
|
126
|
+
.metric-card {
|
|
127
|
+
@apply panel p-4 flex flex-col gap-1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.metric-label {
|
|
131
|
+
@apply text-2xs font-mono uppercase tracking-widest text-muted-foreground;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.metric-value {
|
|
135
|
+
@apply text-2xl font-mono font-semibold text-foreground tabular-nums;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.metric-unit {
|
|
139
|
+
@apply text-sm text-muted-foreground ml-1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Chat message bubbles */
|
|
143
|
+
.message-user {
|
|
144
|
+
@apply bg-primary/10 border border-primary/20 rounded-lg px-4 py-3;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.message-assistant {
|
|
148
|
+
@apply bg-secondary border border-border rounded-lg px-4 py-3;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.message-system {
|
|
152
|
+
@apply bg-muted/50 border border-dashed border-border rounded-lg px-4 py-3 text-muted-foreground;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Code blocks in chat */
|
|
156
|
+
.prose-code {
|
|
157
|
+
@apply font-mono text-sm bg-muted rounded px-1.5 py-0.5;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Glowing accent for important elements */
|
|
161
|
+
.glow-accent {
|
|
162
|
+
box-shadow: 0 0 20px hsl(38 92% 50% / 0.15);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* Navigation active state */
|
|
166
|
+
.nav-active {
|
|
167
|
+
@apply bg-primary/10 text-primary border-l-2 border-primary;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* Loading skeleton with industrial shimmer */
|
|
171
|
+
.skeleton {
|
|
172
|
+
@apply bg-muted animate-pulse rounded;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@layer utilities {
|
|
177
|
+
/* Text gradient for headings */
|
|
178
|
+
.text-gradient {
|
|
179
|
+
@apply bg-clip-text text-transparent;
|
|
180
|
+
background-image: linear-gradient(
|
|
181
|
+
135deg,
|
|
182
|
+
hsl(40 15% 92%) 0%,
|
|
183
|
+
hsl(38 92% 50%) 100%
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Noise texture overlay */
|
|
188
|
+
.noise-overlay {
|
|
189
|
+
position: relative;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.noise-overlay::before {
|
|
193
|
+
content: "";
|
|
194
|
+
position: absolute;
|
|
195
|
+
inset: 0;
|
|
196
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
|
197
|
+
opacity: 0.03;
|
|
198
|
+
pointer-events: none;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Grid pattern background */
|
|
202
|
+
.grid-pattern {
|
|
203
|
+
background-image:
|
|
204
|
+
linear-gradient(hsl(220 10% 15% / 0.5) 1px, transparent 1px),
|
|
205
|
+
linear-gradient(90deg, hsl(220 10% 15% / 0.5) 1px, transparent 1px);
|
|
206
|
+
background-size: 20px 20px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Scanline effect for retro feel */
|
|
210
|
+
.scanlines::after {
|
|
211
|
+
content: "";
|
|
212
|
+
position: absolute;
|
|
213
|
+
inset: 0;
|
|
214
|
+
background: repeating-linear-gradient(
|
|
215
|
+
0deg,
|
|
216
|
+
transparent,
|
|
217
|
+
transparent 2px,
|
|
218
|
+
hsl(0 0% 0% / 0.03) 2px,
|
|
219
|
+
hsl(0 0% 0% / 0.03) 4px
|
|
220
|
+
);
|
|
221
|
+
pointer-events: none;
|
|
222
|
+
}
|
|
223
|
+
}
|