@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 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,3 @@
1
+ export { buildChatMarkdown, chatExportFileName, downloadChatMarkdown } from './chunk-DRJGZ5TB.js';
2
+ //# sourceMappingURL=chat-export.js.map
3
+ //# sourceMappingURL=chat-export.js.map
@@ -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,3 @@
1
+ export { ChatHistoryButton } from './chunk-YYPO24TA.js';
2
+ //# sourceMappingURL=chat-history-button.js.map
3
+ //# sourceMappingURL=chat-history-button.js.map
@@ -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,3 @@
1
+ export { ChatToolsCatalog } from './chunk-ZSCL34WT.js';
2
+ //# sourceMappingURL=chat-tools-catalog.js.map
3
+ //# sourceMappingURL=chat-tools-catalog.js.map
@@ -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,3 @@
1
+ export { ChatTraceMeta } from './chunk-QMQ73C23.js';
2
+ //# sourceMappingURL=chat-trace-meta.js.map
3
+ //# sourceMappingURL=chat-trace-meta.js.map
@@ -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,3 @@
1
+ export { ChatTraceTimeline } from './chunk-5KFOQEOG.js';
2
+ //# sourceMappingURL=chat-trace-timeline.js.map
3
+ //# sourceMappingURL=chat-trace-timeline.js.map
@@ -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"]}