@neocode-ai/web 1.1.1
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/README.md +54 -0
- package/astro.config.mjs +145 -0
- package/config.mjs +14 -0
- package/package.json +41 -0
- package/public/robots.txt +6 -0
- package/public/theme.json +183 -0
- package/src/assets/lander/check.svg +2 -0
- package/src/assets/lander/copy.svg +2 -0
- package/src/assets/lander/screenshot-github.png +0 -0
- package/src/assets/lander/screenshot-splash.png +0 -0
- package/src/assets/lander/screenshot-vscode.png +0 -0
- package/src/assets/lander/screenshot.png +0 -0
- package/src/assets/logo-dark.svg +20 -0
- package/src/assets/logo-light.svg +20 -0
- package/src/assets/logo-ornate-dark.svg +18 -0
- package/src/assets/logo-ornate-light.svg +18 -0
- package/src/assets/web/web-homepage-active-session.png +0 -0
- package/src/assets/web/web-homepage-new-session.png +0 -0
- package/src/assets/web/web-homepage-see-servers.png +0 -0
- package/src/components/Head.astro +50 -0
- package/src/components/Header.astro +128 -0
- package/src/components/Hero.astro +11 -0
- package/src/components/Lander.astro +713 -0
- package/src/components/Share.tsx +634 -0
- package/src/components/SiteTitle.astro +59 -0
- package/src/components/icons/custom.tsx +87 -0
- package/src/components/icons/index.tsx +4454 -0
- package/src/components/share/common.tsx +77 -0
- package/src/components/share/content-bash.module.css +85 -0
- package/src/components/share/content-bash.tsx +67 -0
- package/src/components/share/content-code.module.css +26 -0
- package/src/components/share/content-code.tsx +32 -0
- package/src/components/share/content-diff.module.css +153 -0
- package/src/components/share/content-diff.tsx +231 -0
- package/src/components/share/content-error.module.css +64 -0
- package/src/components/share/content-error.tsx +24 -0
- package/src/components/share/content-markdown.module.css +154 -0
- package/src/components/share/content-markdown.tsx +75 -0
- package/src/components/share/content-text.module.css +63 -0
- package/src/components/share/content-text.tsx +37 -0
- package/src/components/share/copy-button.module.css +30 -0
- package/src/components/share/copy-button.tsx +28 -0
- package/src/components/share/part.module.css +428 -0
- package/src/components/share/part.tsx +780 -0
- package/src/components/share.module.css +832 -0
- package/src/content/docs/1-0.mdx +67 -0
- package/src/content/docs/acp.mdx +156 -0
- package/src/content/docs/agents.mdx +720 -0
- package/src/content/docs/cli.mdx +597 -0
- package/src/content/docs/commands.mdx +323 -0
- package/src/content/docs/config.mdx +683 -0
- package/src/content/docs/custom-tools.mdx +170 -0
- package/src/content/docs/ecosystem.mdx +76 -0
- package/src/content/docs/enterprise.mdx +170 -0
- package/src/content/docs/formatters.mdx +130 -0
- package/src/content/docs/github.mdx +321 -0
- package/src/content/docs/gitlab.mdx +195 -0
- package/src/content/docs/ide.mdx +48 -0
- package/src/content/docs/index.mdx +359 -0
- package/src/content/docs/keybinds.mdx +191 -0
- package/src/content/docs/lsp.mdx +188 -0
- package/src/content/docs/mcp-servers.mdx +511 -0
- package/src/content/docs/models.mdx +223 -0
- package/src/content/docs/modes.mdx +331 -0
- package/src/content/docs/network.mdx +57 -0
- package/src/content/docs/permissions.mdx +237 -0
- package/src/content/docs/plugins.mdx +362 -0
- package/src/content/docs/providers.mdx +1889 -0
- package/src/content/docs/rules.mdx +180 -0
- package/src/content/docs/sdk.mdx +391 -0
- package/src/content/docs/server.mdx +286 -0
- package/src/content/docs/share.mdx +128 -0
- package/src/content/docs/skills.mdx +220 -0
- package/src/content/docs/themes.mdx +369 -0
- package/src/content/docs/tools.mdx +345 -0
- package/src/content/docs/troubleshooting.mdx +300 -0
- package/src/content/docs/tui.mdx +390 -0
- package/src/content/docs/web.mdx +136 -0
- package/src/content/docs/windows-wsl.mdx +113 -0
- package/src/content/docs/zen.mdx +251 -0
- package/src/content.config.ts +7 -0
- package/src/pages/[...slug].md.ts +18 -0
- package/src/pages/s/[id].astro +113 -0
- package/src/styles/custom.css +405 -0
- package/src/types/lang-map.d.ts +27 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { For, Show, onMount, Suspense, onCleanup, createMemo, createSignal, SuspenseList, createEffect } from "solid-js"
|
|
2
|
+
import { DateTime } from "luxon"
|
|
3
|
+
import { createStore, reconcile, unwrap } from "solid-js/store"
|
|
4
|
+
import { IconArrowDown } from "./icons"
|
|
5
|
+
import { IconNeocode } from "./icons/custom"
|
|
6
|
+
import styles from "./share.module.css"
|
|
7
|
+
import type { MessageV2 } from "neocode/session/message-v2"
|
|
8
|
+
import type { Message } from "neocode/session/message"
|
|
9
|
+
import type { Session } from "neocode/session/index"
|
|
10
|
+
import { Part, ProviderIcon } from "./share/part"
|
|
11
|
+
|
|
12
|
+
type MessageWithParts = MessageV2.Info & { parts: MessageV2.Part[] }
|
|
13
|
+
|
|
14
|
+
type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting"
|
|
15
|
+
|
|
16
|
+
function scrollToAnchor(id: string) {
|
|
17
|
+
const el = document.getElementById(id)
|
|
18
|
+
if (!el) return
|
|
19
|
+
|
|
20
|
+
el.scrollIntoView({ behavior: "smooth" })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getStatusText(status: [Status, string?]): string {
|
|
24
|
+
switch (status[0]) {
|
|
25
|
+
case "connected":
|
|
26
|
+
return "Connected, waiting for messages..."
|
|
27
|
+
case "connecting":
|
|
28
|
+
return "Connecting..."
|
|
29
|
+
case "disconnected":
|
|
30
|
+
return "Disconnected"
|
|
31
|
+
case "reconnecting":
|
|
32
|
+
return "Reconnecting..."
|
|
33
|
+
case "error":
|
|
34
|
+
return status[1] || "Error"
|
|
35
|
+
default:
|
|
36
|
+
return "Unknown"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function Share(props: { id: string; api: string; info: Session.Info }) {
|
|
41
|
+
let lastScrollY = 0
|
|
42
|
+
let hasScrolledToAnchor = false
|
|
43
|
+
let scrollTimeout: number | undefined
|
|
44
|
+
let scrollSentinel: HTMLElement | undefined
|
|
45
|
+
let scrollObserver: IntersectionObserver | undefined
|
|
46
|
+
|
|
47
|
+
const params = new URLSearchParams(window.location.search)
|
|
48
|
+
const debug = params.get("debug") === "true"
|
|
49
|
+
|
|
50
|
+
const [showScrollButton, setShowScrollButton] = createSignal(false)
|
|
51
|
+
const [isButtonHovered, setIsButtonHovered] = createSignal(false)
|
|
52
|
+
const [isNearBottom, setIsNearBottom] = createSignal(false)
|
|
53
|
+
|
|
54
|
+
const [store, setStore] = createStore<{
|
|
55
|
+
info?: Session.Info
|
|
56
|
+
messages: Record<string, MessageWithParts>
|
|
57
|
+
}>({
|
|
58
|
+
info: {
|
|
59
|
+
id: props.id,
|
|
60
|
+
title: props.info.title,
|
|
61
|
+
version: props.info.version,
|
|
62
|
+
time: {
|
|
63
|
+
created: props.info.time.created,
|
|
64
|
+
updated: props.info.time.updated,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
messages: {},
|
|
68
|
+
})
|
|
69
|
+
const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)))
|
|
70
|
+
const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"])
|
|
71
|
+
createEffect(() => {
|
|
72
|
+
console.log(unwrap(store))
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
onMount(() => {
|
|
76
|
+
const apiUrl = props.api
|
|
77
|
+
|
|
78
|
+
if (!props.id) {
|
|
79
|
+
setConnectionStatus(["error", "id not found"])
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!apiUrl) {
|
|
84
|
+
console.error("API URL not found in environment variables")
|
|
85
|
+
setConnectionStatus(["error", "API URL not found"])
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let reconnectTimer: number | undefined
|
|
90
|
+
let socket: WebSocket | null = null
|
|
91
|
+
|
|
92
|
+
// Function to create and set up WebSocket with auto-reconnect
|
|
93
|
+
const setupWebSocket = () => {
|
|
94
|
+
// Close any existing connection
|
|
95
|
+
if (socket) {
|
|
96
|
+
socket.close()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
setConnectionStatus(["connecting"])
|
|
100
|
+
|
|
101
|
+
// Always use secure WebSocket protocol (wss)
|
|
102
|
+
const wsBaseUrl = apiUrl.replace(/^https?:\/\//, "wss://")
|
|
103
|
+
const wsUrl = `${wsBaseUrl}/share_poll?id=${props.id}`
|
|
104
|
+
console.log("Connecting to WebSocket URL:", wsUrl)
|
|
105
|
+
|
|
106
|
+
// Create WebSocket connection
|
|
107
|
+
socket = new WebSocket(wsUrl)
|
|
108
|
+
|
|
109
|
+
// Handle connection opening
|
|
110
|
+
socket.onopen = () => {
|
|
111
|
+
setConnectionStatus(["connected"])
|
|
112
|
+
console.log("WebSocket connection established")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle incoming messages
|
|
116
|
+
socket.onmessage = (event) => {
|
|
117
|
+
console.log("WebSocket message received")
|
|
118
|
+
try {
|
|
119
|
+
const d = JSON.parse(event.data)
|
|
120
|
+
const [root, type, ...splits] = d.key.split("/")
|
|
121
|
+
if (root !== "session") return
|
|
122
|
+
if (type === "info") {
|
|
123
|
+
setStore("info", reconcile(d.content))
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
if (type === "message") {
|
|
127
|
+
const [, messageID] = splits
|
|
128
|
+
if ("metadata" in d.content) {
|
|
129
|
+
d.content = fromV1(d.content)
|
|
130
|
+
}
|
|
131
|
+
d.content.parts = d.content.parts ?? store.messages[messageID]?.parts ?? []
|
|
132
|
+
setStore("messages", messageID, reconcile(d.content))
|
|
133
|
+
}
|
|
134
|
+
if (type === "part") {
|
|
135
|
+
setStore("messages", d.content.messageID, "parts", (arr) => {
|
|
136
|
+
const index = arr.findIndex((x) => x.id === d.content.id)
|
|
137
|
+
if (index === -1) arr.push(d.content)
|
|
138
|
+
if (index > -1) arr[index] = d.content
|
|
139
|
+
return [...arr]
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error("Error parsing WebSocket message:", error)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle errors
|
|
148
|
+
socket.onerror = (error) => {
|
|
149
|
+
console.error("WebSocket error:", error)
|
|
150
|
+
setConnectionStatus(["error", "Connection failed"])
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle connection close and reconnection
|
|
154
|
+
socket.onclose = (event) => {
|
|
155
|
+
console.log(`WebSocket closed: ${event.code} ${event.reason}`)
|
|
156
|
+
setConnectionStatus(["reconnecting"])
|
|
157
|
+
|
|
158
|
+
// Try to reconnect after 2 seconds
|
|
159
|
+
clearTimeout(reconnectTimer)
|
|
160
|
+
reconnectTimer = window.setTimeout(setupWebSocket, 2000) as unknown as number
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Initial connection
|
|
165
|
+
setupWebSocket()
|
|
166
|
+
|
|
167
|
+
// Clean up on component unmount
|
|
168
|
+
onCleanup(() => {
|
|
169
|
+
console.log("Cleaning up WebSocket connection")
|
|
170
|
+
if (socket) {
|
|
171
|
+
socket.close()
|
|
172
|
+
}
|
|
173
|
+
clearTimeout(reconnectTimer)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
function checkScrollNeed() {
|
|
178
|
+
const currentScrollY = window.scrollY
|
|
179
|
+
const isScrollingDown = currentScrollY > lastScrollY
|
|
180
|
+
const scrolled = currentScrollY > 200 // Show after scrolling 200px
|
|
181
|
+
|
|
182
|
+
// Only show when scrolling down, scrolled enough, and not near bottom
|
|
183
|
+
const shouldShow = isScrollingDown && scrolled && !isNearBottom()
|
|
184
|
+
|
|
185
|
+
// Update last scroll position
|
|
186
|
+
lastScrollY = currentScrollY
|
|
187
|
+
|
|
188
|
+
if (shouldShow) {
|
|
189
|
+
setShowScrollButton(true)
|
|
190
|
+
// Clear existing timeout
|
|
191
|
+
if (scrollTimeout) {
|
|
192
|
+
clearTimeout(scrollTimeout)
|
|
193
|
+
}
|
|
194
|
+
// Hide button after 3 seconds of no scrolling (unless hovered)
|
|
195
|
+
scrollTimeout = window.setTimeout(() => {
|
|
196
|
+
if (!isButtonHovered()) {
|
|
197
|
+
setShowScrollButton(false)
|
|
198
|
+
}
|
|
199
|
+
}, 1500)
|
|
200
|
+
} else if (!isButtonHovered()) {
|
|
201
|
+
// Only hide if not hovered (to prevent disappearing while user is about to click)
|
|
202
|
+
setShowScrollButton(false)
|
|
203
|
+
if (scrollTimeout) {
|
|
204
|
+
clearTimeout(scrollTimeout)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
onMount(() => {
|
|
210
|
+
lastScrollY = window.scrollY // Initialize scroll position
|
|
211
|
+
|
|
212
|
+
// Create sentinel element
|
|
213
|
+
const sentinel = document.createElement("div")
|
|
214
|
+
sentinel.style.height = "1px"
|
|
215
|
+
sentinel.style.position = "absolute"
|
|
216
|
+
sentinel.style.bottom = "100px"
|
|
217
|
+
sentinel.style.width = "100%"
|
|
218
|
+
sentinel.style.pointerEvents = "none"
|
|
219
|
+
document.body.appendChild(sentinel)
|
|
220
|
+
|
|
221
|
+
// Create intersection observer
|
|
222
|
+
const observer = new IntersectionObserver((entries) => {
|
|
223
|
+
setIsNearBottom(entries[0].isIntersecting)
|
|
224
|
+
})
|
|
225
|
+
observer.observe(sentinel)
|
|
226
|
+
|
|
227
|
+
// Store references for cleanup
|
|
228
|
+
scrollSentinel = sentinel
|
|
229
|
+
scrollObserver = observer
|
|
230
|
+
|
|
231
|
+
checkScrollNeed()
|
|
232
|
+
window.addEventListener("scroll", checkScrollNeed)
|
|
233
|
+
window.addEventListener("resize", checkScrollNeed)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
onCleanup(() => {
|
|
237
|
+
window.removeEventListener("scroll", checkScrollNeed)
|
|
238
|
+
window.removeEventListener("resize", checkScrollNeed)
|
|
239
|
+
|
|
240
|
+
// Clean up observer and sentinel
|
|
241
|
+
if (scrollObserver) {
|
|
242
|
+
scrollObserver.disconnect()
|
|
243
|
+
}
|
|
244
|
+
if (scrollSentinel) {
|
|
245
|
+
document.body.removeChild(scrollSentinel)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (scrollTimeout) {
|
|
249
|
+
clearTimeout(scrollTimeout)
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const data = createMemo(() => {
|
|
254
|
+
const result = {
|
|
255
|
+
rootDir: undefined as string | undefined,
|
|
256
|
+
created: undefined as number | undefined,
|
|
257
|
+
completed: undefined as number | undefined,
|
|
258
|
+
messages: [] as MessageWithParts[],
|
|
259
|
+
models: {} as Record<string, string[]>,
|
|
260
|
+
cost: 0,
|
|
261
|
+
tokens: {
|
|
262
|
+
input: 0,
|
|
263
|
+
output: 0,
|
|
264
|
+
reasoning: 0,
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!store.info) return result
|
|
269
|
+
|
|
270
|
+
result.created = store.info.time.created
|
|
271
|
+
|
|
272
|
+
const msgs = messages()
|
|
273
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
274
|
+
const msg = msgs[i]
|
|
275
|
+
|
|
276
|
+
result.messages.push(msg)
|
|
277
|
+
|
|
278
|
+
if (msg.role === "assistant") {
|
|
279
|
+
result.cost += msg.cost
|
|
280
|
+
result.tokens.input += msg.tokens.input
|
|
281
|
+
result.tokens.output += msg.tokens.output
|
|
282
|
+
result.tokens.reasoning += msg.tokens.reasoning
|
|
283
|
+
|
|
284
|
+
result.models[`${msg.providerID} ${msg.modelID}`] = [msg.providerID, msg.modelID]
|
|
285
|
+
|
|
286
|
+
if (msg.path.root) {
|
|
287
|
+
result.rootDir = msg.path.root
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (msg.time.completed) {
|
|
291
|
+
result.completed = msg.time.completed
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return result
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<Show when={store.info}>
|
|
300
|
+
<main classList={{ [styles.root]: true, "not-content": true }}>
|
|
301
|
+
<div data-component="header">
|
|
302
|
+
<h1 data-component="header-title">{store.info?.title}</h1>
|
|
303
|
+
<div data-component="header-details">
|
|
304
|
+
<ul data-component="header-stats">
|
|
305
|
+
<li title="neocode version" data-slot="item">
|
|
306
|
+
<div data-slot="icon" title="neocode">
|
|
307
|
+
<IconNeocode width={16} height={16} />
|
|
308
|
+
</div>
|
|
309
|
+
<Show when={store.info?.version} fallback="v0.0.1">
|
|
310
|
+
<span>v{store.info?.version}</span>
|
|
311
|
+
</Show>
|
|
312
|
+
</li>
|
|
313
|
+
{Object.values(data().models).length > 0 ? (
|
|
314
|
+
<For each={Object.values(data().models)}>
|
|
315
|
+
{([provider, model]) => (
|
|
316
|
+
<li data-slot="item">
|
|
317
|
+
<div data-slot="icon" title={provider}>
|
|
318
|
+
<ProviderIcon model={model} />
|
|
319
|
+
</div>
|
|
320
|
+
<span data-slot="model">{model}</span>
|
|
321
|
+
</li>
|
|
322
|
+
)}
|
|
323
|
+
</For>
|
|
324
|
+
) : (
|
|
325
|
+
<li>
|
|
326
|
+
<span data-element-label>Models</span>
|
|
327
|
+
<span data-placeholder>—</span>
|
|
328
|
+
</li>
|
|
329
|
+
)}
|
|
330
|
+
</ul>
|
|
331
|
+
<div
|
|
332
|
+
data-component="header-time"
|
|
333
|
+
title={DateTime.fromMillis(data().created || 0).toLocaleString(DateTime.DATETIME_FULL_WITH_SECONDS)}
|
|
334
|
+
>
|
|
335
|
+
{DateTime.fromMillis(data().created || 0).toLocaleString(DateTime.DATETIME_MED)}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<div>
|
|
341
|
+
<Show when={data().messages.length > 0} fallback={<p>Waiting for messages...</p>}>
|
|
342
|
+
<div class={styles.parts}>
|
|
343
|
+
<SuspenseList revealOrder="forwards">
|
|
344
|
+
<For each={data().messages}>
|
|
345
|
+
{(msg, msgIndex) => {
|
|
346
|
+
const filteredParts = createMemo(() =>
|
|
347
|
+
msg.parts.filter((x, index) => {
|
|
348
|
+
if (x.type === "step-start" && index > 0) return false
|
|
349
|
+
if (x.type === "snapshot") return false
|
|
350
|
+
if (x.type === "patch") return false
|
|
351
|
+
if (x.type === "step-finish") return false
|
|
352
|
+
if (x.type === "text" && x.synthetic === true) return false
|
|
353
|
+
if (x.type === "tool" && x.tool === "todoread") return false
|
|
354
|
+
if (x.type === "text" && !x.text) return false
|
|
355
|
+
if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running"))
|
|
356
|
+
return false
|
|
357
|
+
return true
|
|
358
|
+
}),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<Suspense>
|
|
363
|
+
<For each={filteredParts()}>
|
|
364
|
+
{(part, partIndex) => {
|
|
365
|
+
const last = createMemo(
|
|
366
|
+
() =>
|
|
367
|
+
data().messages.length === msgIndex() + 1 && filteredParts().length === partIndex() + 1,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
onMount(() => {
|
|
371
|
+
const hash = window.location.hash.slice(1)
|
|
372
|
+
// Wait till all parts are loaded
|
|
373
|
+
if (
|
|
374
|
+
hash !== "" &&
|
|
375
|
+
!hasScrolledToAnchor &&
|
|
376
|
+
filteredParts().length === partIndex() + 1 &&
|
|
377
|
+
data().messages.length === msgIndex() + 1
|
|
378
|
+
) {
|
|
379
|
+
hasScrolledToAnchor = true
|
|
380
|
+
scrollToAnchor(hash)
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
return <Part last={last()} part={part} index={partIndex()} message={msg} />
|
|
385
|
+
}}
|
|
386
|
+
</For>
|
|
387
|
+
</Suspense>
|
|
388
|
+
)
|
|
389
|
+
}}
|
|
390
|
+
</For>
|
|
391
|
+
</SuspenseList>
|
|
392
|
+
<div data-section="part" data-part-type="summary">
|
|
393
|
+
<div data-section="decoration">
|
|
394
|
+
<span data-status={connectionStatus()[0]}></span>
|
|
395
|
+
</div>
|
|
396
|
+
<div data-section="content">
|
|
397
|
+
<p data-section="copy">{getStatusText(connectionStatus())}</p>
|
|
398
|
+
<ul data-section="stats">
|
|
399
|
+
<li>
|
|
400
|
+
<span data-element-label>Cost</span>
|
|
401
|
+
{data().cost !== undefined ? (
|
|
402
|
+
<span>${data().cost.toFixed(2)}</span>
|
|
403
|
+
) : (
|
|
404
|
+
<span data-placeholder>—</span>
|
|
405
|
+
)}
|
|
406
|
+
</li>
|
|
407
|
+
<li>
|
|
408
|
+
<span data-element-label>Input Tokens</span>
|
|
409
|
+
{data().tokens.input ? <span>{data().tokens.input}</span> : <span data-placeholder>—</span>}
|
|
410
|
+
</li>
|
|
411
|
+
<li>
|
|
412
|
+
<span data-element-label>Output Tokens</span>
|
|
413
|
+
{data().tokens.output ? (
|
|
414
|
+
<span>{data().tokens.output}</span>
|
|
415
|
+
) : (
|
|
416
|
+
<span data-placeholder>—</span>
|
|
417
|
+
)}
|
|
418
|
+
</li>
|
|
419
|
+
<li>
|
|
420
|
+
<span data-element-label>Reasoning Tokens</span>
|
|
421
|
+
{data().tokens.reasoning ? (
|
|
422
|
+
<span>{data().tokens.reasoning}</span>
|
|
423
|
+
) : (
|
|
424
|
+
<span data-placeholder>—</span>
|
|
425
|
+
)}
|
|
426
|
+
</li>
|
|
427
|
+
</ul>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
</Show>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
<Show when={debug}>
|
|
435
|
+
<div style={{ margin: "2rem 0" }}>
|
|
436
|
+
<div
|
|
437
|
+
style={{
|
|
438
|
+
border: "1px solid #ccc",
|
|
439
|
+
padding: "1rem",
|
|
440
|
+
"overflow-y": "auto",
|
|
441
|
+
}}
|
|
442
|
+
>
|
|
443
|
+
<Show when={data().messages.length > 0} fallback={<p>Waiting for messages...</p>}>
|
|
444
|
+
<ul style={{ "list-style-type": "none", padding: 0 }}>
|
|
445
|
+
<For each={data().messages}>
|
|
446
|
+
{(msg) => (
|
|
447
|
+
<li
|
|
448
|
+
style={{
|
|
449
|
+
padding: "0.75rem",
|
|
450
|
+
margin: "0.75rem 0",
|
|
451
|
+
"box-shadow": "0 1px 3px rgba(0,0,0,0.1)",
|
|
452
|
+
}}
|
|
453
|
+
>
|
|
454
|
+
<div>
|
|
455
|
+
<strong>Key:</strong> {msg.id}
|
|
456
|
+
</div>
|
|
457
|
+
<pre>{JSON.stringify(msg, null, 2)}</pre>
|
|
458
|
+
</li>
|
|
459
|
+
)}
|
|
460
|
+
</For>
|
|
461
|
+
</ul>
|
|
462
|
+
</Show>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
</Show>
|
|
466
|
+
|
|
467
|
+
<Show when={showScrollButton()}>
|
|
468
|
+
<button
|
|
469
|
+
type="button"
|
|
470
|
+
class={styles["scroll-button"]}
|
|
471
|
+
onClick={() => document.body.scrollIntoView({ behavior: "smooth", block: "end" })}
|
|
472
|
+
onMouseEnter={() => {
|
|
473
|
+
setIsButtonHovered(true)
|
|
474
|
+
if (scrollTimeout) {
|
|
475
|
+
clearTimeout(scrollTimeout)
|
|
476
|
+
}
|
|
477
|
+
}}
|
|
478
|
+
onMouseLeave={() => {
|
|
479
|
+
setIsButtonHovered(false)
|
|
480
|
+
if (showScrollButton()) {
|
|
481
|
+
scrollTimeout = window.setTimeout(() => {
|
|
482
|
+
if (!isButtonHovered()) {
|
|
483
|
+
setShowScrollButton(false)
|
|
484
|
+
}
|
|
485
|
+
}, 3000)
|
|
486
|
+
}
|
|
487
|
+
}}
|
|
488
|
+
title="Scroll to bottom"
|
|
489
|
+
aria-label="Scroll to bottom"
|
|
490
|
+
>
|
|
491
|
+
<IconArrowDown width={20} height={20} />
|
|
492
|
+
</button>
|
|
493
|
+
</Show>
|
|
494
|
+
</main>
|
|
495
|
+
</Show>
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function fromV1(v1: Message.Info): MessageWithParts {
|
|
500
|
+
if (v1.role === "assistant") {
|
|
501
|
+
return {
|
|
502
|
+
id: v1.id,
|
|
503
|
+
sessionID: v1.metadata.sessionID,
|
|
504
|
+
role: "assistant",
|
|
505
|
+
time: {
|
|
506
|
+
created: v1.metadata.time.created,
|
|
507
|
+
completed: v1.metadata.time.completed,
|
|
508
|
+
},
|
|
509
|
+
cost: v1.metadata.assistant!.cost,
|
|
510
|
+
path: v1.metadata.assistant!.path,
|
|
511
|
+
summary: v1.metadata.assistant!.summary,
|
|
512
|
+
tokens: v1.metadata.assistant!.tokens ?? {
|
|
513
|
+
input: 0,
|
|
514
|
+
output: 0,
|
|
515
|
+
cache: {
|
|
516
|
+
read: 0,
|
|
517
|
+
write: 0,
|
|
518
|
+
},
|
|
519
|
+
reasoning: 0,
|
|
520
|
+
},
|
|
521
|
+
modelID: v1.metadata.assistant!.modelID,
|
|
522
|
+
providerID: v1.metadata.assistant!.providerID,
|
|
523
|
+
mode: "build",
|
|
524
|
+
system: v1.metadata.assistant!.system,
|
|
525
|
+
error: v1.metadata.error,
|
|
526
|
+
parts: v1.parts.flatMap((part, index): MessageV2.Part[] => {
|
|
527
|
+
const base = {
|
|
528
|
+
id: index.toString(),
|
|
529
|
+
messageID: v1.id,
|
|
530
|
+
sessionID: v1.metadata.sessionID,
|
|
531
|
+
}
|
|
532
|
+
if (part.type === "text") {
|
|
533
|
+
return [
|
|
534
|
+
{
|
|
535
|
+
...base,
|
|
536
|
+
type: "text",
|
|
537
|
+
text: part.text,
|
|
538
|
+
},
|
|
539
|
+
]
|
|
540
|
+
}
|
|
541
|
+
if (part.type === "step-start") {
|
|
542
|
+
return [
|
|
543
|
+
{
|
|
544
|
+
...base,
|
|
545
|
+
type: "step-start",
|
|
546
|
+
},
|
|
547
|
+
]
|
|
548
|
+
}
|
|
549
|
+
if (part.type === "tool-invocation") {
|
|
550
|
+
return [
|
|
551
|
+
{
|
|
552
|
+
...base,
|
|
553
|
+
type: "tool",
|
|
554
|
+
callID: part.toolInvocation.toolCallId,
|
|
555
|
+
tool: part.toolInvocation.toolName,
|
|
556
|
+
state: (() => {
|
|
557
|
+
if (part.toolInvocation.state === "partial-call") {
|
|
558
|
+
return {
|
|
559
|
+
status: "pending",
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId]
|
|
564
|
+
if (part.toolInvocation.state === "call") {
|
|
565
|
+
return {
|
|
566
|
+
status: "running",
|
|
567
|
+
input: part.toolInvocation.args,
|
|
568
|
+
time: {
|
|
569
|
+
start: time.start,
|
|
570
|
+
},
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (part.toolInvocation.state === "result") {
|
|
575
|
+
return {
|
|
576
|
+
status: "completed",
|
|
577
|
+
input: part.toolInvocation.args,
|
|
578
|
+
output: part.toolInvocation.result,
|
|
579
|
+
title,
|
|
580
|
+
time,
|
|
581
|
+
metadata,
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
throw new Error("unknown tool invocation state")
|
|
585
|
+
})(),
|
|
586
|
+
},
|
|
587
|
+
]
|
|
588
|
+
}
|
|
589
|
+
return []
|
|
590
|
+
}),
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (v1.role === "user") {
|
|
595
|
+
return {
|
|
596
|
+
id: v1.id,
|
|
597
|
+
sessionID: v1.metadata.sessionID,
|
|
598
|
+
role: "user",
|
|
599
|
+
time: {
|
|
600
|
+
created: v1.metadata.time.created,
|
|
601
|
+
},
|
|
602
|
+
parts: v1.parts.flatMap((part, index): MessageV2.Part[] => {
|
|
603
|
+
const base = {
|
|
604
|
+
id: index.toString(),
|
|
605
|
+
messageID: v1.id,
|
|
606
|
+
sessionID: v1.metadata.sessionID,
|
|
607
|
+
}
|
|
608
|
+
if (part.type === "text") {
|
|
609
|
+
return [
|
|
610
|
+
{
|
|
611
|
+
...base,
|
|
612
|
+
type: "text",
|
|
613
|
+
text: part.text,
|
|
614
|
+
},
|
|
615
|
+
]
|
|
616
|
+
}
|
|
617
|
+
if (part.type === "file") {
|
|
618
|
+
return [
|
|
619
|
+
{
|
|
620
|
+
...base,
|
|
621
|
+
type: "file",
|
|
622
|
+
mime: part.mediaType,
|
|
623
|
+
filename: part.filename,
|
|
624
|
+
url: part.url,
|
|
625
|
+
},
|
|
626
|
+
]
|
|
627
|
+
}
|
|
628
|
+
return []
|
|
629
|
+
}),
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
throw new Error("unknown message type")
|
|
634
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { logos } from 'virtual:starlight/user-images';
|
|
3
|
+
import config from 'virtual:starlight/user-config';
|
|
4
|
+
const { siteTitle, siteTitleHref } = Astro.locals.starlightRoute;
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<a href="/" class="site-title sl-flex">
|
|
8
|
+
{
|
|
9
|
+
config.logo && logos.dark && (
|
|
10
|
+
<>
|
|
11
|
+
<img
|
|
12
|
+
class:list={{ 'light:sl-hidden print:hidden': !('src' in config.logo) }}
|
|
13
|
+
alt={config.logo.alt}
|
|
14
|
+
src={logos.dark.src}
|
|
15
|
+
width={logos.dark.width}
|
|
16
|
+
height={logos.dark.height}
|
|
17
|
+
/>
|
|
18
|
+
{/* Show light alternate if a user configure both light and dark logos. */}
|
|
19
|
+
{!('src' in config.logo) && (
|
|
20
|
+
<img
|
|
21
|
+
class="dark:sl-hidden print:block"
|
|
22
|
+
alt={config.logo.alt}
|
|
23
|
+
src={logos.light?.src}
|
|
24
|
+
width={logos.light?.width}
|
|
25
|
+
height={logos.light?.height}
|
|
26
|
+
/>
|
|
27
|
+
)}
|
|
28
|
+
</>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
<span class:list={{ 'sr-only': config.logo?.replacesTitle }} translate="no">
|
|
32
|
+
{siteTitle}
|
|
33
|
+
</span>
|
|
34
|
+
</a>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
@layer starlight.core {
|
|
38
|
+
.site-title {
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: var(--sl-nav-gap);
|
|
41
|
+
font-size: var(--sl-text-h4);
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
color: var(--sl-color-text-accent);
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
min-width: 0;
|
|
47
|
+
}
|
|
48
|
+
span {
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
}
|
|
51
|
+
img {
|
|
52
|
+
height: calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y));
|
|
53
|
+
width: auto;
|
|
54
|
+
max-width: 100%;
|
|
55
|
+
object-fit: contain;
|
|
56
|
+
object-position: 0 50%;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</style>
|