@jskit-ai/assistant 0.1.4

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 (57) hide show
  1. package/package.descriptor.mjs +284 -0
  2. package/package.json +31 -0
  3. package/src/client/components/AssistantClientElement.vue +1316 -0
  4. package/src/client/components/AssistantConsoleSettingsClientElement.vue +71 -0
  5. package/src/client/components/AssistantSettingsFormCard.vue +76 -0
  6. package/src/client/components/AssistantWorkspaceClientElement.vue +15 -0
  7. package/src/client/components/AssistantWorkspaceSettingsClientElement.vue +73 -0
  8. package/src/client/composables/useAssistantWorkspaceRuntime.js +789 -0
  9. package/src/client/index.js +12 -0
  10. package/src/client/lib/assistantApi.js +137 -0
  11. package/src/client/lib/assistantHttpClient.js +10 -0
  12. package/src/client/lib/markdownRenderer.js +31 -0
  13. package/src/client/providers/AssistantWebClientProvider.js +25 -0
  14. package/src/server/AssistantServiceProvider.js +179 -0
  15. package/src/server/actionIds.js +11 -0
  16. package/src/server/actions.js +191 -0
  17. package/src/server/diTokens.js +19 -0
  18. package/src/server/lib/aiClient.js +43 -0
  19. package/src/server/lib/ndjson.js +47 -0
  20. package/src/server/lib/providers/anthropicClient.js +375 -0
  21. package/src/server/lib/providers/common.js +158 -0
  22. package/src/server/lib/providers/deepSeekClient.js +22 -0
  23. package/src/server/lib/providers/openAiClient.js +13 -0
  24. package/src/server/lib/providers/openAiCompatibleClient.js +69 -0
  25. package/src/server/lib/resolveWorkspaceSlug.js +24 -0
  26. package/src/server/lib/serviceToolCatalog.js +459 -0
  27. package/src/server/registerRoutes.js +384 -0
  28. package/src/server/repositories/assistantSettingsRepository.js +100 -0
  29. package/src/server/repositories/conversationsRepository.js +244 -0
  30. package/src/server/repositories/messagesRepository.js +154 -0
  31. package/src/server/repositories/repositoryPersistenceUtils.js +63 -0
  32. package/src/server/services/assistantSettingsService.js +153 -0
  33. package/src/server/services/chatService.js +987 -0
  34. package/src/server/services/transcriptService.js +334 -0
  35. package/src/shared/assistantPaths.js +50 -0
  36. package/src/shared/assistantResource.js +323 -0
  37. package/src/shared/assistantSettingsResource.js +214 -0
  38. package/src/shared/index.js +39 -0
  39. package/src/shared/queryKeys.js +69 -0
  40. package/src/shared/settingsEvents.js +7 -0
  41. package/src/shared/streamEvents.js +31 -0
  42. package/src/shared/support/positiveInteger.js +9 -0
  43. package/templates/migrations/assistant_settings_initial.cjs +39 -0
  44. package/templates/migrations/assistant_transcripts_initial.cjs +51 -0
  45. package/templates/src/pages/admin/workspace/assistant/index.vue +7 -0
  46. package/test/aiConfigValidation.test.js +15 -0
  47. package/test/assistantApiSurfaceHeader.test.js +64 -0
  48. package/test/assistantResource.test.js +53 -0
  49. package/test/assistantSettingsResource.test.js +48 -0
  50. package/test/assistantSettingsService.test.js +133 -0
  51. package/test/chatService.test.js +841 -0
  52. package/test/descriptorSurfaceOption.test.js +35 -0
  53. package/test/queryKeys.test.js +41 -0
  54. package/test/resolveWorkspaceSlug.test.js +83 -0
  55. package/test/routeInputContracts.test.js +287 -0
  56. package/test/serviceToolCatalog.test.js +1235 -0
  57. package/test/transcriptService.test.js +175 -0
