@neuronection/assistant-ui 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +5 -0
- package/dist/chat-export.d.ts +29 -0
- package/dist/chat-export.js +3 -0
- package/dist/chat-export.js.map +1 -0
- package/dist/chat-history-button.d.ts +31 -0
- package/dist/chat-history-button.js +3 -0
- package/dist/chat-history-button.js.map +1 -0
- package/dist/chat-tools-catalog.d.ts +50 -0
- package/dist/chat-tools-catalog.js +3 -0
- package/dist/chat-tools-catalog.js.map +1 -0
- package/dist/chat-trace-meta.d.ts +19 -0
- package/dist/chat-trace-meta.js +3 -0
- package/dist/chat-trace-meta.js.map +1 -0
- package/dist/chat-trace-timeline.d.ts +49 -0
- package/dist/chat-trace-timeline.js +3 -0
- package/dist/chat-trace-timeline.js.map +1 -0
- package/dist/chunk-5KFOQEOG.js +154 -0
- package/dist/chunk-5KFOQEOG.js.map +1 -0
- package/dist/chunk-DRJGZ5TB.js +47 -0
- package/dist/chunk-DRJGZ5TB.js.map +1 -0
- package/dist/chunk-QMQ73C23.js +41 -0
- package/dist/chunk-QMQ73C23.js.map +1 -0
- package/dist/chunk-YYPO24TA.js +38 -0
- package/dist/chunk-YYPO24TA.js.map +1 -0
- package/dist/chunk-ZSCL34WT.js +149 -0
- package/dist/chunk-ZSCL34WT.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +23 -18
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +21 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @neuronection/assistant-ui
|
|
2
2
|
|
|
3
|
+
## 0.28.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`a4ca11f`](https://github.com/neuronection/assistant-ui/commit/a4ca11f4789cb49d11b42598fe001e55ab03f8b9) Thanks [@constLiakos](https://github.com/constLiakos)! - New `chat-trace-timeline` module: `ChatTraceTimeline` — a collapsible
|
|
8
|
+
per-turn trace attached to assistant replies (study's `TraceTimeline`,
|
|
9
|
+
generalized). Collapsed: total duration · tool count · model. Expanded:
|
|
10
|
+
duration-proportional bars for flow phases and tool calls, total/token
|
|
11
|
+
row, and a raw-reasoning disclosure. Pure presentational — trace data
|
|
12
|
+
is mapped to `entries` app-side (study adopts it directly; career feeds
|
|
13
|
+
it from persisted `metadata_json`).
|
|
14
|
+
|
|
15
|
+
## 0.27.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- [`8d707aa`](https://github.com/neuronection/assistant-ui/commit/8d707aa237863fc578eb323b79bce9c1818aae6c) Thanks [@constLiakos](https://github.com/constLiakos)! - New `chat-export` module: `buildChatMarkdown` (conversation → Markdown
|
|
20
|
+
with bold or heading role styles and app-supplied blockquote
|
|
21
|
+
annotations), `chatExportFileName` (slugged `.md` name) and
|
|
22
|
+
`downloadChatMarkdown` (blob download). Career's and study's local
|
|
23
|
+
export builders, generalized as a pure utility.
|
|
24
|
+
|
|
25
|
+
- [`8d707aa`](https://github.com/neuronection/assistant-ui/commit/8d707aa237863fc578eb323b79bce9c1818aae6c) Thanks [@constLiakos](https://github.com/constLiakos)! - New `chat-history-button` module: `ChatHistoryButton` — a labelled
|
|
26
|
+
history popover for chat headers (career's `HistoryButton` + study's
|
|
27
|
+
ChatPanel history popover, generalized). Owns the panel sizing, an
|
|
28
|
+
`onOpen` refresh hook and the close-on-pick wiring via a `close`
|
|
29
|
+
render-prop; the session list itself stays app-side
|
|
30
|
+
`ChatSessionList` glue.
|
|
31
|
+
|
|
32
|
+
- [`8d707aa`](https://github.com/neuronection/assistant-ui/commit/8d707aa237863fc578eb323b79bce9c1818aae6c) Thanks [@constLiakos](https://github.com/constLiakos)! - New `chat-tools-catalog` module: `ChatToolsCatalog` renders the catalog of
|
|
33
|
+
tools an assistant can use — searchable disclosure cards with tool name,
|
|
34
|
+
human title, scope chip, description, argument list (name/type/required/
|
|
35
|
+
description), example payload and response shape. Presentational and
|
|
36
|
+
controlled-external: data in via `tools`, fetching and the modal shell
|
|
37
|
+
stay app-side (study's `ToolsDialog` body and career's `/ai/tools`
|
|
38
|
+
surface, generalized).
|
|
39
|
+
|
|
40
|
+
- [`8d707aa`](https://github.com/neuronection/assistant-ui/commit/8d707aa237863fc578eb323b79bce9c1818aae6c) Thanks [@constLiakos](https://github.com/constLiakos)! - New `chat-trace-meta` module: `ChatTraceMeta` — compact
|
|
41
|
+
`model · duration · N tools` turn-trace badges under an assistant reply
|
|
42
|
+
(career's `TraceMeta`, generalized). Renders nothing without data;
|
|
43
|
+
parsing `metadata_json` stays app-side.
|
|
44
|
+
|
|
3
45
|
## 0.26.0
|
|
4
46
|
|
|
5
47
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -94,6 +94,11 @@ Visual reference: [gallery](https://neuronection.github.io/assistant-ui/).
|
|
|
94
94
|
| [`chat-composer`](./docs/components/chat-composer.md) | `ChatComposer` — auto-growing IME-safe input with send/stop states, toolbar/attachment/suggestion slots, drag-drop + paste wiring | [stories](https://neuronection.github.io/assistant-ui/?story=chat-composer--default) |
|
|
95
95
|
| [`chat-transcript`](./docs/components/chat-transcript.md) / [`chat-branch-tree`](./docs/components/chat-branch-tree.md) / [`chat-session-list`](./docs/components/chat-session-list.md) | `ChatTranscript` (log semantics, polite announcements, stick-to-bottom + jump pill, optional virtualization) · `ChatBranchTree` (OpenWebUI-style branch rail, keyboard tree nav) · `ChatSessionList` (fuzzy search, date groups, row actions) | [stories](https://neuronection.github.io/assistant-ui/?story=chat-surfaces--transcript) |
|
|
96
96
|
| [`chat-panel`](./docs/components/chat-panel.md) / [`chat-drawer`](./docs/components/chat-drawer.md) / [`chat-launcher`](./docs/components/chat-launcher.md) | the three chat surfaces from one assembly — `ChatPanel` host (`page` \| `sidebar` \| `bubble`) · `ChatDrawer` (resizable sidepanel on Radix Dialog, keyboard resize) · `ChatLauncher` (floating bubble → anchored panel, badge) | [stories](https://neuronection.github.io/assistant-ui/?story=chat-surfaces--transcript) |
|
|
97
|
+
| [`chat-tools-catalog`](./docs/components/chat-tools-catalog.md) | `ChatToolsCatalog` — searchable disclosure catalog of the tools an assistant can use (name, title, scope, arguments, example, response); fetching stays app-side | [stories](https://neuronection.github.io/assistant-ui/?story=chat-tools-catalog--default) |
|
|
98
|
+
| [`chat-export`](./docs/components/chat-export.md) | `buildChatMarkdown` / `chatExportFileName` / `downloadChatMarkdown` — conversation → Markdown export with role-label styles and app-supplied blockquote annotations | [stories](https://neuronection.github.io/assistant-ui/?story=chat-export--bold-style) |
|
|
99
|
+
| [`chat-history-button`](./docs/components/chat-history-button.md) | `ChatHistoryButton` — labelled history popover for a chat header (open-refresh hook, close-on-pick); the session list stays app-side | [stories](https://neuronection.github.io/assistant-ui/?story=chat-history-button--default) |
|
|
100
|
+
| [`chat-trace-meta`](./docs/components/chat-trace-meta.md) | `ChatTraceMeta` — compact `model · duration · N tools` trace badges; renders nothing without data | [stories](https://neuronection.github.io/assistant-ui/?story=chat-trace-meta--default) |
|
|
101
|
+
| [`chat-trace-timeline`](./docs/components/chat-trace-timeline.md) | `ChatTraceTimeline` — collapsible per-turn trace: duration-proportional phase/tool bars, tokens, model, reasoning disclosure | [stories](https://neuronection.github.io/assistant-ui/?story=chat-trace-timeline--default) |
|
|
97
102
|
| [`flow-status`](./docs/components/flow-status.md) | `FlowStatusCard` (multi-step AI-flow progress from the family event vocabulary: per-node status, current-step emphasis, progress summary, error + retryable, controlled retry/cancel/resume, `detail` slot for HITL cards) | [stories](https://neuronection.github.io/assistant-ui/?story=flow-status--running) |
|
|
98
103
|
| [`settings-shell`](./docs/components/settings-shell.md) / [`provider-form`](./docs/components/provider-form.md) / [`connection-test-row`](./docs/components/connection-test-row.md) | `SettingsShell` (controlled two-pane nav) · `ProviderForm` (write-only API-key field) · `ConnectionTestRow` (idle/testing/ok/fail + latency, `inline` variant + meta slot) | [stories](https://neuronection.github.io/assistant-ui/?story=settings-trio--provider-form-story) |
|
|
99
104
|
| [`sidebar-nav`](./docs/components/sidebar-nav.md) | `SidebarNav` (family-standard vertical nav: groups + sections, badges, controlled active id, pinned secondary items, optional collapsed icon rail with hover flyouts, header/footer slots) | [stories](https://neuronection.github.io/assistant-ui/?story=sidebar-nav--groups-story) |
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
interface ChatExportMessage {
|
|
2
|
+
role: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
interface ChatExportOptions {
|
|
6
|
+
/** Role label for user messages. Default `You`. */
|
|
7
|
+
userLabel?: string;
|
|
8
|
+
/** Role label for assistant messages. Default `Assistant`. */
|
|
9
|
+
assistantLabel?: string;
|
|
10
|
+
/** `bold` renders `**You:**` (career style); `heading` renders `### 🙋 Question` (study style). */
|
|
11
|
+
roleStyle?: 'bold' | 'heading';
|
|
12
|
+
/**
|
|
13
|
+
* Per-message annotation lines rendered as blockquotes directly after
|
|
14
|
+
* the content (career: referenced jobs/postings; study: citations).
|
|
15
|
+
*/
|
|
16
|
+
annotations?: (message: ChatExportMessage, index: number) => string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Conversation → Markdown builder (career's `buildChatMarkdown` + study's
|
|
20
|
+
* `messagesToMarkdown`, generalized): title header, one labelled section
|
|
21
|
+
* per message, app-supplied blockquote annotations. Pure — no DOM.
|
|
22
|
+
*/
|
|
23
|
+
declare function buildChatMarkdown(title: string, messages: ChatExportMessage[], options?: ChatExportOptions): string;
|
|
24
|
+
/** Slugged `.md` file name for a chat title (`My Chat!` → `my-chat.md`). */
|
|
25
|
+
declare function chatExportFileName(title: string): string;
|
|
26
|
+
/** Client-side download of the built markdown (blob + object URL). */
|
|
27
|
+
declare function downloadChatMarkdown(markdown: string, fileName: string): void;
|
|
28
|
+
|
|
29
|
+
export { type ChatExportMessage, type ChatExportOptions, buildChatMarkdown, chatExportFileName, downloadChatMarkdown };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chat-export.js"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { LucideIcon } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
interface ChatHistoryButtonLabels {
|
|
5
|
+
open: string;
|
|
6
|
+
}
|
|
7
|
+
interface ChatHistoryButtonProps {
|
|
8
|
+
/**
|
|
9
|
+
* Popover content, typically the app's session-list glue. Called with
|
|
10
|
+
* `close` — invoke it after a pick to dismiss the popover (the
|
|
11
|
+
* `closeSignal` pattern underneath).
|
|
12
|
+
*/
|
|
13
|
+
children: (close: () => void) => React.ReactNode;
|
|
14
|
+
icon?: LucideIcon;
|
|
15
|
+
/** Called every time the popover opens — refresh the session list here. */
|
|
16
|
+
onOpen?: () => void;
|
|
17
|
+
align?: 'start' | 'center' | 'end';
|
|
18
|
+
panelClassName?: string;
|
|
19
|
+
labels?: Partial<ChatHistoryButtonLabels>;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Header entry point for a chat's session history (career's
|
|
24
|
+
* `HistoryButton` + study's ChatPanel history popover, generalized): a
|
|
25
|
+
* labelled popover trigger wrapping the app's session list. The list
|
|
26
|
+
* itself (search, actions) stays app-side via
|
|
27
|
+
* [`ChatSessionList`](../chat-session-list) glue.
|
|
28
|
+
*/
|
|
29
|
+
declare function ChatHistoryButton({ children, icon: Icon, onOpen, align, panelClassName, labels, className, }: ChatHistoryButtonProps): React.JSX.Element;
|
|
30
|
+
|
|
31
|
+
export { ChatHistoryButton, type ChatHistoryButtonLabels, type ChatHistoryButtonProps };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chat-history-button.js"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { LucideIcon } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
interface ChatToolCatalogArgument {
|
|
5
|
+
name: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
description?: string | null;
|
|
9
|
+
}
|
|
10
|
+
interface ChatToolCatalogEntry {
|
|
11
|
+
/** Stable tool identifier (rendered mono). */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Human title; falls back to `name`. */
|
|
14
|
+
title?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
arguments?: ChatToolCatalogArgument[];
|
|
17
|
+
example?: string | null;
|
|
18
|
+
response?: string | null;
|
|
19
|
+
scope?: string | null;
|
|
20
|
+
}
|
|
21
|
+
interface ChatToolsCatalogLabels {
|
|
22
|
+
/** Accessible name of the tool list. */
|
|
23
|
+
tools: string;
|
|
24
|
+
search: string;
|
|
25
|
+
searchPlaceholder: string;
|
|
26
|
+
arguments: string;
|
|
27
|
+
response: string;
|
|
28
|
+
required: string;
|
|
29
|
+
optional: string;
|
|
30
|
+
empty: string;
|
|
31
|
+
noResults: string;
|
|
32
|
+
}
|
|
33
|
+
interface ChatToolsCatalogProps {
|
|
34
|
+
tools: ChatToolCatalogEntry[];
|
|
35
|
+
searchable?: boolean;
|
|
36
|
+
/** Expand every entry's details initially (uncontrolled per entry after that). */
|
|
37
|
+
defaultOpen?: boolean;
|
|
38
|
+
labels?: Partial<ChatToolsCatalogLabels>;
|
|
39
|
+
icon?: LucideIcon;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Catalog of the tools an assistant can use (study's `ToolsDialog` body,
|
|
44
|
+
* generalized): searchable disclosure cards with description, argument
|
|
45
|
+
* list, example and response shape. Presentational only — fetching and
|
|
46
|
+
* the surrounding modal/popover stay app-side.
|
|
47
|
+
*/
|
|
48
|
+
declare const ChatToolsCatalog: React.ForwardRefExoticComponent<ChatToolsCatalogProps & React.RefAttributes<HTMLDivElement>>;
|
|
49
|
+
|
|
50
|
+
export { type ChatToolCatalogArgument, type ChatToolCatalogEntry, ChatToolsCatalog, type ChatToolsCatalogLabels, type ChatToolsCatalogProps };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chat-tools-catalog.js"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface ChatTraceMetaProps {
|
|
4
|
+
/** Model identifier that produced the reply. */
|
|
5
|
+
model?: string;
|
|
6
|
+
/** Turn elapsed time (formatted ms below 1 s, then seconds). */
|
|
7
|
+
durationMs?: number;
|
|
8
|
+
/** Number of tools the turn executed (renders `N tools`). */
|
|
9
|
+
toolCount?: number;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Compact turn-trace badges ("gpt-5.6 · 1.9 s · 3 tools", career's
|
|
14
|
+
* `TraceMeta`, generalized): renders nothing without data. Reads a
|
|
15
|
+
* normalized view-model — parsing `metadata_json` stays app-side.
|
|
16
|
+
*/
|
|
17
|
+
declare const ChatTraceMeta: React.ForwardRefExoticComponent<ChatTraceMetaProps & React.RefAttributes<HTMLDivElement>>;
|
|
18
|
+
|
|
19
|
+
export { ChatTraceMeta, type ChatTraceMetaProps };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chat-trace-meta.js"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface ChatTraceTimelineEntry {
|
|
4
|
+
/** `phase` = model/flow round (primary bar); `tool` = tool call (warning bar). */
|
|
5
|
+
kind: 'phase' | 'tool';
|
|
6
|
+
/** Phase label or tool name (translated at the call site). */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Secondary detail, e.g. the tool arguments summary (title tooltip). */
|
|
9
|
+
detail?: string | null;
|
|
10
|
+
startMs?: number | null;
|
|
11
|
+
durationMs?: number | null;
|
|
12
|
+
}
|
|
13
|
+
interface ChatTraceTimelineTrace {
|
|
14
|
+
model?: string | null;
|
|
15
|
+
/** Total turn latency the bars are proportional to. */
|
|
16
|
+
latencyMs?: number | null;
|
|
17
|
+
inputTokens?: number | null;
|
|
18
|
+
outputTokens?: number | null;
|
|
19
|
+
/** Raw model reasoning, shown in a nested disclosure. */
|
|
20
|
+
thinking?: string | null;
|
|
21
|
+
}
|
|
22
|
+
interface ChatTraceTimelineLabels {
|
|
23
|
+
/** Accessible name of the toggle (summary is composed from data). */
|
|
24
|
+
toggle: string;
|
|
25
|
+
/** Word appended to the tool count, e.g. `tools`. */
|
|
26
|
+
tools: string;
|
|
27
|
+
/** Prefix of the expanded total row, e.g. `Total`. */
|
|
28
|
+
total: string;
|
|
29
|
+
/** Suffix of the token count, e.g. `tokens`. */
|
|
30
|
+
tokens: string;
|
|
31
|
+
reasoning: string;
|
|
32
|
+
}
|
|
33
|
+
interface ChatTraceTimelineProps {
|
|
34
|
+
trace: ChatTraceTimelineTrace;
|
|
35
|
+
entries: ChatTraceTimelineEntry[];
|
|
36
|
+
defaultOpen?: boolean;
|
|
37
|
+
labels?: Partial<ChatTraceTimelineLabels>;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Collapsible per-turn trace timeline (study's `TraceTimeline`,
|
|
42
|
+
* generalized): collapsed summary (duration · N tools · model), expanded
|
|
43
|
+
* duration-proportional bar rows for flow phases and tool calls plus a
|
|
44
|
+
* raw-reasoning disclosure. Presentational — persisted or live trace data
|
|
45
|
+
* is mapped to `entries` app-side.
|
|
46
|
+
*/
|
|
47
|
+
declare const ChatTraceTimeline: React.ForwardRefExoticComponent<ChatTraceTimelineProps & React.RefAttributes<HTMLDivElement>>;
|
|
48
|
+
|
|
49
|
+
export { ChatTraceTimeline, type ChatTraceTimelineEntry, type ChatTraceTimelineLabels, type ChatTraceTimelineProps, type ChatTraceTimelineTrace };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chat-trace-timeline.js"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { cn } from './chunk-BLJWR4ZV.js';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Timer, ChevronDown, Brain } from 'lucide-react';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function formatDuration(ms) {
|
|
7
|
+
if (ms < 1e3) {
|
|
8
|
+
return `${Math.round(ms)} ms`;
|
|
9
|
+
}
|
|
10
|
+
return `${(ms / 1e3).toFixed(1)} s`;
|
|
11
|
+
}
|
|
12
|
+
var ChatTraceTimeline = React.forwardRef(
|
|
13
|
+
function ChatTraceTimeline2({ trace, entries, defaultOpen = false, labels, className }, ref) {
|
|
14
|
+
const [open, setOpen] = React.useState(defaultOpen);
|
|
15
|
+
const totalMs = Math.max(1, trace.latencyMs ?? 0);
|
|
16
|
+
const duration = formatDuration(trace.latencyMs ?? 0);
|
|
17
|
+
const toolsLabel = labels?.tools ?? "tools";
|
|
18
|
+
const toolCount = entries.filter((entry) => entry.kind === "tool").length;
|
|
19
|
+
const items = [...entries].sort(
|
|
20
|
+
(a, b) => (a.startMs ?? 0) - (b.startMs ?? 0)
|
|
21
|
+
);
|
|
22
|
+
return /* @__PURE__ */ jsxs(
|
|
23
|
+
"div",
|
|
24
|
+
{
|
|
25
|
+
ref,
|
|
26
|
+
"data-as": "chat-trace-timeline",
|
|
27
|
+
className: cn("w-full max-w-[92%]", className),
|
|
28
|
+
children: [
|
|
29
|
+
/* @__PURE__ */ jsxs(
|
|
30
|
+
"button",
|
|
31
|
+
{
|
|
32
|
+
type: "button",
|
|
33
|
+
onClick: () => setOpen((value) => !value),
|
|
34
|
+
"aria-expanded": open,
|
|
35
|
+
"aria-label": labels?.toggle ?? "Show response trace",
|
|
36
|
+
className: "flex items-center gap-1.5 text-[11px] text-[var(--as-muted-fg)] transition-colors hover:text-[var(--as-fg)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--as-focus-ring)]",
|
|
37
|
+
children: [
|
|
38
|
+
/* @__PURE__ */ jsx(Timer, { className: "size-3.5 shrink-0", "aria-hidden": true }),
|
|
39
|
+
/* @__PURE__ */ jsx("span", { children: `${duration} \xB7 ${toolCount} ${toolsLabel}` }),
|
|
40
|
+
trace.model ? /* @__PURE__ */ jsxs("span", { className: "font-mono text-[10px]", children: [
|
|
41
|
+
"\xB7 ",
|
|
42
|
+
trace.model
|
|
43
|
+
] }) : null,
|
|
44
|
+
/* @__PURE__ */ jsx(
|
|
45
|
+
ChevronDown,
|
|
46
|
+
{
|
|
47
|
+
className: cn(
|
|
48
|
+
"size-3.5 shrink-0 transition-transform duration-150",
|
|
49
|
+
open && "rotate-180"
|
|
50
|
+
),
|
|
51
|
+
"aria-hidden": true
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
open ? /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-col gap-1.5 overflow-hidden", children: [
|
|
58
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-[11px] text-[var(--as-muted-fg)]", children: [
|
|
59
|
+
/* @__PURE__ */ jsx("span", { children: `${labels?.total ?? "Total"} ${duration}` }),
|
|
60
|
+
/* @__PURE__ */ jsx("span", { children: `${trace.outputTokens ?? 0} ${labels?.tokens ?? "tokens"}` })
|
|
61
|
+
] }),
|
|
62
|
+
items.map((item, index) => {
|
|
63
|
+
const width = Math.max(
|
|
64
|
+
2,
|
|
65
|
+
Math.min(100, (item.durationMs ?? 0) / totalMs * 100)
|
|
66
|
+
);
|
|
67
|
+
return /* @__PURE__ */ jsxs(
|
|
68
|
+
"div",
|
|
69
|
+
{
|
|
70
|
+
"data-kind": item.kind,
|
|
71
|
+
className: "flex items-center gap-2 text-[11px]",
|
|
72
|
+
children: [
|
|
73
|
+
/* @__PURE__ */ jsx(
|
|
74
|
+
"span",
|
|
75
|
+
{
|
|
76
|
+
className: "w-16 shrink-0 truncate text-[var(--as-muted-fg)]",
|
|
77
|
+
title: item.detail ?? void 0,
|
|
78
|
+
children: item.label
|
|
79
|
+
}
|
|
80
|
+
),
|
|
81
|
+
/* @__PURE__ */ jsx("div", { className: "h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-[var(--as-surface-raised)]", children: /* @__PURE__ */ jsx(
|
|
82
|
+
"div",
|
|
83
|
+
{
|
|
84
|
+
className: cn(
|
|
85
|
+
"h-full rounded-full",
|
|
86
|
+
item.kind === "phase" ? "bg-[var(--as-primary)]" : "bg-[var(--as-warning)]"
|
|
87
|
+
),
|
|
88
|
+
style: { width: `${width}%` }
|
|
89
|
+
}
|
|
90
|
+
) }),
|
|
91
|
+
/* @__PURE__ */ jsx("span", { className: "w-12 shrink-0 text-right font-mono text-[10px] text-[var(--as-muted-fg)]", children: formatDuration(item.durationMs ?? 0) })
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
`${item.kind}-${index}`
|
|
95
|
+
);
|
|
96
|
+
}),
|
|
97
|
+
trace.thinking ? /* @__PURE__ */ jsx(
|
|
98
|
+
ThinkingDisclosure,
|
|
99
|
+
{
|
|
100
|
+
thinking: trace.thinking,
|
|
101
|
+
label: labels?.reasoning ?? "Reasoning"
|
|
102
|
+
}
|
|
103
|
+
) : null
|
|
104
|
+
] }) : null
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
function ThinkingDisclosure({
|
|
111
|
+
thinking,
|
|
112
|
+
label
|
|
113
|
+
}) {
|
|
114
|
+
const [open, setOpen] = React.useState(false);
|
|
115
|
+
const regionId = React.useId();
|
|
116
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-[var(--as-radius)] border border-[var(--as-border)] bg-[var(--as-surface)]", children: [
|
|
117
|
+
/* @__PURE__ */ jsxs(
|
|
118
|
+
"button",
|
|
119
|
+
{
|
|
120
|
+
type: "button",
|
|
121
|
+
onClick: () => setOpen((value) => !value),
|
|
122
|
+
"aria-expanded": open,
|
|
123
|
+
"aria-controls": regionId,
|
|
124
|
+
className: "flex w-full items-center gap-1.5 rounded-[var(--as-radius)] px-2 py-1.5 text-left text-[11px] text-[var(--as-muted-fg)] transition-colors hover:text-[var(--as-fg)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--as-focus-ring)]",
|
|
125
|
+
children: [
|
|
126
|
+
/* @__PURE__ */ jsx(Brain, { className: "size-3.5 shrink-0", "aria-hidden": true }),
|
|
127
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: label }),
|
|
128
|
+
/* @__PURE__ */ jsx(
|
|
129
|
+
ChevronDown,
|
|
130
|
+
{
|
|
131
|
+
className: cn(
|
|
132
|
+
"size-3.5 shrink-0 transition-transform duration-150",
|
|
133
|
+
open && "rotate-180"
|
|
134
|
+
),
|
|
135
|
+
"aria-hidden": true
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
open ? /* @__PURE__ */ jsx(
|
|
142
|
+
"p",
|
|
143
|
+
{
|
|
144
|
+
id: regionId,
|
|
145
|
+
className: "whitespace-pre-wrap px-2.5 pb-2 text-[11px] text-[var(--as-muted-fg)]",
|
|
146
|
+
children: thinking
|
|
147
|
+
}
|
|
148
|
+
) : null
|
|
149
|
+
] });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { ChatTraceTimeline };
|
|
153
|
+
//# sourceMappingURL=chunk-5KFOQEOG.js.map
|
|
154
|
+
//# sourceMappingURL=chunk-5KFOQEOG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/chat-trace-timeline/ChatTraceTimeline.tsx"],"names":["ChatTraceTimeline"],"mappings":"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","file":"chunk-5KFOQEOG.js","sourcesContent":["import * as React from 'react'\nimport { Brain, ChevronDown, Timer } from 'lucide-react'\n\nimport { cn } from '../../lib/utils'\n\nexport interface ChatTraceTimelineEntry {\n /** `phase` = model/flow round (primary bar); `tool` = tool call (warning bar). */\n kind: 'phase' | 'tool'\n /** Phase label or tool name (translated at the call site). */\n label: string\n /** Secondary detail, e.g. the tool arguments summary (title tooltip). */\n detail?: string | null\n startMs?: number | null\n durationMs?: number | null\n}\n\nexport interface ChatTraceTimelineTrace {\n model?: string | null\n /** Total turn latency the bars are proportional to. */\n latencyMs?: number | null\n inputTokens?: number | null\n outputTokens?: number | null\n /** Raw model reasoning, shown in a nested disclosure. */\n thinking?: string | null\n}\n\nexport interface ChatTraceTimelineLabels {\n /** Accessible name of the toggle (summary is composed from data). */\n toggle: string\n /** Word appended to the tool count, e.g. `tools`. */\n tools: string\n /** Prefix of the expanded total row, e.g. `Total`. */\n total: string\n /** Suffix of the token count, e.g. `tokens`. */\n tokens: string\n reasoning: string\n}\n\nexport interface ChatTraceTimelineProps {\n trace: ChatTraceTimelineTrace\n entries: ChatTraceTimelineEntry[]\n defaultOpen?: boolean\n labels?: Partial<ChatTraceTimelineLabels>\n className?: string\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) {\n return `${Math.round(ms)} ms`\n }\n return `${(ms / 1000).toFixed(1)} s`\n}\n\n/**\n * Collapsible per-turn trace timeline (study's `TraceTimeline`,\n * generalized): collapsed summary (duration · N tools · model), expanded\n * duration-proportional bar rows for flow phases and tool calls plus a\n * raw-reasoning disclosure. Presentational — persisted or live trace data\n * is mapped to `entries` app-side.\n */\nexport const ChatTraceTimeline = React.forwardRef<HTMLDivElement, ChatTraceTimelineProps>(\n function ChatTraceTimeline(\n { trace, entries, defaultOpen = false, labels, className },\n ref,\n ) {\n const [open, setOpen] = React.useState(defaultOpen)\n const totalMs = Math.max(1, trace.latencyMs ?? 0)\n const duration = formatDuration(trace.latencyMs ?? 0)\n const toolsLabel = labels?.tools ?? 'tools'\n const toolCount = entries.filter((entry) => entry.kind === 'tool').length\n const items = [...entries].sort(\n (a, b) => (a.startMs ?? 0) - (b.startMs ?? 0),\n )\n\n return (\n <div\n ref={ref}\n data-as=\"chat-trace-timeline\"\n className={cn('w-full max-w-[92%]', className)}\n >\n <button\n type=\"button\"\n onClick={() => setOpen((value) => !value)}\n aria-expanded={open}\n aria-label={labels?.toggle ?? 'Show response trace'}\n className=\"flex items-center gap-1.5 text-[11px] text-[var(--as-muted-fg)] transition-colors hover:text-[var(--as-fg)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--as-focus-ring)]\"\n >\n <Timer className=\"size-3.5 shrink-0\" aria-hidden />\n <span>\n {`${duration} · ${toolCount} ${toolsLabel}`}\n </span>\n {trace.model ? (\n <span className=\"font-mono text-[10px]\">· {trace.model}</span>\n ) : null}\n <ChevronDown\n className={cn(\n 'size-3.5 shrink-0 transition-transform duration-150',\n open && 'rotate-180',\n )}\n aria-hidden\n />\n </button>\n {open ? (\n <div className=\"mt-2 flex flex-col gap-1.5 overflow-hidden\">\n <div className=\"flex items-center justify-between text-[11px] text-[var(--as-muted-fg)]\">\n <span>{`${labels?.total ?? 'Total'} ${duration}`}</span>\n <span>\n {`${trace.outputTokens ?? 0} ${labels?.tokens ?? 'tokens'}`}\n </span>\n </div>\n {items.map((item, index) => {\n const width = Math.max(\n 2,\n Math.min(100, ((item.durationMs ?? 0) / totalMs) * 100),\n )\n return (\n <div\n key={`${item.kind}-${index}`}\n data-kind={item.kind}\n className=\"flex items-center gap-2 text-[11px]\"\n >\n <span\n className=\"w-16 shrink-0 truncate text-[var(--as-muted-fg)]\"\n title={item.detail ?? undefined}\n >\n {item.label}\n </span>\n <div className=\"h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-[var(--as-surface-raised)]\">\n <div\n className={cn(\n 'h-full rounded-full',\n item.kind === 'phase'\n ? 'bg-[var(--as-primary)]'\n : 'bg-[var(--as-warning)]',\n )}\n style={{ width: `${width}%` }}\n />\n </div>\n <span className=\"w-12 shrink-0 text-right font-mono text-[10px] text-[var(--as-muted-fg)]\">\n {formatDuration(item.durationMs ?? 0)}\n </span>\n </div>\n )\n })}\n {trace.thinking ? (\n <ThinkingDisclosure\n thinking={trace.thinking}\n label={labels?.reasoning ?? 'Reasoning'}\n />\n ) : null}\n </div>\n ) : null}\n </div>\n )\n },\n)\n\nfunction ThinkingDisclosure({\n thinking,\n label,\n}: {\n thinking: string\n label: string\n}) {\n const [open, setOpen] = React.useState(false)\n const regionId = React.useId()\n return (\n <div className=\"rounded-[var(--as-radius)] border border-[var(--as-border)] bg-[var(--as-surface)]\">\n <button\n type=\"button\"\n onClick={() => setOpen((value) => !value)}\n aria-expanded={open}\n aria-controls={regionId}\n className=\"flex w-full items-center gap-1.5 rounded-[var(--as-radius)] px-2 py-1.5 text-left text-[11px] text-[var(--as-muted-fg)] transition-colors hover:text-[var(--as-fg)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--as-focus-ring)]\"\n >\n <Brain className=\"size-3.5 shrink-0\" aria-hidden />\n <span className=\"flex-1\">{label}</span>\n <ChevronDown\n className={cn(\n 'size-3.5 shrink-0 transition-transform duration-150',\n open && 'rotate-180',\n )}\n aria-hidden\n />\n </button>\n {open ? (\n <p\n id={regionId}\n className=\"whitespace-pre-wrap px-2.5 pb-2 text-[11px] text-[var(--as-muted-fg)]\"\n >\n {thinking}\n </p>\n ) : null}\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/components/chat-export/buildChatMarkdown.ts
|
|
2
|
+
function roleLabel(role, options) {
|
|
3
|
+
if (role === "user") {
|
|
4
|
+
return options.userLabel ?? "You";
|
|
5
|
+
}
|
|
6
|
+
if (role === "assistant") {
|
|
7
|
+
return options.assistantLabel ?? "Assistant";
|
|
8
|
+
}
|
|
9
|
+
return role.charAt(0).toUpperCase() + role.slice(1);
|
|
10
|
+
}
|
|
11
|
+
function buildChatMarkdown(title, messages, options = {}) {
|
|
12
|
+
const heading = options.roleStyle === "heading";
|
|
13
|
+
const lines = [`# ${title}`, ""];
|
|
14
|
+
messages.forEach((message, index) => {
|
|
15
|
+
const who = roleLabel(message.role, options);
|
|
16
|
+
if (heading) {
|
|
17
|
+
lines.push(`### ${who}`, "", message.content);
|
|
18
|
+
} else {
|
|
19
|
+
lines.push(`**${who}:**`, "", message.content);
|
|
20
|
+
}
|
|
21
|
+
for (const annotation of options.annotations?.(message, index) ?? []) {
|
|
22
|
+
lines.push(`> ${annotation}`);
|
|
23
|
+
}
|
|
24
|
+
lines.push("");
|
|
25
|
+
});
|
|
26
|
+
return `${lines.join("\n").trimEnd()}
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
function chatExportFileName(title) {
|
|
30
|
+
const slug = title.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
31
|
+
return `${slug || "chat"}.md`;
|
|
32
|
+
}
|
|
33
|
+
function downloadChatMarkdown(markdown, fileName) {
|
|
34
|
+
const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
|
|
35
|
+
const url = URL.createObjectURL(blob);
|
|
36
|
+
const anchor = document.createElement("a");
|
|
37
|
+
anchor.href = url;
|
|
38
|
+
anchor.download = fileName;
|
|
39
|
+
document.body.appendChild(anchor);
|
|
40
|
+
anchor.click();
|
|
41
|
+
anchor.remove();
|
|
42
|
+
URL.revokeObjectURL(url);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { buildChatMarkdown, chatExportFileName, downloadChatMarkdown };
|
|
46
|
+
//# sourceMappingURL=chunk-DRJGZ5TB.js.map
|
|
47
|
+
//# sourceMappingURL=chunk-DRJGZ5TB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/chat-export/buildChatMarkdown.ts"],"names":[],"mappings":";AAmBA,SAAS,SAAA,CAAU,MAAc,OAAA,EAAoC;AACnE,EAAA,IAAI,SAAS,MAAA,EAAQ;AACnB,IAAA,OAAO,QAAQ,SAAA,IAAa,KAAA;AAAA,EAC9B;AACA,EAAA,IAAI,SAAS,WAAA,EAAa;AACxB,IAAA,OAAO,QAAQ,cAAA,IAAkB,WAAA;AAAA,EACnC;AACA,EAAA,OAAO,IAAA,CAAK,OAAO,CAAC,CAAA,CAAE,aAAY,GAAI,IAAA,CAAK,MAAM,CAAC,CAAA;AACpD;AAOO,SAAS,iBAAA,CACd,KAAA,EACA,QAAA,EACA,OAAA,GAA6B,EAAC,EACtB;AACR,EAAA,MAAM,OAAA,GAAU,QAAQ,SAAA,KAAc,SAAA;AACtC,EAAA,MAAM,KAAA,GAAkB,CAAC,CAAA,EAAA,EAAK,KAAK,IAAI,EAAE,CAAA;AACzC,EAAA,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,EAAS,KAAA,KAAU;AACnC,IAAA,MAAM,GAAA,GAAM,SAAA,CAAU,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA;AAC3C,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,KAAA,CAAM,KAAK,CAAA,IAAA,EAAO,GAAG,CAAA,CAAA,EAAI,EAAA,EAAI,QAAQ,OAAO,CAAA;AAAA,IAC9C,CAAA,MAAO;AACL,MAAA,KAAA,CAAM,KAAK,CAAA,EAAA,EAAK,GAAG,CAAA,GAAA,CAAA,EAAO,EAAA,EAAI,QAAQ,OAAO,CAAA;AAAA,IAC/C;AACA,IAAA,KAAA,MAAW,cAAc,OAAA,CAAQ,WAAA,GAAc,SAAS,KAAK,CAAA,IAAK,EAAC,EAAG;AACpE,MAAA,KAAA,CAAM,IAAA,CAAK,CAAA,EAAA,EAAK,UAAU,CAAA,CAAE,CAAA;AAAA,IAC9B;AACA,IAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AAAA,EACf,CAAC,CAAA;AACD,EAAA,OAAO,GAAG,KAAA,CAAM,IAAA,CAAK,IAAI,CAAA,CAAE,SAAS;AAAA,CAAA;AACtC;AAGO,SAAS,mBAAmB,KAAA,EAAuB;AACxD,EAAA,MAAM,OAAO,KAAA,CACV,IAAA,EAAK,CACL,WAAA,GACA,OAAA,CAAQ,aAAA,EAAe,GAAG,CAAA,CAC1B,QAAQ,UAAA,EAAY,EAAE,CAAA,CACtB,KAAA,CAAM,GAAG,EAAE,CAAA;AACd,EAAA,OAAO,CAAA,EAAG,QAAQ,MAAM,CAAA,GAAA,CAAA;AAC1B;AAGO,SAAS,oBAAA,CAAqB,UAAkB,QAAA,EAAwB;AAC7E,EAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,QAAQ,CAAA,EAAG,EAAE,IAAA,EAAM,6BAAA,EAA+B,CAAA;AACzE,EAAA,MAAM,GAAA,GAAM,GAAA,CAAI,eAAA,CAAgB,IAAI,CAAA;AACpC,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,aAAA,CAAc,GAAG,CAAA;AACzC,EAAA,MAAA,CAAO,IAAA,GAAO,GAAA;AACd,EAAA,MAAA,CAAO,QAAA,GAAW,QAAA;AAClB,EAAA,QAAA,CAAS,IAAA,CAAK,YAAY,MAAM,CAAA;AAChC,EAAA,MAAA,CAAO,KAAA,EAAM;AACb,EAAA,MAAA,CAAO,MAAA,EAAO;AACd,EAAA,GAAA,CAAI,gBAAgB,GAAG,CAAA;AACzB","file":"chunk-DRJGZ5TB.js","sourcesContent":["export interface ChatExportMessage {\n role: string\n content: string\n}\n\nexport interface ChatExportOptions {\n /** Role label for user messages. Default `You`. */\n userLabel?: string\n /** Role label for assistant messages. Default `Assistant`. */\n assistantLabel?: string\n /** `bold` renders `**You:**` (career style); `heading` renders `### 🙋 Question` (study style). */\n roleStyle?: 'bold' | 'heading'\n /**\n * Per-message annotation lines rendered as blockquotes directly after\n * the content (career: referenced jobs/postings; study: citations).\n */\n annotations?: (message: ChatExportMessage, index: number) => string[]\n}\n\nfunction roleLabel(role: string, options: ChatExportOptions): string {\n if (role === 'user') {\n return options.userLabel ?? 'You'\n }\n if (role === 'assistant') {\n return options.assistantLabel ?? 'Assistant'\n }\n return role.charAt(0).toUpperCase() + role.slice(1)\n}\n\n/**\n * Conversation → Markdown builder (career's `buildChatMarkdown` + study's\n * `messagesToMarkdown`, generalized): title header, one labelled section\n * per message, app-supplied blockquote annotations. Pure — no DOM.\n */\nexport function buildChatMarkdown(\n title: string,\n messages: ChatExportMessage[],\n options: ChatExportOptions = {},\n): string {\n const heading = options.roleStyle === 'heading'\n const lines: string[] = [`# ${title}`, '']\n messages.forEach((message, index) => {\n const who = roleLabel(message.role, options)\n if (heading) {\n lines.push(`### ${who}`, '', message.content)\n } else {\n lines.push(`**${who}:**`, '', message.content)\n }\n for (const annotation of options.annotations?.(message, index) ?? []) {\n lines.push(`> ${annotation}`)\n }\n lines.push('')\n })\n return `${lines.join('\\n').trimEnd()}\\n`\n}\n\n/** Slugged `.md` file name for a chat title (`My Chat!` → `my-chat.md`). */\nexport function chatExportFileName(title: string): string {\n const slug = title\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 60)\n return `${slug || 'chat'}.md`\n}\n\n/** Client-side download of the built markdown (blob + object URL). */\nexport function downloadChatMarkdown(markdown: string, fileName: string): void {\n const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' })\n const url = URL.createObjectURL(blob)\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = fileName\n document.body.appendChild(anchor)\n anchor.click()\n anchor.remove()\n URL.revokeObjectURL(url)\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cn } from './chunk-BLJWR4ZV.js';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
function formatDuration(ms) {
|
|
6
|
+
if (ms < 1e3) {
|
|
7
|
+
return `${Math.round(ms)} ms`;
|
|
8
|
+
}
|
|
9
|
+
return `${(ms / 1e3).toFixed(1)} s`;
|
|
10
|
+
}
|
|
11
|
+
var ChatTraceMeta = React.forwardRef(
|
|
12
|
+
function ChatTraceMeta2({ model, durationMs, toolCount, className }, ref) {
|
|
13
|
+
const hasModel = typeof model === "string" && model !== "";
|
|
14
|
+
const hasDuration = typeof durationMs === "number";
|
|
15
|
+
const hasTools = typeof toolCount === "number" && toolCount > 0;
|
|
16
|
+
if (!hasModel && !hasDuration && !hasTools) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return /* @__PURE__ */ jsxs(
|
|
20
|
+
"div",
|
|
21
|
+
{
|
|
22
|
+
ref,
|
|
23
|
+
"data-as": "chat-trace-meta",
|
|
24
|
+
className: cn("mt-1 flex flex-wrap items-center gap-2 font-mono text-[10px] text-[var(--as-muted-fg)]", className),
|
|
25
|
+
children: [
|
|
26
|
+
hasModel ? /* @__PURE__ */ jsx("span", { children: model }) : null,
|
|
27
|
+
hasDuration ? /* @__PURE__ */ jsx("span", { children: formatDuration(durationMs) }) : null,
|
|
28
|
+
hasTools ? /* @__PURE__ */ jsxs("span", { children: [
|
|
29
|
+
toolCount,
|
|
30
|
+
" ",
|
|
31
|
+
toolCount === 1 ? "tool" : "tools"
|
|
32
|
+
] }) : null
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export { ChatTraceMeta };
|
|
40
|
+
//# sourceMappingURL=chunk-QMQ73C23.js.map
|
|
41
|
+
//# sourceMappingURL=chunk-QMQ73C23.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/chat-trace-meta/ChatTraceMeta.tsx"],"names":["ChatTraceMeta"],"mappings":";;;;AAcA,SAAS,eAAe,EAAA,EAAoB;AAC1C,EAAA,IAAI,KAAK,GAAA,EAAM;AACb,IAAA,OAAO,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,EAAE,CAAC,CAAA,GAAA,CAAA;AAAA,EAC1B;AACA,EAAA,OAAO,CAAA,EAAA,CAAI,EAAA,GAAK,GAAA,EAAM,OAAA,CAAQ,CAAC,CAAC,CAAA,EAAA,CAAA;AAClC;AAOO,IAAM,aAAA,GAAsB,KAAA,CAAA,UAAA;AAAA,EACjC,SAASA,eAAc,EAAE,KAAA,EAAO,YAAY,SAAA,EAAW,SAAA,IAAa,GAAA,EAAK;AACvE,IAAA,MAAM,QAAA,GAAW,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,EAAA;AACxD,IAAA,MAAM,WAAA,GAAc,OAAO,UAAA,KAAe,QAAA;AAC1C,IAAA,MAAM,QAAA,GAAW,OAAO,SAAA,KAAc,QAAA,IAAY,SAAA,GAAY,CAAA;AAC9D,IAAA,IAAI,CAAC,QAAA,IAAY,CAAC,WAAA,IAAe,CAAC,QAAA,EAAU;AAC1C,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,uBACE,IAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,GAAA;AAAA,QACA,SAAA,EAAQ,iBAAA;AAAA,QACR,SAAA,EAAW,EAAA,CAAG,wFAAA,EAA0F,SAAS,CAAA;AAAA,QAEhH,QAAA,EAAA;AAAA,UAAA,QAAA,mBAAW,GAAA,CAAC,MAAA,EAAA,EAAM,QAAA,EAAA,KAAA,EAAM,CAAA,GAAU,IAAA;AAAA,UAClC,8BAAc,GAAA,CAAC,MAAA,EAAA,EAAM,QAAA,EAAA,cAAA,CAAe,UAAU,GAAE,CAAA,GAAU,IAAA;AAAA,UAC1D,QAAA,wBACE,MAAA,EAAA,EACE,QAAA,EAAA;AAAA,YAAA,SAAA;AAAA,YAAU,GAAA;AAAA,YAAE,SAAA,KAAc,IAAI,MAAA,GAAS;AAAA,WAAA,EAC1C,CAAA,GACE;AAAA;AAAA;AAAA,KACN;AAAA,EAEJ;AACF","file":"chunk-QMQ73C23.js","sourcesContent":["import * as React from 'react'\n\nimport { cn } from '../../lib/utils'\n\nexport interface ChatTraceMetaProps {\n /** Model identifier that produced the reply. */\n model?: string\n /** Turn elapsed time (formatted ms below 1 s, then seconds). */\n durationMs?: number\n /** Number of tools the turn executed (renders `N tools`). */\n toolCount?: number\n className?: string\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) {\n return `${Math.round(ms)} ms`\n }\n return `${(ms / 1000).toFixed(1)} s`\n}\n\n/**\n * Compact turn-trace badges (\"gpt-5.6 · 1.9 s · 3 tools\", career's\n * `TraceMeta`, generalized): renders nothing without data. Reads a\n * normalized view-model — parsing `metadata_json` stays app-side.\n */\nexport const ChatTraceMeta = React.forwardRef<HTMLDivElement, ChatTraceMetaProps>(\n function ChatTraceMeta({ model, durationMs, toolCount, className }, ref) {\n const hasModel = typeof model === 'string' && model !== ''\n const hasDuration = typeof durationMs === 'number'\n const hasTools = typeof toolCount === 'number' && toolCount > 0\n if (!hasModel && !hasDuration && !hasTools) {\n return null\n }\n return (\n <div\n ref={ref}\n data-as=\"chat-trace-meta\"\n className={cn('mt-1 flex flex-wrap items-center gap-2 font-mono text-[10px] text-[var(--as-muted-fg)]', className)}\n >\n {hasModel ? <span>{model}</span> : null}\n {hasDuration ? <span>{formatDuration(durationMs)}</span> : null}\n {hasTools ? (\n <span>\n {toolCount} {toolCount === 1 ? 'tool' : 'tools'}\n </span>\n ) : null}\n </div>\n )\n },\n)\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PopoverButton } from './chunk-BE3XFZWU.js';
|
|
2
|
+
import { cn } from './chunk-BLJWR4ZV.js';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { History } from 'lucide-react';
|
|
5
|
+
import { jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
function ChatHistoryButton({
|
|
8
|
+
children,
|
|
9
|
+
icon: Icon = History,
|
|
10
|
+
onOpen,
|
|
11
|
+
align = "end",
|
|
12
|
+
panelClassName,
|
|
13
|
+
labels,
|
|
14
|
+
className
|
|
15
|
+
}) {
|
|
16
|
+
const [closeSignal, setCloseSignal] = React.useState(0);
|
|
17
|
+
const close = React.useCallback(() => setCloseSignal((signal) => signal + 1), []);
|
|
18
|
+
return /* @__PURE__ */ jsx("span", { className: cn("inline-flex", className), "data-as": "chat-history-button", children: /* @__PURE__ */ jsx(
|
|
19
|
+
PopoverButton,
|
|
20
|
+
{
|
|
21
|
+
label: labels?.open ?? "Chat history",
|
|
22
|
+
trigger: /* @__PURE__ */ jsx(Icon, { className: "size-4", "aria-hidden": true }),
|
|
23
|
+
align,
|
|
24
|
+
closeSignal,
|
|
25
|
+
onOpenChange: (open) => {
|
|
26
|
+
if (open) {
|
|
27
|
+
onOpen?.();
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
panelClassName: cn("h-96 w-72 p-2", panelClassName),
|
|
31
|
+
children: children(close)
|
|
32
|
+
}
|
|
33
|
+
) });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { ChatHistoryButton };
|
|
37
|
+
//# sourceMappingURL=chunk-YYPO24TA.js.map
|
|
38
|
+
//# sourceMappingURL=chunk-YYPO24TA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/chat-history-button/ChatHistoryButton.tsx"],"names":[],"mappings":";;;;;;AAkCO,SAAS,iBAAA,CAAkB;AAAA,EAChC,QAAA;AAAA,EACA,MAAM,IAAA,GAAO,OAAA;AAAA,EACb,MAAA;AAAA,EACA,KAAA,GAAQ,KAAA;AAAA,EACR,cAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAA,EAA2B;AACzB,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,CAAA,GAAU,eAAS,CAAC,CAAA;AACtD,EAAA,MAAM,KAAA,GAAc,KAAA,CAAA,WAAA,CAAY,MAAM,cAAA,CAAe,CAAC,WAAW,MAAA,GAAS,CAAC,CAAA,EAAG,EAAE,CAAA;AAChF,EAAA,uBACE,GAAA,CAAC,UAAK,SAAA,EAAW,EAAA,CAAG,eAAe,SAAS,CAAA,EAAG,WAAQ,qBAAA,EACrD,QAAA,kBAAA,GAAA;AAAA,IAAC,aAAA;AAAA,IAAA;AAAA,MACC,KAAA,EAAO,QAAQ,IAAA,IAAQ,cAAA;AAAA,MACvB,yBAAS,GAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAU,QAAA,EAAS,eAAW,IAAA,EAAC,CAAA;AAAA,MAC9C,KAAA;AAAA,MACA,WAAA;AAAA,MACA,YAAA,EAAc,CAAC,IAAA,KAAS;AACtB,QAAA,IAAI,IAAA,EAAM;AACR,UAAA,MAAA,IAAS;AAAA,QACX;AAAA,MACF,CAAA;AAAA,MACA,cAAA,EAAgB,EAAA,CAAG,eAAA,EAAiB,cAAc,CAAA;AAAA,MAEjD,mBAAS,KAAK;AAAA;AAAA,GACjB,EACF,CAAA;AAEJ","file":"chunk-YYPO24TA.js","sourcesContent":["import * as React from 'react'\nimport { History } from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\n\nimport { cn } from '../../lib/utils'\nimport { PopoverButton } from '../popover-button/PopoverButton'\n\nexport interface ChatHistoryButtonLabels {\n open: string\n}\n\nexport interface ChatHistoryButtonProps {\n /**\n * Popover content, typically the app's session-list glue. Called with\n * `close` — invoke it after a pick to dismiss the popover (the\n * `closeSignal` pattern underneath).\n */\n children: (close: () => void) => React.ReactNode\n icon?: LucideIcon\n /** Called every time the popover opens — refresh the session list here. */\n onOpen?: () => void\n align?: 'start' | 'center' | 'end'\n panelClassName?: string\n labels?: Partial<ChatHistoryButtonLabels>\n className?: string\n}\n\n/**\n * Header entry point for a chat's session history (career's\n * `HistoryButton` + study's ChatPanel history popover, generalized): a\n * labelled popover trigger wrapping the app's session list. The list\n * itself (search, actions) stays app-side via\n * [`ChatSessionList`](../chat-session-list) glue.\n */\nexport function ChatHistoryButton({\n children,\n icon: Icon = History,\n onOpen,\n align = 'end',\n panelClassName,\n labels,\n className,\n}: ChatHistoryButtonProps) {\n const [closeSignal, setCloseSignal] = React.useState(0)\n const close = React.useCallback(() => setCloseSignal((signal) => signal + 1), [])\n return (\n <span className={cn('inline-flex', className)} data-as=\"chat-history-button\">\n <PopoverButton\n label={labels?.open ?? 'Chat history'}\n trigger={<Icon className=\"size-4\" aria-hidden />}\n align={align}\n closeSignal={closeSignal}\n onOpenChange={(open) => {\n if (open) {\n onOpen?.()\n }\n }}\n panelClassName={cn('h-96 w-72 p-2', panelClassName)}\n >\n {children(close)}\n </PopoverButton>\n </span>\n )\n}\n"]}
|