@yadsh/dsh-qa-surface 0.1.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 +10 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/SPEC-dsh-qa-surface.md +1921 -0
- package/compatibility.json +15 -0
- package/cordis.patch.yml +9 -0
- package/docs/ARCHITECTURE.md +24 -0
- package/docs/COMPATIBILITY.md +13 -0
- package/docs/CONFIGURATION.md +18 -0
- package/docs/EMBEDDING.md +13 -0
- package/lib/client/QaConfigController.js +65 -0
- package/lib/client/QaConfigController.js.map +1 -0
- package/lib/client/QaRouteController.js +109 -0
- package/lib/client/QaRouteController.js.map +1 -0
- package/lib/client/QaSessionController.js +366 -0
- package/lib/client/QaSessionController.js.map +1 -0
- package/lib/client/QaSurface.js +109 -0
- package/lib/client/QaSurface.js.map +1 -0
- package/lib/client/QaTranscriptAdapter.js +92 -0
- package/lib/client/QaTranscriptAdapter.js.map +1 -0
- package/lib/client/components/Markdown.js +102 -0
- package/lib/client/components/Markdown.js.map +1 -0
- package/lib/client/components/QaComposer.js +33 -0
- package/lib/client/components/QaComposer.js.map +1 -0
- package/lib/client/components/QaMessage.js +14 -0
- package/lib/client/components/QaMessage.js.map +1 -0
- package/lib/client/index.js +46 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/styles.js +58 -0
- package/lib/client/styles.js.map +1 -0
- package/lib/client.js +1131 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +83 -0
- package/lib/config.js.map +1 -0
- package/lib/host-route.js +27 -0
- package/lib/host-route.js.map +1 -0
- package/lib/index.js +83 -0
- package/lib/index.js.map +1 -0
- package/lib/navigation-marker.js +3 -0
- package/lib/navigation-marker.js.map +1 -0
- package/lib/resolve-config.js +143 -0
- package/lib/resolve-config.js.map +1 -0
- package/lib/types/client/QaConfigController.d.ts +21 -0
- package/lib/types/client/QaRouteController.d.ts +43 -0
- package/lib/types/client/QaSessionController.d.ts +59 -0
- package/lib/types/client/QaSurface.d.ts +16 -0
- package/lib/types/client/QaTranscriptAdapter.d.ts +7 -0
- package/lib/types/client/components/Markdown.d.ts +5 -0
- package/lib/types/client/components/QaComposer.d.ts +11 -0
- package/lib/types/client/components/QaMessage.d.ts +8 -0
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/styles.d.ts +2 -0
- package/lib/types/config.d.ts +5 -0
- package/lib/types/host-route.d.ts +9 -0
- package/lib/types/index.d.ts +29 -0
- package/lib/types/navigation-marker.d.ts +3 -0
- package/lib/types/resolve-config.d.ts +6 -0
- package/lib/types/types.d.ts +114 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +137 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1131 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@yadsh/dsh-qa-surface",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/resolve-config.ts
|
|
10
|
+
const DEFAULT_QA_SURFACE_CONFIG = Object.freeze({
|
|
11
|
+
enabled: true,
|
|
12
|
+
route: Object.freeze({
|
|
13
|
+
path: "/qa",
|
|
14
|
+
matchChildren: true
|
|
15
|
+
}),
|
|
16
|
+
branding: Object.freeze({
|
|
17
|
+
title: "Assistant",
|
|
18
|
+
subtitle: "",
|
|
19
|
+
welcomeMessage: "How can I help?",
|
|
20
|
+
placeholder: "Ask a question...",
|
|
21
|
+
logoUrl: null
|
|
22
|
+
}),
|
|
23
|
+
session: Object.freeze({
|
|
24
|
+
policy: "browser-persistent",
|
|
25
|
+
storageKey: "dsh-qa-surface",
|
|
26
|
+
workspaceId: null,
|
|
27
|
+
fixedSessionId: null,
|
|
28
|
+
agentPreset: null,
|
|
29
|
+
provider: null,
|
|
30
|
+
model: null,
|
|
31
|
+
reasoningEffort: null
|
|
32
|
+
}),
|
|
33
|
+
ui: Object.freeze({
|
|
34
|
+
showHeader: true,
|
|
35
|
+
showReset: true,
|
|
36
|
+
showStop: true,
|
|
37
|
+
showTimestamps: false,
|
|
38
|
+
showToolActivity: false,
|
|
39
|
+
showReasoning: false,
|
|
40
|
+
renderMarkdown: true,
|
|
41
|
+
maxContentWidth: 900
|
|
42
|
+
}),
|
|
43
|
+
suggestedQuestions: Object.freeze([]),
|
|
44
|
+
interaction: Object.freeze({
|
|
45
|
+
approvals: "blocked",
|
|
46
|
+
questions: "unsupported"
|
|
47
|
+
}),
|
|
48
|
+
embedding: Object.freeze({ frameAncestors: null })
|
|
49
|
+
});
|
|
50
|
+
function optionalText(value) {
|
|
51
|
+
const trimmed = value?.trim() ?? "";
|
|
52
|
+
return trimmed === "" ? null : trimmed;
|
|
53
|
+
}
|
|
54
|
+
function normalizeRoutePath(value) {
|
|
55
|
+
const trimmed = value.trim();
|
|
56
|
+
if (trimmed === "" || !trimmed.startsWith("/")) throw new TypeError("dsh-qa-surface: route.path must start with /");
|
|
57
|
+
const path = trimmed.length > 1 ? trimmed.replace(/\/+$/u, "") : trimmed;
|
|
58
|
+
if (path === "/") throw new TypeError("dsh-qa-surface: route.path cannot replace the operator root");
|
|
59
|
+
if (path === "/api" || path.startsWith("/api/")) throw new TypeError("dsh-qa-surface: route.path cannot claim /api");
|
|
60
|
+
if (path === "/plugins" || path.startsWith("/plugins/")) throw new TypeError("dsh-qa-surface: route.path cannot claim /plugins");
|
|
61
|
+
return path;
|
|
62
|
+
}
|
|
63
|
+
function uniqueQuestions(values) {
|
|
64
|
+
const questions = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
65
|
+
if (questions.some((value) => value.length > 500)) throw new TypeError("dsh-qa-surface: suggested questions must be at most 500 characters");
|
|
66
|
+
return Object.freeze(questions);
|
|
67
|
+
}
|
|
68
|
+
/** Materialize defaults and enforce route/session/model safety constraints. */
|
|
69
|
+
function resolveConfig(input = {}) {
|
|
70
|
+
const routePath = normalizeRoutePath(input.route?.path ?? DEFAULT_QA_SURFACE_CONFIG.route.path);
|
|
71
|
+
const policy = input.session?.policy ?? DEFAULT_QA_SURFACE_CONFIG.session.policy;
|
|
72
|
+
const fixedSessionId = optionalText(input.session?.fixedSessionId);
|
|
73
|
+
if (policy === "fixed" && fixedSessionId === null) throw new TypeError("dsh-qa-surface: session.fixedSessionId is required for fixed policy");
|
|
74
|
+
const provider = optionalText(input.session?.provider);
|
|
75
|
+
const model = optionalText(input.session?.model);
|
|
76
|
+
if (provider === null !== (model === null)) throw new TypeError("dsh-qa-surface: session.provider and session.model must be set together");
|
|
77
|
+
const storageKey = input.session?.storageKey?.trim() ?? DEFAULT_QA_SURFACE_CONFIG.session.storageKey;
|
|
78
|
+
if (storageKey === "") throw new TypeError("dsh-qa-surface: session.storageKey cannot be empty");
|
|
79
|
+
const maxContentWidth = input.ui?.maxContentWidth ?? DEFAULT_QA_SURFACE_CONFIG.ui.maxContentWidth;
|
|
80
|
+
if (!Number.isSafeInteger(maxContentWidth) || maxContentWidth < 480 || maxContentWidth > 1600) throw new TypeError("dsh-qa-surface: ui.maxContentWidth must be an integer from 480 to 1600");
|
|
81
|
+
if (input.ui?.showReasoning === true) throw new TypeError("dsh-qa-surface: ui.showReasoning is not supported by the safe MVP surface");
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
enabled: input.enabled ?? DEFAULT_QA_SURFACE_CONFIG.enabled,
|
|
84
|
+
route: Object.freeze({
|
|
85
|
+
path: routePath,
|
|
86
|
+
matchChildren: input.route?.matchChildren ?? DEFAULT_QA_SURFACE_CONFIG.route.matchChildren
|
|
87
|
+
}),
|
|
88
|
+
branding: Object.freeze({
|
|
89
|
+
title: input.branding?.title?.trim() || DEFAULT_QA_SURFACE_CONFIG.branding.title,
|
|
90
|
+
subtitle: input.branding?.subtitle?.trim() ?? "",
|
|
91
|
+
welcomeMessage: input.branding?.welcomeMessage?.trim() ?? DEFAULT_QA_SURFACE_CONFIG.branding.welcomeMessage,
|
|
92
|
+
placeholder: input.branding?.placeholder?.trim() || DEFAULT_QA_SURFACE_CONFIG.branding.placeholder,
|
|
93
|
+
logoUrl: optionalText(input.branding?.logoUrl)
|
|
94
|
+
}),
|
|
95
|
+
session: Object.freeze({
|
|
96
|
+
policy,
|
|
97
|
+
storageKey,
|
|
98
|
+
workspaceId: optionalText(input.session?.workspaceId),
|
|
99
|
+
fixedSessionId,
|
|
100
|
+
agentPreset: optionalText(input.session?.agentPreset),
|
|
101
|
+
provider,
|
|
102
|
+
model,
|
|
103
|
+
reasoningEffort: optionalText(input.session?.reasoningEffort)
|
|
104
|
+
}),
|
|
105
|
+
ui: Object.freeze({
|
|
106
|
+
showHeader: input.ui?.showHeader ?? DEFAULT_QA_SURFACE_CONFIG.ui.showHeader,
|
|
107
|
+
showReset: input.ui?.showReset ?? DEFAULT_QA_SURFACE_CONFIG.ui.showReset,
|
|
108
|
+
showStop: input.ui?.showStop ?? DEFAULT_QA_SURFACE_CONFIG.ui.showStop,
|
|
109
|
+
showTimestamps: input.ui?.showTimestamps ?? DEFAULT_QA_SURFACE_CONFIG.ui.showTimestamps,
|
|
110
|
+
showToolActivity: input.ui?.showToolActivity ?? DEFAULT_QA_SURFACE_CONFIG.ui.showToolActivity,
|
|
111
|
+
showReasoning: false,
|
|
112
|
+
renderMarkdown: input.ui?.renderMarkdown ?? DEFAULT_QA_SURFACE_CONFIG.ui.renderMarkdown,
|
|
113
|
+
maxContentWidth
|
|
114
|
+
}),
|
|
115
|
+
suggestedQuestions: uniqueQuestions(input.suggestedQuestions ?? []),
|
|
116
|
+
interaction: Object.freeze({
|
|
117
|
+
approvals: "blocked",
|
|
118
|
+
questions: "unsupported"
|
|
119
|
+
}),
|
|
120
|
+
embedding: Object.freeze({ frameAncestors: optionalText(input.embedding?.frameAncestors) })
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/client/QaConfigController.ts
|
|
125
|
+
/** Browser projection of the Host-owned qa-surface settings namespace. */
|
|
126
|
+
var QaConfigController = class {
|
|
127
|
+
scope;
|
|
128
|
+
listeners = /* @__PURE__ */ new Set();
|
|
129
|
+
snapshot;
|
|
130
|
+
unsubscribe;
|
|
131
|
+
constructor(scope) {
|
|
132
|
+
this.scope = scope;
|
|
133
|
+
this.snapshot = this.project();
|
|
134
|
+
this.unsubscribe = scope.subscribe(() => this.refresh());
|
|
135
|
+
}
|
|
136
|
+
getSnapshot = () => this.snapshot;
|
|
137
|
+
subscribe = (listener) => {
|
|
138
|
+
this.listeners.add(listener);
|
|
139
|
+
return () => this.listeners.delete(listener);
|
|
140
|
+
};
|
|
141
|
+
dispose() {
|
|
142
|
+
this.unsubscribe();
|
|
143
|
+
this.listeners.clear();
|
|
144
|
+
}
|
|
145
|
+
project() {
|
|
146
|
+
const settings = this.scope.getSnapshot();
|
|
147
|
+
if (settings.status === "loading") return {
|
|
148
|
+
status: "loading",
|
|
149
|
+
config: DEFAULT_QA_SURFACE_CONFIG,
|
|
150
|
+
error: null
|
|
151
|
+
};
|
|
152
|
+
if (settings.status === "unavailable") return {
|
|
153
|
+
status: "unavailable",
|
|
154
|
+
config: DEFAULT_QA_SURFACE_CONFIG,
|
|
155
|
+
error: null
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
return {
|
|
159
|
+
status: "ready",
|
|
160
|
+
config: resolveConfig(settings.value ?? {}),
|
|
161
|
+
error: null
|
|
162
|
+
};
|
|
163
|
+
} catch {
|
|
164
|
+
return {
|
|
165
|
+
status: "error",
|
|
166
|
+
config: DEFAULT_QA_SURFACE_CONFIG,
|
|
167
|
+
error: "The assistant configuration is unavailable."
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
refresh() {
|
|
172
|
+
const next = this.project();
|
|
173
|
+
if (next.status === this.snapshot.status && next.config === this.snapshot.config && next.error === this.snapshot.error) return;
|
|
174
|
+
this.snapshot = next;
|
|
175
|
+
for (const listener of this.listeners) listener();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/navigation-marker.ts
|
|
180
|
+
/** Query hand-off used only by the Host navigation compatibility redirect. */
|
|
181
|
+
const QA_NAVIGATION_MARKER = "__dsh_qa_route";
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/client/QaRouteController.ts
|
|
184
|
+
function matchesQaRoute(pathname, route) {
|
|
185
|
+
if (pathname === route.path || pathname === `${route.path}/`) return true;
|
|
186
|
+
if (route.path === "/") return route.matchChildren;
|
|
187
|
+
if (!route.matchChildren) return false;
|
|
188
|
+
return pathname.startsWith(`${route.path}/`);
|
|
189
|
+
}
|
|
190
|
+
/** Observable route matcher with reversible History API instrumentation. */
|
|
191
|
+
var QaRouteController = class {
|
|
192
|
+
target;
|
|
193
|
+
listeners = /* @__PURE__ */ new Set();
|
|
194
|
+
route = {
|
|
195
|
+
path: "/qa",
|
|
196
|
+
matchChildren: true
|
|
197
|
+
};
|
|
198
|
+
enabled = false;
|
|
199
|
+
snapshot;
|
|
200
|
+
originalPushState;
|
|
201
|
+
originalReplaceState;
|
|
202
|
+
wrappedPushState;
|
|
203
|
+
wrappedReplaceState;
|
|
204
|
+
disposed = false;
|
|
205
|
+
constructor(target = window) {
|
|
206
|
+
this.target = target;
|
|
207
|
+
this.originalPushState = target.history.pushState;
|
|
208
|
+
this.originalReplaceState = target.history.replaceState;
|
|
209
|
+
this.restoreRedirectedPath();
|
|
210
|
+
this.snapshot = this.project();
|
|
211
|
+
const refresh = () => this.refresh();
|
|
212
|
+
const history = target.history;
|
|
213
|
+
const originalPushState = this.originalPushState;
|
|
214
|
+
const originalReplaceState = this.originalReplaceState;
|
|
215
|
+
this.wrappedPushState = function(...args) {
|
|
216
|
+
Reflect.apply(originalPushState, history, args);
|
|
217
|
+
refresh();
|
|
218
|
+
};
|
|
219
|
+
this.wrappedReplaceState = function(...args) {
|
|
220
|
+
Reflect.apply(originalReplaceState, history, args);
|
|
221
|
+
refresh();
|
|
222
|
+
};
|
|
223
|
+
target.history.pushState = this.wrappedPushState;
|
|
224
|
+
target.history.replaceState = this.wrappedReplaceState;
|
|
225
|
+
target.addEventListener("popstate", refresh);
|
|
226
|
+
this.removePopstate = () => target.removeEventListener("popstate", refresh);
|
|
227
|
+
}
|
|
228
|
+
removePopstate;
|
|
229
|
+
getSnapshot = () => this.snapshot;
|
|
230
|
+
subscribe = (listener) => {
|
|
231
|
+
this.listeners.add(listener);
|
|
232
|
+
return () => this.listeners.delete(listener);
|
|
233
|
+
};
|
|
234
|
+
configure(config, ready = true) {
|
|
235
|
+
this.route = config.route;
|
|
236
|
+
this.enabled = ready && config.enabled;
|
|
237
|
+
this.refresh();
|
|
238
|
+
}
|
|
239
|
+
dispose() {
|
|
240
|
+
if (this.disposed) return;
|
|
241
|
+
this.disposed = true;
|
|
242
|
+
this.removePopstate();
|
|
243
|
+
if (this.target.history.pushState === this.wrappedPushState) this.target.history.pushState = this.originalPushState;
|
|
244
|
+
if (this.target.history.replaceState === this.wrappedReplaceState) this.target.history.replaceState = this.originalReplaceState;
|
|
245
|
+
this.listeners.clear();
|
|
246
|
+
}
|
|
247
|
+
project() {
|
|
248
|
+
const pathname = this.target.location.pathname;
|
|
249
|
+
return {
|
|
250
|
+
pathname,
|
|
251
|
+
active: this.enabled && matchesQaRoute(pathname, this.route)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
restoreRedirectedPath() {
|
|
255
|
+
const search = this.target.location.search;
|
|
256
|
+
if (search === void 0 || search === "") return;
|
|
257
|
+
const requested = new URLSearchParams(search).get(QA_NAVIGATION_MARKER);
|
|
258
|
+
if (requested === null || !requested.startsWith("/") || requested.startsWith("//")) return;
|
|
259
|
+
Reflect.apply(this.originalReplaceState, this.target.history, [
|
|
260
|
+
null,
|
|
261
|
+
"",
|
|
262
|
+
requested
|
|
263
|
+
]);
|
|
264
|
+
}
|
|
265
|
+
refresh() {
|
|
266
|
+
if (this.disposed) return;
|
|
267
|
+
const next = this.project();
|
|
268
|
+
if (next.pathname === this.snapshot.pathname && next.active === this.snapshot.active) return;
|
|
269
|
+
this.snapshot = next;
|
|
270
|
+
for (const listener of this.listeners) listener();
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/client/QaTranscriptAdapter.ts
|
|
275
|
+
function visibleContentText(content) {
|
|
276
|
+
return content.flatMap((block) => {
|
|
277
|
+
if (typeof block !== "object" || block === null) return [];
|
|
278
|
+
return Reflect.get(block, "type") === "text" && typeof Reflect.get(block, "text") === "string" ? [Reflect.get(block, "text")] : [];
|
|
279
|
+
}).join("");
|
|
280
|
+
}
|
|
281
|
+
function visibleAssistantText(blocks) {
|
|
282
|
+
return blocks.filter((block) => block.kind === "text" && typeof block.text === "string").map((block) => block.text).join("");
|
|
283
|
+
}
|
|
284
|
+
/** Project only end-user-safe text from the public DSH conversation snapshot. */
|
|
285
|
+
function projectTranscript(snapshot, options = {}) {
|
|
286
|
+
const messages = [];
|
|
287
|
+
for (const node of snapshot.nodes) {
|
|
288
|
+
if (node.kind === "user" || node.kind === "steering") {
|
|
289
|
+
const text = visibleContentText(node.content);
|
|
290
|
+
if (text !== "") messages.push({
|
|
291
|
+
id: `${node.kind}:${node.seq}`,
|
|
292
|
+
role: "user",
|
|
293
|
+
text,
|
|
294
|
+
status: "committed",
|
|
295
|
+
timestamp: node.time
|
|
296
|
+
});
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (node.kind === "assistant") {
|
|
300
|
+
const text = visibleAssistantText(node.blocks);
|
|
301
|
+
if (text !== "") messages.push({
|
|
302
|
+
id: `assistant:${node.messageId ?? node.seq}`,
|
|
303
|
+
role: "assistant",
|
|
304
|
+
text,
|
|
305
|
+
status: "committed",
|
|
306
|
+
timestamp: node.time
|
|
307
|
+
});
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (node.kind === "turn-error") {
|
|
311
|
+
messages.push({
|
|
312
|
+
id: `turn-error:${node.seq}`,
|
|
313
|
+
role: "system",
|
|
314
|
+
text: "The assistant could not complete this response.",
|
|
315
|
+
status: "error",
|
|
316
|
+
timestamp: node.time
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (node.kind === "turn-max-tokens") messages.push({
|
|
321
|
+
id: `turn-max-tokens:${node.seq}`,
|
|
322
|
+
role: "system",
|
|
323
|
+
text: "The response reached its length limit.",
|
|
324
|
+
status: "info",
|
|
325
|
+
timestamp: node.time
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (snapshot.partial !== null) {
|
|
329
|
+
const text = visibleAssistantText(snapshot.partial.blocks);
|
|
330
|
+
if (text !== "") messages.push({
|
|
331
|
+
id: `assistant:partial:${snapshot.partial.turn}:${snapshot.partial.step}`,
|
|
332
|
+
role: "assistant",
|
|
333
|
+
text,
|
|
334
|
+
status: "streaming"
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
if (options.showToolActivity === true && snapshot.runningCalls.length > 0 && snapshot.partial === null) messages.push({
|
|
338
|
+
id: "tool-activity",
|
|
339
|
+
role: "system",
|
|
340
|
+
text: "Working…",
|
|
341
|
+
status: "info"
|
|
342
|
+
});
|
|
343
|
+
return messages;
|
|
344
|
+
}
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/client/QaSessionController.ts
|
|
347
|
+
const EMPTY_STATE = Object.freeze({
|
|
348
|
+
phase: "idle",
|
|
349
|
+
sessionId: null,
|
|
350
|
+
messages: Object.freeze([]),
|
|
351
|
+
error: null,
|
|
352
|
+
canSend: false,
|
|
353
|
+
canStop: false
|
|
354
|
+
});
|
|
355
|
+
function waitFor(source, predicate, timeoutMs) {
|
|
356
|
+
const current = source.getSnapshot();
|
|
357
|
+
if (predicate(current)) return Promise.resolve(current);
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
let settled = false;
|
|
360
|
+
const timeout = {};
|
|
361
|
+
let unsubscribe = () => void 0;
|
|
362
|
+
const finish = (value) => {
|
|
363
|
+
if (settled) return;
|
|
364
|
+
settled = true;
|
|
365
|
+
if (timeout.id !== void 0) clearTimeout(timeout.id);
|
|
366
|
+
unsubscribe();
|
|
367
|
+
resolve(value);
|
|
368
|
+
};
|
|
369
|
+
unsubscribe = source.subscribe(() => {
|
|
370
|
+
const next = source.getSnapshot();
|
|
371
|
+
if (predicate(next)) finish(next);
|
|
372
|
+
});
|
|
373
|
+
if (settled) {
|
|
374
|
+
unsubscribe();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const afterSubscribe = source.getSnapshot();
|
|
378
|
+
if (predicate(afterSubscribe)) {
|
|
379
|
+
finish(afterSubscribe);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
timeout.id = setTimeout(() => {
|
|
383
|
+
if (settled) return;
|
|
384
|
+
settled = true;
|
|
385
|
+
unsubscribe();
|
|
386
|
+
reject(/* @__PURE__ */ new Error("Timed out waiting for the DSH runtime."));
|
|
387
|
+
}, timeoutMs);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/** The only module that couples QA behavior to DSH Session/client APIs. */
|
|
391
|
+
var QaSessionController = class {
|
|
392
|
+
listeners = /* @__PURE__ */ new Set();
|
|
393
|
+
sessions;
|
|
394
|
+
api;
|
|
395
|
+
connection;
|
|
396
|
+
config;
|
|
397
|
+
storage;
|
|
398
|
+
timeoutMs;
|
|
399
|
+
state = EMPTY_STATE;
|
|
400
|
+
session;
|
|
401
|
+
unsubscribeSession;
|
|
402
|
+
unsubscribeConnection;
|
|
403
|
+
ensuring;
|
|
404
|
+
operationError = null;
|
|
405
|
+
admissionPending = false;
|
|
406
|
+
connectedOnce;
|
|
407
|
+
disposed = false;
|
|
408
|
+
generation = 0;
|
|
409
|
+
constructor(options) {
|
|
410
|
+
this.sessions = options.sessions;
|
|
411
|
+
this.api = options.api;
|
|
412
|
+
this.connection = options.connection;
|
|
413
|
+
this.config = options.config;
|
|
414
|
+
this.storage = options.storage;
|
|
415
|
+
this.timeoutMs = options.timeoutMs ?? 15e3;
|
|
416
|
+
this.connectedOnce = this.connection.getSnapshot() !== void 0;
|
|
417
|
+
this.unsubscribeConnection = this.connection.subscribe(() => {
|
|
418
|
+
if (this.connection.getSnapshot() !== void 0) this.connectedOnce = true;
|
|
419
|
+
this.publish();
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
getSnapshot = () => this.state;
|
|
423
|
+
subscribe = (listener) => {
|
|
424
|
+
this.listeners.add(listener);
|
|
425
|
+
return () => this.listeners.delete(listener);
|
|
426
|
+
};
|
|
427
|
+
ensureSession() {
|
|
428
|
+
if (this.ensuring !== void 0) return this.ensuring;
|
|
429
|
+
const operation = this.ensureSessionNow().finally(() => {
|
|
430
|
+
if (this.ensuring === operation) this.ensuring = void 0;
|
|
431
|
+
});
|
|
432
|
+
this.ensuring = operation;
|
|
433
|
+
return operation;
|
|
434
|
+
}
|
|
435
|
+
async send(text) {
|
|
436
|
+
const prompt = text.trim();
|
|
437
|
+
if (prompt === "" || this.session === void 0 || !this.state.canSend) return false;
|
|
438
|
+
if (prompt.startsWith("/")) {
|
|
439
|
+
this.operationError = "Slash commands are not available in this assistant view.";
|
|
440
|
+
this.publish();
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
this.operationError = null;
|
|
444
|
+
this.admissionPending = true;
|
|
445
|
+
this.publish();
|
|
446
|
+
try {
|
|
447
|
+
if (!(await this.session.prompt([{
|
|
448
|
+
type: "text",
|
|
449
|
+
text: prompt
|
|
450
|
+
}], "queue")).ok) {
|
|
451
|
+
this.admissionPending = false;
|
|
452
|
+
this.operationError = "Your message could not be sent.";
|
|
453
|
+
this.publish();
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
this.publish();
|
|
457
|
+
return true;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
this.admissionPending = false;
|
|
460
|
+
console.error("dsh-qa-surface: prompt failed", error);
|
|
461
|
+
this.operationError = "Your message could not be sent.";
|
|
462
|
+
this.publish();
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async stop() {
|
|
467
|
+
if (this.session === void 0 || !this.state.canStop) return;
|
|
468
|
+
try {
|
|
469
|
+
if (!(await this.session.cancel()).ok) this.operationError = "The response could not be stopped.";
|
|
470
|
+
} catch (error) {
|
|
471
|
+
console.error("dsh-qa-surface: stop failed", error);
|
|
472
|
+
this.operationError = "The response could not be stopped.";
|
|
473
|
+
}
|
|
474
|
+
this.publish();
|
|
475
|
+
}
|
|
476
|
+
async reset() {
|
|
477
|
+
if (this.config.session.policy === "fixed") return;
|
|
478
|
+
const previous = this.session;
|
|
479
|
+
if (previous?.getSnapshot().running === true) try {
|
|
480
|
+
await previous.cancel();
|
|
481
|
+
} catch (error) {
|
|
482
|
+
console.error("dsh-qa-surface: stop before reset failed", error);
|
|
483
|
+
}
|
|
484
|
+
const operation = ++this.generation;
|
|
485
|
+
this.unbind();
|
|
486
|
+
this.operationError = null;
|
|
487
|
+
this.admissionPending = false;
|
|
488
|
+
this.state = {
|
|
489
|
+
...EMPTY_STATE,
|
|
490
|
+
phase: "creating"
|
|
491
|
+
};
|
|
492
|
+
this.emit();
|
|
493
|
+
try {
|
|
494
|
+
await this.waitForConnection();
|
|
495
|
+
const id = await this.createSession();
|
|
496
|
+
if (this.disposed || operation !== this.generation) return;
|
|
497
|
+
this.persist(id);
|
|
498
|
+
await this.bind(id);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
this.fail("Unable to start a new chat.", error);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
dispose() {
|
|
504
|
+
if (this.disposed) return;
|
|
505
|
+
this.disposed = true;
|
|
506
|
+
this.generation += 1;
|
|
507
|
+
this.unsubscribeConnection();
|
|
508
|
+
this.unbind();
|
|
509
|
+
this.listeners.clear();
|
|
510
|
+
}
|
|
511
|
+
async ensureSessionNow() {
|
|
512
|
+
const operation = ++this.generation;
|
|
513
|
+
this.state = {
|
|
514
|
+
...EMPTY_STATE,
|
|
515
|
+
phase: "creating"
|
|
516
|
+
};
|
|
517
|
+
this.emit();
|
|
518
|
+
try {
|
|
519
|
+
await this.waitForConnection();
|
|
520
|
+
const list = await waitFor(this.sessions.list, (snapshot) => snapshot.phase === "ready", this.timeoutMs);
|
|
521
|
+
let id = null;
|
|
522
|
+
if (this.config.session.policy === "fixed") {
|
|
523
|
+
id = this.config.session.fixedSessionId;
|
|
524
|
+
if (id === null || !Object.hasOwn(list.byId, id)) throw new Error("Configured fixed session is unavailable.");
|
|
525
|
+
} else if (this.config.session.policy === "browser-persistent") {
|
|
526
|
+
const stored = this.readPersisted();
|
|
527
|
+
if (stored !== null && Object.hasOwn(list.byId, stored)) id = stored;
|
|
528
|
+
else if (stored !== null) this.clearPersisted();
|
|
529
|
+
}
|
|
530
|
+
if (id === null) {
|
|
531
|
+
id = await this.createSession();
|
|
532
|
+
if (this.config.session.policy === "browser-persistent") this.persist(id);
|
|
533
|
+
}
|
|
534
|
+
if (this.disposed || operation !== this.generation) return;
|
|
535
|
+
await this.bind(id);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
this.fail("Unable to start a chat.", error);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async createSession() {
|
|
541
|
+
const response = await this.api.create({
|
|
542
|
+
...this.config.session.workspaceId === null ? {} : { workspaceId: this.config.session.workspaceId },
|
|
543
|
+
...this.config.session.agentPreset === null ? {} : { agentPreset: this.config.session.agentPreset }
|
|
544
|
+
});
|
|
545
|
+
if (!response.result.ok) throw new Error(response.result.error.code);
|
|
546
|
+
const id = String(response.result.value.sessionId);
|
|
547
|
+
await waitFor(this.sessions.list, (snapshot) => Object.hasOwn(snapshot.byId, id), this.timeoutMs);
|
|
548
|
+
if (this.config.session.provider !== null && this.config.session.model !== null) {
|
|
549
|
+
const selected = await this.api.selectModel({
|
|
550
|
+
sessionId: id,
|
|
551
|
+
provider: this.config.session.provider,
|
|
552
|
+
model: this.config.session.model,
|
|
553
|
+
...this.config.session.reasoningEffort === null ? {} : { reasoningEffort: this.config.session.reasoningEffort }
|
|
554
|
+
});
|
|
555
|
+
if (!selected.result.ok) throw new Error(selected.result.error.code);
|
|
556
|
+
}
|
|
557
|
+
return id;
|
|
558
|
+
}
|
|
559
|
+
async bind(id) {
|
|
560
|
+
this.sessions.open(id);
|
|
561
|
+
let binding = this.sessions.binding(id);
|
|
562
|
+
if (binding === void 0) {
|
|
563
|
+
await waitFor(this.sessions.list, (snapshot) => Object.hasOwn(snapshot.byId, id), this.timeoutMs);
|
|
564
|
+
binding = this.sessions.binding(id);
|
|
565
|
+
}
|
|
566
|
+
if (binding === void 0) throw new Error("Session binding is unavailable.");
|
|
567
|
+
this.unbind();
|
|
568
|
+
this.session = binding.session;
|
|
569
|
+
this.unsubscribeSession = binding.session.subscribe(() => {
|
|
570
|
+
this.admissionPending = false;
|
|
571
|
+
this.operationError = null;
|
|
572
|
+
this.publish();
|
|
573
|
+
});
|
|
574
|
+
this.publish();
|
|
575
|
+
}
|
|
576
|
+
unbind() {
|
|
577
|
+
this.unsubscribeSession?.();
|
|
578
|
+
this.unsubscribeSession = void 0;
|
|
579
|
+
this.session = void 0;
|
|
580
|
+
this.admissionPending = false;
|
|
581
|
+
}
|
|
582
|
+
waitForConnection() {
|
|
583
|
+
return waitFor(this.connection, (description) => description !== void 0, this.timeoutMs);
|
|
584
|
+
}
|
|
585
|
+
publish() {
|
|
586
|
+
if (this.disposed) return;
|
|
587
|
+
const connected = this.connection.getSnapshot() !== void 0;
|
|
588
|
+
if (this.session === void 0) {
|
|
589
|
+
this.state = {
|
|
590
|
+
...this.state,
|
|
591
|
+
phase: !connected && this.connectedOnce ? "reconnecting" : this.state.phase,
|
|
592
|
+
error: this.operationError ?? this.state.error
|
|
593
|
+
};
|
|
594
|
+
this.emit();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const snapshot = this.session.getSnapshot();
|
|
598
|
+
const blocked = snapshot.pending.length > 0;
|
|
599
|
+
const error = this.operationError ?? (blocked ? "This request requires an interaction that is not available in this assistant view." : snapshot.removed || snapshot.openState === "error" ? "This chat is no longer available." : null);
|
|
600
|
+
const phase = !connected ? "reconnecting" : blocked ? "blocked" : snapshot.removed || snapshot.openState === "error" ? "error" : snapshot.openState !== "open" ? "creating" : snapshot.running || this.admissionPending ? "running" : "ready";
|
|
601
|
+
this.state = {
|
|
602
|
+
phase,
|
|
603
|
+
sessionId: String(this.session.sessionId),
|
|
604
|
+
messages: projectTranscript(snapshot, { showToolActivity: this.config.ui.showToolActivity }),
|
|
605
|
+
error,
|
|
606
|
+
canSend: connected && phase === "ready",
|
|
607
|
+
canStop: connected && snapshot.running && this.config.ui.showStop && !blocked
|
|
608
|
+
};
|
|
609
|
+
this.emit();
|
|
610
|
+
}
|
|
611
|
+
fail(message, error) {
|
|
612
|
+
console.error("dsh-qa-surface: session operation failed", error);
|
|
613
|
+
if (this.disposed) return;
|
|
614
|
+
this.operationError = message;
|
|
615
|
+
this.state = {
|
|
616
|
+
...EMPTY_STATE,
|
|
617
|
+
phase: "error",
|
|
618
|
+
error: message
|
|
619
|
+
};
|
|
620
|
+
this.emit();
|
|
621
|
+
}
|
|
622
|
+
storageKey() {
|
|
623
|
+
return `${this.config.session.storageKey}:v1:${this.config.route.path}:session`;
|
|
624
|
+
}
|
|
625
|
+
readPersisted() {
|
|
626
|
+
try {
|
|
627
|
+
const value = this.storage?.getItem(this.storageKey())?.trim() ?? "";
|
|
628
|
+
return value === "" ? null : value;
|
|
629
|
+
} catch {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
persist(id) {
|
|
634
|
+
try {
|
|
635
|
+
this.storage?.setItem(this.storageKey(), id);
|
|
636
|
+
} catch {}
|
|
637
|
+
}
|
|
638
|
+
clearPersisted() {
|
|
639
|
+
try {
|
|
640
|
+
this.storage?.removeItem(this.storageKey());
|
|
641
|
+
} catch {}
|
|
642
|
+
}
|
|
643
|
+
emit() {
|
|
644
|
+
for (const listener of this.listeners) listener();
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/client/components/QaComposer.tsx
|
|
649
|
+
function QaComposer(props) {
|
|
650
|
+
const [draft, setDraft] = (0, react.useState)("");
|
|
651
|
+
const [submitting, setSubmitting] = (0, react.useState)(false);
|
|
652
|
+
const textarea = (0, react.useRef)(null);
|
|
653
|
+
const send = async () => {
|
|
654
|
+
if (submitting || draft.trim() === "" || !props.canSend) return;
|
|
655
|
+
setSubmitting(true);
|
|
656
|
+
try {
|
|
657
|
+
if (await props.onSend(draft)) setDraft("");
|
|
658
|
+
} finally {
|
|
659
|
+
setSubmitting(false);
|
|
660
|
+
textarea.current?.focus();
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
(0, react.useEffect)(() => {
|
|
664
|
+
if (!props.running) textarea.current?.focus();
|
|
665
|
+
}, [props.running]);
|
|
666
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
667
|
+
className: "dsh-qa-composer",
|
|
668
|
+
children: [
|
|
669
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
670
|
+
className: "dsh-qa-sr-only",
|
|
671
|
+
htmlFor: "dsh-qa-prompt",
|
|
672
|
+
children: "Ask a question"
|
|
673
|
+
}),
|
|
674
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
675
|
+
ref: textarea,
|
|
676
|
+
id: "dsh-qa-prompt",
|
|
677
|
+
rows: 1,
|
|
678
|
+
value: draft,
|
|
679
|
+
placeholder: props.placeholder,
|
|
680
|
+
disabled: !props.canSend && !props.running,
|
|
681
|
+
onChange: (event) => setDraft(event.currentTarget.value),
|
|
682
|
+
onKeyDown: (event) => {
|
|
683
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
684
|
+
event.preventDefault();
|
|
685
|
+
send();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}),
|
|
689
|
+
props.running && props.showStop ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
690
|
+
type: "button",
|
|
691
|
+
className: "dsh-qa-button dsh-qa-button--stop",
|
|
692
|
+
disabled: !props.canStop,
|
|
693
|
+
onClick: () => void props.onStop(),
|
|
694
|
+
children: "Stop"
|
|
695
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
696
|
+
type: "button",
|
|
697
|
+
className: "dsh-qa-button dsh-qa-button--send",
|
|
698
|
+
disabled: !props.canSend || submitting || draft.trim() === "",
|
|
699
|
+
onClick: () => void send(),
|
|
700
|
+
children: "Send"
|
|
701
|
+
})
|
|
702
|
+
]
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/client/components/Markdown.tsx
|
|
707
|
+
const INLINE = /(\[[^\]]+\]\([^\s)]+\)|`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*)/gu;
|
|
708
|
+
function safeHref(value) {
|
|
709
|
+
try {
|
|
710
|
+
const url = new URL(value, window.location.origin);
|
|
711
|
+
return [
|
|
712
|
+
"http:",
|
|
713
|
+
"https:",
|
|
714
|
+
"mailto:"
|
|
715
|
+
].includes(url.protocol) ? value : void 0;
|
|
716
|
+
} catch {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function inline(text, keyPrefix) {
|
|
721
|
+
const nodes = [];
|
|
722
|
+
let offset = 0;
|
|
723
|
+
for (const match of text.matchAll(INLINE)) {
|
|
724
|
+
const index = match.index;
|
|
725
|
+
if (index > offset) nodes.push(text.slice(offset, index));
|
|
726
|
+
const token = match[0];
|
|
727
|
+
const key = `${keyPrefix}:${index}`;
|
|
728
|
+
if (token.startsWith("[")) {
|
|
729
|
+
const parts = /^\[([^\]]+)\]\(([^\s)]+)\)$/u.exec(token);
|
|
730
|
+
const href = parts?.[2] === void 0 ? void 0 : safeHref(parts[2]);
|
|
731
|
+
nodes.push(href === void 0 ? token : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
732
|
+
href,
|
|
733
|
+
target: "_blank",
|
|
734
|
+
rel: "noreferrer",
|
|
735
|
+
children: parts?.[1]
|
|
736
|
+
}, key));
|
|
737
|
+
} else if (token.startsWith("`")) nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: token.slice(1, -1) }, key));
|
|
738
|
+
else if (token.startsWith("**")) nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: token.slice(2, -2) }, key));
|
|
739
|
+
else nodes.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", { children: token.slice(1, -1) }, key));
|
|
740
|
+
offset = index + token.length;
|
|
741
|
+
}
|
|
742
|
+
if (offset < text.length) nodes.push(text.slice(offset));
|
|
743
|
+
return nodes;
|
|
744
|
+
}
|
|
745
|
+
/** Deliberately small, HTML-free Markdown renderer for assistant-visible text. */
|
|
746
|
+
function Markdown({ text }) {
|
|
747
|
+
const lines = text.replace(/\r\n?/gu, "\n").split("\n");
|
|
748
|
+
const blocks = [];
|
|
749
|
+
for (let index = 0; index < lines.length;) {
|
|
750
|
+
const line = lines[index] ?? "";
|
|
751
|
+
if (line.startsWith("```")) {
|
|
752
|
+
const language = line.slice(3).trim();
|
|
753
|
+
const code = [];
|
|
754
|
+
index += 1;
|
|
755
|
+
while (index < lines.length && !(lines[index] ?? "").startsWith("```")) {
|
|
756
|
+
code.push(lines[index] ?? "");
|
|
757
|
+
index += 1;
|
|
758
|
+
}
|
|
759
|
+
if (index < lines.length) index += 1;
|
|
760
|
+
blocks.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
761
|
+
"data-language": language || void 0,
|
|
762
|
+
children: code.join("\n")
|
|
763
|
+
}) }, `code:${index}`));
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const heading = /^(#{1,3})\s+(.+)$/u.exec(line);
|
|
767
|
+
if (heading !== null) {
|
|
768
|
+
const content = inline(heading[2] ?? "", `heading:${index}`);
|
|
769
|
+
const key = `heading:${index}`;
|
|
770
|
+
blocks.push(heading[1]?.length === 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: content }, key) : heading[1]?.length === 2 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: content }, key) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: content }, key));
|
|
771
|
+
index += 1;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (/^[-*]\s+/u.test(line)) {
|
|
775
|
+
const items = [];
|
|
776
|
+
while (index < lines.length && /^[-*]\s+/u.test(lines[index] ?? "")) {
|
|
777
|
+
const content = (lines[index] ?? "").replace(/^[-*]\s+/u, "");
|
|
778
|
+
items.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: inline(content, `item:${index}`) }, `item:${index}`));
|
|
779
|
+
index += 1;
|
|
780
|
+
}
|
|
781
|
+
blocks.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: items }, `list:${index}`));
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (line.startsWith("> ")) {
|
|
785
|
+
blocks.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("blockquote", { children: inline(line.slice(2), `quote:${index}`) }, `quote:${index}`));
|
|
786
|
+
index += 1;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (line.trim() === "") {
|
|
790
|
+
index += 1;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const paragraph = [line];
|
|
794
|
+
index += 1;
|
|
795
|
+
while (index < lines.length && (lines[index] ?? "").trim() !== "" && !/^(#{1,3})\s+|^```|^[-*]\s+|^>\s/u.test(lines[index] ?? "")) {
|
|
796
|
+
paragraph.push(lines[index] ?? "");
|
|
797
|
+
index += 1;
|
|
798
|
+
}
|
|
799
|
+
blocks.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: paragraph.map((part, partIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [partIndex === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}), inline(part, `paragraph:${index}:${partIndex}`)] }, `line:${partIndex}`)) }, `paragraph:${index}`));
|
|
800
|
+
}
|
|
801
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: blocks });
|
|
802
|
+
}
|
|
803
|
+
//#endregion
|
|
804
|
+
//#region src/client/components/QaMessage.tsx
|
|
805
|
+
function QaMessage({ message, renderMarkdown, showTimestamp }) {
|
|
806
|
+
const label = message.role === "assistant" ? "Assistant" : message.role === "user" ? "You" : "Status";
|
|
807
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
|
|
808
|
+
className: `dsh-qa-message dsh-qa-message--${message.role}`,
|
|
809
|
+
"data-status": message.status,
|
|
810
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
811
|
+
className: "dsh-qa-message__meta",
|
|
812
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), showTimestamp && message.timestamp !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
|
|
813
|
+
dateTime: new Date(message.timestamp).toISOString(),
|
|
814
|
+
children: new Date(message.timestamp).toLocaleTimeString([], {
|
|
815
|
+
hour: "2-digit",
|
|
816
|
+
minute: "2-digit"
|
|
817
|
+
})
|
|
818
|
+
}) : null]
|
|
819
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
820
|
+
className: "dsh-qa-message__content",
|
|
821
|
+
children: [message.role === "assistant" && renderMarkdown ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Markdown, { text: message.text }) : message.text, message.status === "streaming" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
822
|
+
className: "dsh-qa-message__cursor",
|
|
823
|
+
"aria-hidden": "true"
|
|
824
|
+
}) : null]
|
|
825
|
+
})]
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/client/QaSurface.tsx
|
|
830
|
+
const INACTIVE_STATE = Object.freeze({
|
|
831
|
+
phase: "idle",
|
|
832
|
+
sessionId: null,
|
|
833
|
+
messages: Object.freeze([]),
|
|
834
|
+
error: null,
|
|
835
|
+
canSend: false,
|
|
836
|
+
canStop: false
|
|
837
|
+
});
|
|
838
|
+
const noopSubscribe = () => () => void 0;
|
|
839
|
+
function focusable(root) {
|
|
840
|
+
return [...root.querySelectorAll("button:not([disabled]), textarea:not([disabled]), a[href], [tabindex]:not([tabindex='-1'])")].filter((element) => !element.hidden);
|
|
841
|
+
}
|
|
842
|
+
function trapKeys(event) {
|
|
843
|
+
event.stopPropagation();
|
|
844
|
+
if (event.key !== "Tab") return;
|
|
845
|
+
const items = focusable(event.currentTarget);
|
|
846
|
+
if (items.length === 0) {
|
|
847
|
+
event.preventDefault();
|
|
848
|
+
event.currentTarget.focus();
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const first = items[0];
|
|
852
|
+
const last = items.at(-1);
|
|
853
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
854
|
+
event.preventDefault();
|
|
855
|
+
last?.focus();
|
|
856
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
857
|
+
event.preventDefault();
|
|
858
|
+
first?.focus();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function statusText(state) {
|
|
862
|
+
if (state.phase === "creating") return "Connecting…";
|
|
863
|
+
if (state.phase === "reconnecting") return "Connection lost. Reconnecting…";
|
|
864
|
+
if (state.phase === "running") return "Assistant is responding…";
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
function QaSurface(props) {
|
|
868
|
+
const route = (0, react.useSyncExternalStore)(props.route.subscribe, props.route.getSnapshot, props.route.getSnapshot);
|
|
869
|
+
const config = (0, react.useSyncExternalStore)(props.config.subscribe, props.config.getSnapshot, props.config.getSnapshot).config;
|
|
870
|
+
const [controller, setController] = (0, react.useState)();
|
|
871
|
+
const transcript = (0, react.useRef)(null);
|
|
872
|
+
const nearBottom = (0, react.useRef)(true);
|
|
873
|
+
(0, react.useEffect)(() => {
|
|
874
|
+
if (!route.active) {
|
|
875
|
+
setController(void 0);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const next = new QaSessionController({
|
|
879
|
+
sessions: props.sessions,
|
|
880
|
+
api: props.api,
|
|
881
|
+
connection: props.connection,
|
|
882
|
+
config,
|
|
883
|
+
storage: window.localStorage
|
|
884
|
+
});
|
|
885
|
+
setController(next);
|
|
886
|
+
next.ensureSession();
|
|
887
|
+
return () => next.dispose();
|
|
888
|
+
}, [
|
|
889
|
+
config,
|
|
890
|
+
props.api,
|
|
891
|
+
props.connection,
|
|
892
|
+
props.sessions,
|
|
893
|
+
route.active
|
|
894
|
+
]);
|
|
895
|
+
const state = (0, react.useSyncExternalStore)(controller?.subscribe ?? noopSubscribe, controller?.getSnapshot ?? (() => INACTIVE_STATE), controller?.getSnapshot ?? (() => INACTIVE_STATE));
|
|
896
|
+
(0, react.useEffect)(() => {
|
|
897
|
+
if (!route.active) return;
|
|
898
|
+
const previousBodyOverflow = document.body.style.overflow;
|
|
899
|
+
const previousHtmlOverflow = document.documentElement.style.overflow;
|
|
900
|
+
document.body.dataset.dshQaSurface = "active";
|
|
901
|
+
document.body.style.overflow = "hidden";
|
|
902
|
+
document.documentElement.style.overflow = "hidden";
|
|
903
|
+
const frame = requestAnimationFrame(() => {
|
|
904
|
+
document.querySelector("#dsh-qa-prompt")?.focus();
|
|
905
|
+
});
|
|
906
|
+
return () => {
|
|
907
|
+
cancelAnimationFrame(frame);
|
|
908
|
+
delete document.body.dataset.dshQaSurface;
|
|
909
|
+
document.body.style.overflow = previousBodyOverflow;
|
|
910
|
+
document.documentElement.style.overflow = previousHtmlOverflow;
|
|
911
|
+
};
|
|
912
|
+
}, [route.active]);
|
|
913
|
+
(0, react.useLayoutEffect)(() => {
|
|
914
|
+
const element = transcript.current;
|
|
915
|
+
if (element !== null && nearBottom.current) element.scrollTop = element.scrollHeight;
|
|
916
|
+
}, [state.messages]);
|
|
917
|
+
if (!route.active) return null;
|
|
918
|
+
const status = statusText(state);
|
|
919
|
+
const empty = state.messages.length === 0;
|
|
920
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
921
|
+
className: "dsh-qa-surface",
|
|
922
|
+
"aria-label": config.branding.title,
|
|
923
|
+
tabIndex: -1,
|
|
924
|
+
onKeyDown: trapKeys,
|
|
925
|
+
children: [
|
|
926
|
+
config.ui.showHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
|
|
927
|
+
className: "dsh-qa-header",
|
|
928
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
929
|
+
className: "dsh-qa-header__inner",
|
|
930
|
+
style: { maxWidth: config.ui.maxContentWidth },
|
|
931
|
+
children: [
|
|
932
|
+
config.branding.logoUrl === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
933
|
+
className: "dsh-qa-header__logo",
|
|
934
|
+
src: config.branding.logoUrl,
|
|
935
|
+
alt: ""
|
|
936
|
+
}),
|
|
937
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
938
|
+
className: "dsh-qa-header__text",
|
|
939
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: config.branding.title }), config.branding.subtitle === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: config.branding.subtitle })]
|
|
940
|
+
}),
|
|
941
|
+
config.ui.showReset && config.session.policy !== "fixed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
942
|
+
type: "button",
|
|
943
|
+
className: "dsh-qa-button dsh-qa-button--secondary",
|
|
944
|
+
disabled: controller === void 0 || state.phase === "creating",
|
|
945
|
+
onClick: () => void controller?.reset(),
|
|
946
|
+
children: "New chat"
|
|
947
|
+
}) : null
|
|
948
|
+
]
|
|
949
|
+
})
|
|
950
|
+
}) : null,
|
|
951
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
952
|
+
ref: transcript,
|
|
953
|
+
className: "dsh-qa-transcript",
|
|
954
|
+
onScroll: (event) => {
|
|
955
|
+
const element = event.currentTarget;
|
|
956
|
+
nearBottom.current = element.scrollHeight - element.scrollTop - element.clientHeight < 96;
|
|
957
|
+
},
|
|
958
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
959
|
+
className: "dsh-qa-transcript__inner",
|
|
960
|
+
style: { maxWidth: config.ui.maxContentWidth },
|
|
961
|
+
children: [empty ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
962
|
+
className: "dsh-qa-welcome",
|
|
963
|
+
"aria-labelledby": "dsh-qa-welcome-title",
|
|
964
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
965
|
+
id: "dsh-qa-welcome-title",
|
|
966
|
+
children: config.branding.welcomeMessage
|
|
967
|
+
}), config.suggestedQuestions.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
968
|
+
className: "dsh-qa-suggestions",
|
|
969
|
+
"aria-label": "Suggested questions",
|
|
970
|
+
children: config.suggestedQuestions.map((question) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
971
|
+
type: "button",
|
|
972
|
+
disabled: !state.canSend,
|
|
973
|
+
onClick: () => void controller?.send(question),
|
|
974
|
+
children: question
|
|
975
|
+
}, question))
|
|
976
|
+
}) : null]
|
|
977
|
+
}) : state.messages.map((message) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QaMessage, {
|
|
978
|
+
message,
|
|
979
|
+
renderMarkdown: config.ui.renderMarkdown,
|
|
980
|
+
showTimestamp: config.ui.showTimestamps
|
|
981
|
+
}, message.id)), state.error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
982
|
+
className: "dsh-qa-error",
|
|
983
|
+
role: "alert",
|
|
984
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: state.error }), state.phase === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
985
|
+
type: "button",
|
|
986
|
+
onClick: () => void controller?.ensureSession(),
|
|
987
|
+
children: "Retry"
|
|
988
|
+
}) : null]
|
|
989
|
+
})]
|
|
990
|
+
})
|
|
991
|
+
}),
|
|
992
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
|
|
993
|
+
className: "dsh-qa-footer",
|
|
994
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
995
|
+
className: "dsh-qa-footer__inner",
|
|
996
|
+
style: { maxWidth: config.ui.maxContentWidth },
|
|
997
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
998
|
+
className: "dsh-qa-status",
|
|
999
|
+
"aria-live": "polite",
|
|
1000
|
+
"aria-atomic": "true",
|
|
1001
|
+
children: status
|
|
1002
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QaComposer, {
|
|
1003
|
+
placeholder: config.branding.placeholder,
|
|
1004
|
+
canSend: state.canSend,
|
|
1005
|
+
canStop: state.canStop,
|
|
1006
|
+
running: state.phase === "running",
|
|
1007
|
+
showStop: config.ui.showStop,
|
|
1008
|
+
onSend: (text) => controller?.send(text) ?? Promise.resolve(false),
|
|
1009
|
+
onStop: () => controller?.stop() ?? Promise.resolve()
|
|
1010
|
+
})]
|
|
1011
|
+
})
|
|
1012
|
+
})
|
|
1013
|
+
]
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region src/client/styles.ts
|
|
1018
|
+
const QA_SURFACE_STYLES = String.raw`
|
|
1019
|
+
.dsh-qa-surface{position:fixed;inset:0;z-index:2147483000;pointer-events:auto;display:flex;flex-direction:column;min-width:0;height:100vh;height:100dvh;overflow:hidden;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-family:inherit}
|
|
1020
|
+
.dsh-qa-surface *{box-sizing:border-box}
|
|
1021
|
+
.dsh-qa-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
1022
|
+
.dsh-qa-header{flex:none;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2)}
|
|
1023
|
+
.dsh-qa-header__inner,.dsh-qa-footer__inner,.dsh-qa-transcript__inner{width:100%;margin:0 auto}
|
|
1024
|
+
.dsh-qa-header__inner{min-height:72px;display:flex;align-items:center;gap:12px;padding:12px 24px}
|
|
1025
|
+
.dsh-qa-header__logo{width:36px;height:36px;object-fit:contain;border-radius:9px;flex:none}
|
|
1026
|
+
.dsh-qa-header__text{flex:1;min-width:0}
|
|
1027
|
+
.dsh-qa-header h1{margin:0;color:var(--dsw-alias-label-primary);font-size:17px;font-weight:650;line-height:1.35}
|
|
1028
|
+
.dsh-qa-header p{margin:2px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.4}
|
|
1029
|
+
.dsh-qa-transcript{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable}
|
|
1030
|
+
.dsh-qa-transcript__inner{min-height:100%;padding:32px 24px 24px}
|
|
1031
|
+
.dsh-qa-welcome{min-height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:40px 0}
|
|
1032
|
+
.dsh-qa-welcome h2{max-width:620px;margin:0;color:var(--dsw-alias-label-primary);font-size:clamp(24px,4vw,34px);font-weight:600;line-height:1.25}
|
|
1033
|
+
.dsh-qa-suggestions{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:24px}
|
|
1034
|
+
.dsh-qa-suggestions button{max-width:360px;padding:9px 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;line-height:1.35;cursor:pointer}
|
|
1035
|
+
.dsh-qa-suggestions button:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed);color:var(--dsw-alias-label-primary)}
|
|
1036
|
+
.dsh-qa-suggestions button:disabled{opacity:.5;cursor:default}
|
|
1037
|
+
.dsh-qa-message{display:flex;flex-direction:column;max-width:min(82%,720px);margin:0 0 24px}
|
|
1038
|
+
.dsh-qa-message--user{align-items:flex-end;margin-left:auto}
|
|
1039
|
+
.dsh-qa-message--assistant{align-items:flex-start;margin-right:auto}
|
|
1040
|
+
.dsh-qa-message--system{align-items:center;max-width:100%;margin-left:auto;margin-right:auto}
|
|
1041
|
+
.dsh-qa-message__meta{display:flex;gap:8px;margin:0 4px 6px;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:600;line-height:1.3}
|
|
1042
|
+
.dsh-qa-message__content{max-width:100%;white-space:pre-wrap;overflow-wrap:anywhere;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;padding:11px 14px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-size:15px;line-height:1.62}
|
|
1043
|
+
.dsh-qa-message--user .dsh-qa-message__content{background:var(--dsw-alias-bg-module-platform)}
|
|
1044
|
+
.dsh-qa-message--system .dsh-qa-message__content{padding:7px 11px;border-radius:9px;color:var(--dsw-alias-label-secondary);font-size:13px}
|
|
1045
|
+
.dsh-qa-message[data-status="error"] .dsh-qa-message__content{border-color:var(--dsw-alias-status-error)}
|
|
1046
|
+
.dsh-qa-message__content p,.dsh-qa-message__content ul,.dsh-qa-message__content blockquote,.dsh-qa-message__content pre{margin:0 0 10px}
|
|
1047
|
+
.dsh-qa-message__content :last-child{margin-bottom:0}
|
|
1048
|
+
.dsh-qa-message__content h2,.dsh-qa-message__content h3,.dsh-qa-message__content h4{margin:14px 0 7px;line-height:1.35}
|
|
1049
|
+
.dsh-qa-message__content h2{font-size:18px}.dsh-qa-message__content h3{font-size:16px}.dsh-qa-message__content h4{font-size:15px}
|
|
1050
|
+
.dsh-qa-message__content ul{padding-left:22px}.dsh-qa-message__content a{color:var(--dsw-alias-brand-primary)}
|
|
1051
|
+
.dsh-qa-message__content code{border-radius:5px;padding:1px 4px;background:var(--dsw-alias-bg-layer-2);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:.9em}
|
|
1052
|
+
.dsh-qa-message__content pre{overflow-x:auto;border-radius:9px;padding:12px;background:var(--dsw-alias-bg-layer-2)}
|
|
1053
|
+
.dsh-qa-message__content pre code{padding:0;background:transparent}
|
|
1054
|
+
.dsh-qa-message__content blockquote{border-left:3px solid var(--dsw-alias-border-l2);padding-left:12px;color:var(--dsw-alias-label-secondary)}
|
|
1055
|
+
.dsh-qa-message__cursor{display:inline-block;width:7px;height:1em;margin-left:3px;vertical-align:-2px;background:var(--dsw-alias-label-secondary);animation:dsh-qa-blink 1s steps(2,start) infinite}
|
|
1056
|
+
@keyframes dsh-qa-blink{50%{opacity:0}}
|
|
1057
|
+
.dsh-qa-error{display:flex;align-items:center;justify-content:center;gap:10px;margin:16px auto;padding:10px 12px;border:1px solid var(--dsw-alias-status-error);border-radius:10px;color:var(--dsw-alias-label-primary);font-size:13px}
|
|
1058
|
+
.dsh-qa-error button{border:0;background:transparent;color:var(--dsw-alias-brand-primary);font:inherit;font-weight:600;cursor:pointer}
|
|
1059
|
+
.dsh-qa-footer{flex:none;border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);padding:0 24px max(14px,env(safe-area-inset-bottom))}
|
|
1060
|
+
.dsh-qa-footer__inner{position:relative;padding-top:14px}
|
|
1061
|
+
.dsh-qa-status{min-height:18px;margin-bottom:5px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
1062
|
+
.dsh-qa-composer{display:flex;align-items:flex-end;gap:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;padding:7px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s}
|
|
1063
|
+
.dsh-qa-composer:focus-within{border-color:var(--dsw-alias-brand-primary)}
|
|
1064
|
+
.dsh-qa-composer textarea{flex:1;min-width:0;max-height:160px;resize:none;overflow-y:auto;border:0;outline:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:15px;line-height:22px;padding:7px 9px}
|
|
1065
|
+
.dsh-qa-composer textarea::placeholder{color:var(--dsw-alias-label-tertiary)}
|
|
1066
|
+
.dsh-qa-button{appearance:none;flex:none;border:1px solid transparent;border-radius:9px;padding:8px 13px;font:inherit;font-size:13px;font-weight:600;line-height:18px;cursor:pointer}
|
|
1067
|
+
.dsh-qa-button:focus-visible,.dsh-qa-suggestions button:focus-visible,.dsh-qa-error button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
1068
|
+
.dsh-qa-button:disabled{opacity:.45;cursor:default}
|
|
1069
|
+
.dsh-qa-button--send{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-on-primary)}
|
|
1070
|
+
.dsh-qa-button--stop,.dsh-qa-button--secondary{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}
|
|
1071
|
+
@media (max-width:600px){.dsh-qa-header__inner{min-height:60px;padding:9px 14px}.dsh-qa-header__logo{width:32px;height:32px}.dsh-qa-header p{display:none}.dsh-qa-transcript__inner{padding:22px 14px 16px}.dsh-qa-message{max-width:90%;margin-bottom:18px}.dsh-qa-message__content{font-size:14px;padding:10px 12px}.dsh-qa-footer{padding-left:10px;padding-right:10px}.dsh-qa-button{padding-left:11px;padding-right:11px}}
|
|
1072
|
+
@media (max-height:480px) and (orientation:landscape){.dsh-qa-header__inner{min-height:48px;padding-top:6px;padding-bottom:6px}.dsh-qa-header p{display:none}.dsh-qa-transcript__inner{padding-top:14px}.dsh-qa-footer__inner{padding-top:7px}.dsh-qa-status{position:absolute;right:0;top:-18px}}
|
|
1073
|
+
@media (prefers-reduced-motion:reduce){.dsh-qa-message__cursor{animation:none}}
|
|
1074
|
+
`;
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/client/index.tsx
|
|
1077
|
+
const SETTINGS_NAMESPACE = "qa-surface";
|
|
1078
|
+
const inject = [
|
|
1079
|
+
"slots",
|
|
1080
|
+
"sessions",
|
|
1081
|
+
"connection",
|
|
1082
|
+
"settingsScope"
|
|
1083
|
+
];
|
|
1084
|
+
/** Register the route-aware, full-frame QA entry in the additive overlay slot. */
|
|
1085
|
+
function apply(ctx) {
|
|
1086
|
+
const route = new QaRouteController();
|
|
1087
|
+
const config = new QaConfigController(ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE }));
|
|
1088
|
+
const syncRoute = () => {
|
|
1089
|
+
const snapshot = config.getSnapshot();
|
|
1090
|
+
route.configure(snapshot.config, snapshot.status === "ready" || snapshot.status === "unavailable");
|
|
1091
|
+
};
|
|
1092
|
+
syncRoute();
|
|
1093
|
+
const unsubscribeConfig = config.subscribe(syncRoute);
|
|
1094
|
+
ctx.effect(() => {
|
|
1095
|
+
const style = document.createElement("style");
|
|
1096
|
+
style.dataset.dshQaSurface = "styles";
|
|
1097
|
+
style.textContent = QA_SURFACE_STYLES;
|
|
1098
|
+
document.head.append(style);
|
|
1099
|
+
return () => style.remove();
|
|
1100
|
+
}, "dsh-qa-surface: styles");
|
|
1101
|
+
ctx.effect(() => () => {
|
|
1102
|
+
unsubscribeConfig();
|
|
1103
|
+
config.dispose();
|
|
1104
|
+
route.dispose();
|
|
1105
|
+
}, "dsh-qa-surface: controllers");
|
|
1106
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
1107
|
+
name: "shell.overlay",
|
|
1108
|
+
id: "dsh-qa-surface",
|
|
1109
|
+
order: -1e4,
|
|
1110
|
+
inject: () => ({
|
|
1111
|
+
route,
|
|
1112
|
+
config,
|
|
1113
|
+
sessions: ctx.sessions,
|
|
1114
|
+
api: ctx.connection.api.sessions,
|
|
1115
|
+
connection: ctx.connection.hostDescription
|
|
1116
|
+
})
|
|
1117
|
+
}, QaSurface));
|
|
1118
|
+
}
|
|
1119
|
+
//#endregion
|
|
1120
|
+
exports.QaConfigController = QaConfigController;
|
|
1121
|
+
exports.QaRouteController = QaRouteController;
|
|
1122
|
+
exports.QaSessionController = QaSessionController;
|
|
1123
|
+
exports.apply = apply;
|
|
1124
|
+
exports.inject = inject;
|
|
1125
|
+
exports.matchesQaRoute = matchesQaRoute;
|
|
1126
|
+
exports.projectTranscript = projectTranscript;
|
|
1127
|
+
return module.exports;
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
//# sourceMappingURL=client.js.map
|