@@ -0,0 +1,1316 @@
1
+ <template>
2
+ <section
3
+ ref="rootRef"
4
+ :class="rootClasses.concat(['d-flex', 'flex-column', 'h-100', 'ga-1'])"
5
+ :data-testid="uiTestIds.root"
6
+ tabindex="-1"
7
+ @focus="onRootFocus"
8
+ @pointerdown.capture="onRootPointerDown"
9
+ >
10
+ <v-row class="assistant-layout h-100 flex-grow-1 my-0">
11
+ <v-col cols="12" lg="8" class="assistant-main-col d-flex flex-column overflow-hidden">
12
+ <v-card rounded="lg" elevation="1" border class="assistant-main-card d-flex flex-column flex-grow-1">
13
+ <v-card-text class="assistant-main-card-text d-flex flex-column flex-grow-1">
14
+ <div
15
+ ref="messagesPanelRef"
16
+ class="messages-panel mb-3 flex-grow-1"
17
+ :class="[{ 'messages-panel--empty': messages.length < 1 }, uiClasses.messagesPanel]"
18
+ :data-testid="uiTestIds.messagesPanel"
19
+ @scroll.passive="handleMessagesPanelScroll"
20
+ >
21
+ <div v-if="messages.length < 1" class="messages-empty-state">
22
+ <slot name="empty-state" :state="state" :actions="actions">{{ copyText.emptyState }}</slot>
23
+ </div>
24
+ <div
25
+ v-for="message in messages"
26
+ :key="message.id"
27
+ class="message-row d-flex align-end ga-2 mb-3"
28
+ :class="[`message-row--${message.role}`, { 'flex-row-reverse': message.role === 'user' }]"
29
+ >
30
+ <v-avatar v-if="message.role === 'user'" size="36" class="message-avatar message-avatar--user">
31
+ <v-img v-if="currentUserAvatarUrl" :src="currentUserAvatarUrl" cover />
32
+ <span v-else class="message-avatar-initials">{{ currentUserInitials }}</span>
33
+ </v-avatar>
34
+ <v-avatar v-else size="36" class="message-avatar message-avatar--assistant" aria-hidden="true" />
35
+ <div class="message-body d-flex flex-column">
36
+ <div class="message-meta mb-1 text-caption text-medium-emphasis">
37
+ <span class="message-author">{{ messageAuthorLabel(message) }}</span>
38
+ </div>
39
+ <div class="message-bubble">
40
+ <div
41
+ v-if="showAssistantTypingIndicator(message)"
42
+ class="message-typing d-inline-flex align-center ga-1"
43
+ aria-label="Assistant is typing"
44
+ >
45
+ <span class="message-typing-dot" />
46
+ <span class="message-typing-dot" />
47
+ <span class="message-typing-dot" />
48
+ </div>
49
+ <div
50
+ v-else-if="isAssistantChatMessage(message)"
51
+ class="message-text message-text--markdown text-body-2"
52
+ v-html="assistantMessageHtml(message)"
53
+ />
54
+ <div v-else class="message-text text-body-2">{{ message.text }}</div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="assistant-composer-shell d-grid" :class="uiClasses.composer">
61
+ <div class="assistant-composer-row d-flex align-end ga-2">
62
+ <v-textarea
63
+ ref="composerRef"
64
+ v-model="input"
65
+ class="assistant-composer-textarea flex-grow-1"
66
+ :placeholder="copyText.messagePlaceholder"
67
+ :aria-label="copyText.messagePlaceholder"
68
+ rows="1"
69
+ max-rows="4"
70
+ variant="solo-filled"
71
+ density="comfortable"
72
+ auto-grow
73
+ hide-details="auto"
74
+ :disabled="isRestoringConversation"
75
+ @keydown="onHandleInputKeydown"
76
+ />
77
+
78
+ <v-btn
79
+ :color="isStreaming ? 'error' : 'primary'"
80
+ :class="{ 'assistant-stop-button': isStreaming }"
81
+ :disabled="isStreaming ? false : !canSend"
82
+ :data-testid="uiTestIds.sendButton"
83
+ @click="onSendMessage"
84
+ >
85
+ {{ isStreaming ? copyText.stop : copyText.send }}
86
+ </v-btn>
87
+ </div>
88
+
89
+ <slot name="composer-extra" :state="state" :actions="actions" />
90
+
91
+ <div v-if="resolvedFeatures.composerActions" class="assistant-actions d-flex ga-2 flex-wrap mt-2">
92
+ <v-btn
93
+ v-if="resolvedFeatures.mobilePicker"
94
+ class="d-lg-none"
95
+ variant="tonal"
96
+ :disabled="isStreaming || isRestoringConversation"
97
+ @click="onConversationPickerOpen"
98
+ >
99
+ {{ copyText.conversations }}
100
+ </v-btn>
101
+ </div>
102
+ </div>
103
+ </v-card-text>
104
+ </v-card>
105
+ </v-col>
106
+
107
+ <v-col cols="12" lg="4" class="assistant-side-col d-flex flex-column overflow-hidden">
108
+ <v-card
109
+ v-if="resolvedFeatures.historyPanel"
110
+ rounded="lg"
111
+ elevation="1"
112
+ border
113
+ class="d-none d-lg-flex flex-column mb-3 assistant-history-card overflow-hidden"
114
+ >
115
+ <v-card-item>
116
+ <v-card-title class="text-subtitle-2 font-weight-bold">{{ copyText.conversationHistory }}</v-card-title>
117
+ <template #append>
118
+ <v-btn
119
+ variant="text"
120
+ size="small"
121
+ :disabled="isStreaming || isRestoringConversation || conversationHistoryLoading"
122
+ @click="refreshConversationHistory"
123
+ >
124
+ {{ copyText.refresh }}
125
+ </v-btn>
126
+ </template>
127
+ <slot name="history-header-extra" :state="state" :actions="actions" />
128
+ </v-card-item>
129
+ <v-divider />
130
+ <v-card-text class="pt-2 assistant-history-card-text d-flex flex-column flex-grow-1">
131
+ <v-btn
132
+ block
133
+ variant="outlined"
134
+ color="primary"
135
+ class="mb-2 assistant-history-start-button"
136
+ :disabled="!canStartNewConversation"
137
+ @click="onStartNewConversation"
138
+ >
139
+ {{ copyText.startNewConversation }}
140
+ </v-btn>
141
+ <div v-if="conversationHistoryError" class="text-caption text-error mb-2">
142
+ {{ conversationHistoryError }}
143
+ </div>
144
+ <v-list density="compact" class="assistant-history-list flex-grow-1 overflow-y-auto">
145
+ <v-list-item v-if="conversationHistory.length < 1" :title="copyText.noConversations" />
146
+ <v-list-item
147
+ v-for="conversation in conversationHistory"
148
+ :key="conversation.id"
149
+ :title="conversationDisplayTitle(conversation)"
150
+ :subtitle="conversationSubtitle(conversation)"
151
+ :active="isActiveConversation(conversation)"
152
+ :disabled="isStreaming || isRestoringConversation"
153
+ @click="onSelectConversation(conversation)"
154
+ />
155
+ </v-list>
156
+ <v-btn
157
+ v-if="conversationHistoryHasMore"
158
+ block
159
+ variant="text"
160
+ size="small"
161
+ class="mt-2"
162
+ :loading="conversationHistoryLoadingMore"
163
+ :disabled="isStreaming || isRestoringConversation || conversationHistoryLoadingMore"
164
+ @click="onLoadMoreConversations"
165
+ >
166
+ {{ copyText.loadOlderConversations }}
167
+ </v-btn>
168
+ </v-card-text>
169
+ </v-card>
170
+
171
+ <v-card
172
+ v-if="resolvedFeatures.toolsPanel"
173
+ rounded="lg"
174
+ elevation="1"
175
+ border
176
+ class="assistant-tools-card d-flex flex-column flex-grow-1 overflow-hidden"
177
+ >
178
+ <v-card-item>
179
+ <v-card-title class="text-subtitle-2 font-weight-bold">{{ copyText.toolTimeline }}</v-card-title>
180
+ <slot name="tools-header-extra" :state="state" :actions="actions" />
181
+ </v-card-item>
182
+ <v-divider />
183
+ <v-list density="compact" class="assistant-tools-list flex-grow-1 overflow-y-auto">
184
+ <v-list-item v-if="pendingToolEvents.length < 1" :title="copyText.noToolEvents" />
185
+ <v-list-item
186
+ v-for="toolEvent in pendingToolEvents"
187
+ :key="toolEvent.id"
188
+ :title="toolEvent.name"
189
+ :subtitle="toolEvent.status"
190
+ />
191
+ </v-list>
192
+ </v-card>
193
+ </v-col>
194
+ </v-row>
195
+
196
+ <v-bottom-sheet v-if="resolvedFeatures.mobilePicker" v-model="conversationPickerOpen">
197
+ <v-card rounded="t-lg" border>
198
+ <v-card-item>
199
+ <v-card-title class="text-subtitle-1 font-weight-bold">{{ copyText.conversations }}</v-card-title>
200
+ </v-card-item>
201
+ <v-divider />
202
+ <v-card-text class="pt-3">
203
+ <v-btn
204
+ block
205
+ variant="outlined"
206
+ color="primary"
207
+ class="mb-2"
208
+ :disabled="!canStartNewConversation"
209
+ @click="startNewConversationFromPicker"
210
+ >
211
+ {{ copyText.startNewConversation }}
212
+ </v-btn>
213
+ <div v-if="conversationHistoryError" class="text-caption text-error mb-2">{{ conversationHistoryError }}</div>
214
+ <v-list density="compact">
215
+ <v-list-item v-if="conversationHistory.length < 1" :title="copyText.noConversations" />
216
+ <v-list-item
217
+ v-for="conversation in conversationHistory"
218
+ :key="conversation.id"
219
+ :title="conversationDisplayTitle(conversation)"
220
+ :subtitle="conversationSubtitle(conversation)"
221
+ :active="isActiveConversation(conversation)"
222
+ :disabled="isStreaming || isRestoringConversation"
223
+ @click="selectConversationFromPicker(conversation)"
224
+ />
225
+ </v-list>
226
+ <v-btn
227
+ v-if="conversationHistoryHasMore"
228
+ block
229
+ variant="text"
230
+ size="small"
231
+ class="mt-2"
232
+ :loading="conversationHistoryLoadingMore"
233
+ :disabled="isStreaming || isRestoringConversation || conversationHistoryLoadingMore"
234
+ @click="onLoadMoreConversations"
235
+ >
236
+ {{ copyText.loadOlderConversations }}
237
+ </v-btn>
238
+ </v-card-text>
239
+ </v-card>
240
+ </v-bottom-sheet>
241
+
242
+ <slot name="footer-extra" :state="state" :actions="actions" />
243
+ </section>
244
+ </template>
245
+
246
+ <script setup>
247
+ import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from "vue";
248
+ import { createComponentInteractionEmitter } from "@jskit-ai/kernel/client";
249
+ import {
250
+ normalizeObject,
251
+ normalizeOneOf
252
+ } from "@jskit-ai/kernel/shared/support/normalize";
253
+ import { renderMarkdownToSafeHtml } from "../lib/markdownRenderer.js";
254
+
255
+ const DEFAULT_COPY = Object.freeze({
256
+ emptyState: "I am here to help",
257
+ assistantLabel: "Assistant",
258
+ systemLabel: "System",
259
+ messagePlaceholder: "Message",
260
+ stop: "STOP",
261
+ send: "Send",
262
+ conversations: "Conversations",
263
+ conversationHistory: "Conversation History",
264
+ refresh: "Refresh",
265
+ startNewConversation: "Start new conversation",
266
+ loadOlderConversations: "Load older conversations",
267
+ noConversations: "No conversations yet.",
268
+ unknownConversationTitle: "New conversation",
269
+ unknownUser: "Unknown user",
270
+ unknownDate: "unknown",
271
+ toolTimeline: "Tool Timeline",
272
+ noToolEvents: "No tool events yet."
273
+ });
274
+
275
+ const props = defineProps({
276
+ meta: {
277
+ type: Object,
278
+ required: true
279
+ },
280
+ state: {
281
+ type: Object,
282
+ required: true
283
+ },
284
+ actions: {
285
+ type: Object,
286
+ required: true
287
+ },
288
+ viewer: {
289
+ type: Object,
290
+ default: () => ({})
291
+ },
292
+ copy: {
293
+ type: Object,
294
+ default: () => ({})
295
+ },
296
+ variant: {
297
+ type: Object,
298
+ default: () => ({})
299
+ },
300
+ features: {
301
+ type: Object,
302
+ default: () => ({})
303
+ },
304
+ ui: {
305
+ type: Object,
306
+ default: () => ({})
307
+ }
308
+ });
309
+
310
+ const emit = defineEmits([
311
+ "action:started",
312
+ "action:succeeded",
313
+ "action:failed",
314
+ "interaction",
315
+ "conversation:start",
316
+ "conversation:select",
317
+ "message:send",
318
+ "stream:cancel"
319
+ ]);
320
+ const {
321
+ emitInteraction,
322
+ invokeAction
323
+ } = createComponentInteractionEmitter(emit);
324
+
325
+ const SCROLL_BOTTOM_THRESHOLD_PX = 30;
326
+ const MIN_VIEWPORT_HEIGHT_PX = 360;
327
+ const VIEWPORT_BOTTOM_GUTTER_PX = 12;
328
+ const ROOT_FOCUS_POINTER_GUARD_MS = 200;
329
+ const MESSAGE_MARKDOWN_RENDER_THROTTLE_MS = 40;
330
+
331
+ const meta = props.meta;
332
+ const state = props.state;
333
+ const actions = props.actions;
334
+
335
+ const messages = state.messages;
336
+ const input = state.input;
337
+ const isStreaming = state.isStreaming;
338
+ const isRestoringConversation = state.isRestoringConversation;
339
+ const pendingToolEvents = state.pendingToolEvents;
340
+ const conversationId = state.conversationId;
341
+ const conversationHistory = state.conversationHistory;
342
+ const conversationHistoryLoading = state.conversationHistoryLoading;
343
+ const conversationHistoryLoadingMore = state.conversationHistoryLoadingMore;
344
+ const conversationHistoryHasMore = state.conversationHistoryHasMore;
345
+ const conversationHistoryError = state.conversationHistoryError;
346
+ const isAdminSurface = state.isAdminSurface;
347
+ const canSend = state.canSend;
348
+ const canStartNewConversation = state.canStartNewConversation;
349
+
350
+ const sendMessage = actions.sendMessage;
351
+ const handleInputKeydown = actions.handleInputKeydown;
352
+ const cancelStream = actions.cancelStream;
353
+ const startNewConversation = actions.startNewConversation;
354
+ const selectConversation = actions.selectConversation;
355
+ const refreshConversationHistory = actions.refreshConversationHistory;
356
+ const loadMoreConversationHistory = actions.loadMoreConversationHistory;
357
+
358
+ const copyText = computed(() => ({
359
+ ...DEFAULT_COPY,
360
+ ...normalizeObject(props.copy)
361
+ }));
362
+
363
+ const resolvedVariant = computed(() => {
364
+ const variant = normalizeObject(props.variant);
365
+ return {
366
+ layout: normalizeOneOf(variant.layout, ["compact", "comfortable"], "comfortable"),
367
+ surface: normalizeOneOf(variant.surface, ["plain", "carded"], "carded"),
368
+ density: normalizeOneOf(variant.density, ["compact", "comfortable"], "comfortable"),
369
+ tone: normalizeOneOf(variant.tone, ["neutral", "emphasized"], "neutral")
370
+ };
371
+ });
372
+
373
+ const resolvedFeatures = computed(() => {
374
+ const features = normalizeObject(props.features);
375
+ return {
376
+ historyPanel: features.historyPanel !== false,
377
+ toolsPanel: features.toolsPanel !== false,
378
+ mobilePicker: features.mobilePicker !== false,
379
+ composerActions: features.composerActions !== false
380
+ };
381
+ });
382
+
383
+ const uiClasses = computed(() => {
384
+ const classes = normalizeObject(normalizeObject(props.ui).classes);
385
+ return {
386
+ root: String(classes.root || "").trim(),
387
+ messagesPanel: String(classes.messagesPanel || "").trim(),
388
+ composer: String(classes.composer || "").trim()
389
+ };
390
+ });
391
+
392
+ const uiTestIds = computed(() => {
393
+ const testIds = normalizeObject(normalizeObject(props.ui).testIds);
394
+ return {
395
+ root: String(testIds.root || "assistant-client-element"),
396
+ messagesPanel: String(testIds.messagesPanel || "assistant-messages-panel"),
397
+ sendButton: String(testIds.sendButton || "assistant-send-button")
398
+ };
399
+ });
400
+
401
+ const rootClasses = computed(() => {
402
+ const classes = [
403
+ "assistant-view",
404
+ "assistant-client-element",
405
+ `assistant-client-element--layout-${resolvedVariant.value.layout}`,
406
+ `assistant-client-element--surface-${resolvedVariant.value.surface}`,
407
+ `assistant-client-element--density-${resolvedVariant.value.density}`,
408
+ `assistant-client-element--tone-${resolvedVariant.value.tone}`
409
+ ];
410
+ if (uiClasses.value.root) {
411
+ classes.push(uiClasses.value.root);
412
+ }
413
+ return classes;
414
+ });
415
+
416
+ function normalizeText(value) {
417
+ return String(value || "").trim();
418
+ }
419
+
420
+ function formatConversationStartedAt(value) {
421
+ const formatter = meta?.formatConversationStartedAt;
422
+ if (typeof formatter === "function") {
423
+ return formatter(value);
424
+ }
425
+ return copyText.value.unknownDate;
426
+ }
427
+
428
+ function normalizeConversationStatus(value) {
429
+ const normalizer = meta?.normalizeConversationStatus;
430
+ if (typeof normalizer === "function") {
431
+ return normalizer(value);
432
+ }
433
+ return normalizeText(value).toLowerCase() || "unknown";
434
+ }
435
+
436
+ const viewer = computed(() => {
437
+ const source = normalizeObject(props.viewer);
438
+ return {
439
+ displayName: normalizeText(source.displayName) || "You",
440
+ avatarUrl: normalizeText(source.avatarUrl)
441
+ };
442
+ });
443
+
444
+ const conversationPickerOpen = ref(false);
445
+ const rootRef = ref(null);
446
+ const messagesPanelRef = ref(null);
447
+ const composerRef = ref(null);
448
+ const shouldAutoScrollToBottom = ref(true);
449
+ const lastRootPointerDownAt = ref(0);
450
+ const renderedAssistantMessagesById = shallowRef(Object.freeze({}));
451
+
452
+ const assistantMarkdownCacheById = new Map();
453
+ let markdownRenderTimeoutId = null;
454
+
455
+ const currentUserScreenName = computed(() => viewer.value.displayName);
456
+ const currentUserAvatarUrl = computed(() => viewer.value.avatarUrl);
457
+ const currentUserInitials = computed(() => {
458
+ const raw = currentUserScreenName.value || "You";
459
+ const parts = raw
460
+ .split(/\s+/)
461
+ .map((part) => normalizeText(part))
462
+ .filter(Boolean);
463
+
464
+ if (parts.length < 1) {
465
+ return "Y";
466
+ }
467
+ if (parts.length === 1) {
468
+ return parts[0].slice(0, 2).toUpperCase();
469
+ }
470
+
471
+ return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
472
+ });
473
+
474
+ function messageAuthorLabel(message) {
475
+ const role = normalizeText(message?.role).toLowerCase();
476
+ if (role === "user") {
477
+ return currentUserScreenName.value;
478
+ }
479
+ if (role === "assistant") {
480
+ return copyText.value.assistantLabel;
481
+ }
482
+
483
+ return copyText.value.systemLabel;
484
+ }
485
+
486
+ function showAssistantTypingIndicator(message) {
487
+ return (
488
+ normalizeText(message?.role).toLowerCase() === "assistant" &&
489
+ normalizeText(message?.status).toLowerCase() === "streaming" &&
490
+ normalizeText(message?.text).length < 1
491
+ );
492
+ }
493
+
494
+ function isAssistantChatMessage(message) {
495
+ return (
496
+ normalizeText(message?.role).toLowerCase() === "assistant" &&
497
+ normalizeText(message?.kind).toLowerCase() === "chat"
498
+ );
499
+ }
500
+
501
+ function keepMessagesPanelPinnedToBottom({ behavior = "auto" } = {}) {
502
+ if (!shouldAutoScrollToBottom.value) {
503
+ return;
504
+ }
505
+
506
+ scrollMessagesToBottom({
507
+ behavior
508
+ });
509
+
510
+ void nextTick(() => {
511
+ if (!shouldAutoScrollToBottom.value) {
512
+ return;
513
+ }
514
+ scrollMessagesToBottom({
515
+ behavior
516
+ });
517
+
518
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
519
+ return;
520
+ }
521
+
522
+ window.requestAnimationFrame(() => {
523
+ if (!shouldAutoScrollToBottom.value) {
524
+ return;
525
+ }
526
+ scrollMessagesToBottom({
527
+ behavior
528
+ });
529
+ });
530
+ });
531
+ }
532
+
533
+ function renderAssistantMarkdownSnapshot() {
534
+ const entries = Array.isArray(messages.value) ? messages.value : [];
535
+ const nextRenderedById = {};
536
+ const activeMessageIds = new Set();
537
+
538
+ for (const message of entries) {
539
+ if (!isAssistantChatMessage(message)) {
540
+ continue;
541
+ }
542
+
543
+ const messageId = String(message?.id || "");
544
+ if (!messageId) {
545
+ continue;
546
+ }
547
+
548
+ activeMessageIds.add(messageId);
549
+ const text = String(message?.text || "");
550
+ const cached = assistantMarkdownCacheById.get(messageId);
551
+ const cacheKey = text;
552
+
553
+ if (cached && cached.cacheKey === cacheKey) {
554
+ nextRenderedById[messageId] = cached.html;
555
+ continue;
556
+ }
557
+
558
+ const renderedHtml = renderMarkdownToSafeHtml(text);
559
+ assistantMarkdownCacheById.set(messageId, {
560
+ cacheKey,
561
+ html: renderedHtml
562
+ });
563
+ nextRenderedById[messageId] = renderedHtml;
564
+ }
565
+
566
+ for (const [messageId] of assistantMarkdownCacheById) {
567
+ if (!activeMessageIds.has(messageId)) {
568
+ assistantMarkdownCacheById.delete(messageId);
569
+ }
570
+ }
571
+
572
+ renderedAssistantMessagesById.value = Object.freeze(nextRenderedById);
573
+ keepMessagesPanelPinnedToBottom({
574
+ behavior: "auto"
575
+ });
576
+ }
577
+
578
+ function scheduleAssistantMarkdownRender({ immediate = false } = {}) {
579
+ if (immediate) {
580
+ if (markdownRenderTimeoutId) {
581
+ clearTimeout(markdownRenderTimeoutId);
582
+ markdownRenderTimeoutId = null;
583
+ }
584
+ renderAssistantMarkdownSnapshot();
585
+ return;
586
+ }
587
+
588
+ if (markdownRenderTimeoutId) {
589
+ return;
590
+ }
591
+
592
+ markdownRenderTimeoutId = setTimeout(() => {
593
+ markdownRenderTimeoutId = null;
594
+ renderAssistantMarkdownSnapshot();
595
+ }, MESSAGE_MARKDOWN_RENDER_THROTTLE_MS);
596
+ }
597
+
598
+ function assistantMessageHtml(message) {
599
+ const messageId = String(message?.id || "");
600
+ if (!messageId) {
601
+ return "";
602
+ }
603
+
604
+ return String(renderedAssistantMessagesById.value[messageId] || "");
605
+ }
606
+
607
+ function resolveConversationActorLabel(conversation) {
608
+ const displayName = normalizeText(conversation?.createdByUserDisplayName);
609
+ if (displayName) {
610
+ return displayName;
611
+ }
612
+
613
+ const email = normalizeText(conversation?.createdByUserEmail);
614
+ if (email) {
615
+ return email;
616
+ }
617
+
618
+ const userId = Number(conversation?.createdByUserId);
619
+ if (Number.isInteger(userId) && userId > 0) {
620
+ return `User #${userId}`;
621
+ }
622
+
623
+ return copyText.value.unknownUser;
624
+ }
625
+
626
+ function conversationSubtitle(conversation) {
627
+ const id = Number(conversation?.id) || 0;
628
+ const status = normalizeConversationStatus(conversation?.status);
629
+ const startedAt = formatConversationStartedAt(conversation?.startedAt);
630
+ const messageCount = Number(conversation?.messageCount || 0);
631
+ const actorSegment = isAdminSurface.value ? ` • ${resolveConversationActorLabel(conversation)}` : "";
632
+ return `#${id} • ${status} • ${startedAt} • ${messageCount} messages${actorSegment}`;
633
+ }
634
+
635
+ function conversationDisplayTitle(conversation) {
636
+ const explicitTitle = normalizeText(conversation?.title);
637
+ if (explicitTitle) {
638
+ return explicitTitle;
639
+ }
640
+
641
+ return copyText.value.unknownConversationTitle;
642
+ }
643
+
644
+ function isActiveConversation(conversation) {
645
+ return String(conversation?.id || "") === String(conversationId.value || "");
646
+ }
647
+
648
+ async function onSelectConversation(conversation) {
649
+ const payload = {
650
+ conversationId: String(conversation?.id || "")
651
+ };
652
+ emit("conversation:select", payload);
653
+ emitInteraction("conversation:select", payload);
654
+ await invokeAction("selectConversation", payload, () => selectConversation(conversation));
655
+ await requestComposerFocus({
656
+ selectText: true
657
+ });
658
+ }
659
+
660
+ async function selectConversationFromPicker(conversation) {
661
+ await onSelectConversation(conversation);
662
+ conversationPickerOpen.value = false;
663
+ await requestComposerFocus({
664
+ selectText: true
665
+ });
666
+ }
667
+
668
+ async function onStartNewConversation() {
669
+ emit("conversation:start", {
670
+ source: "history"
671
+ });
672
+ emitInteraction("conversation:start", {
673
+ source: "history"
674
+ });
675
+ await invokeAction("startNewConversation", {}, startNewConversation);
676
+ await requestComposerFocus({
677
+ selectText: true
678
+ });
679
+ }
680
+
681
+ async function startNewConversationFromPicker() {
682
+ await onStartNewConversation();
683
+ conversationPickerOpen.value = false;
684
+ await requestComposerFocus({
685
+ selectText: true
686
+ });
687
+ }
688
+
689
+ async function onLoadMoreConversations() {
690
+ if (typeof loadMoreConversationHistory !== "function") {
691
+ return;
692
+ }
693
+
694
+ emitInteraction("conversation:load-more");
695
+ await invokeAction("loadMoreConversationHistory", {}, loadMoreConversationHistory);
696
+ }
697
+
698
+ async function onSendMessage() {
699
+ if (isStreaming.value) {
700
+ emit("stream:cancel", {
701
+ source: "composer"
702
+ });
703
+ emitInteraction("stream:cancel", {
704
+ source: "composer"
705
+ });
706
+ await invokeAction("cancelStream", {}, cancelStream);
707
+ return;
708
+ }
709
+
710
+ const payload = {
711
+ textLength: String(input.value || "").length
712
+ };
713
+ emit("message:send", payload);
714
+ emitInteraction("message:send", payload);
715
+ await invokeAction("sendMessage", payload, sendMessage);
716
+ }
717
+
718
+ function onConversationPickerOpen() {
719
+ conversationPickerOpen.value = true;
720
+ emitInteraction("conversation:picker-open");
721
+ }
722
+
723
+ function onHandleInputKeydown(event) {
724
+ emitInteraction("composer:keydown", {
725
+ key: String(event?.key || "")
726
+ });
727
+ if (typeof handleInputKeydown === "function") {
728
+ handleInputKeydown(event);
729
+ }
730
+ }
731
+
732
+ function resolveComposerTextarea() {
733
+ const composer = composerRef.value;
734
+ if (!composer || !(composer.$el instanceof HTMLElement)) {
735
+ return null;
736
+ }
737
+
738
+ const textarea = composer.$el.querySelector("textarea");
739
+ if (!(textarea instanceof HTMLTextAreaElement)) {
740
+ return null;
741
+ }
742
+
743
+ return textarea;
744
+ }
745
+
746
+ function focusComposer(selectText = false) {
747
+ const composer = composerRef.value;
748
+ if (!composer) {
749
+ return false;
750
+ }
751
+
752
+ if (typeof composer.focus === "function") {
753
+ composer.focus();
754
+ }
755
+
756
+ const textarea = resolveComposerTextarea();
757
+ if (!textarea || textarea.disabled) {
758
+ return false;
759
+ }
760
+
761
+ if (selectText && typeof textarea.select === "function") {
762
+ textarea.select();
763
+ } else if (typeof textarea.focus === "function") {
764
+ textarea.focus();
765
+ }
766
+
767
+ return document.activeElement === textarea;
768
+ }
769
+
770
+ async function focusComposerWithRetry(selectText = false) {
771
+ if (focusComposer(selectText)) {
772
+ return;
773
+ }
774
+
775
+ await nextTick();
776
+ if (focusComposer(selectText)) {
777
+ return;
778
+ }
779
+
780
+ await new Promise((resolve) => {
781
+ if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
782
+ window.requestAnimationFrame(() => resolve());
783
+ return;
784
+ }
785
+ setTimeout(resolve, 0);
786
+ });
787
+
788
+ if (focusComposer(selectText)) {
789
+ return;
790
+ }
791
+
792
+ await new Promise((resolve) => {
793
+ setTimeout(resolve, 40);
794
+ });
795
+ focusComposer(selectText);
796
+ }
797
+
798
+ async function requestComposerFocus({ selectText = false } = {}) {
799
+ await focusComposerWithRetry(selectText);
800
+ }
801
+
802
+ function onRootFocus(event) {
803
+ const rootElement = rootRef.value;
804
+ if (!(rootElement instanceof HTMLElement)) {
805
+ return;
806
+ }
807
+ if (event?.target !== rootElement) {
808
+ return;
809
+ }
810
+ if (Date.now() - Number(lastRootPointerDownAt.value || 0) < ROOT_FOCUS_POINTER_GUARD_MS) {
811
+ return;
812
+ }
813
+
814
+ void requestComposerFocus();
815
+ }
816
+
817
+ function onRootPointerDown() {
818
+ lastRootPointerDownAt.value = Date.now();
819
+ }
820
+
821
+ function normalizeScrollValue(value) {
822
+ const parsed = Number(value);
823
+ if (!Number.isFinite(parsed) || parsed < 0) {
824
+ return 0;
825
+ }
826
+ return parsed;
827
+ }
828
+
829
+ function distanceFromBottom(element) {
830
+ if (!element) {
831
+ return Number.POSITIVE_INFINITY;
832
+ }
833
+ const scrollTop = normalizeScrollValue(element.scrollTop);
834
+ const scrollHeight = normalizeScrollValue(element.scrollHeight);
835
+ const clientHeight = normalizeScrollValue(element.clientHeight);
836
+ return Math.max(0, scrollHeight - (scrollTop + clientHeight));
837
+ }
838
+
839
+ function isScrolledToBottom(element) {
840
+ return distanceFromBottom(element) <= SCROLL_BOTTOM_THRESHOLD_PX;
841
+ }
842
+
843
+ function scrollMessagesToBottom({ behavior = "auto" } = {}) {
844
+ const panel = messagesPanelRef.value;
845
+ if (!panel) {
846
+ return;
847
+ }
848
+
849
+ if (behavior === "auto") {
850
+ panel.scrollTop = panel.scrollHeight;
851
+ return;
852
+ }
853
+
854
+ const targetTop = Math.max(0, normalizeScrollValue(panel.scrollHeight) - normalizeScrollValue(panel.clientHeight));
855
+ panel.scrollTo({
856
+ top: targetTop,
857
+ behavior
858
+ });
859
+ }
860
+
861
+ function handleMessagesPanelScroll() {
862
+ const panel = messagesPanelRef.value;
863
+ if (!panel) {
864
+ shouldAutoScrollToBottom.value = true;
865
+ return;
866
+ }
867
+
868
+ shouldAutoScrollToBottom.value = isScrolledToBottom(panel);
869
+ }
870
+
871
+ function syncViewportHeight() {
872
+ if (typeof window === "undefined") {
873
+ return;
874
+ }
875
+
876
+ const rootElement = rootRef.value;
877
+ if (!(rootElement instanceof HTMLElement)) {
878
+ return;
879
+ }
880
+
881
+ const viewportHeight = Number(window.innerHeight) || 0;
882
+ const topOffset = Math.max(0, Number(rootElement.getBoundingClientRect().top) || 0);
883
+ const targetHeight = Math.max(MIN_VIEWPORT_HEIGHT_PX, Math.floor(viewportHeight - topOffset - VIEWPORT_BOTTOM_GUTTER_PX));
884
+
885
+ rootElement.style.setProperty("--assistant-viewport-height", `${targetHeight}px`);
886
+ }
887
+
888
+ const lastMessageSignature = computed(() => {
889
+ const entries = Array.isArray(messages.value) ? messages.value : [];
890
+ const last = entries[entries.length - 1];
891
+ if (!last) {
892
+ return "none";
893
+ }
894
+
895
+ return `${entries.length}|${last.id}|${last.role}|${last.kind}|${String(last.text || "").length}|${last.status}`;
896
+ });
897
+
898
+ const assistantMarkdownSignature = computed(() => {
899
+ const entries = Array.isArray(messages.value) ? messages.value : [];
900
+ return entries
901
+ .filter((message) => isAssistantChatMessage(message))
902
+ .map((message) => `${String(message.id || "")}\u0000${String(message.text || "")}`)
903
+ .join("\u0001");
904
+ });
905
+
906
+ watch(
907
+ () => normalizeText(conversationId.value),
908
+ async (nextConversationId, previousConversationId) => {
909
+ if (!nextConversationId || nextConversationId === previousConversationId) {
910
+ return;
911
+ }
912
+
913
+ shouldAutoScrollToBottom.value = true;
914
+ await nextTick();
915
+ scrollMessagesToBottom();
916
+ await requestComposerFocus();
917
+ },
918
+ {
919
+ immediate: true
920
+ }
921
+ );
922
+
923
+ watch(
924
+ assistantMarkdownSignature,
925
+ () => {
926
+ scheduleAssistantMarkdownRender();
927
+ },
928
+ {
929
+ immediate: true
930
+ }
931
+ );
932
+
933
+ watch(
934
+ lastMessageSignature,
935
+ () => {
936
+ const entries = Array.isArray(messages.value) ? messages.value : [];
937
+ const last = entries[entries.length - 1];
938
+ if (!last) {
939
+ return;
940
+ }
941
+
942
+ keepMessagesPanelPinnedToBottom({
943
+ behavior: "auto"
944
+ });
945
+ },
946
+ {
947
+ immediate: true
948
+ }
949
+ );
950
+
951
+ watch(
952
+ () => isStreaming.value,
953
+ async (isNowStreaming, wasStreaming) => {
954
+ if (isNowStreaming || !wasStreaming) {
955
+ return;
956
+ }
957
+
958
+ scheduleAssistantMarkdownRender({
959
+ immediate: true
960
+ });
961
+
962
+ await nextTick();
963
+ if (shouldAutoScrollToBottom.value) {
964
+ scrollMessagesToBottom({
965
+ behavior: "auto"
966
+ });
967
+ keepMessagesPanelPinnedToBottom({
968
+ behavior: "auto"
969
+ });
970
+ }
971
+ }
972
+ );
973
+
974
+ onMounted(async () => {
975
+ scheduleAssistantMarkdownRender({
976
+ immediate: true
977
+ });
978
+ await nextTick();
979
+ syncViewportHeight();
980
+ window.addEventListener("resize", syncViewportHeight, {
981
+ passive: true
982
+ });
983
+ await requestComposerFocus();
984
+ });
985
+
986
+ onActivated(async () => {
987
+ syncViewportHeight();
988
+ await requestComposerFocus();
989
+ });
990
+
991
+ watch(
992
+ () => isRestoringConversation.value,
993
+ async (isNowRestoring, wasRestoring) => {
994
+ if (isNowRestoring || !wasRestoring) {
995
+ return;
996
+ }
997
+
998
+ await requestComposerFocus();
999
+ }
1000
+ );
1001
+
1002
+ onBeforeUnmount(() => {
1003
+ if (markdownRenderTimeoutId) {
1004
+ clearTimeout(markdownRenderTimeoutId);
1005
+ markdownRenderTimeoutId = null;
1006
+ }
1007
+
1008
+ assistantMarkdownCacheById.clear();
1009
+ if (typeof window === "undefined") {
1010
+ return;
1011
+ }
1012
+
1013
+ window.removeEventListener("resize", syncViewportHeight);
1014
+ });
1015
+ </script>
1016
+
1017
+ <style scoped>
1018
+ .assistant-view {
1019
+ height: var(--assistant-viewport-height, 100dvh);
1020
+ max-height: var(--assistant-viewport-height, 100dvh);
1021
+ min-height: 0;
1022
+ overflow: hidden;
1023
+ padding-block: 0.1rem 0;
1024
+ }
1025
+
1026
+ .assistant-layout {
1027
+ min-height: 0;
1028
+ overflow: hidden;
1029
+ }
1030
+
1031
+ .assistant-client-element--layout-compact .messages-panel {
1032
+ padding: 0.6rem 0.72rem;
1033
+ }
1034
+
1035
+ .assistant-client-element--surface-plain .assistant-main-card,
1036
+ .assistant-client-element--surface-plain .assistant-history-card,
1037
+ .assistant-client-element--surface-plain .assistant-tools-card {
1038
+ box-shadow: none;
1039
+ border-width: 0;
1040
+ }
1041
+
1042
+ .assistant-client-element--density-compact :deep(.v-card-item) {
1043
+ padding-block: 0.6rem;
1044
+ }
1045
+
1046
+ .assistant-client-element--tone-emphasized .message-row--user .message-bubble {
1047
+ --bubble-bg: rgba(var(--v-theme-primary), 0.22);
1048
+ }
1049
+
1050
+ .assistant-main-col,
1051
+ .assistant-side-col,
1052
+ .assistant-main-card,
1053
+ .assistant-main-card-text,
1054
+ .assistant-history-card,
1055
+ .assistant-tools-card,
1056
+ .assistant-history-card-text,
1057
+ .assistant-history-list,
1058
+ .assistant-tools-list {
1059
+ min-height: 0;
1060
+ }
1061
+
1062
+ .messages-panel {
1063
+ flex: 1 1 auto;
1064
+ border: 1px solid rgba(var(--v-theme-on-surface), 0.14);
1065
+ border-radius: 10px;
1066
+ padding: 12px;
1067
+ min-height: 0;
1068
+ overflow: auto;
1069
+ overflow-anchor: none;
1070
+ background: rgba(var(--v-theme-surface-variant), 0.14);
1071
+ }
1072
+
1073
+ .messages-panel :deep(*) {
1074
+ overflow-anchor: none;
1075
+ }
1076
+
1077
+ .messages-panel--empty {
1078
+ display: grid;
1079
+ place-items: center;
1080
+ }
1081
+
1082
+ .messages-empty-state {
1083
+ min-height: 240px;
1084
+ text-align: center;
1085
+ font-size: clamp(1.25rem, 1.2rem + 1vw, 2rem);
1086
+ font-weight: 600;
1087
+ line-height: 1.2;
1088
+ color: rgba(var(--v-theme-on-surface), 0.68);
1089
+ }
1090
+
1091
+ .message-body {
1092
+ max-width: min(82%, 700px);
1093
+ }
1094
+
1095
+ .message-row--user .message-body {
1096
+ align-items: flex-end;
1097
+ }
1098
+
1099
+ .message-avatar {
1100
+ border: 2px solid rgba(var(--v-theme-surface), 0.95);
1101
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
1102
+ flex: 0 0 auto;
1103
+ }
1104
+
1105
+ .message-avatar--assistant {
1106
+ background: linear-gradient(180deg, #2f9a45, #1f7a35) !important;
1107
+ }
1108
+
1109
+ .message-avatar-initials {
1110
+ color: rgba(var(--v-theme-on-primary), 1);
1111
+ font-size: 0.75rem;
1112
+ font-weight: 700;
1113
+ letter-spacing: 0.04em;
1114
+ text-transform: uppercase;
1115
+ }
1116
+
1117
+ .message-bubble {
1118
+ --bubble-bg: rgba(var(--v-theme-surface), 0.92);
1119
+ --bubble-border: rgba(var(--v-theme-on-surface), 0.16);
1120
+ background: var(--bubble-bg);
1121
+ border: 1px solid var(--bubble-border);
1122
+ border-radius: 16px;
1123
+ box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08);
1124
+ padding: 10px 12px;
1125
+ position: relative;
1126
+ }
1127
+
1128
+ .message-row--assistant .message-bubble::after,
1129
+ .message-row--user .message-bubble::after {
1130
+ background: var(--bubble-bg);
1131
+ bottom: 11px;
1132
+ content: "";
1133
+ height: 12px;
1134
+ position: absolute;
1135
+ width: 12px;
1136
+ }
1137
+
1138
+ .message-row--assistant .message-bubble::after {
1139
+ clip-path: polygon(100% 0, 0 50%, 100% 100%);
1140
+ left: -10px;
1141
+ }
1142
+
1143
+ .message-row--user .message-bubble {
1144
+ --bubble-bg: rgba(var(--v-theme-primary), 0.14);
1145
+ --bubble-border: rgba(var(--v-theme-primary), 0.34);
1146
+ }
1147
+
1148
+ .message-row--user .message-bubble::after {
1149
+ clip-path: polygon(0 0, 100% 50%, 0 100%);
1150
+ right: -10px;
1151
+ }
1152
+
1153
+ .message-author {
1154
+ color: rgba(var(--v-theme-on-surface), 0.84);
1155
+ font-weight: 600;
1156
+ letter-spacing: 0.01em;
1157
+ }
1158
+
1159
+ .message-row--user .message-author {
1160
+ color: rgba(var(--v-theme-primary), 1);
1161
+ }
1162
+
1163
+ .message-text {
1164
+ white-space: pre-wrap;
1165
+ word-break: break-word;
1166
+ }
1167
+
1168
+ .message-text--markdown {
1169
+ white-space: normal;
1170
+ }
1171
+
1172
+ .message-text--markdown :deep(p),
1173
+ .message-text--markdown :deep(ul),
1174
+ .message-text--markdown :deep(ol),
1175
+ .message-text--markdown :deep(pre),
1176
+ .message-text--markdown :deep(blockquote),
1177
+ .message-text--markdown :deep(h1),
1178
+ .message-text--markdown :deep(h2),
1179
+ .message-text--markdown :deep(h3),
1180
+ .message-text--markdown :deep(h4) {
1181
+ margin-block: 0 0.6rem;
1182
+ }
1183
+
1184
+ .message-text--markdown :deep(p:last-child),
1185
+ .message-text--markdown :deep(ul:last-child),
1186
+ .message-text--markdown :deep(ol:last-child),
1187
+ .message-text--markdown :deep(pre:last-child),
1188
+ .message-text--markdown :deep(blockquote:last-child),
1189
+ .message-text--markdown :deep(h1:last-child),
1190
+ .message-text--markdown :deep(h2:last-child),
1191
+ .message-text--markdown :deep(h3:last-child),
1192
+ .message-text--markdown :deep(h4:last-child) {
1193
+ margin-bottom: 0;
1194
+ }
1195
+
1196
+ .message-text--markdown :deep(code) {
1197
+ background: rgba(var(--v-theme-on-surface), 0.08);
1198
+ border-radius: 4px;
1199
+ padding: 0.1rem 0.28rem;
1200
+ }
1201
+
1202
+ .message-text--markdown :deep(pre) {
1203
+ background: rgba(var(--v-theme-on-surface), 0.06);
1204
+ border-radius: 8px;
1205
+ overflow-x: auto;
1206
+ padding: 0.6rem 0.72rem;
1207
+ }
1208
+
1209
+ .message-text--markdown :deep(pre code) {
1210
+ background: transparent;
1211
+ padding: 0;
1212
+ }
1213
+
1214
+ .message-typing {
1215
+ min-height: 20px;
1216
+ }
1217
+
1218
+ .message-typing-dot {
1219
+ animation: message-typing-blink 1.1s infinite ease-in-out;
1220
+ background: rgba(var(--v-theme-on-surface), 0.62);
1221
+ border-radius: 50%;
1222
+ display: inline-block;
1223
+ height: 7px;
1224
+ width: 7px;
1225
+ }
1226
+
1227
+ .message-typing-dot:nth-child(2) {
1228
+ animation-delay: 0.16s;
1229
+ }
1230
+
1231
+ .message-typing-dot:nth-child(3) {
1232
+ animation-delay: 0.32s;
1233
+ }
1234
+
1235
+ @keyframes message-typing-blink {
1236
+ 0%,
1237
+ 80%,
1238
+ 100% {
1239
+ opacity: 0.3;
1240
+ transform: translateY(0);
1241
+ }
1242
+ 40% {
1243
+ opacity: 1;
1244
+ transform: translateY(-2px);
1245
+ }
1246
+ }
1247
+
1248
+ .assistant-composer-shell {
1249
+ border: 1px solid rgba(var(--v-theme-on-surface), 0.12);
1250
+ background: rgba(var(--v-theme-surface), 0.94);
1251
+ border-radius: 18px;
1252
+ padding: 0.4rem 0.5rem;
1253
+ gap: 0.3rem;
1254
+ box-shadow: 0 8px 20px rgba(17, 26, 42, 0.05);
1255
+ }
1256
+
1257
+ .assistant-composer-textarea {
1258
+ min-width: 0;
1259
+ }
1260
+
1261
+ .assistant-composer-textarea :deep(.v-field) {
1262
+ border-radius: 14px;
1263
+ background: rgba(var(--v-theme-on-surface), 0.03);
1264
+ }
1265
+
1266
+ .assistant-composer-textarea :deep(.v-field__outline),
1267
+ .assistant-composer-textarea :deep(.v-field::before),
1268
+ .assistant-composer-textarea :deep(.v-field::after) {
1269
+ display: none;
1270
+ }
1271
+
1272
+ .assistant-composer-textarea :deep(.v-field__overlay) {
1273
+ display: none;
1274
+ }
1275
+
1276
+ .assistant-composer-textarea :deep(.v-field__input) {
1277
+ padding-block: 0.5rem 0.46rem;
1278
+ }
1279
+
1280
+ .assistant-composer-textarea :deep(textarea) {
1281
+ line-height: 1.45;
1282
+ }
1283
+
1284
+ .assistant-history-start-button {
1285
+ flex: 0 0 auto;
1286
+ }
1287
+
1288
+ .assistant-stop-button {
1289
+ background-color: #c62828 !important;
1290
+ border: 3px solid #ffffff !important;
1291
+ border-radius: 0 !important;
1292
+ clip-path: polygon(30% 0, 70% 0, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0 70%, 0 30%);
1293
+ color: #ffffff !important;
1294
+ font-weight: 800;
1295
+ letter-spacing: 0.08em;
1296
+ min-height: 46px;
1297
+ min-width: 70px;
1298
+ text-transform: uppercase;
1299
+ }
1300
+
1301
+ @media (min-width: 1280px) {
1302
+ .assistant-layout {
1303
+ flex-wrap: nowrap;
1304
+ }
1305
+
1306
+ .assistant-history-card {
1307
+ flex: 1 1 auto;
1308
+ }
1309
+
1310
+ .assistant-tools-card {
1311
+ flex: 0 0 var(--assistant-tools-panel-height, 320px);
1312
+ max-height: var(--assistant-tools-panel-height, 320px);
1313
+ min-height: var(--assistant-tools-panel-height, 320px);
1314
+ }
1315
+ }
1316
+ </style>