feat(ui-chat): split turn stats into usage and time pills with dialogs
The turn tail's exposed meta line collapses into two icon pills — Usage (database glyph, turn total) and Ran-for (clock glyph, wall time) — each click-opening a details dialog; the calendar clock trails as plain text. Cache hit, TPS, and TTFT move dialog-only, and narrow viewports collapse the pills to bare icons. The TEMPORARY flat variant keeps the whole-line trigger for the A/B test.
This commit is contained in:
parent
f15078532f
commit
9effa0c6b3
60 changed files with 834 additions and 218 deletions
|
|
@ -44,7 +44,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -35,7 +35,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -48,7 +48,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m2 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m2 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r2
|
||||
|
|
@ -60,7 +63,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m3 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m3 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r3
|
||||
|
|
@ -72,7 +78,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m4 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m4 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r4
|
||||
|
|
@ -84,7 +93,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m5 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m5 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r5
|
||||
|
|
@ -96,7 +108,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m6 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m6 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r6
|
||||
|
|
@ -108,7 +123,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m7 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m7 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r7
|
||||
|
|
@ -120,7 +138,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m8 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m8 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r8
|
||||
|
|
@ -132,7 +153,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m9 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m9 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r9
|
||||
|
|
@ -144,7 +168,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m10 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m10 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r10
|
||||
|
|
@ -156,7 +183,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m11 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m11 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r11
|
||||
|
|
@ -168,7 +198,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m12 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m12 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r12
|
||||
|
|
@ -180,7 +213,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m13 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m13 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r13
|
||||
|
|
@ -192,7 +228,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m14 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m14 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r14
|
||||
|
|
@ -204,7 +243,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m15 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m15 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r15
|
||||
|
|
@ -216,7 +258,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m16 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m16 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r16
|
||||
|
|
@ -228,7 +273,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m17 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m17 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r17
|
||||
|
|
@ -240,7 +288,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m18 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m18 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r18
|
||||
|
|
@ -252,7 +303,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m19 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m19 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r19
|
||||
|
|
@ -264,7 +318,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m20 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m20 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r20
|
||||
|
|
@ -276,7 +333,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m21 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m21 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r21
|
||||
|
|
@ -288,7 +348,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m22 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m22 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r22
|
||||
|
|
@ -300,7 +363,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m23 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m23 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r23
|
||||
|
|
@ -312,7 +378,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m24 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m24 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r24
|
||||
|
|
@ -324,7 +393,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m25 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m25 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r25
|
||||
|
|
@ -336,7 +408,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m26 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m26 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r26
|
||||
|
|
@ -348,7 +423,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m27 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m27 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r27
|
||||
|
|
@ -360,7 +438,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} m28 7/25 {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}} m28 7/25 {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- paragraph: r28
|
||||
|
|
@ -372,7 +453,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}}
|
||||
- button "Back to bottom":
|
||||
- img
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
|
|
|
|||
|
|
@ -46,7 +46,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -173,10 +173,12 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
|
|||
const trigger = page.getByRole('button', { name: /Usage 15\.8K tok/ })
|
||||
await expect.poll(() => trigger.count(), { timeout: 10_000 }).toBe(1)
|
||||
expect(await trigger.getAttribute('aria-expanded')).toBe('false')
|
||||
// The pill carries the icon, the turn total, and the cache-hit rate;
|
||||
// timing facts stay in the plain meta line beside it.
|
||||
expect(await trigger.textContent()).toBe('Usage 15.8K tok · Cache hit 49.7%')
|
||||
expect(await page.getByText(/^.+ · Ran for .+ · \d+ tok\/s · TTFT \S+$/).count()).toBe(1)
|
||||
// The usage pill carries the icon and the turn total; the time pill beside
|
||||
// it carries the run time, and both keep their details dialog-only.
|
||||
expect(await trigger.textContent()).toBe('Usage 15.8K tok')
|
||||
const timeTrigger = page.getByRole('button', { name: /^Ran for \S+$/ })
|
||||
expect(await timeTrigger.count()).toBe(1)
|
||||
expect(await page.locator('[data-turn-tail]').getByText(/tok\/s|TTFT/).count()).toBe(0)
|
||||
expect(await page.getByRole('dialog').count()).toBe(0)
|
||||
|
||||
await trigger.click()
|
||||
|
|
@ -189,6 +191,16 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
|
|||
expect(await dialog.getByText('7,808 tok', { exact: true }).count()).toBe(1)
|
||||
expect(await dialog.getByText('112 tok (42 tok reasoning)', { exact: true }).count()).toBe(1)
|
||||
expect(await dialog.getByText('15,811 tok', { exact: true }).count()).toBe(1)
|
||||
await page.keyboard.press('Escape')
|
||||
expect(await page.getByRole('dialog').count()).toBe(0)
|
||||
|
||||
await timeTrigger.click()
|
||||
const timeDialog = page.getByRole('dialog', { name: 'Turn time and speed' })
|
||||
expect(await timeDialog.count()).toBe(1)
|
||||
expect(await timeDialog.getByText(/tok\/s/).count()).toBe(1)
|
||||
expect(await timeDialog.getByText('Average time to first token (TTFT)', { exact: true }).count()).toBe(1)
|
||||
await page.keyboard.press('Escape')
|
||||
await trigger.click()
|
||||
|
||||
const expanded = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
|
||||
await compareOrRefreshGolden(USAGE_EXPANDED_EXPECTED, expanded, MODE)
|
||||
|
|
|
|||
|
|
@ -20,36 +20,16 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The assistant tail's clock reads the secondary tier: its row carries the
|
||||
detailed-usage cluster, so the whole meta line sits one step under body. */
|
||||
/* The assistant tail's clock sits at the row end after the stat pills; the
|
||||
row's 8px flex gap spaces it, and it reads the secondary tier so it matches
|
||||
the pill labels beside it. */
|
||||
.timeEnd {
|
||||
padding-left: 12px;
|
||||
font-size: var(--dsh-content-font-size-secondary, 13px);
|
||||
line-height: calc(24px + var(--dsh-content-font-delta, 0px));
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Separator between the clock and the run-time label (time · Ran for 15s). */
|
||||
.runTimeDot {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
/* Separator between the usage pill and the plain meta text; the row's 8px
|
||||
flex gap spaces it, so the meta line drops its own 12px inset. The negative
|
||||
margin rebates most of the pill's internal 8px padding so the label-to-dot
|
||||
distance reads like the dot's other side, while keeping 2px clear of the
|
||||
pill's hover background. */
|
||||
.actionsDot {
|
||||
margin-left: -6px;
|
||||
font-size: var(--dsh-content-font-size-secondary, 13px);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.actionsDot + .timeEnd {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* Row containers (user rows and turn tails) opt in with
|
||||
data-actions-reveal='hover': every row but the latest of its kind reveals
|
||||
the entire actions row (icons and text) on hover or focus-within, while the
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
// Shared IconActions chrome for user and assistant messages: copy
|
||||
// live, optional branch wiring, and an optional date-aware clock.
|
||||
|
||||
import { Fragment, useCallback, useEffect, useId, useRef, useState, type ReactNode } from 'react'
|
||||
import { useCallback, useEffect, useId, useRef, useState, type ReactNode } from 'react'
|
||||
import {
|
||||
IconBranchOutline16, IconCheckOutline16, IconCopyOutline16, Tooltip, writeClipboard,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ChatViewSlotProps } from '../contract/slots.ts'
|
||||
import {
|
||||
formatLatencySeconds, formatMessageClock, formatRunDuration, formatTokensPerSecond,
|
||||
} from './message-chrome.ts'
|
||||
import { formatMessageClock } from './message-chrome.ts'
|
||||
import { useCalendarDay } from './use-calendar-day.ts'
|
||||
import css from './MessageIconActions.module.css'
|
||||
|
||||
|
|
@ -17,12 +15,6 @@ export interface MessageIconActionsProps {
|
|||
text: string
|
||||
/** Unix epoch ms for the clock label; omitted for transient messages. */
|
||||
time?: number | undefined
|
||||
/** Turn wall time in ms, appended to the clock as `· Ran for 15s`; omitted when the turn's start is unknown. */
|
||||
runMs?: number | undefined
|
||||
/** Turn decode throughput, appended as `· 34 tok/s`; omitted when unrecorded. */
|
||||
tokensPerSecond?: number | undefined
|
||||
/** Turn first-step TTFT in ms, appended as `· TTFT 1.2s`; omitted when unrecorded. */
|
||||
ttftMs?: number | undefined
|
||||
/** Clock before icons (user) or after (assistant). */
|
||||
clock: 'start' | 'end'
|
||||
/** Fork the session at this message; omission hides the branch action. */
|
||||
|
|
@ -51,7 +43,7 @@ export interface MessageIconActionsProps {
|
|||
* @returns The actions row element.
|
||||
*/
|
||||
export function MessageIconActions({
|
||||
text, time, runMs, tokensPerSecond, ttftMs, clock, onBranch, branchUnavailable = false, className,
|
||||
text, time, clock, onBranch, branchUnavailable = false, className,
|
||||
extraActions, usageAction, t,
|
||||
}: MessageIconActionsProps) {
|
||||
const day = useCalendarDay()
|
||||
|
|
@ -82,30 +74,9 @@ export function MessageIconActions({
|
|||
}, 1000)
|
||||
})
|
||||
}, [copied, text])
|
||||
// The dots are decorative and stay hidden, but their margins separate the
|
||||
// readings only on screen: without the flanking spaces a reader hears one
|
||||
// run-on string ("8/26 22:08Ran for 13s") instead of separate facts.
|
||||
const meta: string[] = []
|
||||
if (time !== undefined) meta.push(formatMessageClock(time, t, day))
|
||||
if (runMs !== undefined) meta.push(t('message.ranFor', { duration: formatRunDuration(runMs, t) }))
|
||||
if (tokensPerSecond !== undefined) {
|
||||
meta.push(t('message.turnUsage.speed', { tps: formatTokensPerSecond(tokensPerSecond) }))
|
||||
}
|
||||
if (ttftMs !== undefined) meta.push(t('message.ttft', { seconds: formatLatencySeconds(ttftMs) }))
|
||||
const clockEl = meta.length === 0 ? null : (
|
||||
const clockEl = time === undefined ? null : (
|
||||
<span className={clock === 'start' ? css.timeStart : css.timeEnd}>
|
||||
{meta.map((segment, index) => (
|
||||
<Fragment key={segment}>
|
||||
{index > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
<span className={css.runTimeDot} aria-hidden>·</span>
|
||||
{' '}
|
||||
</>
|
||||
)}
|
||||
{segment}
|
||||
</Fragment>
|
||||
))}
|
||||
{formatMessageClock(time, t, day)}
|
||||
</span>
|
||||
)
|
||||
return (
|
||||
|
|
@ -137,9 +108,6 @@ export function MessageIconActions({
|
|||
<span id={reasonId} className={css.visuallyHidden}>{t('message.branchUnavailable')}</span>
|
||||
)}
|
||||
{usageAction}
|
||||
{usageAction != null && clock === 'end' && clockEl !== null && (
|
||||
<span className={css.actionsDot} aria-hidden>·</span>
|
||||
)}
|
||||
{clock === 'end' ? clockEl : null}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { memo } from 'react'
|
|||
import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { ChatNodeViewProps, TurnTailOwnerProps } from '../contract/slots.ts'
|
||||
import { MessageIconActions } from './MessageIconActions.tsx'
|
||||
import { TurnUsagePanel } from './TurnUsagePanel.tsx'
|
||||
import { TurnTimePanel, TurnUsagePanel } from './TurnUsagePanel.tsx'
|
||||
import { assistantText } from './turn-assistant.ts'
|
||||
import css from './TurnTailNodeView.module.css'
|
||||
|
||||
|
|
@ -28,6 +28,14 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({
|
|||
const runMs = turn.start === undefined || turn.end === undefined
|
||||
? undefined
|
||||
: Math.max(0, turn.end.time - turn.start.time)
|
||||
// TEMPORARY usage-variant debug switch for user testing: `?usage-variant=flat`
|
||||
// or `#usage-variant=flat` (the hash form survives the login token's 303
|
||||
// redirect to a clean `/`) hands the whole meta line to TurnUsagePanel as the
|
||||
// dialog trigger; the default keeps the icon pill beside plain meta text.
|
||||
// Delete after the test.
|
||||
const flatVariant = data.tokenUsage !== undefined
|
||||
&& [window.location.search, window.location.hash.replace(/^#/, '')]
|
||||
.some(query => new URLSearchParams(query).get('usage-variant') === 'flat')
|
||||
// Interruption-frozen partials carry no messageId, so they address no
|
||||
// durable message and contribute no per-message actions.
|
||||
const messageId = closing.finalNode.messageId
|
||||
|
|
@ -43,18 +51,37 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({
|
|||
{tail}
|
||||
<MessageIconActions
|
||||
text={assistantText(closing.blocks)}
|
||||
time={closing.time}
|
||||
runMs={runMs}
|
||||
tokensPerSecond={data.tokensPerSecond}
|
||||
ttftMs={data.ttftMs}
|
||||
{...flatVariant ? {} : { time: closing.time }}
|
||||
clock="end"
|
||||
onBranch={() => { forkAt(closing.finalNode.seq) }}
|
||||
branchUnavailable={data.branchUnavailable || hasLaterChatNode}
|
||||
className={css.actions}
|
||||
extraActions={assistantActions}
|
||||
usageAction={data.tokenUsage === undefined ? null : (
|
||||
<TurnUsagePanel usage={data.tokenUsage} t={t} />
|
||||
)}
|
||||
usageAction={flatVariant
|
||||
? data.tokenUsage !== undefined && (
|
||||
<TurnUsagePanel
|
||||
usage={data.tokenUsage}
|
||||
variant="flat"
|
||||
time={closing.time}
|
||||
runMs={runMs}
|
||||
tokensPerSecond={data.tokensPerSecond}
|
||||
ttftMs={data.ttftMs}
|
||||
t={t}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{data.tokenUsage !== undefined && <TurnUsagePanel usage={data.tokenUsage} t={t} />}
|
||||
{runMs !== undefined && (
|
||||
<TurnTimePanel
|
||||
runMs={runMs}
|
||||
tokensPerSecond={data.tokensPerSecond}
|
||||
ttftMs={data.ttftMs}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,13 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Adjacent stat pills (usage then time) rebate part of their combined internal
|
||||
padding so the pair reads as one cluster inside the row's 8px flex gap,
|
||||
while keeping 2px clear between their hover backgrounds. */
|
||||
.root + .root {
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
/* Same pill rules as the sibling `.action` icon buttons (28px hit height,
|
||||
15px glyph, tertiary → secondary on hover with the interactive hover
|
||||
background), widened to carry the turn-total label. */
|
||||
|
|
@ -36,6 +43,36 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* TEMPORARY (usage-variant debug): the flat whole-line trigger reads like the
|
||||
plain meta text, brightening on hover to hint clickability. The margin
|
||||
widens the row's 8px flex gap off the branch icon beside it. */
|
||||
.flatTrigger {
|
||||
min-width: 0;
|
||||
margin-left: 6px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font-size: var(--dsh-content-font-size-secondary, 13px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: calc(24px + var(--dsh-content-font-delta, 0px));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flatTrigger:hover,
|
||||
.flatTrigger[aria-expanded='true'] {
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* TEMPORARY (usage-variant debug): flat-line separators breathe wider than the
|
||||
plain meta line's 5px runTimeDot rhythm. */
|
||||
.flatDot {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.trigger svg {
|
||||
width: calc(15px + var(--dsh-content-font-delta, 0px));
|
||||
height: calc(15px + var(--dsh-content-font-delta, 0px));
|
||||
|
|
@ -48,6 +85,13 @@
|
|||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* Narrow viewport: the pills collapse to bare icons; the dialogs keep the words. */
|
||||
@media (max-width: 480px) {
|
||||
.trigger .label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Portal surface: fixed in the viewport, left/top supplied inline from the
|
||||
anchored-position clamp so the panel keeps its 12px viewport margin instead
|
||||
of hanging off the trigger and clipping at the window edge. Portaled panels
|
||||
|
|
@ -73,13 +117,46 @@
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
/* Dialog heading, same size and weight as the total row it concludes with. */
|
||||
/* Dialog heading row: the section name left, the section's headline value
|
||||
(the usage total) right, both at the primary weight. */
|
||||
.title {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Second-section heading (Turn time), spaced well off the usage rows above. */
|
||||
.timeTitle {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Rule under each section heading, above its rows. */
|
||||
.titleRule {
|
||||
margin-bottom: 10px;
|
||||
border-top: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
.titleValue {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Section-heading glyph seated left of the section name. */
|
||||
.titleLabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.titleLabel svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(76px, auto) minmax(0, 1fr);
|
||||
|
|
@ -108,17 +185,3 @@
|
|||
color: var(--dsw-alias-label-tertiary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Full-bleed rule above the total row: the grid's column gap would break a
|
||||
per-cell border-top, so the line is its own row spanning both columns. */
|
||||
.separator {
|
||||
grid-column: 1 / -1;
|
||||
margin: 4px 0;
|
||||
border-top: 1px solid var(--dsw-alias-border-l2);
|
||||
}
|
||||
|
||||
.totalLabel,
|
||||
.details .totalValue {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,48 @@
|
|||
// Icon-row Turn-usage action: a data-icon pill labelled with the turn total
|
||||
// (and the cache-hit rate when known) that click-opens the per-Turn details
|
||||
// dialog. Sits right of the branch action in the tail's IconActions row.
|
||||
// Icon-row Turn-stat actions: a database pill labelled with the turn total
|
||||
// click-opens the per-Turn usage dialog, and a clock pill labelled with the
|
||||
// turn wall time click-opens the Turn-time dialog. Both sit right of the
|
||||
// branch action in the tail's IconActions row, ahead of the plain clock text.
|
||||
// TEMPORARY: the `flat` variant (usage-variant debug switch) renders the
|
||||
// whole meta line as one trigger whose dialog stacks both sections; one of
|
||||
// the two variants will be deleted after user testing.
|
||||
|
||||
import { useEffect, useRef, useState, type CSSProperties } from 'react'
|
||||
import { Fragment, useEffect, useRef, useState, type CSSProperties, type MutableRefObject } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { IconDataOutline16, useAnchoredPosition } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import {
|
||||
IconClockOutline16, IconDatabaseOutline16, useAnchoredPosition,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { TurnTokenUsage } from '../contract/chat-nodes.ts'
|
||||
import type { ChatViewSlotProps } from '../contract/slots.ts'
|
||||
import {
|
||||
formatLatencySeconds, formatMessageClock, formatRunDuration, formatTokensPerSecond,
|
||||
} from './message-chrome.ts'
|
||||
import { formatCacheHitPercent, formatExactTokens, formatTokens } from './token-format.ts'
|
||||
import { useCalendarDay } from './use-calendar-day.ts'
|
||||
import css from './TurnUsagePanel.module.css'
|
||||
|
||||
export interface TurnUsagePanelProps {
|
||||
usage: TurnTokenUsage
|
||||
/** Trigger form: the default icon pill or the temporary flat whole-line text. */
|
||||
variant?: 'pill' | 'flat'
|
||||
/** Unix epoch ms for the flat trigger's clock segment. */
|
||||
time?: number | undefined
|
||||
/** Turn wall time in ms; the flat trigger and its dialog's Turn-time section show it. */
|
||||
runMs?: number | undefined
|
||||
/** Turn decode throughput; the flat trigger and its dialog's Turn-time section show it. */
|
||||
tokensPerSecond?: number | undefined
|
||||
/** Turn first-step TTFT in ms; the flat trigger and its dialog's Turn-time section show it. */
|
||||
ttftMs?: number | undefined
|
||||
/** The owning view's locale seat, passed down as a plain prop. */
|
||||
t: ChatViewSlotProps['t']
|
||||
}
|
||||
|
||||
export interface TurnTimePanelProps {
|
||||
/** Turn wall time in ms, the pill's label. */
|
||||
runMs: number
|
||||
/** Turn decode throughput, a dialog row when known. */
|
||||
tokensPerSecond?: number | undefined
|
||||
/** Turn first-step TTFT in ms, a dialog row when known. */
|
||||
ttftMs?: number | undefined
|
||||
/** The owning view's locale seat, passed down as a plain prop. */
|
||||
t: ChatViewSlotProps['t']
|
||||
}
|
||||
|
|
@ -36,12 +67,16 @@ const PANEL_GAP = 8
|
|||
*/
|
||||
const MEASURE_STYLE: CSSProperties = { visibility: 'hidden', left: 0, top: 0 }
|
||||
|
||||
/**
|
||||
* Turn-usage IconActions pill with a click-open Turn-details dialog.
|
||||
* @param props - Turn usage buckets and locale seat.
|
||||
* @returns The icon-and-total trigger and, while open, its portaled dialog anchored above the trigger.
|
||||
*/
|
||||
export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) {
|
||||
interface StatDialogSeat {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
rootRef: MutableRefObject<HTMLSpanElement | null>
|
||||
panelRef: MutableRefObject<HTMLDivElement | null>
|
||||
pos: CSSProperties | null
|
||||
}
|
||||
|
||||
/** One trigger-anchored dialog seat: open state, viewport-clamped placement, outside-close. */
|
||||
function useStatDialog(): StatDialogSeat {
|
||||
const [open, setOpen] = useState(false)
|
||||
const rootRef = useRef<HTMLSpanElement | null>(null)
|
||||
const panelRef = useRef<HTMLDivElement | null>(null)
|
||||
|
|
@ -77,27 +112,88 @@ export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) {
|
|||
}
|
||||
}, [open])
|
||||
|
||||
return { open, setOpen, rootRef, panelRef, pos }
|
||||
}
|
||||
|
||||
/** Turn-time dialog rows: run time plus throughput and TTFT when known. */
|
||||
function TimeDetails({ runMs, tokensPerSecond, ttftMs, t }: {
|
||||
runMs: number | undefined
|
||||
tokensPerSecond: number | undefined
|
||||
ttftMs: number | undefined
|
||||
t: ChatViewSlotProps['t']
|
||||
}) {
|
||||
return (
|
||||
<dl className={css.details} data-turn-time-details>
|
||||
{runMs !== undefined && (
|
||||
<>
|
||||
<dt>{t('message.turnTime.duration')}</dt>
|
||||
<dd>{formatRunDuration(runMs, t)}</dd>
|
||||
</>
|
||||
)}
|
||||
{tokensPerSecond !== undefined && (
|
||||
<>
|
||||
<dt>{t('message.turnTime.speed')}</dt>
|
||||
<dd>{t('message.tokensPerSecond', { tps: formatTokensPerSecond(tokensPerSecond) })}</dd>
|
||||
</>
|
||||
)}
|
||||
{ttftMs !== undefined && (
|
||||
<>
|
||||
<dt>{t('message.turnTime.ttft')}</dt>
|
||||
<dd>{t('duration.seconds', { seconds: formatLatencySeconds(ttftMs) })}</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn-usage IconActions pill (or the temporary flat meta-line trigger) with
|
||||
* a click-open Turn-usage details dialog.
|
||||
* @param props - Turn usage buckets, trigger form, and locale seat.
|
||||
* @returns The trigger and, while open, its portaled dialog anchored above the trigger.
|
||||
*/
|
||||
export function TurnUsagePanel({ usage, variant = 'pill', time, runMs, tokensPerSecond, ttftMs, t }: TurnUsagePanelProps) {
|
||||
const { open, setOpen, rootRef, panelRef, pos } = useStatDialog()
|
||||
const day = useCalendarDay()
|
||||
|
||||
const cacheHit = usage.cacheReadTokens === undefined
|
||||
? null
|
||||
: formatCacheHitPercent(usage.cacheReadTokens, usage.totalTokens - usage.outputTokens, 1)
|
||||
const total = formatCompactCount(usage.totalTokens, t)
|
||||
const routes = usage.routes?.map(route => `${route.provider}/${route.model}`).join(', ') ?? ''
|
||||
const flatSegments: string[] = []
|
||||
if (variant === 'flat') {
|
||||
if (time !== undefined) flatSegments.push(formatMessageClock(time, t, day))
|
||||
if (runMs !== undefined) flatSegments.push(t('message.ranFor', { duration: formatRunDuration(runMs, t) }))
|
||||
flatSegments.push(t('message.turnUsage.consumed', { total }))
|
||||
if (cacheHit !== null) flatSegments.push(t('message.turnUsage.cacheHitRate', { percent: cacheHit }))
|
||||
if (tokensPerSecond !== undefined) {
|
||||
flatSegments.push(t('message.turnUsage.speed', { tps: formatTokensPerSecond(tokensPerSecond) }))
|
||||
}
|
||||
if (ttftMs !== undefined) flatSegments.push(t('message.ttft', { seconds: formatLatencySeconds(ttftMs) }))
|
||||
}
|
||||
const hasTimeFacts = runMs !== undefined || tokensPerSecond !== undefined || ttftMs !== undefined
|
||||
|
||||
return (
|
||||
<span ref={rootRef} className={css.root}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.trigger}
|
||||
className={variant === 'flat' ? css.flatTrigger : css.trigger}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
onClick={() => { setOpen(!open) }}
|
||||
>
|
||||
<IconDataOutline16 />
|
||||
<span className={css.label}>
|
||||
{cacheHit === null
|
||||
? t('message.turnUsage.consumed', { total })
|
||||
: `${t('message.turnUsage.consumed', { total })} · ${t('message.turnUsage.cacheHitRate', { percent: cacheHit })}`}
|
||||
</span>
|
||||
{variant === 'flat' ? flatSegments.map((segment, index) => (
|
||||
<Fragment key={index}>
|
||||
{index > 0 && <span className={css.flatDot} aria-hidden>·</span>}
|
||||
{segment}
|
||||
</Fragment>
|
||||
)) : (
|
||||
<>
|
||||
<IconDatabaseOutline16 />
|
||||
<span className={css.label}>{t('message.turnUsage.consumed', { total })}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{open && createPortal(
|
||||
<div
|
||||
|
|
@ -107,7 +203,14 @@ export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) {
|
|||
aria-label={t('message.turnUsage.title')}
|
||||
style={pos ?? MEASURE_STYLE}
|
||||
>
|
||||
<div className={css.title}>{t('message.turnUsage.title')}</div>
|
||||
<div className={css.title}>
|
||||
<span className={css.titleLabel}>
|
||||
<IconDatabaseOutline16 />
|
||||
{t('message.turnUsage.title')}
|
||||
</span>
|
||||
<span className={css.titleValue}>{formatExactCount(usage.totalTokens, t)}</span>
|
||||
</div>
|
||||
<div className={css.titleRule} aria-hidden />
|
||||
<dl className={css.details} data-turn-usage-details>
|
||||
{routes !== '' && (
|
||||
<>
|
||||
|
|
@ -144,10 +247,61 @@ export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) {
|
|||
</span>
|
||||
)}
|
||||
</dd>
|
||||
<div className={css.separator} aria-hidden />
|
||||
<dt className={css.totalLabel}>{t('message.turnUsage.total')}</dt>
|
||||
<dd className={css.totalValue}>{formatExactCount(usage.totalTokens, t)}</dd>
|
||||
</dl>
|
||||
{variant === 'flat' && hasTimeFacts && (
|
||||
<>
|
||||
<div className={`${css.title} ${css.timeTitle}`}>
|
||||
<span className={css.titleLabel}>
|
||||
<IconClockOutline16 />
|
||||
{t('message.turnTime.title')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={css.titleRule} aria-hidden />
|
||||
<TimeDetails runMs={runMs} tokensPerSecond={tokensPerSecond} ttftMs={ttftMs} t={t} />
|
||||
</>
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn-time IconActions pill with a click-open Turn-time details dialog.
|
||||
* @param props - Turn timing facts and locale seat.
|
||||
* @returns The clock-and-duration trigger and, while open, its portaled dialog anchored above the trigger.
|
||||
*/
|
||||
export function TurnTimePanel({ runMs, tokensPerSecond, ttftMs, t }: TurnTimePanelProps) {
|
||||
const { open, setOpen, rootRef, panelRef, pos } = useStatDialog()
|
||||
return (
|
||||
<span ref={rootRef} className={css.root}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.trigger}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
onClick={() => { setOpen(!open) }}
|
||||
>
|
||||
<IconClockOutline16 />
|
||||
<span className={css.label}>{t('message.ranFor', { duration: formatRunDuration(runMs, t) })}</span>
|
||||
</button>
|
||||
{open && createPortal(
|
||||
<div
|
||||
ref={panelRef}
|
||||
className={css.panel}
|
||||
role="dialog"
|
||||
aria-label={t('message.turnTime.title')}
|
||||
style={pos ?? MEASURE_STYLE}
|
||||
>
|
||||
<div className={css.title}>
|
||||
<span className={css.titleLabel}>
|
||||
<IconClockOutline16 />
|
||||
{t('message.turnTime.title')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={css.titleRule} aria-hidden />
|
||||
<TimeDetails runMs={runMs} tokensPerSecond={tokensPerSecond} ttftMs={ttftMs} t={t} />
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -101,8 +101,11 @@ export const zh = {
|
|||
'message.turnUsage.cacheWrite': '缓存写入',
|
||||
'message.turnUsage.output': '输出',
|
||||
'message.turnUsage.reasoning': '(其中推理 {tokens})',
|
||||
'message.turnUsage.total': '总计',
|
||||
'message.turnUsage.count': '{count} tok',
|
||||
'message.turnTime.title': '本轮用时和速度',
|
||||
'message.turnTime.duration': '本轮总用时',
|
||||
'message.turnTime.speed': '输出速度(TPS)',
|
||||
'message.turnTime.ttft': '首 token 平均用时(TTFT)',
|
||||
'duration.seconds': '{seconds}秒',
|
||||
'duration.minutes': '{minutes}分{seconds}秒',
|
||||
'command.running': '执行中…',
|
||||
|
|
@ -217,8 +220,11 @@ export const en = {
|
|||
'message.turnUsage.cacheWrite': 'Cache write',
|
||||
'message.turnUsage.output': 'Output',
|
||||
'message.turnUsage.reasoning': ' ({tokens} reasoning)',
|
||||
'message.turnUsage.total': 'Total',
|
||||
'message.turnUsage.count': '{count} tok',
|
||||
'message.turnTime.title': 'Turn time and speed',
|
||||
'message.turnTime.duration': 'Total run time',
|
||||
'message.turnTime.speed': 'Tokens per second (TPS)',
|
||||
'message.turnTime.ttft': 'Average time to first token (TTFT)',
|
||||
'duration.seconds': '{seconds}s',
|
||||
'duration.minutes': '{minutes}m {seconds}s',
|
||||
'command.running': 'Running…',
|
||||
|
|
|
|||
|
|
@ -1562,19 +1562,71 @@ describe('ChatView', () => {
|
|||
}]]),
|
||||
})
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
// First-step ttft (1.2s) plus 100 tokens over 5s of decode stay plain
|
||||
// text; the usage pill in the icon row is the only clickable trigger.
|
||||
// The usage pill carries the compact total; cache hit stays dialog-only.
|
||||
const trigger = view.getByRole('button', { name: /用量 10\.1K tok/ })
|
||||
expect(trigger.textContent).toBe('用量 10.1K tok · 缓存命中 49.4%')
|
||||
expect(view.getByText(/用时 19秒/).textContent)
|
||||
.toMatch(/^.+ · 用时 19秒 · 速度 20 tok\/s · 首 token 1\.2秒$/)
|
||||
expect(trigger.textContent).toBe('用量 10.1K tok')
|
||||
expect(view.queryByRole('dialog')).toBeNull()
|
||||
fireEvent.click(trigger)
|
||||
const dialog = view.getByRole('dialog')
|
||||
expect(dialog.getAttribute('aria-label')).toBe('本轮用量')
|
||||
expect(dialog.firstChild?.textContent).toBe('本轮用量10,100 tok')
|
||||
expect(dialog.textContent).toContain('缓存命中49.4%')
|
||||
expect(dialog.textContent).toContain('未缓存输入5,060 tok')
|
||||
expect(dialog.textContent).toContain('总计10,100 tok')
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
// The time pill carries the run time; first-step ttft (1.2s) and 100
|
||||
// tokens over 5s of decode move into its dialog.
|
||||
const timeTrigger = view.getByRole('button', { name: /用时 19秒/ })
|
||||
expect(timeTrigger.textContent).toBe('用时 19秒')
|
||||
expect(view.queryByText(/速度 20 tok\/s|首 token/)).toBeNull()
|
||||
fireEvent.click(timeTrigger)
|
||||
const timeDialog = view.getByRole('dialog')
|
||||
expect(timeDialog.getAttribute('aria-label')).toBe('本轮用时和速度')
|
||||
expect(timeDialog.textContent).toContain('本轮总用时19秒')
|
||||
expect(timeDialog.textContent).toContain('输出速度(TPS)20 tok/s')
|
||||
expect(timeDialog.textContent).toContain('首 token 平均用时(TTFT)1.2秒')
|
||||
})
|
||||
|
||||
it('TEMPORARY: ?usage-variant=flat renders the whole meta line as the dialog trigger', () => {
|
||||
window.history.replaceState(null, '', '?usage-variant=flat')
|
||||
try {
|
||||
const first: AssistantMessageNode = {
|
||||
kind: 'assistant', seq: 2, time: 2_000, turn: 1, step: 1, blocks: [{ kind: 'text', text: 'draft' }],
|
||||
timing: { stepStartTime: 1_000, firstTokenTime: 2_200, completedTime: 5_200 },
|
||||
usage: { outputTokens: 40 },
|
||||
}
|
||||
const second: AssistantMessageNode = {
|
||||
kind: 'assistant', seq: 16, time: 16_000, turn: 1, step: 2, blocks: [{ kind: 'text', text: 'final' }],
|
||||
timing: { stepStartTime: 10_000, firstTokenTime: 10_200, completedTime: 12_200 },
|
||||
usage: { outputTokens: 60 },
|
||||
}
|
||||
const h = makeHarness({
|
||||
nodes: [user(1, 'hi'), first, second],
|
||||
turnTimings: new Map([[1, { startTime: 1_000, endTime: 20_000 }]]),
|
||||
turnEnds: new Map([[1, 20]]),
|
||||
turnUsages: new Map([[1, {
|
||||
uncachedInputTokens: 5_060,
|
||||
cacheReadTokens: 4_940,
|
||||
outputTokens: 100,
|
||||
totalTokens: 10_100,
|
||||
}]]),
|
||||
})
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
const trigger = view.getByRole('button', { name: /用量 10\.1K tok/ })
|
||||
// Separator dots live in spaced spans, so textContent carries bare `·`.
|
||||
expect(trigger.textContent)
|
||||
.toMatch(/^.+·用时 19秒·用量 10\.1K tok·缓存命中 49\.4%·速度 20 tok\/s·首 token 1\.2秒$/)
|
||||
// The flat trigger absorbs the meta line, so no plain copy remains.
|
||||
expect(view.getByText(/用时 19秒/)).toBe(trigger)
|
||||
fireEvent.click(trigger)
|
||||
const dialog = view.getByRole('dialog')
|
||||
expect(dialog.firstChild?.textContent).toBe('本轮用量10,100 tok')
|
||||
expect(dialog.textContent).toContain('本轮用时和速度')
|
||||
expect(dialog.textContent).toContain('本轮总用时19秒')
|
||||
expect(dialog.textContent).toContain('输出速度(TPS)20 tok/s')
|
||||
expect(dialog.textContent).toContain('首 token 平均用时(TTFT)1.2秒')
|
||||
} finally {
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
}
|
||||
})
|
||||
|
||||
it('withholds the usage-details trigger when turn usage is outside the window', () => {
|
||||
|
|
@ -1589,9 +1641,9 @@ describe('ChatView', () => {
|
|||
turnEnds: new Map([[1, 20]]),
|
||||
})
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
// Timing facts stay visible as plain text, but with no usage in the
|
||||
// window there is no pill to click.
|
||||
expect(view.getByText(/首 token/)).toBeTruthy()
|
||||
// Timing facts keep their pill, but with no usage in the window there is
|
||||
// no usage pill to click.
|
||||
expect(view.getByRole('button', { name: /用时/ })).toBeTruthy()
|
||||
expect(view.queryByRole('button', { name: /用量/ })).toBeNull()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { afterEach, describe, expect, it } from 'vitest'
|
|||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
|
||||
import { TurnUsagePanel } from '../src/client/chat/TurnUsagePanel.tsx'
|
||||
import { TurnTimePanel, TurnUsagePanel } from '../src/client/chat/TurnUsagePanel.tsx'
|
||||
import type { TurnTokenUsage } from '../src/client/contract/chat-nodes.ts'
|
||||
import { en } from '../src/client/locale.ts'
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ describe('TurnUsagePanel', () => {
|
|||
const view = render(<TurnUsagePanel usage={usage} t={t} />)
|
||||
|
||||
const trigger = view.getByRole('button')
|
||||
expect(trigger.textContent).toBe('Usage 15.8K tok · Cache hit 49.4%')
|
||||
expect(trigger.textContent).toBe('Usage 15.8K tok')
|
||||
expect(trigger.querySelector('svg')).not.toBeNull()
|
||||
expect(trigger.getAttribute('aria-haspopup')).toBe('dialog')
|
||||
expect(trigger.getAttribute('aria-expanded')).toBe('false')
|
||||
|
|
@ -36,9 +36,9 @@ describe('TurnUsagePanel', () => {
|
|||
expect(trigger.getAttribute('aria-expanded')).toBe('true')
|
||||
const dialog = view.getByRole('dialog')
|
||||
expect(dialog.getAttribute('aria-label')).toBe('Turn usage')
|
||||
// Portaled out of the trigger's row, with a visible heading above the rows.
|
||||
// Portaled out of the trigger's row, with a heading row carrying the total.
|
||||
expect(dialog.parentElement).toBe(document.body)
|
||||
expect(dialog.firstChild?.textContent).toBe('Turn usage')
|
||||
expect(dialog.firstChild?.textContent).toBe('Turn usage15,800 tok')
|
||||
const details = dialog.querySelector('[data-turn-usage-details]') as HTMLElement
|
||||
expect(details).toBeTruthy()
|
||||
expect(details.textContent).toContain('Provider / modeldeepseek/deepseek-chat')
|
||||
|
|
@ -47,7 +47,7 @@ describe('TurnUsagePanel', () => {
|
|||
expect(details.textContent).toContain('Cached input4,940 tok')
|
||||
expect(details.textContent).toContain('Cache write0 tok')
|
||||
expect(details.textContent).toContain('Output5,800 tok (42 tok reasoning)')
|
||||
expect(details.textContent).toContain('Total15,800 tok')
|
||||
expect(details.textContent).not.toContain('Total')
|
||||
})
|
||||
|
||||
it('omits unavailable optional facts instead of inventing values', () => {
|
||||
|
|
@ -77,9 +77,9 @@ describe('TurnUsagePanel', () => {
|
|||
}
|
||||
const view = render(<TurnUsagePanel usage={usage} t={t} />)
|
||||
const trigger = view.getByRole('button')
|
||||
// The pill carries the compact total and cache-hit rate; exact token
|
||||
// The pill carries the compact total; cache-hit rate and exact token
|
||||
// counts stay in the dialog.
|
||||
expect(trigger.textContent).toBe('Usage 1.1K tok · Cache hit 99.9%')
|
||||
expect(trigger.textContent).toBe('Usage 1.1K tok')
|
||||
|
||||
fireEvent.click(trigger)
|
||||
const dialog = view.getByRole('dialog')
|
||||
|
|
@ -95,4 +95,88 @@ describe('TurnUsagePanel', () => {
|
|||
fireEvent.pointerDown(document.body)
|
||||
expect(view.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
it('TEMPORARY flat variant: the whole meta line triggers a dialog with usage and time sections', () => {
|
||||
const usage: TurnTokenUsage = {
|
||||
uncachedInputTokens: 5_060,
|
||||
cacheReadTokens: 4_940,
|
||||
outputTokens: 5_800,
|
||||
totalTokens: 15_800,
|
||||
}
|
||||
const view = render(
|
||||
<TurnUsagePanel
|
||||
usage={usage}
|
||||
variant="flat"
|
||||
time={Date.now()}
|
||||
runMs={19_000}
|
||||
tokensPerSecond={20}
|
||||
ttftMs={1_200}
|
||||
t={t}
|
||||
/>,
|
||||
)
|
||||
const trigger = view.getByRole('button')
|
||||
// Separator dots live in spaced spans, so textContent carries bare `·`.
|
||||
expect(trigger.textContent)
|
||||
.toMatch(/^.+·Ran for 19s·Usage 15\.8K tok·Cache hit 49\.4%·20 tok\/s·TTFT 1\.2s$/)
|
||||
expect(trigger.querySelector('svg')).toBeNull()
|
||||
|
||||
fireEvent.click(trigger)
|
||||
const dialog = view.getByRole('dialog')
|
||||
expect(dialog.firstChild?.textContent).toBe('Turn usage15,800 tok')
|
||||
expect(dialog.textContent).toContain('Turn time and speed')
|
||||
const timeDetails = dialog.querySelector('[data-turn-time-details]') as HTMLElement
|
||||
expect(timeDetails.textContent).toContain('Total run time19s')
|
||||
expect(timeDetails.textContent).toContain('Tokens per second (TPS)20 tok/s')
|
||||
expect(timeDetails.textContent).toContain('Average time to first token (TTFT)1.2s')
|
||||
})
|
||||
|
||||
it('TEMPORARY flat variant: absent time facts leave no Turn-time section', () => {
|
||||
const usage: TurnTokenUsage = {
|
||||
uncachedInputTokens: 120,
|
||||
outputTokens: 30,
|
||||
totalTokens: 150,
|
||||
}
|
||||
const view = render(<TurnUsagePanel usage={usage} variant="flat" t={t} />)
|
||||
const trigger = view.getByRole('button')
|
||||
expect(trigger.textContent).toBe('Usage 150 tok')
|
||||
fireEvent.click(trigger)
|
||||
const dialog = view.getByRole('dialog')
|
||||
expect(dialog.textContent).not.toContain('Turn time')
|
||||
expect(dialog.querySelector('[data-turn-time-details]')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('TurnTimePanel', () => {
|
||||
it('shows a clock-and-duration pill and opens the time dialog on click', () => {
|
||||
const view = render(
|
||||
<TurnTimePanel runMs={19_000} tokensPerSecond={20} ttftMs={1_200} t={t} />,
|
||||
)
|
||||
const trigger = view.getByRole('button')
|
||||
expect(trigger.textContent).toBe('Ran for 19s')
|
||||
expect(trigger.querySelector('svg')).not.toBeNull()
|
||||
expect(trigger.getAttribute('aria-haspopup')).toBe('dialog')
|
||||
expect(view.queryByRole('dialog')).toBeNull()
|
||||
|
||||
fireEvent.click(trigger)
|
||||
expect(trigger.getAttribute('aria-expanded')).toBe('true')
|
||||
const dialog = view.getByRole('dialog')
|
||||
expect(dialog.getAttribute('aria-label')).toBe('Turn time and speed')
|
||||
expect(dialog.parentElement).toBe(document.body)
|
||||
const details = dialog.querySelector('[data-turn-time-details]') as HTMLElement
|
||||
expect(details.textContent).toContain('Total run time19s')
|
||||
expect(details.textContent).toContain('Tokens per second (TPS)20 tok/s')
|
||||
expect(details.textContent).toContain('Average time to first token (TTFT)1.2s')
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(view.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
it('omits unrecorded speed and TTFT rows', () => {
|
||||
const view = render(<TurnTimePanel runMs={3_000} t={t} />)
|
||||
fireEvent.click(view.getByRole('button'))
|
||||
const dialog = view.getByRole('dialog')
|
||||
expect(dialog.textContent).toContain('Total run time3s')
|
||||
expect(dialog.textContent).not.toContain('Tokens per second')
|
||||
expect(dialog.textContent).not.toContain('Time to first token')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -761,6 +761,23 @@ export const IconDataOutline16 = ({ size = 16, className }: IconProps) => (
|
|||
</svg>
|
||||
)
|
||||
|
||||
/** IconDataOutline16 without its gear: a three-tier database cylinder. */
|
||||
export const IconDatabaseOutline16 = ({ size = 16, className }: IconProps) => (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="8" cy="3.6" rx="5.75" ry="2.4" stroke="currentColor" strokeWidth="1.25" />
|
||||
<path d="M2.25 3.6V12.3A5.75 2.4 0 0 0 13.75 12.3V3.6" stroke="currentColor" strokeWidth="1.25" />
|
||||
<path d="M2.25 7.95A5.75 2.4 0 0 0 13.75 7.95" stroke="currentColor" strokeWidth="1.25" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** Thin-stroke clock: outlined dial with square-cut hour and minute hands. */
|
||||
export const IconClockOutline16 = ({ size = 16, className }: IconProps) => (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="6.375" stroke="currentColor" strokeWidth="1.25" />
|
||||
<path d="M8 4.4V8.3L10.7 9.85" stroke="currentColor" strokeWidth="1.25" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** ic_send_outline_14 (figma extract): thin-stroke upward send arrow. */
|
||||
export const IconSendOutline14 = ({ size = 14, className }: IconProps) => (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
|
|
|||
|
|
@ -49,7 +49,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -72,7 +72,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- button "Thought for a while":
|
||||
- text: Thought for a while
|
||||
- img
|
||||
|
|
@ -85,7 +88,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. {{clock}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "1 tool call" [expanded]:
|
||||
|
|
@ -104,7 +110,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled."':
|
||||
- img
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is enabled."':
|
||||
- img
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is feedback-gated; recording feedback uploads the session records not yet shared."':
|
||||
- img
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- 'button "feedback Feedback recorded for session session-{{uuid}} Anonymous user: {{uuid}}. Session sharing is feedback-gated; recording feedback uploads the session records not yet shared."':
|
||||
- img
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -97,7 +97,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- button "System prompt":
|
||||
- img
|
||||
- img
|
||||
|
|
@ -217,7 +220,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- button "Back to bottom":
|
||||
- img
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- button "System prompt":
|
||||
- img
|
||||
- img
|
||||
|
|
@ -107,7 +110,10 @@
|
|||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- tooltip "Branch into a new conversation"
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -38,7 +38,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@
|
|||
- img
|
||||
- button "Branch into a new conversation" [disabled]:
|
||||
- img
|
||||
- text: Available only on the last message of a completed turn 7/25 {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- text: Available only on the last message of a completed turn
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}}
|
||||
- button "Read a.txt":
|
||||
- img
|
||||
- img
|
||||
|
|
@ -58,7 +62,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -54,7 +54,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -46,7 +46,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- button "2 queued messages" [expanded]
|
||||
- list:
|
||||
- listitem:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- button "2 queued messages" [expanded]
|
||||
- list:
|
||||
- listitem:
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}}
|
||||
- button "compact Compacted 5 history items (~{{tokens}} tokens)"
|
||||
- button "Context injection AGENTS.md":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}}
|
||||
- button "compact Compacted 5 history items (~{{tokens}} tokens)"
|
||||
- button "Context injection AGENTS.md":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}}
|
||||
- button "compact Compacted 5 history items (~{{tokens}} tokens)"
|
||||
- button "Context injection AGENTS.md":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: 7/25 {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: 7/25 {{clock}}
|
||||
- button "compact Compacted 5 history items (~{{tokens}} tokens)"
|
||||
- button "Context injection AGENTS.md":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{date}} {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- button "System prompt":
|
||||
- img
|
||||
- img
|
||||
|
|
@ -68,7 +71,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- button "System prompt":
|
||||
- img
|
||||
- img
|
||||
|
|
@ -56,7 +59,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -28,10 +28,13 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "Usage 15.8K tok · Cache hit 49.7%":
|
||||
- button "Usage 15.8K tok":
|
||||
- img
|
||||
- text: Usage 15.8K tok · Cache hit 49.7%
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- text: Usage 15.8K tok
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -41,10 +41,13 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "Usage 15.8K tok · Cache hit 49.7%":
|
||||
- button "Usage 15.8K tok":
|
||||
- img
|
||||
- text: Usage 15.8K tok · Cache hit 49.7%
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- text: Usage 15.8K tok
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -28,10 +28,13 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "Usage 15.8K tok · Cache hit 49.7%" [expanded]:
|
||||
- button "Usage 15.8K tok" [expanded]:
|
||||
- img
|
||||
- text: Usage 15.8K tok · Cache hit 49.7%
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- text: Usage 15.8K tok
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
- textbox "Message or run a task... / commands, @ files or sessions"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
|
|
|||
|
|
@ -36,4 +36,7 @@
|
|||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} {{throughput}} tok/s TTFT {{duration}}
|
||||
- button "Ran for {{duration}}":
|
||||
- img
|
||||
- text: Ran for {{duration}}
|
||||
- text: {{clock}}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue