@dxos/react-ui-assistant 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +105 -0
- package/dist/lib/index.mjs +839 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing.mjs +214 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/types/src/components/ChatThread/ChatThread.d.ts +52 -0
- package/dist/types/src/components/ChatThread/ChatThread.d.ts.map +1 -0
- package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts +38 -0
- package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts.map +1 -0
- package/dist/types/src/components/ChatThread/index.d.ts +2 -0
- package/dist/types/src/components/ChatThread/index.d.ts.map +1 -0
- package/dist/types/src/components/MessageChrome/MessageChrome.d.ts +28 -0
- package/dist/types/src/components/MessageChrome/MessageChrome.d.ts.map +1 -0
- package/dist/types/src/components/MessageChrome/index.d.ts +2 -0
- package/dist/types/src/components/MessageChrome/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +4 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/components/widgets/FallbackWidget.d.ts +7 -0
- package/dist/types/src/components/widgets/FallbackWidget.d.ts.map +1 -0
- package/dist/types/src/components/widgets/ReasoningWidget.d.ts +25 -0
- package/dist/types/src/components/widgets/ReasoningWidget.d.ts.map +1 -0
- package/dist/types/src/components/widgets/ReasoningWidget.stories.d.ts +18 -0
- package/dist/types/src/components/widgets/ReasoningWidget.stories.d.ts.map +1 -0
- package/dist/types/src/components/widgets/ReferenceWidget.d.ts +9 -0
- package/dist/types/src/components/widgets/ReferenceWidget.d.ts.map +1 -0
- package/dist/types/src/components/widgets/SelectWidget.d.ts +14 -0
- package/dist/types/src/components/widgets/SelectWidget.d.ts.map +1 -0
- package/dist/types/src/components/widgets/StatsWidget.d.ts +12 -0
- package/dist/types/src/components/widgets/StatsWidget.d.ts.map +1 -0
- package/dist/types/src/components/widgets/StatusWidget.d.ts +12 -0
- package/dist/types/src/components/widgets/StatusWidget.d.ts.map +1 -0
- package/dist/types/src/components/widgets/SuggestionWidget.d.ts +11 -0
- package/dist/types/src/components/widgets/SuggestionWidget.d.ts.map +1 -0
- package/dist/types/src/components/widgets/SummaryWidget.d.ts +4 -0
- package/dist/types/src/components/widgets/SummaryWidget.d.ts.map +1 -0
- package/dist/types/src/components/widgets/ToolWidget.d.ts +10 -0
- package/dist/types/src/components/widgets/ToolWidget.d.ts.map +1 -0
- package/dist/types/src/components/widgets/ToolWidget.stories.d.ts +31 -0
- package/dist/types/src/components/widgets/ToolWidget.stories.d.ts.map +1 -0
- package/dist/types/src/components/widgets/defaults.d.ts +11 -0
- package/dist/types/src/components/widgets/defaults.d.ts.map +1 -0
- package/dist/types/src/components/widgets/format-time.d.ts +2 -0
- package/dist/types/src/components/widgets/format-time.d.ts.map +1 -0
- package/dist/types/src/components/widgets/index.d.ts +11 -0
- package/dist/types/src/components/widgets/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +6 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/registry.d.ts +11 -0
- package/dist/types/src/registry.d.ts.map +1 -0
- package/dist/types/src/renderer.d.ts +25 -0
- package/dist/types/src/renderer.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/test-generator.d.ts +7 -0
- package/dist/types/src/testing/test-generator.d.ts.map +1 -0
- package/dist/types/src/testing/test-generator.test.d.ts +2 -0
- package/dist/types/src/testing/test-generator.test.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +19 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +28 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +67 -0
- package/src/components/ChatThread/ChatThread.stories.tsx +419 -0
- package/src/components/ChatThread/ChatThread.tsx +188 -0
- package/src/components/ChatThread/index.ts +5 -0
- package/src/components/MessageChrome/MessageChrome.tsx +147 -0
- package/src/components/MessageChrome/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/components/widgets/FallbackWidget.tsx +26 -0
- package/src/components/widgets/ReasoningWidget.stories.tsx +68 -0
- package/src/components/widgets/ReasoningWidget.ts +139 -0
- package/src/components/widgets/ReferenceWidget.ts +28 -0
- package/src/components/widgets/SelectWidget.ts +39 -0
- package/src/components/widgets/StatsWidget.ts +31 -0
- package/src/components/widgets/StatusWidget.ts +53 -0
- package/src/components/widgets/SuggestionWidget.ts +50 -0
- package/src/components/widgets/SummaryWidget.tsx +27 -0
- package/src/components/widgets/ToolWidget.stories.tsx +55 -0
- package/src/components/widgets/ToolWidget.tsx +185 -0
- package/src/components/widgets/defaults.ts +14 -0
- package/src/components/widgets/format-time.ts +45 -0
- package/src/components/widgets/index.ts +14 -0
- package/src/index.ts +9 -0
- package/src/registry.tsx +144 -0
- package/src/renderer.ts +216 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/test-generator.test.ts +75 -0
- package/src/testing/test-generator.ts +280 -0
- package/src/translations.ts +27 -0
- package/src/types.ts +32 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import { createContext } from "@radix-ui/react-context";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Icon, IconBlock, IconButton, setRef, useTranslation } from "@dxos/react-ui";
|
|
4
|
+
import { MessageList, useMessageList } from "@dxos/react-ui-feed";
|
|
5
|
+
import { AnchorWidget, getXmlTextChild } from "@dxos/ui-editor";
|
|
6
|
+
import { NumericTabs, TextCrawl, TogglePanel } from "@dxos/react-ui-components";
|
|
7
|
+
import { JsonHighlighter } from "@dxos/react-ui-syntax-highlighter";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { WidgetType } from "@codemirror/view";
|
|
10
|
+
import { Domino, mx } from "@dxos/ui";
|
|
11
|
+
import { isNonNullable, safeParseJson } from "@dxos/util";
|
|
12
|
+
import { Message } from "@dxos/types";
|
|
13
|
+
import { mx as mx$1 } from "@dxos/ui-theme";
|
|
14
|
+
import * as Schema from "effect/Schema";
|
|
15
|
+
//#region src/components/widgets/format-time.ts
|
|
16
|
+
/**
|
|
17
|
+
* Relative ("5 min ago", "yesterday") within two days, absolute date beyond that. The exact
|
|
18
|
+
* timestamp is always available via the `title` tooltip.
|
|
19
|
+
*
|
|
20
|
+
* Recent prompts are the ones a reader places by elapsed time; older ones by date.
|
|
21
|
+
*/
|
|
22
|
+
var DAY_MS = 864e5;
|
|
23
|
+
var formatTime = (created, now = Date.now()) => {
|
|
24
|
+
const date = new Date(created);
|
|
25
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
26
|
+
const elapsed = now - date.getTime();
|
|
27
|
+
if (elapsed < 0 || elapsed >= 2 * DAY_MS) return date.toLocaleDateString(void 0, {
|
|
28
|
+
month: "short",
|
|
29
|
+
day: "numeric"
|
|
30
|
+
});
|
|
31
|
+
const format = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
|
|
32
|
+
const minutes = Math.round(elapsed / 6e4);
|
|
33
|
+
if (minutes < 1) return format.format(-Math.max(1, Math.round(elapsed / 1e3)), "second");
|
|
34
|
+
if (minutes < 60) return format.format(-minutes, "minute");
|
|
35
|
+
const hours = Math.round(minutes / 60);
|
|
36
|
+
if (hours < 24) return format.format(-hours, "hour");
|
|
37
|
+
return format.format(-Math.round(hours / 24), "day");
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/components/widgets/FallbackWidget.tsx
|
|
41
|
+
var FallbackWidget = ({ _tag, ...props }) => {
|
|
42
|
+
return /* @__PURE__ */ jsx(TogglePanel.Root, { children: /* @__PURE__ */ jsxs(TogglePanel.Content, { children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
|
|
43
|
+
classNames: "dx-group-surface",
|
|
44
|
+
children: _tag
|
|
45
|
+
}), /* @__PURE__ */ jsx(TogglePanel.Body, {
|
|
46
|
+
classNames: "dx-modal-surface",
|
|
47
|
+
children: /* @__PURE__ */ jsx(TogglePanel.Viewport, { children: /* @__PURE__ */ jsx(JsonHighlighter, {
|
|
48
|
+
classNames: "p-2! text-sm",
|
|
49
|
+
data: props
|
|
50
|
+
}) })
|
|
51
|
+
})] }) });
|
|
52
|
+
};
|
|
53
|
+
FallbackWidget.displayName = "Fallback";
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/components/widgets/defaults.ts
|
|
56
|
+
var styles = {
|
|
57
|
+
/**
|
|
58
|
+
* Vertical rhythm for block widgets. Padding, never margin: CodeMirror measures a widget from its own
|
|
59
|
+
* box, so a margin sits outside what it can see and its height model drifts from what is rendered —
|
|
60
|
+
* the error accumulates down the document until the turn-fold gutter markers are visibly misaligned.
|
|
61
|
+
* Note padding does not collapse between adjacent siblings the way margins do
|
|
62
|
+
*/
|
|
63
|
+
padding: "pt-2 pb-4",
|
|
64
|
+
border: "border border-subdued-separator rounded-sm"
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/components/widgets/ReasoningWidget.ts
|
|
68
|
+
/**
|
|
69
|
+
* AI reasoning widget.
|
|
70
|
+
*/
|
|
71
|
+
var ReasoningWidget = class extends WidgetType {
|
|
72
|
+
text;
|
|
73
|
+
#pos;
|
|
74
|
+
/** The timer id last registered for this instance (may differ from the map if superseded). */
|
|
75
|
+
#ownedTimerId = null;
|
|
76
|
+
constructor(text, pos) {
|
|
77
|
+
super();
|
|
78
|
+
this.text = text;
|
|
79
|
+
this.#pos = pos === void 0 || pos === "" ? "reasoning" : String(pos);
|
|
80
|
+
}
|
|
81
|
+
eq(other) {
|
|
82
|
+
return this.text === other.text && this.#pos === other.#pos;
|
|
83
|
+
}
|
|
84
|
+
toDOM() {
|
|
85
|
+
return Domino.of("div").classNames(styles.padding).append(Domino.of("div").classNames("relative overflow-hidden p-px", styles.border).attributes({ "data-trail-container": "" }).append(Domino.of("div").classNames("relative z-10 dx-base-surface rounded-sm text-sm text-subdued py-1", "grid grid-cols-[24px_1fr] gap-x-0.5 items-start").append(Domino.of("div").classNames("flex h-5 w-full shrink-0 items-center justify-center self-start").append(Domino.svg("ph--brain--regular").classNames("shrink-0 size-4 opacity-70")), Domino.of("div").classNames("px-2 max-h-[5lh] overflow-y-auto dx-scrollbar-thin min-w-0").text(this.text).attributes({ "data-reasoning-text": "" })), Domino.of("div").attributes({ "data-id": this.#pos }))).root;
|
|
86
|
+
}
|
|
87
|
+
updateDOM(dom) {
|
|
88
|
+
dom.querySelector("[data-reasoning-text]")?.replaceChildren(this.text);
|
|
89
|
+
const container = dom.querySelector("[data-trail-container]");
|
|
90
|
+
let trailHost = container?.querySelector("[data-id]");
|
|
91
|
+
if (container && !trailHost) {
|
|
92
|
+
trailHost = Domino.of("div").attributes({ "data-id": this.#pos }).root;
|
|
93
|
+
container.append(trailHost);
|
|
94
|
+
}
|
|
95
|
+
if (trailHost?.childElementCount === 0) trailHost.append(...createTrailLayers());
|
|
96
|
+
this.#scheduleTrailRemoval(dom);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
destroy(_dom) {
|
|
100
|
+
this.#clearOwnedTrailTimer();
|
|
101
|
+
}
|
|
102
|
+
#scheduleTrailRemoval(dom) {
|
|
103
|
+
const previous = trailRemovalTimers.get(this.#pos);
|
|
104
|
+
if (previous !== void 0) clearTimeout(previous);
|
|
105
|
+
const timerId = setTimeout(() => {
|
|
106
|
+
if (trailRemovalTimers.get(this.#pos) !== timerId) return;
|
|
107
|
+
trailRemovalTimers.delete(this.#pos);
|
|
108
|
+
this.#ownedTimerId = null;
|
|
109
|
+
dom.querySelector("[data-id]")?.remove();
|
|
110
|
+
}, TRAIL_REMOVAL_DELAY_MS);
|
|
111
|
+
trailRemovalTimers.set(this.#pos, timerId);
|
|
112
|
+
this.#ownedTimerId = timerId;
|
|
113
|
+
}
|
|
114
|
+
#clearOwnedTrailTimer() {
|
|
115
|
+
const active = trailRemovalTimers.get(this.#pos);
|
|
116
|
+
if (active !== void 0 && active === this.#ownedTimerId) {
|
|
117
|
+
clearTimeout(active);
|
|
118
|
+
trailRemovalTimers.delete(this.#pos);
|
|
119
|
+
}
|
|
120
|
+
this.#ownedTimerId = null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var trail = [
|
|
124
|
+
"absolute z-0 aspect-[2/1] w-16",
|
|
125
|
+
"bg-[radial-gradient(at_100%_50%,_theme(colors.green.700),_transparent_80%)]",
|
|
126
|
+
"[offset-anchor:100%_50%] [offset-path:border-box]"
|
|
127
|
+
];
|
|
128
|
+
/** Delay after the last content update before hiding the trail. */
|
|
129
|
+
var TRAIL_REMOVAL_DELAY_MS = 1e3;
|
|
130
|
+
/**
|
|
131
|
+
* One pending removal timer per logical block. Streaming creates a new widget instance each tick;
|
|
132
|
+
* `destroy` may run after the next instance has already scheduled, so we must not clear a newer timer.
|
|
133
|
+
*/
|
|
134
|
+
var trailRemovalTimers = /* @__PURE__ */ new Map();
|
|
135
|
+
var createTrailLayers = () => [Domino.of("div").classNames(...trail, "animate-trail").root, Domino.of("div").classNames(...trail, "animate-trail-offset").root];
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/components/widgets/ReferenceWidget.ts
|
|
138
|
+
var ReferenceWidget = class extends WidgetType {
|
|
139
|
+
text;
|
|
140
|
+
dxn;
|
|
141
|
+
constructor(text, dxn) {
|
|
142
|
+
super();
|
|
143
|
+
this.text = text;
|
|
144
|
+
this.dxn = dxn;
|
|
145
|
+
}
|
|
146
|
+
eq(other) {
|
|
147
|
+
return this.dxn === other.dxn;
|
|
148
|
+
}
|
|
149
|
+
toDOM() {
|
|
150
|
+
return Domino.of("div").classNames(styles.padding).append(Domino.of("dx-anchor").classNames("dx-tag--anchor").attributes({ dxn: this.dxn }).text(this.text)).root;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/components/widgets/SelectWidget.ts
|
|
155
|
+
/**
|
|
156
|
+
* Simple prompt widget.
|
|
157
|
+
*/
|
|
158
|
+
var SelectWidget = class extends WidgetType {
|
|
159
|
+
options;
|
|
160
|
+
constructor(options) {
|
|
161
|
+
super();
|
|
162
|
+
this.options = options;
|
|
163
|
+
}
|
|
164
|
+
eq(other) {
|
|
165
|
+
return JSON.stringify(this.options) === JSON.stringify(other.options);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* NOTE: Container must set var based on user's identity.
|
|
169
|
+
*/
|
|
170
|
+
toDOM() {
|
|
171
|
+
return Domino.of("div").attributes({ role: "group" }).classNames(styles.padding, "flex flex-wrap gap-1").append(...this.options.map((option) => Domino.of("button").classNames("dx-button inline-block max-w-[100cqi]").attributes({
|
|
172
|
+
"data-action": "submit",
|
|
173
|
+
"data-value": option,
|
|
174
|
+
"data-density": "md"
|
|
175
|
+
}).text(option))).root;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/components/widgets/StatsWidget.ts
|
|
180
|
+
/**
|
|
181
|
+
* Simple stats widget.
|
|
182
|
+
*/
|
|
183
|
+
var StatsWidget = class extends WidgetType {
|
|
184
|
+
text;
|
|
185
|
+
constructor(text) {
|
|
186
|
+
super();
|
|
187
|
+
this.text = text;
|
|
188
|
+
}
|
|
189
|
+
eq(other) {
|
|
190
|
+
return this.text === other.text;
|
|
191
|
+
}
|
|
192
|
+
toDOM() {
|
|
193
|
+
return Domino.of("div").classNames(styles.padding, "text-sm text-placeholder").text(this.text).root;
|
|
194
|
+
}
|
|
195
|
+
updateDOM(dom) {
|
|
196
|
+
dom.textContent = this.text;
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/components/widgets/StatusWidget.ts
|
|
202
|
+
/**
|
|
203
|
+
* AI status widget.
|
|
204
|
+
*/
|
|
205
|
+
var StatusWidget = class extends WidgetType {
|
|
206
|
+
text;
|
|
207
|
+
constructor(text) {
|
|
208
|
+
super();
|
|
209
|
+
this.text = text;
|
|
210
|
+
}
|
|
211
|
+
eq(other) {
|
|
212
|
+
return this.text === other.text;
|
|
213
|
+
}
|
|
214
|
+
toDOM() {
|
|
215
|
+
return Domino.of("div").classNames(styles.padding).append(Domino.of("div").classNames("relative overflow-hidden rounded-sm").append(Domino.of("div").classNames("grid grid-cols-[24px_1fr] gap-x-0.5 gap-y-0 items-start px-0.5 py-0.5 text-placeholder").append(Domino.of("div").classNames("flex h-5 w-full shrink-0 items-center justify-center self-start").append(Domino.svg("ph--info--regular").classNames("shrink-0 size-4 opacity-70")), Domino.of("div").classNames("relative min-w-0").append(Domino.of("div").classNames("relative z-10 rounded-sm text-sm leading-5").attributes({ "data-status-text": "" }).text(this.text))))).root;
|
|
216
|
+
}
|
|
217
|
+
updateDOM(dom) {
|
|
218
|
+
dom.querySelector("[data-status-text]")?.replaceChildren(this.text);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/components/widgets/SuggestionWidget.ts
|
|
224
|
+
/**
|
|
225
|
+
* Simple prompt widget.
|
|
226
|
+
*/
|
|
227
|
+
var SuggestionWidget = class extends WidgetType {
|
|
228
|
+
text;
|
|
229
|
+
constructor(text) {
|
|
230
|
+
super();
|
|
231
|
+
this.text = text;
|
|
232
|
+
}
|
|
233
|
+
eq(other) {
|
|
234
|
+
return this.text === other.text;
|
|
235
|
+
}
|
|
236
|
+
toDOM() {
|
|
237
|
+
return Domino.of("span").classNames(mx("inline-flex max-w-[calc(100cqi-8px)] py-1 pe-1 overflow-hidden")).append(Domino.of("button").attributes({
|
|
238
|
+
"data-density": "md",
|
|
239
|
+
"data-action": "submit",
|
|
240
|
+
"data-value": this.text
|
|
241
|
+
}).classNames(mx("dx-button gap-2 w-full overflow-hidden")).append(Domino.of("dx-icon").attributes({ icon: "ph--lightning--regular" }), Domino.of("span").classNames("flex-1 truncate min-w-0").text(this.text))).root;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/translations.ts
|
|
246
|
+
var translationKey = "@dxos/react-ui-assistant";
|
|
247
|
+
var translations = [{ "en-US": { [translationKey]: {
|
|
248
|
+
"copy.label": "Copy",
|
|
249
|
+
"rewind.label": "Rewind to this prompt",
|
|
250
|
+
"summary.label": "Summary",
|
|
251
|
+
"stats.label": "Stats",
|
|
252
|
+
"tool-call.label": "Calling",
|
|
253
|
+
"tool-result.label": "Result",
|
|
254
|
+
"tool-error.label": "Tool error",
|
|
255
|
+
"nav-first.label": "First message",
|
|
256
|
+
"nav-previous.label": "Previous message",
|
|
257
|
+
"nav-next.label": "Next message",
|
|
258
|
+
"nav-last.label": "Last message"
|
|
259
|
+
} } }];
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/components/widgets/SummaryWidget.tsx
|
|
262
|
+
var SummaryWidget = ({ children }) => {
|
|
263
|
+
const { t } = useTranslation(translationKey);
|
|
264
|
+
return /* @__PURE__ */ jsx(TogglePanel.Root, { children: /* @__PURE__ */ jsxs(TogglePanel.Content, {
|
|
265
|
+
classNames: styles.border,
|
|
266
|
+
children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
|
|
267
|
+
classNames: "text-sm dx-group-surface",
|
|
268
|
+
children: t("summary.label")
|
|
269
|
+
}), /* @__PURE__ */ jsx(TogglePanel.Body, { children: /* @__PURE__ */ jsx("div", {
|
|
270
|
+
className: "p-1 text-sm text-subdued",
|
|
271
|
+
children: getXmlTextChild(children ?? [])
|
|
272
|
+
}) })]
|
|
273
|
+
}) });
|
|
274
|
+
};
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/components/widgets/ToolWidget.tsx
|
|
277
|
+
/**
|
|
278
|
+
* A run of tool blocks as one collapsible panel with a tab per call. The `<toolkit>` tag carries
|
|
279
|
+
* the run as JSON — the item is rebuilt from its message alone, so there is no widget-state side
|
|
280
|
+
* channel to accumulate blocks through.
|
|
281
|
+
*/
|
|
282
|
+
var ToolWidget = ({ view, children }) => {
|
|
283
|
+
const { t } = useTranslation(translationKey);
|
|
284
|
+
const blocks = useMemo(() => {
|
|
285
|
+
const parsed = safeParseJson(getXmlTextChild(children ?? []) ?? "");
|
|
286
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
287
|
+
}, [children]);
|
|
288
|
+
const items = useMemo(() => {
|
|
289
|
+
let lastToolCall;
|
|
290
|
+
const tools = [];
|
|
291
|
+
return blocks.filter((block) => block._tag === "toolCall" || block._tag === "toolResult" || block._tag === "stats").map((block) => {
|
|
292
|
+
switch (block._tag) {
|
|
293
|
+
case "toolCall": {
|
|
294
|
+
if (block.pending && lastToolCall?.block.toolCallId === block.toolCallId) return null;
|
|
295
|
+
const tool = tools.find((tool) => tool.name === block.name);
|
|
296
|
+
lastToolCall = {
|
|
297
|
+
tool,
|
|
298
|
+
block
|
|
299
|
+
};
|
|
300
|
+
return {
|
|
301
|
+
title: tool?.description ?? [t("tool-call.label"), block.name].filter(Boolean).join(" "),
|
|
302
|
+
icon: block.operationIcon,
|
|
303
|
+
content: {
|
|
304
|
+
...block,
|
|
305
|
+
input: safeParseJson(block.input)
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
case "toolResult": {
|
|
310
|
+
if (block.error) return {
|
|
311
|
+
title: t("tool-error.label"),
|
|
312
|
+
icon: lastToolCall?.block.operationIcon,
|
|
313
|
+
content: block
|
|
314
|
+
};
|
|
315
|
+
const title = lastToolCall?.tool?.description ?? [t("tool-result.label"), lastToolCall?.block.name].filter(Boolean).join(" ");
|
|
316
|
+
const icon = lastToolCall?.block.operationIcon;
|
|
317
|
+
lastToolCall = void 0;
|
|
318
|
+
return {
|
|
319
|
+
title,
|
|
320
|
+
icon,
|
|
321
|
+
content: {
|
|
322
|
+
...block,
|
|
323
|
+
result: typeof block.result === "string" ? safeParseJson(block.result) : block.result
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
case "stats":
|
|
328
|
+
if (!lastToolCall) return null;
|
|
329
|
+
return {
|
|
330
|
+
title: t("stats.label"),
|
|
331
|
+
icon: lastToolCall.block.operationIcon,
|
|
332
|
+
content: block
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}).filter(isNonNullable);
|
|
336
|
+
}, [blocks, t]);
|
|
337
|
+
const handleChangeOpen = useCallback(() => {
|
|
338
|
+
setTimeout(() => {
|
|
339
|
+
view?.requestMeasure();
|
|
340
|
+
}, 1e3);
|
|
341
|
+
}, [view]);
|
|
342
|
+
if (!items.length) return null;
|
|
343
|
+
return /* @__PURE__ */ jsx(ToolPanel, {
|
|
344
|
+
items,
|
|
345
|
+
onChangeOpen: handleChangeOpen
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
var DEFAULT_TOOL_ICON = "ph--wrench--regular";
|
|
349
|
+
var ToolPanel = ({ items, onChangeOpen }) => {
|
|
350
|
+
const tabsRef = useRef(null);
|
|
351
|
+
const [selected, setSelected] = useState(0);
|
|
352
|
+
const [open, setOpen] = useState(false);
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
setSelected((prev) => Math.min(prev, Math.max(0, items.length - 1)));
|
|
355
|
+
}, [items.length]);
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
onChangeOpen?.(open);
|
|
358
|
+
if (open) tabsRef.current?.focus();
|
|
359
|
+
}, [open, onChangeOpen]);
|
|
360
|
+
const handleSelect = useCallback((index) => {
|
|
361
|
+
setSelected(index);
|
|
362
|
+
}, []);
|
|
363
|
+
const headerIcon = items[items.length - 1]?.icon ?? items[0]?.icon ?? DEFAULT_TOOL_ICON;
|
|
364
|
+
return /* @__PURE__ */ jsx(TogglePanel.Root, {
|
|
365
|
+
open,
|
|
366
|
+
onChangeOpen: setOpen,
|
|
367
|
+
children: /* @__PURE__ */ jsxs(TogglePanel.Content, { children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
|
|
368
|
+
classNames: "flex items-center gap-2 text-sm",
|
|
369
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
370
|
+
className: "w-full grid grid-cols-[1fr_auto] items-center gap-2",
|
|
371
|
+
children: [/* @__PURE__ */ jsx(TextCrawl, {
|
|
372
|
+
classNames: "text-description",
|
|
373
|
+
lines: items.map((item) => item.title),
|
|
374
|
+
autoAdvance: true,
|
|
375
|
+
greedy: true
|
|
376
|
+
}, "status-roll"), /* @__PURE__ */ jsx(IconBlock, { children: /* @__PURE__ */ jsx(Icon, {
|
|
377
|
+
icon: headerIcon,
|
|
378
|
+
size: 4
|
|
379
|
+
}) })]
|
|
380
|
+
})
|
|
381
|
+
}), /* @__PURE__ */ jsx(TogglePanel.Body, { children: /* @__PURE__ */ jsxs(TogglePanel.Viewport, {
|
|
382
|
+
classNames: "grid grid-cols-[32px_1fr]",
|
|
383
|
+
children: [/* @__PURE__ */ jsx(NumericTabs, {
|
|
384
|
+
ref: tabsRef,
|
|
385
|
+
classNames: "p-1",
|
|
386
|
+
length: items.length,
|
|
387
|
+
selected,
|
|
388
|
+
onSelect: handleSelect
|
|
389
|
+
}), /* @__PURE__ */ jsx(JsonHighlighter, {
|
|
390
|
+
data: items[selected]?.content,
|
|
391
|
+
classNames: "p-1 text-xs bg-transparent",
|
|
392
|
+
replacer: {
|
|
393
|
+
maxDepth: 3,
|
|
394
|
+
maxArrayLen: 10,
|
|
395
|
+
maxStringLen: 128
|
|
396
|
+
}
|
|
397
|
+
})]
|
|
398
|
+
}) })] })
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
//#endregion
|
|
402
|
+
//#region src/registry.tsx
|
|
403
|
+
/**
|
|
404
|
+
* Floor for a portaled panel's collapsed height, reserved before its content paints. `heightMode:
|
|
405
|
+
* 'min'` makes it a floor rather than a pin — without it the row is measured at the height of an
|
|
406
|
+
* empty box and grows a frame later, a row moving under a reader scrolling up.
|
|
407
|
+
*/
|
|
408
|
+
var COLLAPSED_HEIGHT = 42;
|
|
409
|
+
/**
|
|
410
|
+
* Widgets for the tags {@link createRenderer} emits.
|
|
411
|
+
*
|
|
412
|
+
* Host-extensible by spreading: the plugin adds its `surface` widget (which renders an
|
|
413
|
+
* app-framework Surface this package must not depend on) over the fallback registered here.
|
|
414
|
+
* DOM widgets render synchronously from the tag alone; React widgets are portaled and declare a
|
|
415
|
+
* collapsed-height floor so the row's first measurement is honest.
|
|
416
|
+
*/
|
|
417
|
+
var assistantRegistry = {
|
|
418
|
+
"prompt": { block: true },
|
|
419
|
+
"link-preview": {
|
|
420
|
+
block: false,
|
|
421
|
+
urlSchemes: ["dxn:", "echo:"],
|
|
422
|
+
factory: ({ label, dxn }) => typeof label === "string" && typeof dxn === "string" ? new AnchorWidget(label, dxn) : null
|
|
423
|
+
},
|
|
424
|
+
"synthetic": {
|
|
425
|
+
block: true,
|
|
426
|
+
factory: ({ children, range }) => {
|
|
427
|
+
const text = getXmlTextChild(children ?? []);
|
|
428
|
+
return text ? new ReasoningWidget(text, range?.from) : null;
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
"reasoning": {
|
|
432
|
+
block: true,
|
|
433
|
+
streaming: true,
|
|
434
|
+
factory: ({ children, range }) => {
|
|
435
|
+
const text = getXmlTextChild(children ?? []);
|
|
436
|
+
return text ? new ReasoningWidget(text, range?.from) : null;
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
"status": {
|
|
440
|
+
block: true,
|
|
441
|
+
streaming: true,
|
|
442
|
+
factory: ({ children }) => {
|
|
443
|
+
const text = getXmlTextChild(children ?? []);
|
|
444
|
+
return text ? new StatusWidget(text) : null;
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
"reference": {
|
|
448
|
+
block: false,
|
|
449
|
+
factory: ({ children, ref }) => {
|
|
450
|
+
const text = getXmlTextChild(children ?? []);
|
|
451
|
+
return text && typeof ref === "string" ? new ReferenceWidget(text, ref) : null;
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
"suggestion": {
|
|
455
|
+
block: false,
|
|
456
|
+
factory: ({ children }) => {
|
|
457
|
+
const text = getXmlTextChild(children ?? []);
|
|
458
|
+
return text ? new SuggestionWidget(text) : null;
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
"select": {
|
|
462
|
+
block: true,
|
|
463
|
+
factory: ({ children }) => {
|
|
464
|
+
const options = (children ?? []).map((option) => option?._tag === "option" && getXmlTextChild(option.children)).filter(Boolean);
|
|
465
|
+
return options.length ? new SelectWidget(options) : null;
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
"stats": {
|
|
469
|
+
block: true,
|
|
470
|
+
factory: ({ children }) => {
|
|
471
|
+
const text = getXmlTextChild(children ?? []);
|
|
472
|
+
return text ? new StatsWidget(text) : null;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
"toolkit": {
|
|
476
|
+
block: true,
|
|
477
|
+
streaming: true,
|
|
478
|
+
estimatedHeight: () => COLLAPSED_HEIGHT,
|
|
479
|
+
heightMode: "min",
|
|
480
|
+
Component: ToolWidget
|
|
481
|
+
},
|
|
482
|
+
"summary": {
|
|
483
|
+
block: true,
|
|
484
|
+
estimatedHeight: () => COLLAPSED_HEIGHT,
|
|
485
|
+
heightMode: "min",
|
|
486
|
+
Component: SummaryWidget
|
|
487
|
+
},
|
|
488
|
+
"surface": {
|
|
489
|
+
block: true,
|
|
490
|
+
estimatedHeight: () => COLLAPSED_HEIGHT,
|
|
491
|
+
heightMode: "min",
|
|
492
|
+
Component: FallbackWidget
|
|
493
|
+
},
|
|
494
|
+
"json": {
|
|
495
|
+
block: true,
|
|
496
|
+
estimatedHeight: () => COLLAPSED_HEIGHT,
|
|
497
|
+
heightMode: "min",
|
|
498
|
+
Component: FallbackWidget
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region src/renderer.ts
|
|
503
|
+
/**
|
|
504
|
+
* The assistant's projection of a message: each block flattened to markdown, everything that is not
|
|
505
|
+
* prose emitted as an XML tag for the registry's widgets. The view type is a filter over blocks —
|
|
506
|
+
* the model always carries everything, so switching views is a re-render, not a re-fetch.
|
|
507
|
+
*
|
|
508
|
+
* Consecutive tool blocks (`toolCall`/`toolResult`/`stats`) render as ONE `<toolkit>` tag carrying
|
|
509
|
+
* the run as JSON: the panel widget shows a run of calls as tabs, and the item is rebuilt from its
|
|
510
|
+
* message alone — there is no side channel accumulating widget state (the old `MessageSyncer`
|
|
511
|
+
* machinery this package retires).
|
|
512
|
+
*/
|
|
513
|
+
var createRenderer = (viewType, { getObjectLabel = () => "Object" } = {}) => {
|
|
514
|
+
return (message) => {
|
|
515
|
+
const blocks = message.blocks.filter((block) => isBlockVisible(viewType, block));
|
|
516
|
+
const segments = [];
|
|
517
|
+
let tools = [];
|
|
518
|
+
const flushTools = () => {
|
|
519
|
+
if (tools.length) {
|
|
520
|
+
segments.push(toolkitTag(tools));
|
|
521
|
+
tools = [];
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
for (const block of blocks) {
|
|
525
|
+
if (block._tag === "toolCall" || block._tag === "toolResult" || block._tag === "stats" && tools.length) {
|
|
526
|
+
tools.push(block);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
flushTools();
|
|
530
|
+
const rendered = blockToMarkdown(message, block, getObjectLabel);
|
|
531
|
+
if (rendered) segments.push(rendered);
|
|
532
|
+
}
|
|
533
|
+
flushTools();
|
|
534
|
+
return {
|
|
535
|
+
kind: "markdown",
|
|
536
|
+
text: segments.reduce((parts, segment) => {
|
|
537
|
+
const previous = parts[parts.length - 1];
|
|
538
|
+
if (previous?.startsWith("<suggestion") && segment.startsWith("<suggestion")) parts[parts.length - 1] = `${previous} ${segment}`;
|
|
539
|
+
else parts.push(segment);
|
|
540
|
+
return parts;
|
|
541
|
+
}, []).join("\n\n")
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
};
|
|
545
|
+
var isBlockVisible = (viewType, block) => {
|
|
546
|
+
switch (viewType) {
|
|
547
|
+
case "debug": return true;
|
|
548
|
+
case "summary": return block._tag === "text" && block.disposition !== "synthetic";
|
|
549
|
+
case "normal": return block._tag !== "reasoning";
|
|
550
|
+
default: return true;
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
var blockToMarkdown = (message, block, getObjectLabel) => {
|
|
554
|
+
switch (block._tag) {
|
|
555
|
+
case "text":
|
|
556
|
+
if (message.sender.role === "user") return block.disposition === "synthetic" ? tag("synthetic", block.text, block) : tag("prompt", block.text, block);
|
|
557
|
+
return block.text.trim() || void 0;
|
|
558
|
+
case "reasoning": return tag("reasoning", block.reasoningText ?? block.redactedText ?? "", block);
|
|
559
|
+
case "status": return tag("status", block.statusText, block);
|
|
560
|
+
case "summary": return tag("summary", block.content, block);
|
|
561
|
+
case "suggestion": return block.pending ? void 0 : tag("suggestion", block.text, block);
|
|
562
|
+
case "select": return block.pending || !block.options.length ? void 0 : `<select>${block.options.map((option) => `<option>${escapeXml(option)}</option>`).join("")}</select>`;
|
|
563
|
+
case "reference": {
|
|
564
|
+
if (block.pending) return;
|
|
565
|
+
const uri = block.reference.uri;
|
|
566
|
+
return `<reference ref="${escapeAttribute(uri.toString())}">${escapeXml(getObjectLabel(uri))}</reference>`;
|
|
567
|
+
}
|
|
568
|
+
case "stats": return;
|
|
569
|
+
case "surface": return block.pending ? void 0 : `<surface role="${escapeAttribute(block.role)}">${escapeXml(JSON.stringify(block.data ?? {}))}</surface>`;
|
|
570
|
+
default: return tag("json", JSON.stringify(block), block);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
/** A run of tool blocks as one tag; the widget parses the payload back out. */
|
|
574
|
+
var toolkitTag = (blocks) => {
|
|
575
|
+
return `<toolkit${blocks.some((block) => block.pending) ? " pending=\"true\"" : ""}>${escapeXml(JSON.stringify(blocks))}</toolkit>`;
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* An XML tag holding block content.
|
|
579
|
+
*
|
|
580
|
+
* A pending block is emitted **unclosed**, which is the streaming contract: the item reconciles by
|
|
581
|
+
* appending, so a closing tag written before the content is complete would be rewritten per chunk.
|
|
582
|
+
* Paragraph breaks inside tag content are collapsed, since a blank line would end the enclosing
|
|
583
|
+
* markdown block and leave the rest of the tag parsing as prose.
|
|
584
|
+
*/
|
|
585
|
+
var tag = (name, content, block) => {
|
|
586
|
+
const text = escapeXml(content.replace(/\n\n+/g, " ").trim());
|
|
587
|
+
if (!text.length && !block.pending) return "";
|
|
588
|
+
const open = `<${name}>`;
|
|
589
|
+
return block.pending ? `${open}${text}` : `${open}${text}</${name}>`;
|
|
590
|
+
};
|
|
591
|
+
var escapeXml = (raw) => raw.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
592
|
+
var escapeAttribute = (raw) => escapeXml(raw).replace(/"/g, """);
|
|
593
|
+
/** Chrome around every row: the padding and the hover toolbar's reserved line. */
|
|
594
|
+
var ROW_CHROME = 46;
|
|
595
|
+
/** Height of a wrapped line of body text, and how many characters fit at typical thread width. */
|
|
596
|
+
var LINE_HEIGHT = 24;
|
|
597
|
+
var LINE_CHARS = 90;
|
|
598
|
+
/** A collapsed panel: reasoning, a tool run, a summary. */
|
|
599
|
+
var PANEL_HEIGHT = 50;
|
|
600
|
+
/**
|
|
601
|
+
* What a row will measure, from the message alone. Rough on purpose — its only job is to be close
|
|
602
|
+
* enough that measuring the row does not move the rows below it.
|
|
603
|
+
*/
|
|
604
|
+
var estimateRow = (message) => {
|
|
605
|
+
let height = ROW_CHROME;
|
|
606
|
+
for (const block of message.blocks) switch (block._tag) {
|
|
607
|
+
case "text":
|
|
608
|
+
for (const paragraph of block.text.split("\n\n")) height += Math.max(1, Math.ceil(paragraph.length / LINE_CHARS)) * LINE_HEIGHT;
|
|
609
|
+
break;
|
|
610
|
+
case "suggestion":
|
|
611
|
+
height += 32;
|
|
612
|
+
break;
|
|
613
|
+
case "select":
|
|
614
|
+
height += block.options.length * 34;
|
|
615
|
+
break;
|
|
616
|
+
case "toolResult":
|
|
617
|
+
case "stats": break;
|
|
618
|
+
default: height += PANEL_HEIGHT;
|
|
619
|
+
}
|
|
620
|
+
return height;
|
|
621
|
+
};
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/components/MessageChrome/MessageChrome.tsx
|
|
624
|
+
var MESSAGE_CHROME_NAME = "MessageChrome";
|
|
625
|
+
var [MessageChromeProvider, useMessageChromeContext] = createContext(MESSAGE_CHROME_NAME);
|
|
626
|
+
/** Copies the message's extracted text — the model's truth, not the DOM's partial render. */
|
|
627
|
+
var CopyButton = ({ message }) => {
|
|
628
|
+
const { t } = useTranslation(translationKey);
|
|
629
|
+
return /* @__PURE__ */ jsx(IconButton, {
|
|
630
|
+
icon: "ph--copy--regular",
|
|
631
|
+
iconOnly: true,
|
|
632
|
+
label: t("copy.label"),
|
|
633
|
+
variant: "ghost",
|
|
634
|
+
density: "sm",
|
|
635
|
+
onClick: () => void navigator.clipboard?.writeText(Message.extractText(message))
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
var timeOf = (message) => /* @__PURE__ */ jsx("time", {
|
|
639
|
+
dateTime: message.created,
|
|
640
|
+
title: new Date(message.created).toLocaleString(),
|
|
641
|
+
children: formatTime(message.created)
|
|
642
|
+
});
|
|
643
|
+
/**
|
|
644
|
+
* The controls under the reader's own prompt: copy, rewind (edit-and-resend), and when it was
|
|
645
|
+
* sent. Revealed on hover by the chrome, but never removed from flow — chrome that appears and
|
|
646
|
+
* disappears changes the row's height, and a pointer travelling down a scrolling list would then
|
|
647
|
+
* move every row below it.
|
|
648
|
+
*/
|
|
649
|
+
var PromptToolbar = ({ classNames, message }) => {
|
|
650
|
+
const { t } = useTranslation(translationKey);
|
|
651
|
+
const { onRewind } = useMessageChromeContext("PromptToolbar");
|
|
652
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
653
|
+
role: "toolbar",
|
|
654
|
+
className: mx$1("flex items-center gap-1 text-xs text-description", classNames),
|
|
655
|
+
children: [
|
|
656
|
+
/* @__PURE__ */ jsx(CopyButton, { message }),
|
|
657
|
+
onRewind && /* @__PURE__ */ jsx(IconButton, {
|
|
658
|
+
icon: "ph--clock-counter-clockwise--regular",
|
|
659
|
+
iconOnly: true,
|
|
660
|
+
label: t("rewind.label"),
|
|
661
|
+
variant: "ghost",
|
|
662
|
+
density: "sm",
|
|
663
|
+
"data-testid": "chat.rewind",
|
|
664
|
+
onClick: () => onRewind(message.id)
|
|
665
|
+
}),
|
|
666
|
+
timeOf(message)
|
|
667
|
+
]
|
|
668
|
+
});
|
|
669
|
+
};
|
|
670
|
+
PromptToolbar.displayName = "PromptToolbar";
|
|
671
|
+
/** The controls under an answer: copy, and when the answer finished. */
|
|
672
|
+
var AssistantToolbar = ({ classNames, message }) => {
|
|
673
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
674
|
+
role: "toolbar",
|
|
675
|
+
className: mx$1("flex items-center gap-1 text-xs text-description", classNames),
|
|
676
|
+
children: [/* @__PURE__ */ jsx(CopyButton, { message }), timeOf(message)]
|
|
677
|
+
});
|
|
678
|
+
};
|
|
679
|
+
AssistantToolbar.displayName = "AssistantToolbar";
|
|
680
|
+
/** Shared hover reveal: present in flow at all times, visible when the row is under the pointer. */
|
|
681
|
+
var reveal = "pt-1 opacity-0 transition-opacity group-hover:opacity-100";
|
|
682
|
+
var Row = ({ children, classNames }) => /* @__PURE__ */ jsx("div", {
|
|
683
|
+
className: mx$1("group relative px-2 py-2", classNames),
|
|
684
|
+
"data-testid": "feed.message",
|
|
685
|
+
children
|
|
686
|
+
});
|
|
687
|
+
/**
|
|
688
|
+
* The assistant feed's per-message frame: the reader's prompts and the model's answers are framed
|
|
689
|
+
* differently because they are different kinds of thing — a prompt is an instruction the thread
|
|
690
|
+
* can be rewound to, an answer is a result.
|
|
691
|
+
*
|
|
692
|
+
* A prompt sits apart from the model's prose: right-aligned, at most two thirds of the viewport —
|
|
693
|
+
* the chat convention that makes whose-turn legible at a glance — with its toolbar following the
|
|
694
|
+
* bubble's edge.
|
|
695
|
+
*/
|
|
696
|
+
var MessageChrome = ({ message, selected, children }) => {
|
|
697
|
+
const prompt = message.sender.role === "user";
|
|
698
|
+
return /* @__PURE__ */ jsx(Row, {
|
|
699
|
+
classNames: mx$1(selected && "bg-hover-surface"),
|
|
700
|
+
children: prompt ? /* @__PURE__ */ jsx("div", {
|
|
701
|
+
className: "min-w-0 flex flex-col items-end",
|
|
702
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
703
|
+
className: "max-w-[66%] min-w-0",
|
|
704
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
705
|
+
className: "px-4 py-3 border-s-2 border-accent-bg rounded-sm bg-input-surface",
|
|
706
|
+
children
|
|
707
|
+
}), /* @__PURE__ */ jsx(PromptToolbar, {
|
|
708
|
+
classNames: mx$1("justify-end", reveal),
|
|
709
|
+
message
|
|
710
|
+
})]
|
|
711
|
+
})
|
|
712
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
713
|
+
className: "min-w-0",
|
|
714
|
+
children: [children, /* @__PURE__ */ jsx(AssistantToolbar, {
|
|
715
|
+
classNames: reveal,
|
|
716
|
+
message
|
|
717
|
+
})]
|
|
718
|
+
})
|
|
719
|
+
});
|
|
720
|
+
};
|
|
721
|
+
MessageChrome.displayName = MESSAGE_CHROME_NAME;
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/components/ChatThread/ChatThread.tsx
|
|
724
|
+
var [ChatThreadProvider, useChatThreadContext] = createContext("ChatThread");
|
|
725
|
+
/** Bridges the feed's context out to the host's ref; renders nothing. */
|
|
726
|
+
var ControllerBridge = ({ controllerRef }) => {
|
|
727
|
+
const { model, scrollToBottom, scrollToIndex, navigation } = useMessageList("ChatThread.Controller");
|
|
728
|
+
useEffect(() => {
|
|
729
|
+
setRef(controllerRef, {
|
|
730
|
+
model,
|
|
731
|
+
scrollToBottom,
|
|
732
|
+
scrollToIndex,
|
|
733
|
+
navigation
|
|
734
|
+
});
|
|
735
|
+
return () => {
|
|
736
|
+
setRef(controllerRef, null);
|
|
737
|
+
};
|
|
738
|
+
}, [
|
|
739
|
+
controllerRef,
|
|
740
|
+
model,
|
|
741
|
+
scrollToBottom,
|
|
742
|
+
scrollToIndex,
|
|
743
|
+
navigation
|
|
744
|
+
]);
|
|
745
|
+
return null;
|
|
746
|
+
};
|
|
747
|
+
/**
|
|
748
|
+
* Headless root: the feed's `MessageList.Root` configured as an assistant thread — the view-typed
|
|
749
|
+
* renderer, the widget registry, the prompt/answer chrome, and the chat behaviours (follow the
|
|
750
|
+
* streaming tail; reserve room to bring the last prompt to the top). Composes with the feed's own
|
|
751
|
+
* parts: `MessageList.Nav`, `useMessageList`, and the rails all work inside it.
|
|
752
|
+
*/
|
|
753
|
+
var ChatThreadRoot = ({ children, model, viewType, registry, getObjectLabel, userHue, debug, onEvent, onRangeChange, controllerRef }) => {
|
|
754
|
+
const renderer = useMemo(() => createRenderer(viewType, { getObjectLabel }), [viewType, getObjectLabel]);
|
|
755
|
+
const merged = useMemo(() => registry ? {
|
|
756
|
+
...assistantRegistry,
|
|
757
|
+
...registry
|
|
758
|
+
} : assistantRegistry, [registry]);
|
|
759
|
+
const handleRewind = useCallback((id) => onEvent?.({
|
|
760
|
+
type: "rewind",
|
|
761
|
+
id
|
|
762
|
+
}), [onEvent]);
|
|
763
|
+
return /* @__PURE__ */ jsx(ChatThreadProvider, {
|
|
764
|
+
userHue,
|
|
765
|
+
onEvent,
|
|
766
|
+
children: /* @__PURE__ */ jsx(MessageChromeProvider, {
|
|
767
|
+
onRewind: onEvent ? handleRewind : void 0,
|
|
768
|
+
children: /* @__PURE__ */ jsxs(MessageList.Root, {
|
|
769
|
+
model,
|
|
770
|
+
renderer,
|
|
771
|
+
registry: merged,
|
|
772
|
+
Chrome: MessageChrome,
|
|
773
|
+
estimateSize: estimateRow,
|
|
774
|
+
debug,
|
|
775
|
+
stickyBottom: true,
|
|
776
|
+
scrollPastEnd: true,
|
|
777
|
+
onRangeChange,
|
|
778
|
+
children: [controllerRef && /* @__PURE__ */ jsx(ControllerBridge, { controllerRef }), children]
|
|
779
|
+
})
|
|
780
|
+
})
|
|
781
|
+
});
|
|
782
|
+
};
|
|
783
|
+
ChatThreadRoot.displayName = "ChatThread.Root";
|
|
784
|
+
var CHAT_THREAD_VIEWPORT_NAME = "ChatThread.Viewport";
|
|
785
|
+
/**
|
|
786
|
+
* The scrolling thread. Suggestion and select widgets are DOM widgets carrying
|
|
787
|
+
* `data-action="submit"` buttons; one delegated listener here turns those clicks into `submit`
|
|
788
|
+
* events, which is what keeps the widgets renderable from the tag alone.
|
|
789
|
+
*/
|
|
790
|
+
var ChatThreadViewport = ({ children, ...props }) => {
|
|
791
|
+
const { userHue, onEvent } = useChatThreadContext(CHAT_THREAD_VIEWPORT_NAME);
|
|
792
|
+
const handleClick = useCallback((event) => {
|
|
793
|
+
const text = event.target.closest("[data-action=\"submit\"]")?.getAttribute("data-value");
|
|
794
|
+
if (text) {
|
|
795
|
+
event.preventDefault();
|
|
796
|
+
event.stopPropagation();
|
|
797
|
+
onEvent?.({
|
|
798
|
+
type: "submit",
|
|
799
|
+
text
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}, [onEvent]);
|
|
803
|
+
return /* @__PURE__ */ jsx("div", {
|
|
804
|
+
className: "contents",
|
|
805
|
+
"data-testid": "assistant.thread",
|
|
806
|
+
"data-hue": userHue,
|
|
807
|
+
onClickCapture: handleClick,
|
|
808
|
+
children: /* @__PURE__ */ jsx(MessageList.Viewport, {
|
|
809
|
+
...props,
|
|
810
|
+
children
|
|
811
|
+
})
|
|
812
|
+
});
|
|
813
|
+
};
|
|
814
|
+
ChatThreadViewport.displayName = CHAT_THREAD_VIEWPORT_NAME;
|
|
815
|
+
var ChatThread = {
|
|
816
|
+
Root: ChatThreadRoot,
|
|
817
|
+
Viewport: ChatThreadViewport
|
|
818
|
+
};
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region src/types.ts
|
|
821
|
+
/**
|
|
822
|
+
* Which projection of the thread the reader sees. A view is a filter over blocks, applied by the
|
|
823
|
+
* renderer — the model always carries everything.
|
|
824
|
+
*
|
|
825
|
+
* - `summary`: only conversational text (prompts and replies).
|
|
826
|
+
* - `normal`: everything except reasoning.
|
|
827
|
+
* - `thinking`: everything, reasoning included.
|
|
828
|
+
* - `debug`: every block, raw fallbacks visible.
|
|
829
|
+
*/
|
|
830
|
+
var ChatView = Schema.Union([
|
|
831
|
+
Schema.Literal("normal").annotate({ title: "Normal" }),
|
|
832
|
+
Schema.Literal("summary").annotate({ title: "Summary" }),
|
|
833
|
+
Schema.Literal("thinking").annotate({ title: "Thinking" }),
|
|
834
|
+
Schema.Literal("debug").annotate({ title: "Debug" })
|
|
835
|
+
]);
|
|
836
|
+
//#endregion
|
|
837
|
+
export { AssistantToolbar, ChatThread, ChatView, FallbackWidget, MessageChrome, MessageChromeProvider, PromptToolbar, ReasoningWidget, ReferenceWidget, SelectWidget, StatsWidget, StatusWidget, SuggestionWidget, SummaryWidget, ToolWidget, assistantRegistry, createRenderer, estimateRow, formatTime, translationKey, translations };
|
|
838
|
+
|
|
839
|
+
//# sourceMappingURL=index.mjs.map
|