@jskit-ai/assistant-core 0.1.165 → 0.1.166

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.
Files changed (63) hide show
  1. package/examples/conversation/App.vue +104 -0
  2. package/examples/conversation/README.md +42 -0
  3. package/examples/conversation/dist/assets/index-CSTXm6q3.css +1 -0
  4. package/examples/conversation/dist/assets/index-DGgOLzoo.js +30 -0
  5. package/examples/conversation/dist/index.html +4 -0
  6. package/examples/conversation/index.html +2 -0
  7. package/examples/conversation/main.js +9 -0
  8. package/examples/conversation/node_modules/.vite/deps/@mdi_js.js +7450 -0
  9. package/examples/conversation/node_modules/.vite/deps/@mdi_js.js.map +1 -0
  10. package/examples/conversation/node_modules/.vite/deps/VTooltip-jIxO6ULs.js +2311 -0
  11. package/examples/conversation/node_modules/.vite/deps/VTooltip-jIxO6ULs.js.map +1 -0
  12. package/examples/conversation/node_modules/.vite/deps/_metadata.json +70 -0
  13. package/examples/conversation/node_modules/.vite/deps/defineComponent-DK6LSjDW.js +710 -0
  14. package/examples/conversation/node_modules/.vite/deps/defineComponent-DK6LSjDW.js.map +1 -0
  15. package/examples/conversation/node_modules/.vite/deps/hotkey-parsing-DnoZxtbW.js +1214 -0
  16. package/examples/conversation/node_modules/.vite/deps/hotkey-parsing-DnoZxtbW.js.map +1 -0
  17. package/examples/conversation/node_modules/.vite/deps/icons-ze2hmg7j.js +98 -0
  18. package/examples/conversation/node_modules/.vite/deps/icons-ze2hmg7j.js.map +1 -0
  19. package/examples/conversation/node_modules/.vite/deps/package.json +3 -0
  20. package/examples/conversation/node_modules/.vite/deps/theme-D78LVcJg.js +1301 -0
  21. package/examples/conversation/node_modules/.vite/deps/theme-D78LVcJg.js.map +1 -0
  22. package/examples/conversation/node_modules/.vite/deps/vue.js +2 -0
  23. package/examples/conversation/node_modules/.vite/deps/vue.runtime.esm-bundler-YcakWsDn.js +8538 -0
  24. package/examples/conversation/node_modules/.vite/deps/vue.runtime.esm-bundler-YcakWsDn.js.map +1 -0
  25. package/examples/conversation/node_modules/.vite/deps/vuetify.js +413 -0
  26. package/examples/conversation/node_modules/.vite/deps/vuetify.js.map +1 -0
  27. package/examples/conversation/node_modules/.vite/deps/vuetify_components.js +25370 -0
  28. package/examples/conversation/node_modules/.vite/deps/vuetify_components.js.map +1 -0
  29. package/examples/conversation/node_modules/.vite/deps/vuetify_directives.js +132 -0
  30. package/examples/conversation/node_modules/.vite/deps/vuetify_directives.js.map +1 -0
  31. package/examples/conversation/node_modules/.vite/deps/vuetify_iconsets_mdi-svg.js +70 -0
  32. package/examples/conversation/node_modules/.vite/deps/vuetify_iconsets_mdi-svg.js.map +1 -0
  33. package/examples/conversation/package.json +22 -0
  34. package/examples/conversation/server.js +123 -0
  35. package/examples/conversation/vite.config.js +8 -0
  36. package/package.json +59 -7
  37. package/src/client/conversation/AssistantComposerActions.vue +43 -0
  38. package/src/client/conversation/AssistantConversationElement.vue +78 -0
  39. package/src/client/conversation/AssistantProgress.vue +88 -0
  40. package/src/client/conversation/AssistantPromptInput.vue +467 -0
  41. package/src/client/conversation/AssistantTranscript.vue +1361 -0
  42. package/src/client/conversation/LongTextInlineParts.vue +64 -0
  43. package/src/client/conversation/LongTextPreviewBlocks.vue +324 -0
  44. package/src/client/conversation/index.js +9 -0
  45. package/src/client/conversation/submitText.js +29 -0
  46. package/src/client/conversation/useScrollToBottom.js +151 -0
  47. package/src/server/conversation/codexClient.js +328 -0
  48. package/src/server/conversation/codexEvents.js +623 -0
  49. package/src/server/conversation/codexTurn.js +280 -0
  50. package/src/server/conversation/index.js +2 -0
  51. package/src/server/conversation/memoryStorage.js +76 -0
  52. package/src/server/conversation/normalize.js +7 -0
  53. package/src/server/conversation/openCodeClient.js +512 -0
  54. package/src/server/conversation/transcript.js +92 -0
  55. package/src/shared/conversation/answerChoiceSugar.js +191 -0
  56. package/src/shared/conversation/index.js +6 -0
  57. package/src/shared/conversation/longTextBlocks.js +403 -0
  58. package/src/shared/conversation/numberedQuestionSugar.js +295 -0
  59. package/src/shared/conversation/scrollFollowState.js +32 -0
  60. package/src/shared/conversation/thinkingText.js +13 -0
  61. package/src/shared/conversation/turns.js +30 -0
  62. package/src/testing/conversationStorageContract.js +35 -0
  63. package/test/conversation.test.js +57 -0
@@ -0,0 +1,1361 @@
1
+ <template>
2
+ <section
3
+ v-if="visible"
4
+ class="assistant-transcript"
5
+ :class="`assistant-transcript--${variant}`"
6
+ aria-label="Conversation history"
7
+ >
8
+ <v-btn
9
+ v-if="reloadable"
10
+ :aria-label="reloading ? 'Reloading chat' : 'Reload chat'"
11
+ class="assistant-transcript__reload"
12
+ :disabled="reloading"
13
+ :icon="mdiRefresh"
14
+ size="x-small"
15
+ :title="reloading ? 'Reloading chat' : 'Reload chat'"
16
+ type="button"
17
+ variant="text"
18
+ @click="emit('reload')"
19
+ />
20
+
21
+ <v-skeleton-loader
22
+ v-if="loadingIndicatorVisible && !reloadable"
23
+ aria-label="Loading conversation"
24
+ class="assistant-transcript__loading-skeleton"
25
+ type="list-item-avatar-two-line@3"
26
+ />
27
+
28
+ <div
29
+ v-if="initialScrollPending"
30
+ class="assistant-transcript__settling"
31
+ aria-hidden="true"
32
+ >
33
+ <v-skeleton-loader
34
+ aria-label="Preparing conversation"
35
+ class="assistant-transcript__settling-skeleton"
36
+ type="list-item-avatar-two-line@3"
37
+ />
38
+ </div>
39
+
40
+ <v-alert
41
+ v-if="error"
42
+ density="compact"
43
+ type="warning"
44
+ variant="tonal"
45
+ >
46
+ {{ error }}
47
+ </v-alert>
48
+
49
+ <div
50
+ v-else
51
+ ref="bodyElement"
52
+ aria-label="Conversation messages"
53
+ class="assistant-transcript__body"
54
+ :class="{ 'assistant-transcript__body--settling': initialScrollPending }"
55
+ tabindex="0"
56
+ @keydown.capture="markKeyboardScrollIntent"
57
+ @pointerdown.self="markUserScrollIntent"
58
+ @scroll.passive="updateLatestFollowFromScroll"
59
+ @touchmove.passive="markUserScrollIntent"
60
+ @wheel.passive="markUserScrollIntent"
61
+ >
62
+ <div
63
+ v-if="welcomeMessage"
64
+ class="assistant-transcript__welcome"
65
+ role="status"
66
+ >
67
+ <div class="assistant-transcript__assistant-header">
68
+ <span class="assistant-transcript__avatar assistant-transcript__avatar--assistant">
69
+ <v-icon :icon="mdiRobotOutline" size="16" />
70
+ </span>
71
+ <div class="assistant-transcript__message-header">
72
+ <span>{{ assistantLabel }}</span>
73
+ </div>
74
+ </div>
75
+ <p>{{ welcomeMessage }}</p>
76
+ </div>
77
+
78
+ <div
79
+ v-if="hasMoreBefore || loadingMore || loadMoreError"
80
+ class="assistant-transcript__load-more"
81
+ >
82
+ <v-btn
83
+ color="primary"
84
+ :disabled="loadingMore || !hasMoreBefore"
85
+ size="x-small"
86
+ type="button"
87
+ variant="tonal"
88
+ @click="requestLoadMore"
89
+ >
90
+ {{ loadingMore ? "Loading older messages…" : "Load older messages" }}
91
+ </v-btn>
92
+ <div
93
+ v-if="loadMoreError"
94
+ class="assistant-transcript__load-more-error"
95
+ >
96
+ {{ loadMoreError }}
97
+ </div>
98
+ </div>
99
+
100
+ <article
101
+ v-for="turn in displayTurns"
102
+ :key="turn.turnId"
103
+ class="assistant-transcript__turn"
104
+ >
105
+ <div
106
+ v-if="turn.system"
107
+ class="assistant-transcript__system"
108
+ >
109
+ <v-icon
110
+ class="assistant-transcript__system-icon"
111
+ :icon="mdiInformationOutline"
112
+ size="15"
113
+ />
114
+ <div class="assistant-transcript__system-body">
115
+ <div class="assistant-transcript__system-meta">
116
+ <span>{{ systemLabel }}</span>
117
+ <time v-if="turn.system.displayAt">{{ turn.system.displayAt }}</time>
118
+ </div>
119
+ <slot name="system-message" :message="turn.system"><LongTextPreviewBlocks compact :blocks="turn.system.blocks" @link-click="handleLongTextLinkClick" /></slot>
120
+ </div>
121
+ </div>
122
+
123
+ <div
124
+ v-if="turn.user"
125
+ class="assistant-transcript__message-row assistant-transcript__message-row--user"
126
+ >
127
+ <div class="assistant-transcript__message assistant-transcript__message--user">
128
+ <div
129
+ class="assistant-transcript__user-content"
130
+ >
131
+ <p v-if="userMessageFormat === 'plain'" class="assistant-transcript__plain-user">{{ userMessageExpanded(turn) ? turn.user.text : userMessagePreviewText(turn.user.text) || turn.user.text }}</p>
132
+ <LongTextPreviewBlocks
133
+ v-else
134
+ :blocks="userMessageExpanded(turn) ? turn.user.blocks : (turn.user.previewBlocks || turn.user.blocks)"
135
+ @link-click="handleLongTextLinkClick"
136
+ />
137
+ </div>
138
+ <button
139
+ v-if="userMessageCollapsible(turn.user)"
140
+ :aria-expanded="userMessageExpanded(turn)"
141
+ class="assistant-transcript__user-content-toggle"
142
+ type="button"
143
+ @click="toggleUserMessage(turn)"
144
+ >
145
+ {{ userMessageExpanded(turn) ? "Show less" : "Read more" }}
146
+ </button>
147
+ <slot name="attachments" :items="turn.user.attachments" :message="turn.user" />
148
+ <div
149
+ v-if="turn.user.displayAt"
150
+ class="assistant-transcript__message-footer assistant-transcript__message-footer--user"
151
+ >
152
+ <time v-if="turn.user.displayAt">{{ turn.user.displayAt }}</time>
153
+ </div>
154
+ <div
155
+ v-if="turn.optimistic?.status === 'failed'"
156
+ class="assistant-transcript__optimistic-failure"
157
+ >
158
+ <span>{{ turn.optimistic.error || "Message could not be sent." }}</span>
159
+ <div class="assistant-transcript__optimistic-actions">
160
+ <v-btn
161
+ color="primary"
162
+ size="x-small"
163
+ type="button"
164
+ variant="tonal"
165
+ @click="emit('resend-turn', turn.optimistic.id)"
166
+ >
167
+ Resend
168
+ </v-btn>
169
+ <v-btn
170
+ size="x-small"
171
+ type="button"
172
+ variant="text"
173
+ @click="emit('cancel-turn', turn.optimistic.id)"
174
+ >
175
+ Cancel
176
+ </v-btn>
177
+ <v-btn
178
+ size="x-small"
179
+ type="button"
180
+ variant="text"
181
+ @click="emit('edit-turn', turn.optimistic.id)"
182
+ >
183
+ Edit
184
+ </v-btn>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ <span class="assistant-transcript__avatar assistant-transcript__avatar--user">
189
+ <v-icon :icon="mdiAccountOutline" size="15" />
190
+ </span>
191
+ </div>
192
+
193
+ <template
194
+ v-for="entry in turn.agentTimeline"
195
+ :key="entry.key"
196
+ >
197
+ <AssistantProgress
198
+ v-if="entry.role === 'thinking'"
199
+ :key="`${turn.turnId}:${turn.pending ? 'active' : 'completed'}:${entry.key}`"
200
+ class="assistant-transcript__thinking"
201
+ :messages="entry.messages"
202
+ :pending="turn.pending"
203
+ :preview-limit="progressPreviewLimit"
204
+ :aria-label="`${assistantLabel} progress`"
205
+ />
206
+ <div
207
+ v-else
208
+ class="assistant-transcript__message-row assistant-transcript__message-row--assistant"
209
+ :data-message-role="entry.role"
210
+ >
211
+ <div class="assistant-transcript__assistant-header">
212
+ <span class="assistant-transcript__avatar assistant-transcript__avatar--assistant">
213
+ <v-icon :icon="mdiRobotOutline" size="16" />
214
+ </span>
215
+ <div class="assistant-transcript__message-header">
216
+ <span>{{ assistantLabel }}</span>
217
+ </div>
218
+ </div>
219
+ <div class="assistant-transcript__message assistant-transcript__message--assistant">
220
+ <LongTextPreviewBlocks
221
+ v-if="entry.message.blocks.length"
222
+ :blocks="entry.message.blocks"
223
+ @link-click="handleLongTextLinkClick"
224
+ />
225
+ <ol
226
+ v-if="entry.message.questions.length"
227
+ class="assistant-transcript__questions"
228
+ >
229
+ <li
230
+ v-for="question in entry.message.questions"
231
+ :key="question.name"
232
+ class="assistant-transcript__question"
233
+ >
234
+ <span class="assistant-transcript__question-number">{{ question.number }}</span>
235
+ <div class="assistant-transcript__question-content">
236
+ <span class="assistant-transcript__question-text">
237
+ <LongTextInlineParts
238
+ :text="question.label"
239
+ @link-click="handleLongTextLinkClick"
240
+ />
241
+ </span>
242
+ <ul
243
+ v-if="question.choices.length"
244
+ class="assistant-transcript__question-choices"
245
+ >
246
+ <li v-for="choice in question.choices" :key="choice.value">
247
+ <LongTextInlineParts
248
+ :text="choice.label"
249
+ @link-click="handleLongTextLinkClick"
250
+ /><span v-if="choice.recommended"> · Recommended</span>
251
+ </li>
252
+ </ul>
253
+ </div>
254
+ </li>
255
+ </ol>
256
+ <LongTextPreviewBlocks
257
+ v-if="entry.message.outroBlocks.length"
258
+ :blocks="entry.message.outroBlocks"
259
+ @link-click="handleLongTextLinkClick"
260
+ />
261
+ <slot name="message-actions" :message="entry.message" :turn="turn" />
262
+ </div>
263
+ <div
264
+ v-if="entry.message.displayAt"
265
+ class="assistant-transcript__message-footer assistant-transcript__message-footer--assistant"
266
+ >
267
+ <time>{{ entry.message.displayAt }}</time>
268
+ </div>
269
+ </div>
270
+ </template>
271
+ </article>
272
+
273
+ <div
274
+ ref="bottomElement"
275
+ class="assistant-transcript__bottom"
276
+ aria-hidden="true"
277
+ />
278
+ </div>
279
+ </section>
280
+ </template>
281
+
282
+ <script setup>
283
+ import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
284
+ import {
285
+ mdiAccountOutline,
286
+ mdiInformationOutline,
287
+ mdiRefresh,
288
+ mdiRobotOutline
289
+ } from "@mdi/js";
290
+ import { useScrollToBottom } from "./useScrollToBottom.js";
291
+ import LongTextInlineParts from "./LongTextInlineParts.vue";
292
+ import LongTextPreviewBlocks from "./LongTextPreviewBlocks.vue";
293
+ import AssistantProgress from "./AssistantProgress.vue";
294
+ import { parseNumberedQuestionPrompt } from "../../shared/conversation/numberedQuestionSugar.js";
295
+ import { parseLongTextReviewBlocks } from "../../shared/conversation/longTextBlocks.js";
296
+ import {
297
+ scrollElementNearBottom
298
+ } from "../../shared/conversation/scrollFollowState.js";
299
+ import {
300
+ normalizeThinkingMessageText
301
+ } from "../../shared/conversation/thinkingText.js";
302
+
303
+ const props = defineProps({
304
+ systemLabel: { type: String, default: "Status" },
305
+ progressPreviewLimit: { type: Number, default: 2 },
306
+ userMessageFormat: { type: String, default: "formatted", validator: (value) => ["plain", "formatted"].includes(value) },
307
+ assistantLabel: {
308
+ default: "Assistant",
309
+ type: String
310
+ },
311
+ error: {
312
+ default: "",
313
+ type: String
314
+ },
315
+ followLatestKey: {
316
+ default: 0,
317
+ type: [Number, String]
318
+ },
319
+ hasMoreBefore: {
320
+ default: false,
321
+ type: Boolean
322
+ },
323
+ loading: {
324
+ default: false,
325
+ type: Boolean
326
+ },
327
+ loadingMore: {
328
+ default: false,
329
+ type: Boolean
330
+ },
331
+ loadMoreError: {
332
+ default: "",
333
+ type: String
334
+ },
335
+ reloadable: {
336
+ default: false,
337
+ type: Boolean
338
+ },
339
+ reloading: {
340
+ default: false,
341
+ type: Boolean
342
+ },
343
+ scrollKey: {
344
+ default: "",
345
+ type: [Number, String]
346
+ },
347
+ turns: {
348
+ default: () => [],
349
+ type: Array
350
+ },
351
+ variant: {
352
+ default: "main",
353
+ validator: (value) => ["main", "task"].includes(value),
354
+ type: String
355
+ },
356
+ visible: {
357
+ default: true,
358
+ type: Boolean
359
+ },
360
+ welcomeMessage: {
361
+ default: "",
362
+ type: String
363
+ }
364
+ });
365
+
366
+ const emit = defineEmits(["cancel-turn", "edit-turn", "load-more", "link-click", "reload", "resend-turn"]);
367
+
368
+ const USER_MESSAGE_COLLAPSE_MIN_CHARACTERS = 360;
369
+ const USER_MESSAGE_PREVIEW_MAX_CHARACTERS = 280;
370
+ const USER_MESSAGE_PREVIEW_MAX_LINES = 4;
371
+ const DISPLAY_MESSAGE_CACHE_LIMIT = 500;
372
+ const bodyElement = ref(null);
373
+ const bottomElement = ref(null);
374
+ const expandedUserMessages = ref(new Set());
375
+ const followingLatest = ref(true);
376
+ const initialScrollSettled = ref(false);
377
+ const userScrollIntent = ref(false);
378
+ const displayMessageCache = new Map();
379
+ let liveScrollFrame = 0;
380
+ let userScrollIntentTimer = null;
381
+ let initialScrollVersion = 0;
382
+ let loadMoreScrollSnapshot = null;
383
+ let loadMoreRequestVersion = 0;
384
+ let pendingTailFollow = false;
385
+ const USER_SCROLL_INTENT_RESET_MS = 600;
386
+ const KEYBOARD_SCROLL_KEYS = new Set([
387
+ " ",
388
+ "ArrowDown",
389
+ "ArrowUp",
390
+ "End",
391
+ "Home",
392
+ "PageDown",
393
+ "PageUp",
394
+ "Spacebar"
395
+ ]);
396
+ const timeFormatter = new Intl.DateTimeFormat(undefined, {
397
+ hour: "2-digit",
398
+ minute: "2-digit"
399
+ });
400
+
401
+ function displayTime(value = "") {
402
+ const date = new Date(String(value || ""));
403
+ if (Number.isNaN(date.getTime())) {
404
+ return "";
405
+ }
406
+ return timeFormatter.format(date);
407
+ }
408
+
409
+ function userMessagePreviewText(value = "") {
410
+ const text = String(value || "");
411
+ const characters = [...text];
412
+ const lines = text.split(/\r\n?|\n/u);
413
+ if (
414
+ characters.length <= USER_MESSAGE_COLLAPSE_MIN_CHARACTERS &&
415
+ lines.length <= USER_MESSAGE_PREVIEW_MAX_LINES
416
+ ) {
417
+ return "";
418
+ }
419
+ const firstLines = lines.slice(0, USER_MESSAGE_PREVIEW_MAX_LINES).join("\n");
420
+ const preview = [...firstLines]
421
+ .slice(0, USER_MESSAGE_PREVIEW_MAX_CHARACTERS)
422
+ .join("")
423
+ .trimEnd();
424
+ return preview ? `${preview}…` : "";
425
+ }
426
+
427
+ function displayMessage(message = null, {
428
+ allowNumberedQuestions = false,
429
+ preserveParagraphLineBreaks = false,
430
+ previewUserMessage = false
431
+ } = {}, cacheKey = "") {
432
+ if (!message) {
433
+ return null;
434
+ }
435
+ const normalizedCacheKey = String(cacheKey || "").trim();
436
+ const cached = normalizedCacheKey ? displayMessageCache.get(normalizedCacheKey) : null;
437
+ if (
438
+ cached &&
439
+ cached.allowNumberedQuestions === allowNumberedQuestions &&
440
+ cached.attachments === message.attachments &&
441
+ cached.at === message.at &&
442
+ cached.messageId === message.messageId &&
443
+ cached.preserveParagraphLineBreaks === preserveParagraphLineBreaks &&
444
+ cached.previewUserMessage === previewUserMessage &&
445
+ cached.role === message.role &&
446
+ cached.text === message.text
447
+ ) {
448
+ return { ...message, ...cached.value };
449
+ }
450
+ const messageText = message.text;
451
+ const questionInput = allowNumberedQuestions
452
+ ? parseNumberedQuestionPrompt(messageText)
453
+ : {
454
+ intro: "",
455
+ outro: "",
456
+ questions: []
457
+ };
458
+ const hasQuestions = questionInput.questions.length > 0;
459
+ const previewText = previewUserMessage ? userMessagePreviewText(message.text) : "";
460
+ const value = {
461
+ blocks: parseLongTextReviewBlocks(hasQuestions ? questionInput.intro : messageText, {
462
+ preserveParagraphLineBreaks
463
+ }),
464
+ outroBlocks: parseLongTextReviewBlocks(hasQuestions ? questionInput.outro : "", {
465
+ preserveParagraphLineBreaks
466
+ }),
467
+ previewBlocks: previewText
468
+ ? parseLongTextReviewBlocks(previewText, { preserveParagraphLineBreaks: true })
469
+ : null,
470
+ questions: hasQuestions ? questionInput.questions : [],
471
+ displayAt: displayTime(message.at)
472
+ };
473
+ if (normalizedCacheKey) {
474
+ if (
475
+ displayMessageCache.size >= DISPLAY_MESSAGE_CACHE_LIMIT &&
476
+ !displayMessageCache.has(normalizedCacheKey)
477
+ ) {
478
+ displayMessageCache.delete(displayMessageCache.keys().next().value);
479
+ }
480
+ displayMessageCache.set(normalizedCacheKey, {
481
+ allowNumberedQuestions,
482
+ attachments: message.attachments,
483
+ at: message.at,
484
+ messageId: message.messageId,
485
+ preserveParagraphLineBreaks,
486
+ previewUserMessage,
487
+ role: message.role,
488
+ text: message.text,
489
+ value
490
+ });
491
+ }
492
+ return { ...message, ...value };
493
+ }
494
+
495
+ function displayThinkingMessage(message = null) {
496
+ if (!message) {
497
+ return null;
498
+ }
499
+ const text = normalizeThinkingMessageText(message.text);
500
+ if (!text) {
501
+ return null;
502
+ }
503
+ return {
504
+ ...message,
505
+ text,
506
+ displayAt: displayTime(message.at)
507
+ };
508
+ }
509
+
510
+ function conversationAgentMessages(turn = {}) {
511
+ if (Array.isArray(turn.messages)) {
512
+ return turn.messages.filter((message) => (
513
+ ["assistant", "commentary", "thinking"].includes(String(message?.role || "").trim())
514
+ ));
515
+ }
516
+ return [
517
+ ...(Array.isArray(turn.thinking) ? turn.thinking : []),
518
+ ...(Array.isArray(turn.commentary) ? turn.commentary : []),
519
+ turn.assistant
520
+ ].filter(Boolean);
521
+ }
522
+
523
+ function conversationMessageKey(message = {}, index = 0) {
524
+ return [
525
+ String(message.messageId || "").trim(),
526
+ String(message.role || "").trim(),
527
+ String(message.at || "").trim(),
528
+ index
529
+ ].join(":");
530
+ }
531
+
532
+ function displayAgentTimeline(turn = {}, turnKey = "") {
533
+ const timeline = [];
534
+ for (const [index, message] of conversationAgentMessages(turn).entries()) {
535
+ const role = String(message?.role || "").trim();
536
+ if (role === "thinking") {
537
+ const displayed = displayThinkingMessage(message);
538
+ if (!displayed) {
539
+ continue;
540
+ }
541
+ const keyedMessage = {
542
+ ...displayed,
543
+ key: conversationMessageKey(message, index)
544
+ };
545
+ const previous = timeline.at(-1);
546
+ if (previous?.role === "thinking") {
547
+ previous.messages.push(keyedMessage);
548
+ continue;
549
+ }
550
+ timeline.push({
551
+ key: `thinking:${keyedMessage.key}`,
552
+ messages: [keyedMessage],
553
+ role
554
+ });
555
+ continue;
556
+ }
557
+ timeline.push({
558
+ key: conversationMessageKey(message, index),
559
+ message: displayMessage(message, {
560
+ allowNumberedQuestions: true
561
+ }, `${turnKey}:agent:${conversationMessageKey(message, index)}`),
562
+ role
563
+ });
564
+ }
565
+ return timeline;
566
+ }
567
+
568
+ function handleLongTextLinkClick(payload) {
569
+ emit("link-click", payload);
570
+ }
571
+
572
+ const displayTurns = computed(() => (Array.isArray(props.turns) ? props.turns : [])
573
+ .map((turn, index) => {
574
+ const turnId = String(turn.turnId || index + 1);
575
+ return {
576
+ ...turn,
577
+ agentTimeline: displayAgentTimeline(turn, turnId),
578
+ optimistic: turn.optimistic && typeof turn.optimistic === "object" && !Array.isArray(turn.optimistic)
579
+ ? turn.optimistic
580
+ : null,
581
+ pending: turn.pending === true,
582
+ system: displayMessage(turn.system, {}, `${turnId}:system`),
583
+ turnId,
584
+ user: displayMessage(turn.user, {
585
+ preserveParagraphLineBreaks: true,
586
+ previewUserMessage: true
587
+ }, `${turnId}:user`)
588
+ };
589
+ })
590
+ .filter((turn) => turn.system || turn.user || turn.agentTimeline.length));
591
+
592
+ function userMessageCollapsible(message = null) {
593
+ return Array.isArray(message?.previewBlocks);
594
+ }
595
+
596
+ function userMessageExpanded(turn = {}) {
597
+ return expandedUserMessages.value.has(String(turn.turnId || ""));
598
+ }
599
+
600
+ function toggleExpandedKey(expandedKeys, key) {
601
+ const next = new Set(expandedKeys.value);
602
+ if (next.has(key)) {
603
+ next.delete(key);
604
+ } else {
605
+ next.add(key);
606
+ }
607
+ expandedKeys.value = next;
608
+ }
609
+
610
+ function toggleUserMessage(turn = {}) {
611
+ const key = String(turn.turnId || "");
612
+ if (!key) {
613
+ return;
614
+ }
615
+ toggleExpandedKey(expandedUserMessages, key);
616
+ }
617
+
618
+ const loadingIndicatorVisible = computed(() => Boolean(
619
+ props.loading &&
620
+ !displayTurns.value.length
621
+ ));
622
+ const initialScrollPending = computed(() => Boolean(
623
+ props.visible &&
624
+ displayTurns.value.length &&
625
+ !initialScrollSettled.value
626
+ ));
627
+
628
+ function messageScrollKey(message = null) {
629
+ if (!message) {
630
+ return "empty";
631
+ }
632
+ return [
633
+ message.messageId || "",
634
+ message.role || "",
635
+ message.at || "",
636
+ String(message.text || "")
637
+ ].join("/");
638
+ }
639
+
640
+ function agentTimelineScrollKey(timeline = []) {
641
+ return (Array.isArray(timeline) ? timeline : []).map((entry) => (
642
+ entry?.role === "thinking"
643
+ ? (entry.messages || []).map(messageScrollKey).join("+")
644
+ : messageScrollKey(entry?.message)
645
+ )).join("|");
646
+ }
647
+
648
+ function latestRenderedTailScrollKey(turns = []) {
649
+ const turn = Array.isArray(turns) ? turns.at(-1) : null;
650
+ if (!turn) {
651
+ return "";
652
+ }
653
+ const timeline = Array.isArray(turn.agentTimeline) ? turn.agentTimeline : [];
654
+ return [
655
+ turn.turnId,
656
+ messageScrollKey(turn.system),
657
+ messageScrollKey(turn.user),
658
+ turn.optimistic?.id || "",
659
+ turn.optimistic?.status || "",
660
+ turn.optimistic?.error || "",
661
+ turn.pending ? "pending" : "settled",
662
+ agentTimelineScrollKey(timeline)
663
+ ].join(":");
664
+ }
665
+
666
+ const timelineScrollTrigger = computed(() => [
667
+ props.visible ? "visible" : "hidden",
668
+ props.error ? "error" : "body",
669
+ loadingIndicatorVisible.value ? "loading" : "ready",
670
+ displayTurns.value.length ? "has-turns" : "empty",
671
+ props.scrollKey
672
+ ].join(":"));
673
+ const latestRenderedTailKey = computed(() => latestRenderedTailScrollKey(displayTurns.value));
674
+ const autoScrollEnabled = computed(() => Boolean(
675
+ props.visible &&
676
+ followingLatest.value
677
+ ));
678
+
679
+ const {
680
+ clearScheduledScrolls,
681
+ scrollAfterLayout: scrollToLatestMessage,
682
+ scrollNow: scrollToLatestMessageNow
683
+ } = useScrollToBottom({
684
+ anchor: bottomElement,
685
+ enabled: autoScrollEnabled,
686
+ scrollAnchorIntoView: false,
687
+ target: bodyElement
688
+ });
689
+
690
+ function clearUserScrollIntent() {
691
+ if (userScrollIntentTimer && typeof window !== "undefined" && typeof window.clearTimeout === "function") {
692
+ window.clearTimeout(userScrollIntentTimer);
693
+ }
694
+ userScrollIntentTimer = null;
695
+ userScrollIntent.value = false;
696
+ }
697
+
698
+ function resumePendingTailFollow() {
699
+ const shouldResume = pendingTailFollow && followingLatest.value;
700
+ pendingTailFollow = false;
701
+ if (shouldResume) {
702
+ queueLiveBottomScroll();
703
+ }
704
+ }
705
+
706
+ function markUserScrollIntent() {
707
+ userScrollIntent.value = true;
708
+ clearLiveBottomScroll();
709
+ clearScheduledScrolls();
710
+ if (userScrollIntentTimer && typeof window !== "undefined" && typeof window.clearTimeout === "function") {
711
+ window.clearTimeout(userScrollIntentTimer);
712
+ }
713
+ if (typeof window === "undefined" || typeof window.setTimeout !== "function") {
714
+ return;
715
+ }
716
+ userScrollIntentTimer = window.setTimeout(() => {
717
+ userScrollIntentTimer = null;
718
+ userScrollIntent.value = false;
719
+ resumePendingTailFollow();
720
+ }, USER_SCROLL_INTENT_RESET_MS);
721
+ }
722
+
723
+ function markKeyboardScrollIntent(event = {}) {
724
+ if (!KEYBOARD_SCROLL_KEYS.has(String(event.key || "")) || event.defaultPrevented) {
725
+ return;
726
+ }
727
+ const target = event.target;
728
+ const tagName = String(target?.tagName || "").toLowerCase();
729
+ if (
730
+ target?.isContentEditable ||
731
+ ["input", "select", "textarea"].includes(tagName) ||
732
+ ([" ", "Spacebar"].includes(event.key) && ["a", "button"].includes(tagName))
733
+ ) {
734
+ return;
735
+ }
736
+ markUserScrollIntent();
737
+ }
738
+
739
+ function scrollToLatestMessageAfterLayout({
740
+ behavior = "auto",
741
+ force = false
742
+ } = {}) {
743
+ if (force) {
744
+ followingLatest.value = true;
745
+ clearUserScrollIntent();
746
+ }
747
+ return scrollToLatestMessage({
748
+ behavior
749
+ });
750
+ }
751
+
752
+ function queueInitialBottomScroll() {
753
+ const version = initialScrollVersion + 1;
754
+ initialScrollVersion = version;
755
+ initialScrollSettled.value = false;
756
+ void scrollToLatestMessageAfterLayout({
757
+ behavior: "auto",
758
+ force: true
759
+ }).finally(() => {
760
+ if (initialScrollVersion === version) {
761
+ initialScrollSettled.value = true;
762
+ }
763
+ });
764
+ }
765
+
766
+ function queueLiveBottomScroll({
767
+ force = false
768
+ } = {}) {
769
+ if (userScrollIntent.value && !force) {
770
+ pendingTailFollow = true;
771
+ return;
772
+ }
773
+ if (force) {
774
+ pendingTailFollow = false;
775
+ followingLatest.value = true;
776
+ clearUserScrollIntent();
777
+ }
778
+ if (liveScrollFrame) {
779
+ return;
780
+ }
781
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
782
+ void scrollToLatestMessageAfterLayout({ behavior: "auto" });
783
+ return;
784
+ }
785
+ liveScrollFrame = window.requestAnimationFrame(() => {
786
+ liveScrollFrame = 0;
787
+ scrollToLatestMessageNow({ behavior: "auto" });
788
+ });
789
+ }
790
+
791
+ function clearLiveBottomScroll() {
792
+ if (
793
+ liveScrollFrame &&
794
+ typeof window !== "undefined" &&
795
+ typeof window.cancelAnimationFrame === "function"
796
+ ) {
797
+ window.cancelAnimationFrame(liveScrollFrame);
798
+ }
799
+ liveScrollFrame = 0;
800
+ }
801
+
802
+ function updateLatestFollowFromScroll(event = {}) {
803
+ const target = event?.currentTarget || bodyElement.value;
804
+ const shouldFollow = scrollElementNearBottom(target);
805
+ if (!shouldFollow && !userScrollIntent.value && followingLatest.value) {
806
+ return;
807
+ }
808
+ followingLatest.value = shouldFollow;
809
+ if (shouldFollow) {
810
+ clearUserScrollIntent();
811
+ resumePendingTailFollow();
812
+ return;
813
+ }
814
+ if (!shouldFollow) {
815
+ clearLiveBottomScroll();
816
+ clearScheduledScrolls();
817
+ }
818
+ }
819
+
820
+ function requestLoadMore() {
821
+ if (!props.hasMoreBefore || props.loadingMore) {
822
+ return;
823
+ }
824
+ followingLatest.value = false;
825
+ pendingTailFollow = false;
826
+ markUserScrollIntent();
827
+ const element = bodyElement.value;
828
+ const version = loadMoreRequestVersion + 1;
829
+ loadMoreRequestVersion = version;
830
+ loadMoreScrollSnapshot = element
831
+ ? {
832
+ scrollHeight: element.scrollHeight,
833
+ scrollKey: props.scrollKey,
834
+ scrollTop: element.scrollTop,
835
+ version
836
+ }
837
+ : null;
838
+ emit("load-more", {
839
+ complete: ({ changed = false } = {}) => {
840
+ void completeLoadMoreRequest(version, changed);
841
+ }
842
+ });
843
+ }
844
+
845
+ function clearLoadMoreScrollSnapshot() {
846
+ loadMoreRequestVersion += 1;
847
+ loadMoreScrollSnapshot = null;
848
+ }
849
+
850
+ async function completeLoadMoreRequest(version, changed) {
851
+ const snapshot = loadMoreScrollSnapshot;
852
+ if (!snapshot || snapshot.version !== version) {
853
+ return;
854
+ }
855
+ if (!changed || snapshot.scrollKey !== props.scrollKey) {
856
+ clearLoadMoreScrollSnapshot();
857
+ return;
858
+ }
859
+ await nextTick();
860
+ if (loadMoreScrollSnapshot?.version !== version) {
861
+ return;
862
+ }
863
+ const element = bodyElement.value;
864
+ if (element) {
865
+ element.scrollTop = snapshot.scrollTop + (element.scrollHeight - snapshot.scrollHeight);
866
+ }
867
+ clearLoadMoreScrollSnapshot();
868
+ }
869
+
870
+ onBeforeUnmount(() => {
871
+ clearLiveBottomScroll();
872
+ clearUserScrollIntent();
873
+ clearLoadMoreScrollSnapshot();
874
+ });
875
+
876
+ watch(() => [
877
+ timelineScrollTrigger.value,
878
+ latestRenderedTailKey.value
879
+ ], ([timelineKey, value], [previousTimelineKey, previous] = []) => {
880
+ if (timelineKey !== previousTimelineKey) {
881
+ return;
882
+ }
883
+ if (!value || value === previous) {
884
+ return;
885
+ }
886
+ queueLiveBottomScroll({
887
+ force: false
888
+ });
889
+ }, {
890
+ flush: "post"
891
+ });
892
+
893
+ watch(() => [
894
+ props.scrollKey,
895
+ props.followLatestKey
896
+ ], ([scrollKey, value], [previousScrollKey, previous] = []) => {
897
+ if (scrollKey !== previousScrollKey || value === previous) {
898
+ return;
899
+ }
900
+ queueLiveBottomScroll({
901
+ force: true
902
+ });
903
+ }, {
904
+ flush: "post"
905
+ });
906
+
907
+ watch(timelineScrollTrigger, () => {
908
+ clearLoadMoreScrollSnapshot();
909
+ queueInitialBottomScroll();
910
+ }, {
911
+ flush: "post",
912
+ immediate: true
913
+ });
914
+ </script>
915
+
916
+ <style scoped>
917
+ .assistant-transcript__plain-user { white-space: pre-wrap; margin: 0; }
918
+ .assistant-transcript {
919
+ border: 1px solid rgba(var(--v-theme-outline), 0.24);
920
+ border-radius: 8px;
921
+ display: grid;
922
+ gap: 0.35rem;
923
+ grid-template-rows: minmax(0, 1fr);
924
+ min-height: 0;
925
+ overflow: hidden;
926
+ padding: 0.5rem;
927
+ position: relative;
928
+ text-align: left;
929
+ }
930
+
931
+ .assistant-transcript__loading-skeleton {
932
+ inset: 0;
933
+ overflow: hidden;
934
+ position: absolute;
935
+ z-index: 1;
936
+ }
937
+
938
+ .assistant-transcript__settling {
939
+ background: rgb(var(--v-theme-surface));
940
+ inset: 0;
941
+ overflow: hidden;
942
+ padding: 0.5rem;
943
+ position: absolute;
944
+ z-index: 1;
945
+ }
946
+
947
+ .assistant-transcript__settling-skeleton {
948
+ height: 100%;
949
+ }
950
+
951
+ .assistant-transcript__reload {
952
+ color: rgba(var(--v-theme-on-surface), 0.66);
953
+ position: absolute;
954
+ right: 0.38rem;
955
+ top: 0.38rem;
956
+ z-index: 2;
957
+ }
958
+
959
+ .assistant-transcript__body {
960
+ display: flex;
961
+ flex-direction: column;
962
+ gap: 0.65rem;
963
+ min-height: 0;
964
+ min-width: 0;
965
+ overflow-x: hidden;
966
+ overflow-y: auto;
967
+ padding-right: 0.15rem;
968
+ }
969
+
970
+ .assistant-transcript__welcome {
971
+ background: rgba(var(--v-theme-primary), 0.08);
972
+ border: 1px solid rgba(var(--v-theme-primary), 0.18);
973
+ border-radius: 16px;
974
+ color: rgb(var(--v-theme-on-surface));
975
+ display: grid;
976
+ flex: 0 0 auto;
977
+ gap: 0.55rem;
978
+ margin: 0.1rem 0.15rem 0;
979
+ padding: 0.8rem 0.9rem 0.9rem;
980
+ }
981
+
982
+ .assistant-transcript__welcome p {
983
+ line-height: 1.48;
984
+ margin: 0;
985
+ }
986
+
987
+ .assistant-transcript__body--settling {
988
+ visibility: hidden;
989
+ }
990
+
991
+ .assistant-transcript__load-more {
992
+ align-items: center;
993
+ display: flex;
994
+ flex: 0 0 auto;
995
+ flex-direction: column;
996
+ gap: 0.35rem;
997
+ padding: 0.35rem 0 0.55rem;
998
+ }
999
+
1000
+ .assistant-transcript__load-more-error {
1001
+ color: rgb(var(--v-theme-error));
1002
+ font-size: 0.76rem;
1003
+ line-height: 1.3;
1004
+ text-align: center;
1005
+ }
1006
+
1007
+ .assistant-transcript__load-more + .assistant-transcript__turn,
1008
+ .assistant-transcript__body > .assistant-transcript__turn:first-child {
1009
+ margin-top: auto;
1010
+ }
1011
+
1012
+ .assistant-transcript__turn {
1013
+ contain-intrinsic-block-size: auto 12rem;
1014
+ content-visibility: auto;
1015
+ display: flex;
1016
+ flex: 0 0 auto;
1017
+ flex-direction: column;
1018
+ gap: 0.65rem;
1019
+ min-height: 0;
1020
+ min-width: 0;
1021
+ }
1022
+
1023
+ .assistant-transcript__message-row {
1024
+ align-items: start;
1025
+ display: grid;
1026
+ gap: 0.65rem;
1027
+ max-width: 100%;
1028
+ min-width: 0;
1029
+ overflow-x: hidden;
1030
+ }
1031
+
1032
+ .assistant-transcript__message-row--user {
1033
+ grid-template-columns: minmax(0, auto) auto;
1034
+ justify-self: end;
1035
+ max-width: min(28rem, 88%);
1036
+ margin-left: auto;
1037
+ }
1038
+
1039
+ .assistant-transcript__message-row--assistant {
1040
+ display: flex;
1041
+ flex-direction: column;
1042
+ gap: 0.28rem;
1043
+ justify-self: start;
1044
+ max-width: min(42rem, 94%);
1045
+ margin-right: auto;
1046
+ }
1047
+
1048
+ .assistant-transcript__assistant-header {
1049
+ align-items: center;
1050
+ display: grid;
1051
+ gap: 0.65rem;
1052
+ grid-template-columns: auto minmax(0, 1fr);
1053
+ min-width: 0;
1054
+ }
1055
+
1056
+ .assistant-transcript__message {
1057
+ display: flex;
1058
+ flex-direction: column;
1059
+ gap: 0.24rem;
1060
+ max-width: 100%;
1061
+ min-width: 0;
1062
+ overflow-wrap: anywhere;
1063
+ word-break: break-word;
1064
+ }
1065
+
1066
+ .assistant-transcript__avatar {
1067
+ align-items: center;
1068
+ border-radius: 999px;
1069
+ display: inline-flex;
1070
+ height: 1.5rem;
1071
+ justify-content: center;
1072
+ width: 1.5rem;
1073
+ }
1074
+
1075
+ .assistant-transcript__avatar--user {
1076
+ background: #e9f2fc;
1077
+ color: #2f79ca;
1078
+ margin-top: 0.2rem;
1079
+ }
1080
+
1081
+ .assistant-transcript__avatar--assistant {
1082
+ background: #5b9ce1;
1083
+ color: #ffffff;
1084
+ margin-top: 0.05rem;
1085
+ }
1086
+
1087
+ .assistant-transcript__message--user {
1088
+ background: #e3ebf5;
1089
+ border-radius: 16px;
1090
+ color: #202936;
1091
+ gap: 0.75rem;
1092
+ justify-content: space-between;
1093
+ overflow-x: auto;
1094
+ padding: 0.75rem 1rem 0.72rem;
1095
+ width: fit-content;
1096
+ }
1097
+
1098
+ .assistant-transcript__message--assistant {
1099
+ background: rgb(var(--v-theme-surface));
1100
+ color: rgb(var(--v-theme-on-surface));
1101
+ padding: 0;
1102
+ }
1103
+
1104
+ .assistant-transcript--task {
1105
+ border-color: rgba(126, 87, 194, 0.32);
1106
+ }
1107
+
1108
+ .assistant-transcript--task .assistant-transcript__avatar--assistant {
1109
+ background: #7e57c2;
1110
+ }
1111
+
1112
+ .assistant-transcript--task .assistant-transcript__message--user {
1113
+ background: #eee8f8;
1114
+ color: #2d2440;
1115
+ }
1116
+
1117
+ .assistant-transcript__thinking {
1118
+ justify-self: start;
1119
+ margin-left: 2.15rem;
1120
+ max-width: min(34rem, 86%);
1121
+ }
1122
+
1123
+ .assistant-transcript__user-content-toggle {
1124
+ background: transparent;
1125
+ border: 0;
1126
+ color: rgb(var(--v-theme-primary));
1127
+ cursor: pointer;
1128
+ font: inherit;
1129
+ text-align: left;
1130
+ }
1131
+
1132
+ .assistant-transcript__system {
1133
+ align-items: start;
1134
+ background: transparent;
1135
+ color: rgba(var(--v-theme-on-surface), 0.84);
1136
+ display: grid;
1137
+ gap: 0.45rem;
1138
+ grid-template-columns: auto minmax(0, 1fr);
1139
+ justify-self: start;
1140
+ max-width: min(34rem, 96%);
1141
+ min-width: 0;
1142
+ padding: 0.1rem 0.15rem;
1143
+ }
1144
+
1145
+ .assistant-transcript__system-icon {
1146
+ color: rgba(var(--v-theme-primary), 0.82);
1147
+ margin-top: 0.15rem;
1148
+ }
1149
+
1150
+ .assistant-transcript__system-body {
1151
+ display: grid;
1152
+ gap: 0.1rem;
1153
+ min-width: 0;
1154
+ overflow-wrap: anywhere;
1155
+ }
1156
+
1157
+ .assistant-transcript__system-meta {
1158
+ align-items: center;
1159
+ color: rgba(var(--v-theme-on-surface), 0.48);
1160
+ display: flex;
1161
+ font-size: 0.72rem;
1162
+ gap: 0.5rem;
1163
+ line-height: 1.15;
1164
+ }
1165
+
1166
+ .assistant-transcript__message-header {
1167
+ align-items: center;
1168
+ color: rgba(var(--v-theme-on-surface), 0.82);
1169
+ display: flex;
1170
+ font-size: 0.9rem;
1171
+ gap: 0.7rem;
1172
+ justify-content: space-between;
1173
+ line-height: 1.2;
1174
+ }
1175
+
1176
+ .assistant-transcript__message--assistant .assistant-transcript__message-header {
1177
+ color: rgba(var(--v-theme-on-surface), 0.78);
1178
+ font-weight: 560;
1179
+ }
1180
+
1181
+ .assistant-transcript__message-header span {
1182
+ align-items: center;
1183
+ display: flex;
1184
+ gap: 0.25rem;
1185
+ min-width: 0;
1186
+ overflow-wrap: anywhere;
1187
+ }
1188
+
1189
+ .assistant-transcript__message-header time {
1190
+ color: #6d7888;
1191
+ font-weight: 650;
1192
+ }
1193
+
1194
+ .assistant-transcript__message-footer {
1195
+ align-items: center;
1196
+ color: #9aa6b6;
1197
+ display: flex;
1198
+ font-size: 0.88rem;
1199
+ font-weight: 500;
1200
+ gap: 0.65rem;
1201
+ line-height: 1.2;
1202
+ }
1203
+
1204
+ .assistant-transcript__message-footer--user {
1205
+ justify-content: flex-start;
1206
+ }
1207
+
1208
+ .assistant-transcript__message-footer--assistant {
1209
+ justify-content: flex-start;
1210
+ margin-top: -0.15rem;
1211
+ }
1212
+
1213
+ .assistant-transcript__optimistic-failure {
1214
+ align-items: center;
1215
+ color: rgba(var(--v-theme-error), 0.92);
1216
+ display: flex;
1217
+ flex-wrap: wrap;
1218
+ font-size: 0.76rem;
1219
+ gap: 0.35rem 0.55rem;
1220
+ line-height: 1.25;
1221
+ }
1222
+
1223
+ .assistant-transcript__optimistic-actions {
1224
+ align-items: center;
1225
+ display: inline-flex;
1226
+ gap: 0.25rem;
1227
+ }
1228
+
1229
+ .assistant-transcript__message--assistant :deep(.studio-long-text-review__blocks),
1230
+ .assistant-transcript__message--user :deep(.studio-long-text-review__blocks) {
1231
+ color: inherit;
1232
+ font-size: 0.94rem;
1233
+ line-height: 1.5;
1234
+ min-width: 0;
1235
+ overflow-wrap: anywhere;
1236
+ }
1237
+
1238
+ .assistant-transcript__message--assistant :deep(.studio-long-text-review__paragraph),
1239
+ .assistant-transcript__message--user :deep(.studio-long-text-review__paragraph) {
1240
+ font-size: 0.94rem;
1241
+ margin-block: 0;
1242
+ }
1243
+
1244
+ .assistant-transcript__message--user :deep(.studio-long-text-review__paragraph) {
1245
+ white-space: pre-wrap;
1246
+ }
1247
+
1248
+ .assistant-transcript__user-content {
1249
+ min-width: 0;
1250
+ }
1251
+
1252
+ .assistant-transcript__user-content-toggle {
1253
+ align-self: flex-start;
1254
+ font-size: 0.78rem;
1255
+ font-weight: 650;
1256
+ padding: 0;
1257
+ }
1258
+
1259
+ .assistant-transcript__user-content-toggle:hover,
1260
+ .assistant-transcript__user-content-toggle:focus-visible {
1261
+ text-decoration: underline;
1262
+ }
1263
+
1264
+ .assistant-transcript__message--assistant :deep(.studio-long-text-review__paragraph code),
1265
+ .assistant-transcript__message--assistant :deep(.studio-long-text-review__list li > code),
1266
+ .assistant-transcript__message--assistant :deep(.studio-long-text-review__table th > code),
1267
+ .assistant-transcript__message--assistant :deep(.studio-long-text-review__table td > code),
1268
+ .assistant-transcript__message--assistant :deep(.studio-long-text-review__details-summary code),
1269
+ .assistant-transcript__message--user :deep(.studio-long-text-review__paragraph code),
1270
+ .assistant-transcript__message--user :deep(.studio-long-text-review__list li > code),
1271
+ .assistant-transcript__message--user :deep(.studio-long-text-review__table th > code),
1272
+ .assistant-transcript__message--user :deep(.studio-long-text-review__table td > code),
1273
+ .assistant-transcript__message--user :deep(.studio-long-text-review__details-summary code) {
1274
+ background: rgba(var(--v-theme-primary), 0.07);
1275
+ border: 1px solid rgba(var(--v-theme-primary), 0.14);
1276
+ border-radius: 3px;
1277
+ color: rgba(var(--v-theme-on-surface), 0.9);
1278
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
1279
+ font-size: 0.9em;
1280
+ padding: 0.02rem 0.2rem;
1281
+ }
1282
+
1283
+ .assistant-transcript__questions {
1284
+ box-sizing: border-box;
1285
+ display: grid;
1286
+ gap: 0.2rem;
1287
+ list-style: none;
1288
+ margin: 0;
1289
+ max-width: 100%;
1290
+ min-width: 0;
1291
+ padding: 0;
1292
+ }
1293
+
1294
+ .assistant-transcript__question {
1295
+ align-items: start;
1296
+ background: rgba(var(--v-theme-surface), 0.62);
1297
+ border: 1px solid rgba(var(--v-theme-outline), 0.2);
1298
+ border-radius: 8px;
1299
+ box-sizing: border-box;
1300
+ display: grid;
1301
+ gap: 0.32rem;
1302
+ grid-template-columns: auto minmax(0, 1fr);
1303
+ max-width: 100%;
1304
+ min-width: 0;
1305
+ padding: 0.28rem 0.42rem;
1306
+ }
1307
+
1308
+ .assistant-transcript__question-number {
1309
+ align-items: center;
1310
+ background: rgba(var(--v-theme-primary), 0.1);
1311
+ border: 1px solid rgba(var(--v-theme-primary), 0.2);
1312
+ border-radius: 999px;
1313
+ color: rgb(var(--v-theme-primary));
1314
+ display: inline-flex;
1315
+ font-size: 0.72rem;
1316
+ font-weight: 760;
1317
+ height: 1.35rem;
1318
+ justify-content: center;
1319
+ line-height: 1;
1320
+ min-width: 1.35rem;
1321
+ }
1322
+
1323
+ .assistant-transcript__question-text {
1324
+ color: rgb(var(--v-theme-on-surface));
1325
+ font-size: 0.9rem;
1326
+ line-height: 1.35;
1327
+ max-width: 100%;
1328
+ min-width: 0;
1329
+ overflow-wrap: anywhere;
1330
+ }
1331
+
1332
+ .assistant-transcript__question-content {
1333
+ display: block;
1334
+ min-width: 0;
1335
+ }
1336
+
1337
+ .assistant-transcript__question-choices {
1338
+ color: rgba(var(--v-theme-on-surface), 0.72);
1339
+ display: inline;
1340
+ font-size: 0.8rem;
1341
+ margin: 0;
1342
+ padding: 0;
1343
+ }
1344
+
1345
+ .assistant-transcript__question-choices::before {
1346
+ content: " · ";
1347
+ }
1348
+
1349
+ .assistant-transcript__question-choices li {
1350
+ display: inline;
1351
+ overflow-wrap: anywhere;
1352
+ }
1353
+
1354
+ .assistant-transcript__question-choices li + li::before {
1355
+ content: " · ";
1356
+ }
1357
+
1358
+ .assistant-transcript__bottom {
1359
+ height: 1px;
1360
+ }
1361
+ </style>