@8wave/ai-elements 0.119.0-beta.49 → 0.119.0-beta.54

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 (88) hide show
  1. package/dist/_chunks/Agent-DU38SnNd.js.map +1 -1
  2. package/dist/_chunks/markdown-BKLfFJPr.js.map +1 -1
  3. package/dist/ai-elements.es.js +4 -4
  4. package/dist/ai-elements.es.js.map +1 -1
  5. package/dist-vue/PkChatbot.js +1 -1
  6. package/dist-vue/PkChatbotEmptyState.js +1 -1
  7. package/dist-vue/PkChatbotInput.js +1 -1
  8. package/dist-vue/PkChatbotMessages.js +1 -1
  9. package/dist-vue/PkChatbotStepPayload.js +1 -1
  10. package/dist-vue/PkChatbotStepToolDetail.js +1 -1
  11. package/dist-vue/PkChatbotSteps.js +1 -1
  12. package/dist-vue/PkChatbotViewChat.js +1 -1
  13. package/dist-vue/PkChatbotViewConversations.js +1 -1
  14. package/dist-vue/PkChatbotViewFiles.js +1 -1
  15. package/dist-vue/PkChatbotViewProfile.js +1 -1
  16. package/dist-vue/_chunks/{Agent-DDm9cCaN.js → Agent-DUdd9NbF.js} +79 -70
  17. package/dist-vue/_chunks/Agent-DUdd9NbF.js.map +1 -0
  18. package/dist-vue/_chunks/{Document-CZemm1AR.js → Document-B4q9uYmN.js} +10 -10
  19. package/dist-vue/_chunks/Document-B4q9uYmN.js.map +1 -0
  20. package/dist-vue/_chunks/{PkChatbot-viqlDa9r.js → PkChatbot-Ck6RO0Rd.js} +8 -8
  21. package/dist-vue/_chunks/{PkChatbot-viqlDa9r.js.map → PkChatbot-Ck6RO0Rd.js.map} +1 -1
  22. package/dist-vue/_chunks/{PkChatbotEmptyState-B-2GZ2ZU.js → PkChatbotEmptyState-DhJDJhxj.js} +2 -2
  23. package/dist-vue/_chunks/{PkChatbotEmptyState-B-2GZ2ZU.js.map → PkChatbotEmptyState-DhJDJhxj.js.map} +1 -1
  24. package/dist-vue/_chunks/{PkChatbotInput-D4bT_1be.js → PkChatbotInput--ULB71Ld.js} +2 -2
  25. package/dist-vue/_chunks/{PkChatbotInput-D4bT_1be.js.map → PkChatbotInput--ULB71Ld.js.map} +1 -1
  26. package/dist-vue/_chunks/{PkChatbotMessages-BgcGen7D.js → PkChatbotMessages-Bni4EjEB.js} +17 -17
  27. package/dist-vue/_chunks/{PkChatbotMessages-BgcGen7D.js.map → PkChatbotMessages-Bni4EjEB.js.map} +1 -1
  28. package/dist-vue/_chunks/{PkChatbotStepPayload-CzCHmvUG.js → PkChatbotStepPayload-D8Y5I01D.js} +2 -2
  29. package/dist-vue/_chunks/{PkChatbotStepPayload-CzCHmvUG.js.map → PkChatbotStepPayload-D8Y5I01D.js.map} +1 -1
  30. package/dist-vue/_chunks/{PkChatbotStepToolDetail-qSHrVOGO.js → PkChatbotStepToolDetail-CwstaTuG.js} +3 -3
  31. package/dist-vue/_chunks/{PkChatbotStepToolDetail-qSHrVOGO.js.map → PkChatbotStepToolDetail-CwstaTuG.js.map} +1 -1
  32. package/dist-vue/_chunks/{PkChatbotSteps-dgj6C2hA.js → PkChatbotSteps-Caz3eJgv.js} +5 -5
  33. package/dist-vue/_chunks/{PkChatbotSteps-dgj6C2hA.js.map → PkChatbotSteps-Caz3eJgv.js.map} +1 -1
  34. package/dist-vue/_chunks/{PkChatbotViewChat-BtbE6V2u.js → PkChatbotViewChat-B_T3HkON.js} +7 -7
  35. package/dist-vue/_chunks/{PkChatbotViewChat-BtbE6V2u.js.map → PkChatbotViewChat-B_T3HkON.js.map} +1 -1
  36. package/dist-vue/_chunks/{PkChatbotViewConversations-Cw9fL47_.js → PkChatbotViewConversations-CvGu-4t3.js} +3 -3
  37. package/dist-vue/_chunks/{PkChatbotViewConversations-Cw9fL47_.js.map → PkChatbotViewConversations-CvGu-4t3.js.map} +1 -1
  38. package/dist-vue/_chunks/{PkChatbotViewFiles-DDEJqXct.js → PkChatbotViewFiles-CWyaCSXx.js} +3 -3
  39. package/dist-vue/_chunks/{PkChatbotViewFiles-DDEJqXct.js.map → PkChatbotViewFiles-CWyaCSXx.js.map} +1 -1
  40. package/dist-vue/_chunks/{PkChatbotViewProfile-B79UynQ8.js → PkChatbotViewProfile-gGx0SEXR.js} +2 -2
  41. package/dist-vue/_chunks/{PkChatbotViewProfile-B79UynQ8.js.map → PkChatbotViewProfile-gGx0SEXR.js.map} +1 -1
  42. package/dist-vue/_chunks/{PkStreamingMarkdown-DXoUbGsP.js → PkStreamingMarkdown-DZ8jmXzh.js} +3 -3
  43. package/dist-vue/_chunks/{PkStreamingMarkdown-DXoUbGsP.js.map → PkStreamingMarkdown-DZ8jmXzh.js.map} +1 -1
  44. package/dist-vue/_chunks/{PkToolEditSpreadsheet-Dcp3VFzY.js → PkToolEditSpreadsheet-CtRusPSR.js} +2 -2
  45. package/dist-vue/_chunks/{PkToolEditSpreadsheet-Dcp3VFzY.js.map → PkToolEditSpreadsheet-CtRusPSR.js.map} +1 -1
  46. package/dist-vue/_chunks/{PkToolRequestGeolocation-DG4-qKRq.js → PkToolRequestGeolocation-P_QxEXD5.js} +2 -2
  47. package/dist-vue/_chunks/{PkToolRequestGeolocation-DG4-qKRq.js.map → PkToolRequestGeolocation-P_QxEXD5.js.map} +1 -1
  48. package/dist-vue/_chunks/{PkToolRequestOAuthConnection-CZhNEM-o.js → PkToolRequestOAuthConnection-BlngXdPM.js} +2 -2
  49. package/dist-vue/_chunks/{PkToolRequestOAuthConnection-CZhNEM-o.js.map → PkToolRequestOAuthConnection-BlngXdPM.js.map} +1 -1
  50. package/dist-vue/_chunks/{PkToolShowArtifact-BRtz1x7K.js → PkToolShowArtifact-CoBUA6lk.js} +2 -2
  51. package/dist-vue/_chunks/{PkToolShowArtifact-BRtz1x7K.js.map → PkToolShowArtifact-CoBUA6lk.js.map} +1 -1
  52. package/dist-vue/_chunks/{PkToolShowContactForm-D0CR80e2.js → PkToolShowContactForm-BjE4mrDv.js} +2 -2
  53. package/dist-vue/_chunks/{PkToolShowContactForm-D0CR80e2.js.map → PkToolShowContactForm-BjE4mrDv.js.map} +1 -1
  54. package/dist-vue/_chunks/{PkToolShowDiagram-CYpqqWLo.js → PkToolShowDiagram-CVmV8ip6.js} +2 -2
  55. package/dist-vue/_chunks/{PkToolShowDiagram-CYpqqWLo.js.map → PkToolShowDiagram-CVmV8ip6.js.map} +1 -1
  56. package/dist-vue/_chunks/{PkToolShowForm-BIsu4XhQ.js → PkToolShowForm-CbAHlcVY.js} +4 -4
  57. package/dist-vue/_chunks/{PkToolShowForm-BIsu4XhQ.js.map → PkToolShowForm-CbAHlcVY.js.map} +1 -1
  58. package/dist-vue/_chunks/{PkToolShowImageGallery-DkEv-6Nu.js → PkToolShowImageGallery-lwId4MH0.js} +2 -2
  59. package/dist-vue/_chunks/{PkToolShowImageGallery-DkEv-6Nu.js.map → PkToolShowImageGallery-lwId4MH0.js.map} +1 -1
  60. package/dist-vue/_chunks/{PkToolShowProductList-Dr2mQqsb.js → PkToolShowProductList-uilv8927.js} +2 -2
  61. package/dist-vue/_chunks/{PkToolShowProductList-Dr2mQqsb.js.map → PkToolShowProductList-uilv8927.js.map} +1 -1
  62. package/dist-vue/_chunks/{PkToolShowSources-CxUDh6cC.js → PkToolShowSources-Cg2POjp8.js} +3 -3
  63. package/dist-vue/_chunks/{PkToolShowSources-CxUDh6cC.js.map → PkToolShowSources-Cg2POjp8.js.map} +1 -1
  64. package/dist-vue/_chunks/{PkToolShowSuggestedReply-Cl3Pswi4.js → PkToolShowSuggestedReply-CMXG6Ntj.js} +2 -2
  65. package/dist-vue/_chunks/{PkToolShowSuggestedReply-Cl3Pswi4.js.map → PkToolShowSuggestedReply-CMXG6Ntj.js.map} +1 -1
  66. package/dist-vue/_chunks/{PkToolShowVideo-Bh1oagOm.js → PkToolShowVideo-D04Xeojs.js} +2 -2
  67. package/dist-vue/_chunks/{PkToolShowVideo-Bh1oagOm.js.map → PkToolShowVideo-D04Xeojs.js.map} +1 -1
  68. package/dist-vue/_chunks/{PkToolShowWebPages-ilQ1qTEC.js → PkToolShowWebPages-D7bEHkyy.js} +2 -2
  69. package/dist-vue/_chunks/{PkToolShowWebPages-ilQ1qTEC.js.map → PkToolShowWebPages-D7bEHkyy.js.map} +1 -1
  70. package/dist-vue/_chunks/{createChatbotApiClient-DrUsRvCw.js → createChatbotApiClient-pAQ5kfjL.js} +14 -14
  71. package/dist-vue/_chunks/{createChatbotApiClient-DrUsRvCw.js.map → createChatbotApiClient-pAQ5kfjL.js.map} +1 -1
  72. package/dist-vue/_chunks/{useChatbotStore-DHmjHPR5.js → useChatbotStore-B2KlwdHy.js} +3 -3
  73. package/dist-vue/_chunks/{useChatbotStore-DHmjHPR5.js.map → useChatbotStore-B2KlwdHy.js.map} +1 -1
  74. package/dist-vue/_chunks/{utils-Bopp5IPf.js → utils-JCvPK7lV.js} +2 -2
  75. package/dist-vue/_chunks/{utils-Bopp5IPf.js.map → utils-JCvPK7lV.js.map} +1 -1
  76. package/dist-vue/api.js +1 -1
  77. package/dist-vue/composables.js +7 -7
  78. package/dist-vue/composables.js.map +1 -1
  79. package/dist-vue/index.js +3367 -3361
  80. package/dist-vue/index.js.map +1 -1
  81. package/dist-vue/locales.js +4 -0
  82. package/dist-vue/packages/auth/src/invitation-resend-plugin.d.ts +16 -0
  83. package/dist-vue/packages/models/src/schema/Agent.d.ts +22 -0
  84. package/dist-vue/packages/models/src/schema/Invitation.d.ts +7 -0
  85. package/dist-vue/packages/models/src/schema/constants.d.ts +1 -1
  86. package/package.json +1 -1
  87. package/dist-vue/_chunks/Agent-DDm9cCaN.js.map +0 -1
  88. package/dist-vue/_chunks/Document-CZemm1AR.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"PkChatbotMessages-BgcGen7D.js","names":["core._isoDateTime","z.object","z.string","z.uuid","z.iso.datetime","_hoisted_1","_openBlock","_createElementBlock","_toDisplayString","_createCommentVNode","_sfc_main","PkToolApprovalPreviewMessage","_hoisted_1","__unplugin_components_0","__unplugin_components_1","__unplugin_components_2","_openBlock","_createElementBlock","_normalizeClass","_createVNode","_Transition","_withCtx","_toDisplayString","_createCommentVNode","_createBlock","_resolveDynamicComponent","_unref","_withModifiers","_createTextVNode","_sfc_main","$emit","__unplugin_components_0","__unplugin_components_1","__unplugin_components_2","_openBlock","_createElementBlock","_normalizeClass","_normalizeStyle","_createVNode","PkChatbotOutlineRail","_createCommentVNode","_createElementVNode","_Fragment","_renderList","_toDisplayString","_unref","_createTextVNode","_Transition","_withCtx","_createBlock","PkChatbotSteps","_renderSlot","_mergeProps","PkStreamingMarkdown","PkChatbotFilePreview","PkChatbotAudioTranscript","PkToolApprovalCard","_resolveDynamicComponent","PkChatbotMessageFeedback","PkStreamingMarkdownAutoscroll","_withModifiers","PkRelativeTime","PkChatbotFeedbackForm","PkChatbotError","_sfc_main","_isoDateTime","uuid","string","ZodISODateTime","object","isAudioFile","isVoiceRenderAck","VOICE_RENDERED_TOOLS","AgentBuiltInTool","USER_ANSWERED_TOOLS","VvButtonLocalized","PkStreamingMarkdown_default","isTextPart","getToolPartName","isToolPart","getFilePartTranscript","isFilePart","isReasoningPart","getFilePartMedia","PkChatbotAudioTranscript_default","toolLabel","isToolConfirmPart","moveFileOutputsLast","resolveContrastColor","mergeConsecutiveTextParts","dropCorrectedWidgetErrors","isConfirmationCardPart","getPartIcon","getToolPartLabel","getToolApprovalId","getPartTypeLabel","LIVE_REASONING_TAIL_LENGTH","feedPartKey","getPartState","CONFIRM_TOOL_NAME","formatDuration","PkChatbotError_default","PkRelativeTime_default","PkChatbotFilePreview_default","PkChatbotFeedbackForm_default","PkChatbotMessageFeedback_default","PkStreamingMarkdownAutoscroll_default","PkChatbotSteps_default","PkChatbotOutlineRail_default","datetime","params","PendingWakeupSchema","isWakeupSeed","message","PENDING_TOOL_STATES","CARD_TOOLS","stateOf","part","toolNameOf","wakeupAnswerShows","state","toolName","isQuietWakeupAnswer","noop","toolComponentMap","defineAsyncComponent","n","_hoisted_1$2","PkToolApprovalPreviewMessage_default","defineComponent","props","__props","computed","input","openBlock","createElementBlock","toDisplayString","createCommentVNode","toolApprovalPreviews","_hoisted_1$1","PkToolApprovalCard_default","__emit","emit","$t","$te","useI18n","toolPart","isConfirm","isOpen","isPending","isExpired","isApproved","isDenied","preview","prompt","outcome","isBare","_component_VvButton","_component_VvButtonGroup","VvButtonGroup","_component_VvIcon","VvIcon","normalizeClass","createVNode","Transition","withCtx","createBlock","resolveDynamicComponent","createTextVNode","unref","_cache","withModifiers","NEAR_BOTTOM_THRESHOLD","SMOOTH_SCROLL_DURATION","useChatScroll","options","userScrolledUp","ref","isAtBottom","isAutoScrolling","mutationObserver","resizeObserver","initMutationObserver","contentResizeObserver","lastScrollTop","scrollToBottom","scrollEl","behavior","onWheel","event","lastTouchY","onTouchStart","onTouchMove","y","handleScroll","currentScrollTop","scrollHeight","clientHeight","onScrollUp","onScrollDown","autoFollow","el","bottom","startMutationScroll","stopMutationScroll","watch","status","newStatus","oldStatus","messagesLength","lastMessageRole","cleanupInitialObservers","nextTick","beforeNewMessageScroll","initialScrollTimer","tryInitialScroll","onMounted","isSendInFlight","wrapper","contentEl","onBeforeUnmount","buildWakeupFeed","isQuiet","draws","hidden","quietRuns","index","messages","start","count","lastAt","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","_hoisted_27","_hoisted_28","_hoisted_29","_hoisted_30","PkChatbotMessages_default","$d","$n","slots","useSlots","onCardRespond","toolCallId","approved","id","useTemplateRef","wrapperEl","contrastColor","updateAnchorSpacer","onScrollToBottomClick","outlineItems","activeOutlineId","getUserMessageEls","updateActiveOutline","scroller","line","active","onScroll","scrollToUserMessage","item","messageEl","isLoading","height","isSpacerSettling","isSpacerHeld","anchoredUserMessageEl","shallowRef","scrollElHeight","paddingTop","paddingBottom","wrapperGap","userMessageEls","newHeight","useResizeObserver","loading","isRevised","r","messageId","getMessageFeedback","f","RECORD_TOOLS","recordToolProps","isFeedbackButtonVisible","isActionButtonVisible","kind","readonlyFeedbackOf","feedbackVote","feedbackEmoji","feedback","raw","hasReaction","reactionToneClass","vote","reactionTitle","lastTextPartIndex","parts","getMergedParts","activeMessage","activeMessageLastPart","mergedPartsMap","map","isError","isSpokenWaitVisible","isSpokenWaitAnchored","activeMessageLastPartLabel","activeMessageLastPartIcon","getMessageIndexById","m","isLastMessage","isMessageRegenerateButtonVisible","action","isLastTextPart","lastPart","messageHasSteps","showStepsTimeline","isAssistant","isRenderedToolPart","isStreamingPart","messageHasContent","isUser","getPreviousAssistantMessage","getNextAssistantMessage","getDividerDate","previousCreatedAt","nextCreatedAt","previousDate","nextDate","wakeupFeed","messageNumbers","showMessageFooter","formatMessageTime","date","createdAt","isToday","getMessageDuration","completedAt","feedbackMessageEl","normalizeStyle","createElementVNode","Fragment","renderList","partIndex","renderSlot","_ctx","mergeProps","$event"],"sources":["../../../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/iso.js","../../../../packages/models/src/schema/ChatWakeup.ts","../../../../packages/components/src/chat/toolComponentMap.ts","../../../../packages/components/src/chat/PkToolApprovalPreviewMessage.vue","../../../../packages/components/src/chat/PkToolApprovalPreviewMessage.vue","../../../../packages/components/src/chat/toolApprovalPreviews.ts","../../../../packages/components/src/chat/PkToolApprovalCard.vue","../../../../packages/components/src/chat/PkToolApprovalCard.vue","../../../../packages/components/src/chat/useChatScroll.ts","../../../../packages/components/src/chat/wakeupFeed.ts","../../../../packages/components/src/chat/PkChatbotMessages.vue","../../../../packages/components/src/chat/PkChatbotMessages.vue"],"sourcesContent":["import * as core from \"../core/index.js\";\nimport { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from \"./schemas.js\";\nexport { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from \"./schemas.js\";\nexport function datetime(params) {\n return core._isoDateTime(ZodISODateTime, params);\n}\nexport function date(params) {\n return core._isoDate(ZodISODate, params);\n}\nexport function time(params) {\n return core._isoTime(ZodISOTime, params);\n}\nexport function duration(params) {\n return core._isoDuration(ZodISODuration, params);\n}\n","import * as z from 'zod'\nimport {\n AgentBuiltInTool,\n USER_ANSWERED_TOOLS,\n VOICE_RENDERED_TOOLS,\n} from './Agent'\nimport type { UIChatMessage } from './Chat'\n\n/**\n * Lifecycle of a wakeup (spec agent-wakeup). `running` to `skipped` follow the\n * run-ledger convention of the headless runs; `replaced` and `stopped` close a\n * wakeup that never ran.\n */\nexport const ChatWakeupStatus = {\n Scheduled: 'scheduled',\n Running: 'running',\n Succeeded: 'succeeded',\n Failed: 'failed',\n Skipped: 'skipped',\n /** A later call of the same chat took its place. */\n Replaced: 'replaced',\n /** `stop: true`, or the user's \"Stop\" in the chat. */\n Stopped: 'stopped',\n} as const\n\nexport type ChatWakeupStatus =\n (typeof ChatWakeupStatus)[keyof typeof ChatWakeupStatus]\n\n/** Where a platform wakeup posts its answer: the Chat SDK thread, not the channel. */\nexport const ChatWakeupDeliveryTargetSchema = z.object({\n platform: z.string(),\n threadId: z.string(),\n})\nexport type ChatWakeupDeliveryTarget = z.infer<\n typeof ChatWakeupDeliveryTargetSchema\n>\n\n/** The wakeup still waiting in a chat, as the chat UI needs it. */\nexport const PendingWakeupSchema = z.object({\n wakeId: z.uuid(),\n dueAt: z.iso.datetime(),\n reason: z.string(),\n})\nexport type PendingWakeup = z.infer<typeof PendingWakeupSchema>\n\nexport const ChatWakeupStateSchema = z.object({\n pendingWakeup: PendingWakeupSchema.nullable(),\n})\nexport type ChatWakeupState = z.infer<typeof ChatWakeupStateSchema>\n\n/** The body of a scheduled Service Bus message, and of the internal fire call. */\nexport const WakeupFireJobSchema = z.object({\n wakeId: z.uuid(),\n})\nexport type WakeupFireJob = z.infer<typeof WakeupFireJobSchema>\n\n/** Delays a wakeup may take, in seconds: the clamp of Claude Code's ScheduleWakeup. */\nexport const WAKEUP_MIN_DELAY_SECONDS = 60\nexport const WAKEUP_MAX_DELAY_SECONDS = 3600\n\n/** The user message a wakeup injected: the agent's own prompt coming back, not the user's words. */\nexport const isWakeupSeed = (message?: UIChatMessage): boolean =>\n message?.role === 'user' && Boolean(message.metadata?.wakeup)\n\n/** Tool states that still wait for something: the model's input or the user's approval. */\nconst PENDING_TOOL_STATES = new Set([\n 'input-streaming',\n 'input-available',\n 'approval-requested',\n])\n\n/**\n * Tools the chat draws as a card: the client widgets, the cards that ask the\n * user something, and `editSpreadsheet`, a server tool whose output is the\n * download card of the new version.\n */\nconst CARD_TOOLS: ReadonlySet<string> = new Set([\n ...VOICE_RENDERED_TOOLS,\n ...USER_ANSWERED_TOOLS,\n 'editSpreadsheet',\n])\n\n/**\n * Cards that ask the user something. The contact form is one too, although\n * its lead goes through an endpoint of its own and never answers the turn.\n */\nconst QUESTION_TOOLS: ReadonlySet<string> = new Set([\n ...USER_ANSWERED_TOOLS,\n AgentBuiltInTool.ShowContactForm,\n])\n\ntype MessagePart = UIChatMessage['parts'][number]\n\nconst stateOf = (part: MessagePart): string | undefined =>\n (part as { state?: string }).state\n\nconst toolNameOf = (part: MessagePart): string | undefined =>\n part.type.startsWith('tool-') ? part.type.slice('tool-'.length) : undefined\n\n/**\n * Whether a wakeup answer shows the user anything: words, a file, a card, or a\n * tool call still waiting, such as one held for the user's approval. It is\n * what makes a round news (spec agent-wakeup, Fase 1.1), whatever its `noop`\n * said: the model's history keeps the round, the feed draws it and the user\n * gets a notification.\n */\nexport const wakeupAnswerShows = (message?: UIChatMessage): boolean =>\n message?.role === 'assistant' &&\n message.parts.some((part) => {\n if (part.type === 'text') {\n return part.text.trim().length > 0\n }\n if (part.type === 'file') {\n return true\n }\n const state = stateOf(part)\n if (state && PENDING_TOOL_STATES.has(state)) {\n return true\n }\n const toolName = toolNameOf(part)\n return (\n toolName !== undefined &&\n CARD_TOOLS.has(toolName) &&\n state === 'output-available'\n )\n })\n\n/**\n * Whether a wakeup answer leaves the user a question: a card that asks\n * something, still unanswered. Such a round ends its loop, and the answer\n * opens a normal turn (spec agent-wakeup, \"Una domanda ferma il ciclo\").\n */\nexport const wakeupAnswerAsks = (message?: UIChatMessage): boolean =>\n message?.role === 'assistant' &&\n message.parts.some((part) => {\n const toolName = toolNameOf(part)\n return (\n toolName !== undefined &&\n QUESTION_TOOLS.has(toolName) &&\n stateOf(part) === 'input-available'\n )\n })\n\n/**\n * Whether an answer still waits for the user to answer it in the chat: a card\n * answered through the turn, or a call held for the user's approval. A wakeup\n * round must not run below it, because the answer resumes the last message\n * only, and a history with a newer turn records the approval as denied. The\n * contact form does not count: its lead goes through an endpoint of its own,\n * wherever its card is, and its part never leaves `input-available`.\n */\nexport const answerAwaitsUser = (message?: UIChatMessage): boolean =>\n message?.role === 'assistant' &&\n message.parts.some((part) => {\n const state = stateOf(part)\n if (state === 'approval-requested') {\n return true\n }\n const toolName = toolNameOf(part)\n return (\n toolName !== undefined &&\n USER_ANSWERED_TOOLS.has(toolName) &&\n state === 'input-available'\n )\n })\n\n/**\n * An answer that only told its loop nothing had changed: a finished\n * `scheduleWakeup` call with `noop`, and nothing shown to the user\n * ({@link wakeupAnswerShows}). The one rule shared by the model's history\n * (which drops such rounds) and the chat feed (which folds them).\n */\nexport const isQuietWakeupAnswer = (message?: UIChatMessage): boolean => {\n if (message?.role !== 'assistant' || wakeupAnswerShows(message)) {\n return false\n }\n let noop = false\n for (const part of message.parts) {\n if (part.type === 'tool-scheduleWakeup') {\n noop =\n stateOf(part) === 'output-available' &&\n (part as { input?: { noop?: boolean } }).input?.noop === true\n }\n }\n return noop\n}\n","import { defineAsyncComponent } from 'vue'\n\n/**\n * Maps tool `part.type` to the corresponding component for auto-rendering.\n * Only includes simple tools that accept only a `:part` prop.\n * Interactive tools (showContactForm, showSuggestedReply, showSources,\n * showMultipleChoice) must be wired explicitly in the parent with their\n * required callbacks/events. Consent requests (`requestConfirm` and the tool\n * calls suspended for approval) are not here either: `PkChatbotMessages`\n * renders them all with `PkToolApprovalCard`.\n */\nexport const toolComponentMap: Record<\n string,\n ReturnType<typeof defineAsyncComponent>\n> = {\n requestOAuthConnection: defineAsyncComponent(\n () => import('./PkToolRequestOAuthConnection.vue'),\n ),\n showArtifact: defineAsyncComponent(\n () => import('./PkToolShowArtifact.vue'),\n ),\n showCalendarEvent: defineAsyncComponent(\n () => import('./PkToolShowCalendarEvent.vue'),\n ),\n showComparison: defineAsyncComponent(\n () => import('./PkToolShowComparison.vue'),\n ),\n showContactForm: defineAsyncComponent(\n () => import('./PkToolShowContactForm.vue'),\n ),\n showDiagram: defineAsyncComponent(() => import('./PkToolShowDiagram.vue')),\n showEmail: defineAsyncComponent(() => import('./PkToolShowEmail.vue')),\n showImageGallery: defineAsyncComponent(\n () => import('./PkToolShowImageGallery.vue'),\n ),\n showLocation: defineAsyncComponent(\n () => import('./PkToolShowLocation.vue'),\n ),\n showMessage: defineAsyncComponent(() => import('./PkToolShowMessage.vue')),\n showForm: defineAsyncComponent(() => import('./PkToolShowForm.vue')),\n showMultipleChoice: defineAsyncComponent(\n () => import('./PkToolShowForm.vue'),\n ),\n showProductList: defineAsyncComponent(\n () => import('./PkToolShowProductList.vue'),\n ),\n showQrCode: defineAsyncComponent(() => import('./PkToolShowQrCode.vue')),\n showSources: defineAsyncComponent(() => import('./PkToolShowSources.vue')),\n showSuggestedReply: defineAsyncComponent(\n () => import('./PkToolShowSuggestedReply.vue'),\n ),\n showVideo: defineAsyncComponent(() => import('./PkToolShowVideo.vue')),\n showWeather: defineAsyncComponent(() => import('./PkToolShowWeather.vue')),\n showWebPages: defineAsyncComponent(\n () => import('./PkToolShowWebPages.vue'),\n ),\n}\n","<script setup lang=\"ts\">\n import { computed } from 'vue'\n\n /**\n * Preview for the tools whose input is already a sentence written for the\n * user (`requestConfirm`): it is shown as-is, with no key/value framing.\n */\n const props = defineProps<{\n input?: unknown\n }>()\n\n const message = computed(() => {\n const input = props.input as { message?: unknown } | undefined\n return typeof input?.message === 'string' ? input.message : ''\n })\n</script>\n\n<template>\n <p v-if=\"message\" class=\"text-12 text-word-3 m-0 mb-xs\">\n {{ message }}\n </p>\n</template>\n","<script setup lang=\"ts\">\n import { computed } from 'vue'\n\n /**\n * Preview for the tools whose input is already a sentence written for the\n * user (`requestConfirm`): it is shown as-is, with no key/value framing.\n */\n const props = defineProps<{\n input?: unknown\n }>()\n\n const message = computed(() => {\n const input = props.input as { message?: unknown } | undefined\n return typeof input?.message === 'string' ? input.message : ''\n })\n</script>\n\n<template>\n <p v-if=\"message\" class=\"text-12 text-word-3 m-0 mb-xs\">\n {{ message }}\n </p>\n</template>\n","import type { Component } from 'vue'\nimport PkToolApprovalPreviewMessage from './PkToolApprovalPreviewMessage.vue'\nimport { CONFIRM_TOOL_NAME } from './utils'\n\n/**\n * Maps a tool name to the component that renders a human-readable preview of\n * its input inside `PkToolApprovalCard`. The preview receives the tool call\n * input as its `input` prop.\n *\n * Registering a preview needs no change to the card: tools with no entry here\n * show only the localized confirmation prompt (no raw echo of the call input).\n * Reuse the component that already renders that kind of payload when there is\n * one (e.g. `sendEmail` will reuse `PkToolShowEmail`).\n */\nexport const toolApprovalPreviews: Record<string, Component> = {\n // The model already writes the sentence to confirm: show it verbatim.\n [CONFIRM_TOOL_NAME]: PkToolApprovalPreviewMessage,\n}\n","<script setup lang=\"ts\">\n import { computed } from 'vue'\n import { useI18n } from 'vue-i18n'\n import { getToolPartName } from 'utils'\n import { isVoiceRenderAck } from 'models'\n import { CONFIRM_TOOL_NAME, toolLabel } from './utils'\n import { toolApprovalPreviews } from './toolApprovalPreviews'\n\n /**\n * The single confirmation card of the chat. It renders two things that are\n * the same question for the user:\n *\n * - **approval**: a tool call the backend suspended waiting for consent\n * (`approval-requested`), plus its resolved states so the decision stays\n * visible in the transcript;\n * - **confirm**: a `requestConfirm` call, the client-side tool the model uses\n * to have an action it described approved. Same layout, its own wording\n * (Proceed / Cancel, as on the messaging channels).\n *\n * The body comes from `toolApprovalPreviews[toolName]` when the tool has a\n * dedicated preview (localized, human copy: e.g. `sendEmail` will reuse the\n * email renderer); tools without one show only the localized prompt, never\n * a raw echo of the call input. Adding a preview never requires touching\n * this component.\n */\n const props = defineProps<{\n part: unknown\n /**\n * Whether the user can still answer. False in read-only views (chat\n * logs) and, for approvals, once the conversation has moved past the\n * request: the backend closes an abandoned approval as denied, so\n * answering it later would not run the tool.\n */\n actionable?: boolean\n /**\n * The verdict given ON A CALL, when there is one. A confirm answered\n * during a call travels back as a spoken message and its part stays\n * on the render ack, so the outcome only exists client-side: this is\n * what closes the card and names the outcome instead of leaving the\n * buttons alive (or worse, showing the answer as \"expired\").\n */\n callVerdict?: boolean\n }>()\n\n const emit = defineEmits<{\n respond: [approved: boolean]\n }>()\n\n const { t: $t, te: $te } = useI18n({ useScope: 'global' })\n\n const toolPart = computed(\n () =>\n props.part as {\n type?: string\n state?: string\n toolCallId?: string\n input?: unknown\n output?: unknown\n approval?: { id?: string; approved?: boolean; reason?: string }\n },\n )\n\n const toolName = computed(() =>\n getToolPartName(props.part as Parameters<typeof getToolPartName>[0]),\n )\n\n const isConfirm = computed(() => toolName.value === CONFIRM_TOOL_NAME)\n\n /**\n * True while the request still waits for an answer. A confirm drawn\n * during a CALL arrives already resolved with the note that it was drawn\n * (see `isVoiceRenderAck`, same rule as the forms): the note is not the\n * user's verdict, so the card keeps its buttons.\n */\n const isOpen = computed(() =>\n isConfirm.value\n ? (toolPart.value.state === 'input-available' ||\n isVoiceRenderAck(toolPart.value.output)) &&\n props.callVerdict === undefined\n : toolPart.value.state === 'approval-requested',\n )\n\n /** Open and answerable: the only case where the buttons act. */\n const isPending = computed(() => isOpen.value && props.actionable === true)\n\n /** Open but not answerable: shown as an expired request. */\n const isExpired = computed(() => isOpen.value && props.actionable !== true)\n\n const isApproved = computed(() =>\n isConfirm.value\n ? toolPart.value.output === 'confirmed' ||\n props.callVerdict === true\n : toolPart.value.approval?.approved === true,\n )\n\n const isDenied = computed(() =>\n isConfirm.value\n ? toolPart.value.output === 'cancelled' ||\n props.callVerdict === false\n : toolPart.value.approval?.approved === false ||\n toolPart.value.state === 'output-denied',\n )\n\n const preview = computed(() => toolApprovalPreviews[toolName.value])\n\n /**\n * The ask above the buttons. A preview shows what is about to happen and\n * the sentence leads into it; without one the sentence is all the user\n * reads, so it names the tool instead of ending on a colon with nothing\n * after it.\n */\n const prompt = computed(() =>\n preview.value\n ? $t('toolApproval.prompt')\n : $t('toolApproval.promptNamed', {\n tool: toolLabel(toolName.value, { t: $t, te: $te }),\n }),\n )\n\n /**\n * Once the request is answered (or expired) the card slims down: prompt\n * and buttons disappear and this single icon + text line tells the\n * outcome. A confirm has its own wording (the model described an action,\n * no tool ran on its behalf).\n */\n const outcome = computed(() => {\n if (isExpired.value) {\n return { icon: 'ri:time-line', message: $t('toolApproval.expired') }\n }\n if (isApproved.value) {\n return {\n icon: 'ri:check-line',\n message: $t(\n isConfirm.value\n ? 'toolApproval.confirmed'\n : 'toolApproval.approved',\n ),\n }\n }\n if (isDenied.value) {\n return {\n icon: 'ri:close-line',\n message: $t(\n isConfirm.value\n ? 'toolApproval.cancelled'\n : 'toolApproval.denied',\n ),\n }\n }\n return undefined\n })\n\n /**\n * Resolved and nothing to preview: all the card would frame is the\n * outcome line, so the frame goes too and only the line stays in the\n * transcript.\n */\n const isBare = computed(() => Boolean(outcome.value) && !preview.value)\n</script>\n\n<template>\n <!-- Plain block container, NOT flex-col: vv-button-group is inline-flex\n with justify-content center, and a stretched flex child would center\n the buttons instead of keeping them left like the original confirm\n card. -->\n <!-- transition-all eases the frame away (padding, border and background\n to zero) when the resolved card goes bare. -->\n <div\n class=\"w-full overflow-hidden transition-all duration-300\"\n :class=\"{\n 'bg-surface p-sm rounded-xl border border-surface-3': !isBare,\n }\">\n <!-- A confirm carries its own sentence from the model; an approval gets\n one of ours: it leads into the preview when there is one, and names\n the tool when there is not (see `prompt`). -->\n <!-- The ask reads wrong on a resolved request: once answered, the\n outcome line alone carries the whole story. -->\n <transition>\n <p\n v-if=\"!isConfirm && isOpen\"\n class=\"text-12 text-word-3 m-0 mb-xs\">\n {{ prompt }}\n </p>\n </transition>\n\n <component :is=\"preview\" v-if=\"preview\" :part :input=\"toolPart.input\" />\n\n <!-- Buttons only while the answer is still awaited (disabled while the\n confirm input streams in). Once there is an outcome they leave the\n card, so the transcript keeps a slim record instead of a dead\n control. -->\n <transition mode=\"out-in\">\n <VvButtonGroup v-if=\"!outcome\">\n <VvButton\n modifiers=\"primary-small\"\n :disabled=\"!isPending\"\n icon=\"ri:check-line\"\n :label=\"$t('action.proceed')\"\n @click.stop=\"emit('respond', true)\" />\n <VvButton\n modifiers=\"action-quiet-small\"\n :disabled=\"!isPending\"\n icon=\"ri:close-line\"\n :label=\"$t('action.cancel')\"\n @click.stop=\"emit('respond', false)\" />\n </VvButtonGroup>\n\n <!-- The stored denial reason is model-facing copy (and English):\n the card always shows the localized message instead. -->\n <p\n v-else\n class=\"flex items-center gap-4 text-12 text-word-3 m-0\"\n :class=\"{ 'mt-xs': preview }\">\n <VvIcon :name=\"outcome.icon\" class=\"shrink-0\" />\n {{ outcome.message }}\n </p>\n </transition>\n </div>\n</template>\n","<script setup lang=\"ts\">\n import { computed } from 'vue'\n import { useI18n } from 'vue-i18n'\n import { getToolPartName } from 'utils'\n import { isVoiceRenderAck } from 'models'\n import { CONFIRM_TOOL_NAME, toolLabel } from './utils'\n import { toolApprovalPreviews } from './toolApprovalPreviews'\n\n /**\n * The single confirmation card of the chat. It renders two things that are\n * the same question for the user:\n *\n * - **approval**: a tool call the backend suspended waiting for consent\n * (`approval-requested`), plus its resolved states so the decision stays\n * visible in the transcript;\n * - **confirm**: a `requestConfirm` call, the client-side tool the model uses\n * to have an action it described approved. Same layout, its own wording\n * (Proceed / Cancel, as on the messaging channels).\n *\n * The body comes from `toolApprovalPreviews[toolName]` when the tool has a\n * dedicated preview (localized, human copy: e.g. `sendEmail` will reuse the\n * email renderer); tools without one show only the localized prompt, never\n * a raw echo of the call input. Adding a preview never requires touching\n * this component.\n */\n const props = defineProps<{\n part: unknown\n /**\n * Whether the user can still answer. False in read-only views (chat\n * logs) and, for approvals, once the conversation has moved past the\n * request: the backend closes an abandoned approval as denied, so\n * answering it later would not run the tool.\n */\n actionable?: boolean\n /**\n * The verdict given ON A CALL, when there is one. A confirm answered\n * during a call travels back as a spoken message and its part stays\n * on the render ack, so the outcome only exists client-side: this is\n * what closes the card and names the outcome instead of leaving the\n * buttons alive (or worse, showing the answer as \"expired\").\n */\n callVerdict?: boolean\n }>()\n\n const emit = defineEmits<{\n respond: [approved: boolean]\n }>()\n\n const { t: $t, te: $te } = useI18n({ useScope: 'global' })\n\n const toolPart = computed(\n () =>\n props.part as {\n type?: string\n state?: string\n toolCallId?: string\n input?: unknown\n output?: unknown\n approval?: { id?: string; approved?: boolean; reason?: string }\n },\n )\n\n const toolName = computed(() =>\n getToolPartName(props.part as Parameters<typeof getToolPartName>[0]),\n )\n\n const isConfirm = computed(() => toolName.value === CONFIRM_TOOL_NAME)\n\n /**\n * True while the request still waits for an answer. A confirm drawn\n * during a CALL arrives already resolved with the note that it was drawn\n * (see `isVoiceRenderAck`, same rule as the forms): the note is not the\n * user's verdict, so the card keeps its buttons.\n */\n const isOpen = computed(() =>\n isConfirm.value\n ? (toolPart.value.state === 'input-available' ||\n isVoiceRenderAck(toolPart.value.output)) &&\n props.callVerdict === undefined\n : toolPart.value.state === 'approval-requested',\n )\n\n /** Open and answerable: the only case where the buttons act. */\n const isPending = computed(() => isOpen.value && props.actionable === true)\n\n /** Open but not answerable: shown as an expired request. */\n const isExpired = computed(() => isOpen.value && props.actionable !== true)\n\n const isApproved = computed(() =>\n isConfirm.value\n ? toolPart.value.output === 'confirmed' ||\n props.callVerdict === true\n : toolPart.value.approval?.approved === true,\n )\n\n const isDenied = computed(() =>\n isConfirm.value\n ? toolPart.value.output === 'cancelled' ||\n props.callVerdict === false\n : toolPart.value.approval?.approved === false ||\n toolPart.value.state === 'output-denied',\n )\n\n const preview = computed(() => toolApprovalPreviews[toolName.value])\n\n /**\n * The ask above the buttons. A preview shows what is about to happen and\n * the sentence leads into it; without one the sentence is all the user\n * reads, so it names the tool instead of ending on a colon with nothing\n * after it.\n */\n const prompt = computed(() =>\n preview.value\n ? $t('toolApproval.prompt')\n : $t('toolApproval.promptNamed', {\n tool: toolLabel(toolName.value, { t: $t, te: $te }),\n }),\n )\n\n /**\n * Once the request is answered (or expired) the card slims down: prompt\n * and buttons disappear and this single icon + text line tells the\n * outcome. A confirm has its own wording (the model described an action,\n * no tool ran on its behalf).\n */\n const outcome = computed(() => {\n if (isExpired.value) {\n return { icon: 'ri:time-line', message: $t('toolApproval.expired') }\n }\n if (isApproved.value) {\n return {\n icon: 'ri:check-line',\n message: $t(\n isConfirm.value\n ? 'toolApproval.confirmed'\n : 'toolApproval.approved',\n ),\n }\n }\n if (isDenied.value) {\n return {\n icon: 'ri:close-line',\n message: $t(\n isConfirm.value\n ? 'toolApproval.cancelled'\n : 'toolApproval.denied',\n ),\n }\n }\n return undefined\n })\n\n /**\n * Resolved and nothing to preview: all the card would frame is the\n * outcome line, so the frame goes too and only the line stays in the\n * transcript.\n */\n const isBare = computed(() => Boolean(outcome.value) && !preview.value)\n</script>\n\n<template>\n <!-- Plain block container, NOT flex-col: vv-button-group is inline-flex\n with justify-content center, and a stretched flex child would center\n the buttons instead of keeping them left like the original confirm\n card. -->\n <!-- transition-all eases the frame away (padding, border and background\n to zero) when the resolved card goes bare. -->\n <div\n class=\"w-full overflow-hidden transition-all duration-300\"\n :class=\"{\n 'bg-surface p-sm rounded-xl border border-surface-3': !isBare,\n }\">\n <!-- A confirm carries its own sentence from the model; an approval gets\n one of ours: it leads into the preview when there is one, and names\n the tool when there is not (see `prompt`). -->\n <!-- The ask reads wrong on a resolved request: once answered, the\n outcome line alone carries the whole story. -->\n <transition>\n <p\n v-if=\"!isConfirm && isOpen\"\n class=\"text-12 text-word-3 m-0 mb-xs\">\n {{ prompt }}\n </p>\n </transition>\n\n <component :is=\"preview\" v-if=\"preview\" :part :input=\"toolPart.input\" />\n\n <!-- Buttons only while the answer is still awaited (disabled while the\n confirm input streams in). Once there is an outcome they leave the\n card, so the transcript keeps a slim record instead of a dead\n control. -->\n <transition mode=\"out-in\">\n <VvButtonGroup v-if=\"!outcome\">\n <VvButton\n modifiers=\"primary-small\"\n :disabled=\"!isPending\"\n icon=\"ri:check-line\"\n :label=\"$t('action.proceed')\"\n @click.stop=\"emit('respond', true)\" />\n <VvButton\n modifiers=\"action-quiet-small\"\n :disabled=\"!isPending\"\n icon=\"ri:close-line\"\n :label=\"$t('action.cancel')\"\n @click.stop=\"emit('respond', false)\" />\n </VvButtonGroup>\n\n <!-- The stored denial reason is model-facing copy (and English):\n the card always shows the localized message instead. -->\n <p\n v-else\n class=\"flex items-center gap-4 text-12 text-word-3 m-0\"\n :class=\"{ 'mt-xs': preview }\">\n <VvIcon :name=\"outcome.icon\" class=\"shrink-0\" />\n {{ outcome.message }}\n </p>\n </transition>\n </div>\n</template>\n","import type { Ref, ShallowRef } from 'vue'\nimport { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'\n\nconst NEAR_BOTTOM_THRESHOLD = 30\nconst SMOOTH_SCROLL_DURATION = 500\n\ninterface UseChatScrollOptions {\n scrollEl:\n | Readonly<ShallowRef<HTMLDivElement | null>>\n | Ref<HTMLDivElement | undefined>\n status: () => string | undefined\n messagesLength: () => number | undefined\n lastMessageRole: () => string | undefined\n /**\n * Element whose height tracks the content height, watched for late\n * growth (lazy tool outputs). Defaults to the scroll element's first\n * child.\n */\n contentEl?: () => HTMLElement | null\n /**\n * Awaited right before the new-exchange scroll. Lets the host apply the\n * layout that changes the content height (the anchor spacer) first, so\n * the scroll targets the final bottom instead of an intermediate one.\n */\n beforeNewMessageScroll?: () => void | Promise<void>\n onScrollUp?: () => void\n onScrollDown?: () => void\n}\n\nexport function useChatScroll(options: UseChatScrollOptions) {\n const {\n scrollEl,\n status,\n messagesLength,\n lastMessageRole,\n contentEl,\n beforeNewMessageScroll,\n onScrollUp,\n onScrollDown,\n } = options\n\n const userScrolledUp = ref(false)\n const isAtBottom = ref(true)\n let isAutoScrolling = false\n let mutationObserver: MutationObserver | null = null\n let resizeObserver: ResizeObserver | null = null\n let initMutationObserver: MutationObserver | null = null\n let contentResizeObserver: ResizeObserver | null = null\n let lastScrollTop = 0\n\n // --- Core scroll ---\n\n const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {\n if (!scrollEl.value || userScrolledUp.value) {\n // Callers may lock the auto-scroll before awaiting layout: release\n // it here or the lock would mute the auto-follow forever\n isAutoScrolling = false\n return\n }\n isAutoScrolling = true\n scrollEl.value.scrollTo({\n top: scrollEl.value.scrollHeight,\n behavior,\n })\n const unlockDelay = behavior === 'instant' ? 0 : SMOOTH_SCROLL_DURATION\n setTimeout(() => {\n isAutoScrolling = false\n }, unlockDelay)\n }\n\n // --- User scroll detection ---\n // Intent to scroll away comes from input events (wheel/touch): unlike\n // scroll deltas they are never produced by programmatic scrolls, layout\n // shifts or elastic overscroll bounces, so the auto-scroll never fights\n // the user nor disables itself spuriously.\n\n const onWheel = (event: WheelEvent) => {\n if (event.deltaY < 0) {\n userScrolledUp.value = true\n }\n }\n\n let lastTouchY = 0\n const onTouchStart = (event: TouchEvent) => {\n lastTouchY = event.touches[0]?.clientY ?? 0\n }\n const onTouchMove = (event: TouchEvent) => {\n const y = event.touches[0]?.clientY ?? 0\n // Finger moving down drags the content up\n if (y > lastTouchY) {\n userScrolledUp.value = true\n }\n lastTouchY = y\n }\n\n const handleScroll = () => {\n if (!scrollEl.value) {\n return\n }\n const {\n scrollTop: currentScrollTop,\n scrollHeight,\n clientHeight,\n } = scrollEl.value\n isAtBottom.value =\n scrollHeight - clientHeight - currentScrollTop <\n NEAR_BOTTOM_THRESHOLD\n if (isAutoScrolling) {\n lastScrollTop = currentScrollTop\n return\n }\n\n if (currentScrollTop < lastScrollTop) {\n onScrollUp?.()\n }\n\n if (currentScrollTop > lastScrollTop) {\n // Back at the bottom: re-enable auto-scroll\n if (isAtBottom.value) {\n userScrolledUp.value = false\n }\n onScrollDown?.()\n }\n\n lastScrollTop = currentScrollTop\n }\n\n // --- Streaming auto-follow ---\n // Tracks the growing content towards the bottom, never above the\n // current position. While the answer fits the space reserved by the\n // last-message spacer in PkChatbotMessages the content bottom does not\n // move, so the freshly sent user message stays anchored at the top;\n // once the answer overflows, the follow keeps the tail in view until\n // the user scrolls away (wheel/touch sets `userScrolledUp`).\n const autoFollow = () => {\n if (isAutoScrolling || userScrolledUp.value || !scrollEl.value) {\n return\n }\n const el = scrollEl.value\n const bottom = el.scrollHeight - el.clientHeight\n if (bottom <= el.scrollTop) {\n return\n }\n isAtBottom.value = true\n isAutoScrolling = true\n el.scrollTop = bottom\n requestAnimationFrame(() => {\n isAutoScrolling = false\n })\n }\n\n const startMutationScroll = () => {\n if (mutationObserver || !scrollEl.value) {\n return\n }\n mutationObserver = new MutationObserver(autoFollow)\n mutationObserver.observe(scrollEl.value, {\n childList: true,\n subtree: true,\n characterData: true,\n })\n }\n\n const stopMutationScroll = () => {\n mutationObserver?.disconnect()\n mutationObserver = null\n }\n\n // --- Status watcher ---\n\n watch(status, (newStatus, oldStatus) => {\n if (newStatus === 'streaming' || newStatus === 'submitted') {\n startMutationScroll()\n return\n }\n if (newStatus === 'ready' && oldStatus === 'streaming') {\n // Keep observer active to catch footer buttons rendering\n return\n }\n stopMutationScroll()\n })\n\n // --- New message watcher ---\n\n watch(messagesLength, async () => {\n if (!messagesLength()) {\n return\n }\n if (lastMessageRole() === 'assistant') {\n return\n }\n // A user send ends the initial-load phase: on a chat whose content\n // first overflows while the answer streams in, the initial observers\n // would otherwise slam the scroll to the bottom past the anchor\n cleanupInitialObservers()\n userScrolledUp.value = false\n // Lock while the layout settles: the anchor spacer growth would\n // otherwise make the resize auto-follow jump before the real scroll\n isAutoScrolling = true\n try {\n await nextTick()\n // The spacer must be in place first: scrolling to a\n // `scrollHeight` that still misses it lands short of the anchor\n // and nothing brings the view back down (the resize auto-follow\n // is locked out).\n await beforeNewMessageScroll?.()\n await nextTick()\n } finally {\n // Always reaches scrollToBottom: a throw above would otherwise\n // leave the lock set and mute the auto-follow forever\n scrollToBottom()\n }\n })\n\n // --- Initial scroll ---\n\n let initialScrollTimer: ReturnType<typeof setTimeout> | null = null\n const INITIAL_SCROLL_SETTLE_MS = 300\n\n const tryInitialScroll = () => {\n if (!scrollEl.value) {\n return\n }\n if (scrollEl.value.scrollHeight <= scrollEl.value.clientHeight) {\n return\n }\n scrollEl.value.scrollTop = scrollEl.value.scrollHeight\n\n // Reset the settle timer on each mutation — disconnect only after stability\n if (initialScrollTimer) {\n clearTimeout(initialScrollTimer)\n }\n initialScrollTimer = setTimeout(\n cleanupInitialObservers,\n INITIAL_SCROLL_SETTLE_MS,\n )\n }\n\n const cleanupInitialObservers = () => {\n resizeObserver?.disconnect()\n resizeObserver = null\n initMutationObserver?.disconnect()\n initMutationObserver = null\n if (initialScrollTimer) {\n clearTimeout(initialScrollTimer)\n initialScrollTimer = null\n }\n }\n\n onMounted(() => {\n if (!scrollEl.value) {\n return\n }\n scrollEl.value.addEventListener('wheel', onWheel, { passive: true })\n scrollEl.value.addEventListener('touchstart', onTouchStart, {\n passive: true,\n })\n scrollEl.value.addEventListener('touchmove', onTouchMove, {\n passive: true,\n })\n\n // Mounted mid-send (e.g. fullscreen first message: the messages view\n // replaces the empty state after the message is already in) is not a\n // history load: skip the initial bottom-jump and follow the stream\n // from the anchored position instead.\n const isSendInFlight =\n status() === 'submitted' || status() === 'streaming'\n if (isSendInFlight) {\n startMutationScroll()\n } else {\n resizeObserver = new ResizeObserver(tryInitialScroll)\n resizeObserver.observe(scrollEl.value)\n\n initMutationObserver = new MutationObserver(tryInitialScroll)\n initMutationObserver.observe(scrollEl.value, {\n childList: true,\n subtree: true,\n })\n }\n\n // Late content growth: tool outputs render lazily (async chunks,\n // dynamic imports: diagrams, images...) and can change the content\n // height long after the initial scroll settled. The wrapper height\n // tracks the content height (the scroll element itself never\n // resizes with it).\n const wrapper = contentEl?.() ?? scrollEl.value.firstElementChild\n if (wrapper) {\n contentResizeObserver = new ResizeObserver(autoFollow)\n contentResizeObserver.observe(wrapper)\n }\n\n if (!isSendInFlight) {\n tryInitialScroll()\n }\n })\n\n onBeforeUnmount(() => {\n scrollEl.value?.removeEventListener('wheel', onWheel)\n scrollEl.value?.removeEventListener('touchstart', onTouchStart)\n scrollEl.value?.removeEventListener('touchmove', onTouchMove)\n contentResizeObserver?.disconnect()\n contentResizeObserver = null\n stopMutationScroll()\n cleanupInitialObservers()\n })\n\n return {\n handleScroll,\n scrollToBottom,\n userScrolledUp,\n isAtBottom,\n }\n}\n","import { isQuietWakeupAnswer, isWakeupSeed, type UIChatMessage } from 'models'\n\nexport interface WakeupQuietRun {\n /** Checks in the run, each a seed and its quiet answer. */\n count: number\n /** When the last check of the run started, from its seed. */\n lastAt?: number\n}\n\nexport interface WakeupFeed {\n /** Messages the feed does not draw as bubbles. */\n hidden: Set<string>\n /** By the id of the first seed of a run of quiet checks: the line drawn in its place. */\n quietRuns: Map<string, WakeupQuietRun>\n}\n\n/**\n * How a chat with wakeups reads (spec agent-wakeup, \"Interfaccia\"): a run of\n * checks that found nothing is one line (\"nothing new in 5 checks, the last\n * at 14:20\") instead of five prompts the user never wrote and five empty\n * answers, and a quiet answer of the user's own turn draws nothing either.\n * A seed whose answer has news stays, drawn as a thin line by the feed.\n *\n * `draws` is the view's own test of whether a message shows anything (a\n * widget, an approval card): such an answer is never folded, whatever its\n * `scheduleWakeup` said.\n */\nexport function buildWakeupFeed(\n messages: UIChatMessage[],\n draws: (message: UIChatMessage) => boolean = () => false,\n): WakeupFeed {\n const isQuiet = (message?: UIChatMessage) =>\n isQuietWakeupAnswer(message) && !draws(message as UIChatMessage)\n const hidden = new Set<string>()\n const quietRuns = new Map<string, WakeupQuietRun>()\n let index = 0\n while (index < messages.length) {\n const message = messages[index]\n if (isWakeupSeed(message) && isQuiet(messages[index + 1])) {\n const start = message.id\n let count = 0\n let lastAt: number | undefined\n while (\n isWakeupSeed(messages[index]) &&\n isQuiet(messages[index + 1])\n ) {\n count += 1\n lastAt = messages[index].metadata?.createdAt ?? lastAt\n hidden.add(messages[index].id)\n hidden.add(messages[index + 1].id)\n index += 2\n }\n quietRuns.set(start, { count, lastAt })\n continue\n }\n if (isQuiet(message)) {\n hidden.add(message.id)\n }\n index += 1\n }\n return { hidden, quietRuns }\n}\n","<script lang=\"ts\" setup>\n import type {\n ChatMessageActions,\n MessageFeedback,\n RevisedAnswer,\n UIChatMessage,\n } from 'models'\n import { isAudioFile, isWakeupSeed } from 'models'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkChatbotError from './PkChatbotError.vue'\n import PkChatbotFeedbackForm from './PkChatbotFeedbackForm.vue'\n import PkChatbotMessageFeedback from './PkChatbotMessageFeedback.vue'\n import PkChatbotFilePreview from './PkChatbotFilePreview.vue'\n import PkChatbotAudioTranscript from './PkChatbotAudioTranscript.vue'\n import PkRelativeTime from '../PkRelativeTime.vue'\n import { useI18n } from 'vue-i18n'\n import {\n useTemplateRef,\n ref,\n shallowRef,\n watch,\n computed,\n nextTick,\n onMounted,\n useSlots,\n } from 'vue'\n import { useResizeObserver } from '@vueuse/core'\n import { toolComponentMap } from './toolComponentMap'\n import PkToolApprovalCard from './PkToolApprovalCard.vue'\n import PkChatbotSteps from './PkChatbotSteps.vue'\n import {\n resolveContrastColor,\n getPartState,\n isTextPart,\n isFilePart,\n isToolPart,\n isReasoningPart,\n isStreamingPart,\n isToolConfirmPart,\n isConfirmationCardPart,\n getToolApprovalId,\n getPartIcon,\n getPartTypeLabel,\n getToolPartLabel,\n mergeConsecutiveTextParts,\n dropCorrectedWidgetErrors,\n moveFileOutputsLast,\n feedPartKey,\n formatDuration,\n LIVE_REASONING_TAIL_LENGTH,\n } from './utils'\n import {\n getFilePartMedia,\n getFilePartTranscript,\n getToolPartName,\n } from 'utils'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotOutlineRail from './PkChatbotOutlineRail.vue'\n import { useChatScroll } from './useChatScroll'\n import { buildWakeupFeed } from './wakeupFeed'\n\n const props = defineProps<{\n status?: 'submitted' | 'streaming' | 'ready' | 'error'\n messages?: UIChatMessage[]\n error?: Error\n actions?: ChatMessageActions[]\n mainColor?: string\n textColor?: 'auto' | 'white' | 'black'\n revisedAnswers?: RevisedAnswer[]\n messageFeedbacks?: MessageFeedback[]\n /**\n * Render the vote/feedback action buttons as read-only state indicators\n * (e.g. admin chat-logs view): icons reflect the user's feedback but the\n * buttons are not interactive.\n */\n readonlyFeedback?: boolean\n disableHeightAdjustment?: boolean\n showMessageDateTime?: boolean\n showMessageTokensCount?: boolean\n /**\n * Shows each answer's position in the chat, from 1, the numbering the\n * conversation analysis cites (\"message 62\").\n */\n showMessageNumber?: boolean\n showAllMessageParts?: boolean\n /**\n * Lets the user answer the tool requests rendered in the feed (approval\n * and confirm cards). Off by default: the log viewers render the same\n * messages read-only and have nothing to answer with.\n */\n interactive?: boolean\n showExtendedSteps?: boolean\n /**\n * A call is waiting for its answer. The turn runs outside this client\n * (the voice provider drives it), so `status` says nothing about it and\n * the feed would sit still for the seconds it takes.\n */\n awaitingSpokenAnswer?: boolean\n /**\n * Verdicts given ON A CALL, by toolCallId. A confirm answered during a\n * call goes back to the agent as a spoken message, so its part never\n * leaves the render-ack state: without this the card kept its buttons\n * after the answer, and every extra tap said \"Confermo\" again\n * (measured 2026-08-07). Only successful sends are recorded, so a tap\n * that never reached the provider leaves the card usable.\n */\n confirmVerdictsOnCall?: Map<string, boolean>\n isDark?: boolean\n /** Show a floating \"back to bottom\" button when the user scrolls up (fullscreen layout) */\n showScrollToBottom?: boolean\n // TODO: move feedback in a separate component to avoid passing these props\n feedbackMessageId?: string\n feedbackLoading?: boolean\n feedbackSubmitted?: boolean\n feedbackError?: string\n }>()\n\n const emit = defineEmits<{\n (e: 'show-info', message: UIChatMessage): void\n (e: 'regenerate'): void\n (e: 'revise', message: UIChatMessage): void\n (e: 'upvote', message: UIChatMessage): void\n (e: 'downvote', message: UIChatMessage): void\n (e: 'feedback', message: UIChatMessage): void\n (e: 'feedback-submit', comment: string): void\n (e: 'feedback-close'): void\n (e: 'scroll-up'): void\n (e: 'scroll-down'): void\n (e: 'auto-retry'): void\n (e: 'reset-chat'): void\n (\n e: 'approval-respond',\n payload: { id: string; approved: boolean },\n ): void\n (\n e: 'confirm-respond',\n payload: { toolCallId: string; confirmed: boolean },\n ): void\n }>()\n\n const {\n t: $t,\n te: $te,\n d: $d,\n n: $n,\n } = useI18n({\n useScope: 'global',\n })\n\n const slots = useSlots()\n\n /**\n * Forwards the user's verdict from the confirmation card. The two kinds of\n * request travel back on different channels: an approval answers the SDK\n * approval it carries the id of, a `requestConfirm` resolves as a client\n * tool output. Without the id there is nothing to answer, so the click is\n * dropped instead of sending an unmatchable response.\n */\n const onCardRespond = (\n part: UIChatMessage['parts'][number],\n approved: boolean,\n ) => {\n if (isToolConfirmPart(part)) {\n const toolCallId = (part as { toolCallId?: string }).toolCallId\n if (toolCallId) {\n emit('confirm-respond', { toolCallId, confirmed: approved })\n }\n return\n }\n const id = getToolApprovalId(part)\n if (!id) {\n return\n }\n emit('approval-respond', { id, approved })\n }\n\n const scrollEl = useTemplateRef<HTMLDivElement>('scrollEl')\n const wrapperEl = useTemplateRef<HTMLDivElement>('wrapperEl')\n const contrastColor = computed(() =>\n resolveContrastColor(props.textColor, props.mainColor),\n )\n\n // --- Scroll ---\n\n const { handleScroll, scrollToBottom, userScrolledUp, isAtBottom } =\n useChatScroll({\n scrollEl,\n status: () => props.status,\n messagesLength: () => props.messages?.length,\n lastMessageRole: () =>\n props.messages?.[props.messages.length - 1]?.role,\n contentEl: () => wrapperEl.value,\n beforeNewMessageScroll: () => updateAnchorSpacer(),\n onScrollUp: () => emit('scroll-up'),\n onScrollDown: () => emit('scroll-down'),\n })\n\n const onScrollToBottomClick = () => {\n // scrollToBottom() is a no-op while userScrolledUp is set\n userScrolledUp.value = false\n scrollToBottom()\n }\n\n // --- Conversation outline (fullscreen, desktop) ---\n\n // A wakeup's seed is not a question the user asked, and it is not drawn\n // as a user bubble either, which the 1:1 mapping below relies on.\n const outlineItems = computed(() =>\n (props.messages ?? [])\n .filter(\n (message) => message.role === 'user' && !isWakeupSeed(message),\n )\n .map((message) => ({\n id: message.id,\n preview:\n message.parts.find(isTextPart)?.text.trim().slice(0, 120) ||\n '…',\n })),\n )\n const activeOutlineId = ref<string>()\n\n // The rail entries map 1:1 onto the rendered user messages\n const getUserMessageEls = () =>\n scrollEl.value?.querySelectorAll<HTMLElement>(\n '.pk-chatbot-message--user',\n ) ?? []\n\n // Scroll-spy: active is the last user message above the middle of the\n // viewport (the exchange currently being read)\n const updateActiveOutline = () => {\n const scroller = scrollEl.value\n if (!scroller || !props.showScrollToBottom) {\n return\n }\n const line =\n scroller.getBoundingClientRect().top + scroller.clientHeight / 2\n let active = outlineItems.value[0]?.id\n getUserMessageEls().forEach((el, index) => {\n if (el.getBoundingClientRect().top <= line) {\n active = outlineItems.value[index]?.id\n }\n })\n activeOutlineId.value = active\n }\n\n watch(outlineItems, async () => {\n await nextTick()\n updateActiveOutline()\n })\n\n const onScroll = () => {\n handleScroll()\n updateActiveOutline()\n }\n\n const scrollToUserMessage = (id: string) => {\n const scroller = scrollEl.value\n const index = outlineItems.value.findIndex((item) => item.id === id)\n const messageEl = getUserMessageEls()[index]\n if (!scroller || !messageEl) {\n return\n }\n // Intentional jump away from the live tail: the wheel/touch intent\n // detection cannot see programmatic scrolls, pause the auto-follow\n // explicitly (reaching the bottom again re-enables it)\n userScrolledUp.value = true\n // Same resting line as a freshly sent message: top of the content area\n scroller.scrollTo({\n top:\n scroller.scrollTop +\n messageEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top -\n Number.parseFloat(getComputedStyle(scroller).paddingTop),\n behavior: 'smooth',\n })\n }\n\n // The answer is still being worked on: the spacer below holds while it is,\n // and so do the timeline and the activity of the last message.\n const isLoading = computed(() => {\n return props.status === 'submitted' || props.status === 'streaming'\n })\n\n // --- Height adjustment ---\n\n const height = ref<number>(0)\n // The min-height transition only serves the release (see below): while the\n // spacer grows it would hide the new height from the anchor scroll, which\n // reads `scrollHeight` once and would land short of the anchor.\n const isSpacerSettling = ref(false)\n\n // The spacer serves the exchange in flight from its first measure to its\n // release, at zero too (a question taller than the area, a measure\n // skipped while hidden): the height alone cannot tell held from released.\n let isSpacerHeld = false\n // The last user message the spacer was measured against, observed below.\n const anchoredUserMessageEl = shallowRef<HTMLElement>()\n\n // Recalculate when a message is added or the conversation changes, and\n // when one of the two sizes it is computed from changes (see the resize\n // observer below). Idempotent: also called by the scroll composable right\n // before the anchor scroll, so the spacer is always in place first.\n const updateAnchorSpacer = async () => {\n const messages = props.messages\n if (!messages?.length || props.disableHeightAdjustment) {\n return\n }\n // The spacer only serves the in-flight exchange, letting the\n // fresh user message anchor at the top while the answer streams.\n // On plain loads (e.g. opening an old conversation) it would\n // just leave a large void after the last message.\n const isExchangeInFlight =\n messages[messages.length - 1].role === 'user' || isLoading.value\n if (!isExchangeInFlight) {\n return\n }\n isSpacerHeld = true\n isSpacerSettling.value = false\n await nextTick()\n if (!scrollEl.value) {\n return\n }\n // Spacer sized so the freshly sent user message rests at the top\n // of the content area once scrolled to bottom: subtract both\n // paddings (clientHeight includes them) so any extra top\n // clearance (e.g. the fullscreen overlay header) is respected,\n // and the wrapper row gap separating the message from the spacer\n const { paddingTop, paddingBottom } = getComputedStyle(scrollEl.value)\n const scrollElHeight =\n scrollEl.value.clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n const wrapperGap = wrapperEl.value\n ? Number.parseFloat(getComputedStyle(wrapperEl.value).rowGap) || 0\n : 0\n // Measured on the rendered DOM (same lookup as the outline rail):\n // indexing the template-refs array with the `messages` index breaks\n // as soon as a message renders nothing (`v-if` on empty parts), the\n // refs array skips it and the heights shift by one.\n const userMessageEls = getUserMessageEls()\n anchoredUserMessageEl.value = userMessageEls[userMessageEls.length - 1]\n const lastUserMessageHeight =\n anchoredUserMessageEl.value?.clientHeight ?? 0\n const newHeight = scrollElHeight - lastUserMessageHeight - wrapperGap\n // A question taller than the content area leaves nothing to reserve:\n // the spacer goes to zero, never back to the value of the previous\n // exchange. A stale, larger spacer is the very failure the anchor\n // exists to prevent, the scroll landing past the message. Skipped\n // when the element measures nothing (rendered while hidden): there\n // the height already in place is still the good one.\n if (scrollElHeight > 0) {\n height.value = Math.max(newHeight, 0)\n }\n await nextTick()\n }\n\n // Two sources compared one by one: the store recreates `messages` on\n // every token, and a getter returning `[length, id]` would hand back a\n // new array each time and run this at every token. An answer that resumes\n // after a pause (an approval, a form, a tool the client runs) keeps its\n // length and its id, so a spacer released at the pause stays released:\n // bringing it back on the next token threw the view down, and the next\n // pause threw it up again.\n watch(\n [\n () => props.messages?.length,\n () => props.messages?.[props.messages.length - 1]?.id,\n ],\n () => updateAnchorSpacer(),\n )\n\n // The two sizes the spacer is computed from can change while it is held:\n // the question (the transcript of an audio, a box the user opens) and the\n // chat itself (a bar docking above the input, a phone keyboard closing).\n // Followed by observing them rather than by measuring at every token.\n // A released spacer is left alone: an answer resumed after a pause must\n // not bring it back.\n useResizeObserver([scrollEl, anchoredUserMessageEl], () => {\n if (isSpacerHeld) {\n updateAnchorSpacer()\n }\n })\n\n // Mounted mid-send (fullscreen first message: the messages view replaces\n // the empty state after the message is already in): the length watch\n // never fires for that message, apply the anchor spacer now. A first\n // spoken line mounts the same way and never moves `status`, hence the\n // second guard. Plain history loads (mounted at 'ready', nothing in\n // flight) stay untouched.\n onMounted(() => {\n if (isLoading.value || props.awaitingSpokenAnswer) {\n updateAnchorSpacer()\n }\n })\n\n // Release the spacer once the answer stops loading: it would otherwise\n // leave a large empty area after the last message. The min-height\n // transition settles the layout smoothly (the browser clamps the\n // scroll progressively when the viewport sat inside the removed area).\n // Keyed on `isLoading`, not on `status`: whatever keeps the answer\n // loading keeps the spacer too.\n watch(isLoading, (loading) => {\n if (!loading) {\n isSpacerHeld = false\n isSpacerSettling.value = true\n height.value = 0\n }\n })\n\n const isRevised = (messageId: string) => {\n return props.revisedAnswers?.some((r) => r.messageId === messageId)\n }\n\n const getMessageFeedback = (messageId: string) =>\n props.messageFeedbacks?.find((f) => f.messageId === messageId)\n\n // A form the visitor fills in live is, in a feed nobody answers from\n // (the chat logs), the record of what the agent showed: no fields to\n // type in, no button that looks like it sends.\n const RECORD_TOOLS = new Set(['showContactForm'])\n const recordToolProps = (part: Parameters<typeof getToolPartName>[0]) =>\n !props.interactive && RECORD_TOOLS.has(getToolPartName(part))\n ? { record: true }\n : {}\n\n // Interactive vote/feedback buttons. In read-only mode (admin chat-logs)\n // the vote is shown on the message itself (coloured bubble border + an\n // \"active\" thumb in the footer) and the comment, if any, below it.\n const isFeedbackButtonVisible = (\n kind: 'upvote' | 'downvote' | 'feedback',\n index: number,\n ) => !props.readonlyFeedback && isActionButtonVisible(kind, index)\n\n // Read-only feedback for a message (drives the reaction chip anchored to\n // the bubble). undefined when not in read-only mode.\n const readonlyFeedbackOf = (messageId: string) =>\n props.readonlyFeedback ? getMessageFeedback(messageId) : undefined\n const feedbackVote = (messageId: string) =>\n readonlyFeedbackOf(messageId)?.vote\n // The real reaction glyph, shown ONLY for unmapped reactions (no vote):\n // 👍/❤️/👎 keep the classic thumb icon, while e.g. WhatsApp \"😍\" shows the\n // actual emoji. Platforms that send a type word (Teams \"like\") have a vote\n // and therefore use the icon too.\n const feedbackEmoji = (messageId: string) => {\n const feedback = readonlyFeedbackOf(messageId)\n if (feedback?.vote) {\n return undefined\n }\n const raw = feedback?.metadata?.reactionEmoji\n return typeof raw === 'string' && /\\p{Extended_Pictographic}/u.test(raw)\n ? raw\n : undefined\n }\n const hasReaction = (messageId: string) =>\n Boolean(feedbackVote(messageId) || feedbackEmoji(messageId))\n const reactionToneClass = (messageId: string) => {\n // A real emoji carries its own colour → neutral pill. Only the\n // thumb-icon fallback uses the sentiment colour.\n if (feedbackEmoji(messageId)) {\n return 'pk-chatbot-message__reaction--neutral'\n }\n const vote = feedbackVote(messageId)\n if (vote === 'upvote') {\n return 'pk-chatbot-message__reaction--positive'\n }\n if (vote === 'downvote') {\n return 'pk-chatbot-message__reaction--negative'\n }\n return 'pk-chatbot-message__reaction--neutral'\n }\n const reactionTitle = (messageId: string) => {\n const vote = feedbackVote(messageId)\n if (vote === 'upvote') {\n return $t('hint.userFeedbackPositive')\n }\n if (vote === 'downvote') {\n return $t('hint.userFeedbackNegative')\n }\n return $t('hint.userFeedbackReaction')\n }\n\n // Index of the last text part: the reaction chip anchors to that bubble so\n // it renders once even when the message has several text parts.\n const lastTextPartIndex = (message: UIChatMessage) => {\n const parts = getMergedParts(message)\n for (let index = parts.length - 1; index >= 0; index -= 1) {\n if (isTextPart(parts[index])) {\n return index\n }\n }\n return -1\n }\n\n const activeMessage = computed(() => {\n return props.messages?.[props.messages.length - 1]\n })\n const activeMessageLastPart = computed(() => {\n const active = activeMessage.value\n if (!active) {\n return null\n }\n return active.parts[active.parts.length - 1]\n })\n\n const mergedPartsMap = computed(() => {\n const map = new Map<string, UIChatMessage['parts']>()\n for (const message of props.messages ?? []) {\n if (message.role === 'assistant') {\n map.set(\n message.id,\n moveFileOutputsLast(\n dropCorrectedWidgetErrors(\n mergeConsecutiveTextParts(message.parts),\n ),\n ),\n )\n }\n }\n return map\n })\n const getMergedParts = (message: UIChatMessage) => {\n return mergedPartsMap.value.get(message.id) ?? message.parts\n }\n const isError = computed(() => props.status === 'error')\n\n /**\n * The spoken wait shows only while nothing else is working: the wait\n * survives the hang-up (the answer can be written minutes later) and the\n * caller can type meanwhile, and two working indicators stacked would say\n * the same thing twice. It comes back when the written turn is over.\n */\n const isSpokenWaitVisible = computed(\n () => props.awaitingSpokenAnswer === true && !isLoading.value,\n )\n\n /**\n * The wait indicator closes the feed, so it carries the anchor spacer\n * itself, the way the streaming assistant message does. Rendering the\n * standalone spacer as well would stack the two and push the question\n * above the top of the content area, out of view.\n *\n * Only when it is the last thing in the feed: an error card, or an\n * assistant answer already landed, keeps the spacer where it was.\n */\n const isSpokenWaitAnchored = computed(\n () =>\n isSpokenWaitVisible.value &&\n activeMessage.value?.role === 'user' &&\n !isError.value,\n )\n\n const activeMessageLastPartLabel = computed(() => {\n const part = activeMessageLastPart.value\n return getToolPartLabel($t, part)\n })\n const activeMessageLastPartIcon = computed(() => {\n const part = activeMessageLastPart.value\n return getPartIcon(part)\n })\n\n const getMessageIndexById = (messageId: string) => {\n return props.messages?.findIndex((m) => m.id === messageId)\n }\n const isLastMessage = (index?: number) => {\n if (index === undefined || !props.messages) {\n return false\n }\n return index === props.messages.length - 1\n }\n const isMessageRegenerateButtonVisible = (index: number) => {\n return (\n isLastMessage(index) &&\n props.status === 'ready' &&\n !!props.actions?.includes('regenerate')\n )\n }\n // On the last message the actions appear only once the answer is ready;\n // on previous messages they are always available.\n const isActionButtonVisible = (\n action: ChatMessageActions,\n index: number,\n ) => {\n return (\n (!isLastMessage(index) || props.status === 'ready') &&\n !!props.actions?.includes(action)\n )\n }\n const isLastTextPart = (message: UIChatMessage) => {\n const lastPart = message.parts[message.parts.length - 1]\n return isTextPart(lastPart)\n }\n const messageHasSteps = (message: UIChatMessage) =>\n message.parts.some((part) => isReasoningPart(part) || isToolPart(part))\n // The extended steps timeline sits above the answer as soon as the\n // message has at least one step to show.\n const showStepsTimeline = (message: UIChatMessage) =>\n Boolean(props.showExtendedSteps) &&\n isAssistant(message) &&\n messageHasSteps(message)\n // A tool part the feed draws itself: its own component, a slot filled by\n // the host, or the debug chip of `showAllMessageParts`.\n const isRenderedToolPart = (part: UIChatMessage['parts'][number]) =>\n isToolPart(part) &&\n !isStreamingPart(part) &&\n Boolean(\n toolComponentMap[getToolPartName(part)] ||\n slots[part.type] ||\n props.showAllMessageParts,\n )\n // Whether anything is drawn below the steps timeline yet, by the same\n // branches the template walks for each part. Until then the timeline is\n // the bottom of the message and carries the activity itself; from then\n // on it collapses and the compact indicator takes over at the bottom.\n const messageHasContent = (message: UIChatMessage) =>\n message.parts.some(\n (part) =>\n (isTextPart(part) && Boolean(part.text.trim())) ||\n isFilePart(part) ||\n (isConfirmationCardPart(part) && !isStreamingPart(part)) ||\n isRenderedToolPart(part),\n )\n const isAssistant = (message: UIChatMessage) => message.role === 'assistant'\n const isUser = (message: UIChatMessage) => message.role === 'user'\n const getPreviousAssistantMessage = (index: number) => {\n return props.messages\n ?.slice(0, index)\n .reverse()\n .find((message) => isAssistant(message))\n }\n const getNextAssistantMessage = (index: number) => {\n return props.messages\n ?.slice(index + 1)\n .find((message) => isAssistant(message))\n }\n // Returns the date to show in the day divider before the message at\n // `index`, or undefined when the surrounding assistant messages belong\n // to the same (local) day.\n const getDividerDate = (index: number) => {\n const previousCreatedAt =\n getPreviousAssistantMessage(index)?.metadata?.createdAt\n const nextCreatedAt =\n getNextAssistantMessage(index)?.metadata?.createdAt\n if (!previousCreatedAt || !nextCreatedAt) {\n return undefined\n }\n const previousDate = new Date(previousCreatedAt)\n const nextDate = new Date(nextCreatedAt)\n return previousDate.toDateString() !== nextDate.toDateString()\n ? nextDate\n : undefined\n }\n // Wakeups (spec agent-wakeup): the checks that found nothing read as one\n // line and a seed as a thin row, never as words the user wrote. An answer\n // that draws anything (a widget, an approval card) is never folded. The\n // log pages, which draw every part, keep them all.\n const wakeupFeed = computed(() =>\n props.showAllMessageParts\n ? buildWakeupFeed([])\n : buildWakeupFeed(props.messages ?? [], messageHasContent),\n )\n // The numbering the conversation analysis cites: a message with no parts\n // is not rendered and not counted there either.\n const messageNumbers = computed(() => {\n let count = 0\n return (props.messages ?? []).map((message) =>\n message.parts.length > 0 ? ++count : undefined,\n )\n })\n const showMessageFooter = (message: UIChatMessage, index: number) => {\n if (index === 0 || message.role !== 'assistant') {\n return false\n }\n return Boolean(\n props.actions?.length ||\n props.showMessageNumber ||\n (props.showMessageDateTime &&\n message.metadata?.createdAt !== undefined) ||\n message.metadata?.completedAt !== undefined ||\n (props.showMessageTokensCount &&\n message.metadata?.totalTokens !== undefined),\n )\n }\n // Hover-only metadata (time + duration), shown when the fixed variant\n // (showMessageDateTime) is off.\n const formatMessageTime = (createdAt: number) => {\n const date = new Date(createdAt)\n const isToday = date.toDateString() === new Date().toDateString()\n return $d(date, isToday ? 'time' : 'date-time')\n }\n const getMessageDuration = (message: UIChatMessage) => {\n const { createdAt, completedAt } = message.metadata ?? {}\n if (!createdAt || !completedAt) {\n return undefined\n }\n return formatDuration($n, completedAt - createdAt)\n }\n // feedback message auto scroll\n const feedbackMessageEl =\n useTemplateRef<InstanceType<typeof PkChatbotFeedbackForm>[]>(\n 'feedbackMessageEl',\n )\n watch(\n () => props.feedbackMessageId,\n async () => {\n await nextTick()\n if (!props.feedbackMessageId) {\n return\n }\n const el = feedbackMessageEl.value?.[0]?.$el\n if (!el) {\n return\n }\n if (isLastMessage(getMessageIndexById(props.feedbackMessageId))) {\n scrollToBottom()\n return\n }\n el.scrollIntoView({ behavior: 'smooth', block: 'center' })\n },\n )\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n class=\"pk-chatbot-messages scrollbar-thin\"\n :class=\"{ 'pk-chatbot-messages--settling': isSpacerSettling }\"\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n :aria-busy=\"isLoading\"\n :style=\"{\n '--chatbot-main-color': mainColor,\n '--chatbot-contrast-color': contrastColor,\n }\"\n @scroll=\"onScroll\">\n <!-- First in flow (content top): its absolutely-positioned children\n must never inflate the scrollable overflow past the real content -->\n <div\n v-if=\"showScrollToBottom && outlineItems.length > 1\"\n class=\"pk-chatbot-messages__outline\">\n <PkChatbotOutlineRail\n :items=\"outlineItems\"\n :active-id=\"activeOutlineId\"\n @select=\"scrollToUserMessage\" />\n </div>\n <div ref=\"wrapperEl\" class=\"pk-chatbot-messages__wrapper\">\n <template v-for=\"(message, index) in messages\" :key=\"message.id\">\n <!-- The pk-chatbot-divider classes carry no rule of ours: they\n stay as hooks for the custom CSS of an agent. -->\n <div\n v-if=\"isUser(message) && getDividerDate(index)\"\n class=\"pk-chatbot-divider vv-separator\">\n <span class=\"pk-chatbot-divider__label\">\n {{ $d(getDividerDate(index)!, 'short') }}\n </span>\n </div>\n <div\n v-if=\"wakeupFeed.quietRuns.has(message.id)\"\n class=\"pk-chatbot-wakeup\">\n <VvIcon name=\"ri:alarm-line\" class=\"shrink-0\" />\n <span>\n {{\n $t(\n 'label.wakeupQuietRun',\n {\n count: wakeupFeed.quietRuns.get(message.id)!\n .count,\n time: $d(\n wakeupFeed.quietRuns.get(message.id)!\n .lastAt ?? Date.now(),\n 'time',\n ),\n },\n wakeupFeed.quietRuns.get(message.id)!.count,\n )\n }}\n </span>\n </div>\n <div\n v-else-if=\"\n isWakeupSeed(message) &&\n !wakeupFeed.hidden.has(message.id) &&\n !showAllMessageParts\n \"\n class=\"pk-chatbot-wakeup\"\n :title=\"message.metadata?.wakeup?.reason\">\n <VvIcon name=\"ri:alarm-line\" class=\"shrink-0\" />\n <span>\n {{ $t('label.wakeupCheck') }}\n <template v-if=\"message.metadata?.createdAt\">\n · {{ $d(message.metadata.createdAt, 'time') }}\n </template>\n </span>\n </div>\n <div\n v-if=\"\n !(isWakeupSeed(message) && !showAllMessageParts) &&\n !(\n wakeupFeed.hidden.has(message.id) &&\n !(isLoading && isLastMessage(index))\n ) &&\n (message.parts.length ||\n (isLastMessage(index) &&\n isLoading &&\n isAssistant(message)))\n \"\n class=\"pk-chatbot-message\"\n :class=\"[\n `pk-chatbot-message--${message.role}`,\n {\n 'pk-chatbot-message--loading':\n isLoading &&\n isLastMessage(index) &&\n isAssistant(message),\n },\n ]\"\n :style=\"{\n minHeight:\n isLastMessage(index) &&\n isAssistant(message) &&\n !isError\n ? `${height}px`\n : undefined,\n }\">\n <transition appear name=\"pk-chatbot-part\">\n <PkChatbotSteps\n v-if=\"showStepsTimeline(message)\"\n :message\n :is-streaming=\"isLoading && isLastMessage(index)\"\n :has-content=\"messageHasContent(message)\"\n :is-dark />\n </transition>\n <template\n v-for=\"(part, partIndex) in getMergedParts(message)\"\n :key=\"feedPartKey(part, partIndex)\">\n <transition\n v-if=\"isTextPart(part) && part.text.trim()\"\n appear\n name=\"pk-chatbot-part\">\n <div class=\"pk-chatbot-message__text\">\n <slot\n v-bind=\"{\n message,\n part,\n index,\n isLoading,\n }\"\n name=\"text\">\n <PkStreamingMarkdown\n v-if=\"isAssistant(message)\"\n class=\"wysiwyg\"\n :markdown=\"part.text\"\n :is-dark\n :loading=\"\n isLastMessage(index) &&\n status === 'streaming'\n \" />\n <template v-else>\n {{ part.text }}\n </template>\n </slot>\n <!-- Read-only reaction chip overlapping the\n bubble corner, like a messaging app. Hidden\n when a comment is shown: the callout below\n then carries the reaction glyph instead. -->\n <span\n v-if=\"\n hasReaction(message.id) &&\n !getMessageFeedback(message.id)\n ?.comment &&\n partIndex === lastTextPartIndex(message)\n \"\n class=\"pk-chatbot-message__reaction\"\n :class=\"reactionToneClass(message.id)\"\n :title=\"reactionTitle(message.id)\">\n <span\n v-if=\"feedbackEmoji(message.id)\"\n class=\"pk-chatbot-message__reaction-emoji\">\n {{ feedbackEmoji(message.id) }}\n </span>\n <VvIcon\n v-else\n :name=\"\n feedbackVote(message.id) ===\n 'downvote'\n ? 'ri:thumb-down-fill'\n : 'ri:thumb-up-fill'\n \" />\n </span>\n </div>\n </transition>\n <transition\n v-else-if=\"isFilePart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <div\n v-if=\"\n isAudioFile(part.filename, part.mediaType)\n \"\n class=\"flex flex-col items-end gap-8 w-full\">\n <PkChatbotFilePreview\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\"\n :size=\"\n getFilePartMedia(part)?.bytesCount\n \" />\n <!-- Heard once when it arrives: until the\n transcript comes back the turn waits on it. -->\n <PkChatbotAudioTranscript\n :url=\"part.url\"\n :transcript=\"getFilePartTranscript(part)\"\n :pending=\"\n status === 'submitted' &&\n isLastMessage(index)\n \" />\n </div>\n <PkChatbotFilePreview\n v-else\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\"\n :size=\"getFilePartMedia(part)?.bytesCount\" />\n </transition>\n <!-- Anything asking the user for consent (a suspended\n tool call or a `requestConfirm`) shares one card,\n ahead of the per-tool components. An approval stays\n answerable only in the trailing message: the backend\n closes the abandoned ones as denied, while a\n `requestConfirm` can still be resolved later. -->\n <transition\n v-else-if=\"\n isConfirmationCardPart(part) &&\n !isStreamingPart(part)\n \"\n appear\n name=\"pk-chatbot-part\">\n <!-- Key WITHOUT the part state: the card must stay\n mounted across the approval lifecycle so its\n inner transitions play once; remounting per\n state replays the appear animation on every\n hop (requested → responded → output). -->\n <PkToolApprovalCard\n :key=\"`approval-${partIndex}`\"\n :part\n :actionable=\"\n interactive === true &&\n (isToolConfirmPart(part) ||\n isLastMessage(index))\n \"\n :call-verdict=\"\n confirmVerdictsOnCall?.get(\n (part as { toolCallId?: string })\n .toolCallId ?? '',\n )\n \"\n @respond=\"onCardRespond(part, $event)\" />\n </transition>\n <transition\n v-else-if=\"isRenderedToolPart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <component\n :is=\"toolComponentMap[getToolPartName(part)]\"\n v-if=\"\n !slots[part.type] &&\n toolComponentMap[getToolPartName(part)]\n \"\n :key=\"`component-${partIndex}-${getPartState(part)}`\"\n :part\n v-bind=\"recordToolProps(part)\" />\n <slot\n v-else\n :name=\"part.type\"\n v-bind=\"{\n message,\n part,\n index,\n isLoading,\n }\">\n <div\n v-if=\"showAllMessageParts\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon\n v-if=\"getPartIcon(part)\"\n :name=\"getPartIcon(part)!\"\n class=\"shrink-0\" />\n <span class=\"text-12\">\n {{\n getPartTypeLabel(part, {\n t: $t,\n te: $te,\n })\n }}\n </span>\n </div>\n </div>\n </slot>\n </transition>\n <!-- Read-only comment: anchored right after the message\n text (same bubble as the reaction chip) so the\n feedback stays attached to the message body. -->\n <transition mode=\"out-in\">\n <PkChatbotMessageFeedback\n v-if=\"\n partIndex === lastTextPartIndex(message) &&\n readonlyFeedback &&\n getMessageFeedback(message.id)?.comment\n \"\n :feedback=\"getMessageFeedback(message.id)!\" />\n </transition>\n </template>\n <!-- The activity always reads at the bottom of the answer.\n With the steps timeline it shows only once something\n is drawn below the timeline: before that the timeline\n is the bottom, and both would name the same step. -->\n <transition name=\"fade-in\" mode=\"out-in\">\n <div\n v-if=\"\n isLoading &&\n isLastMessage(index) &&\n !isLastTextPart(message) &&\n isAssistant(message) &&\n (!showStepsTimeline(message) ||\n messageHasContent(message))\n \"\n :key=\"`loading-info-${message.id}`\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon name=\"loading-loop\" class=\"shrink-0\" />\n <transition mode=\"out-in\">\n <VvIcon\n v-if=\"activeMessageLastPartIcon\"\n :key=\"activeMessageLastPartIcon\"\n class=\"shrink-0\"\n :name=\"activeMessageLastPartIcon\" />\n </transition>\n <transition mode=\"out-in\">\n <span\n v-if=\"activeMessageLastPartLabel\"\n :key=\"activeMessageLastPartLabel\"\n class=\"text-12\">\n {{ activeMessageLastPartLabel }}\n </span>\n </transition>\n </div>\n <!-- With the timeline the reasoning lives in its\n step, which opens on demand. -->\n <transition name=\"fade-in\" mode=\"out-in\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"\n !showStepsTimeline(message) &&\n activeMessageLastPart &&\n 'text' in activeMessageLastPart &&\n activeMessageLastPart.text.trim()\n \"\n :markdown=\"activeMessageLastPart.text\"\n :is-dark\n :tail-length=\"LIVE_REASONING_TAIL_LENGTH\"\n inner-class=\"wysiwyg\"\n class=\"border border-surface-3 rounded-md p-4 mt-8 bg-surface-1 max-h-64 text-12 w-full\" />\n </transition>\n </div>\n </transition>\n <transition mode=\"out-in\">\n <div\n v-if=\"showMessageFooter(message, index)\"\n class=\"pk-chatbot-message__footer\">\n <VvButtonGroup\n modifiers=\"compact\"\n class=\"mr-auto shrink-0\">\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isMessageRegenerateButtonVisible(\n index,\n )\n \"\n icon=\"ri:reset-right-line\"\n modifiers=\"action-quiet-small\"\n :title=\"$t('action.regenerate')\"\n :aria-label=\"$t('action.regenerate')\"\n @click.stop=\"$emit('regenerate')\" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\n 'show-info',\n index,\n )\n \"\n icon=\"ri:information-line\"\n :title=\"$t('action.getMoreInfo')\"\n :aria-label=\"$t('action.getMoreInfo')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('show-info', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\n 'revise',\n index,\n )\n \"\n :icon=\"\n isRevised(message.id)\n ? 'ri:file-edit-fill'\n : 'ri:file-edit-line'\n \"\n :title=\"\n isRevised(message.id)\n ? $t('action.editRevise')\n : $t('action.createRevise')\n \"\n :aria-label=\"\n isRevised(message.id)\n ? $t('action.editRevise')\n : $t('action.createRevise')\n \"\n modifiers=\"action-quiet-small\"\n :class=\"{\n 'text-brand-graphic': isRevised(\n message.id,\n ),\n }\"\n @click.stop=\"\n $emit('revise', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'upvote',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.vote === 'upvote'\n ? 'ri:thumb-up-fill'\n : 'ri:thumb-up-line'\n \"\n :title=\"$t('action.upvote')\"\n :aria-label=\"$t('action.upvote')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('upvote', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'downvote',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.vote === 'downvote'\n ? 'ri:thumb-down-fill'\n : 'ri:thumb-down-line'\n \"\n :title=\"$t('action.downvote')\"\n :aria-label=\"$t('action.downvote')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('downvote', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'feedback',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.comment\n ? 'ri:feedback-fill'\n : 'ri:feedback-line'\n \"\n :title=\"$t('action.feedback')\"\n :aria-label=\"$t('action.feedback')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('feedback', message)\n \" />\n </transition>\n </VvButtonGroup>\n <div\n v-if=\"\n !showMessageDateTime &&\n message.metadata?.createdAt &&\n message.metadata?.completedAt\n \"\n class=\"pk-chatbot-message__hover-meta\">\n <time\n :datetime=\"\n new Date(\n message.metadata.createdAt,\n ).toISOString()\n \"\n :title=\"\n $d(\n new Date(\n message.metadata.createdAt,\n ),\n 'date-time',\n )\n \"\n class=\"flex items-center gap-4 min-w-0\">\n <VvIcon\n name=\"ri:time-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\">{{\n formatMessageTime(\n message.metadata.createdAt,\n )\n }}</span>\n </time>\n <span\n v-if=\"getMessageDuration(message)\"\n class=\"flex items-center gap-4 min-w-0\">\n <VvIcon\n name=\"ri:hourglass-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\">{{\n getMessageDuration(message)\n }}</span>\n </span>\n </div>\n <span\n v-if=\"showMessageNumber\"\n :title=\"\n $t('label.messageNumber', {\n n: messageNumbers[index],\n })\n \"\n class=\"text-12 text-word-3 tabular-nums\">\n #{{ messageNumbers[index] }}\n </span>\n <span\n v-if=\"\n showMessageTokensCount &&\n message.metadata?.totalTokens\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon\n name=\"ri:ai-generate-2-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\"\n >{{\n $n(\n message.metadata.totalTokens,\n 'integer',\n )\n }}\n {{ $t('label.tokens') }}\n </span>\n </span>\n <time\n v-if=\"\n showMessageDateTime &&\n message.metadata?.createdAt\n \"\n :datetime=\"\n new Date(\n message.metadata?.createdAt,\n ).toISOString()\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon name=\"ri:time-line\" class=\"shrink-0\" />\n <span class=\"truncate\">{{\n $d(\n new Date(message.metadata?.createdAt),\n 'date-time',\n )\n }}</span>\n </time>\n <div\n v-if=\"\n showMessageDateTime &&\n message?.metadata?.completedAt &&\n message?.metadata?.createdAt\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon\n name=\"ri:hourglass-line\"\n class=\"shrink-0\" />\n <PkRelativeTime\n class=\"truncate\"\n :date=\"message.metadata?.createdAt\"\n :end-date=\"message.metadata?.completedAt\" />\n </div>\n </div>\n </transition>\n <transition mode=\"out-in\">\n <PkChatbotFeedbackForm\n v-if=\"message.id === feedbackMessageId\"\n ref=\"feedbackMessageEl\"\n :loading=\"feedbackLoading\"\n :submitted=\"feedbackSubmitted\"\n :error=\"feedbackError\"\n @submit=\"$emit('feedback-submit', $event)\"\n @close=\"$emit('feedback-close')\" />\n </transition>\n </div>\n </template>\n <!-- A spoken turn streams to the provider, not here: nothing in\n this list moves between the question and the answer landing.\n Its own indicator, because the status-driven one hangs off an\n assistant message the SDK never creates for a call. When to\n show it and when it carries the anchor spacer: see\n `isSpokenWaitVisible` and `isSpokenWaitAnchored`. -->\n <transition appear name=\"pk-chatbot-part\">\n <div\n v-if=\"isSpokenWaitVisible\"\n class=\"pk-chatbot-message pk-chatbot-message--assistant pk-chatbot-message--loading\"\n :style=\"{\n minHeight: isSpokenWaitAnchored\n ? `${height}px`\n : undefined,\n }\">\n <div class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon name=\"loading-loop\" class=\"shrink-0\" />\n <span class=\"text-12\">\n {{ $t('label.answeringOnTheCall') }}\n </span>\n </div>\n </div>\n </div>\n </transition>\n <transition appear name=\"pk-chatbot-part\">\n <PkChatbotError\n v-if=\"isError\"\n :error\n @retry=\"$emit('auto-retry')\"\n @reset=\"$emit('reset-chat')\" />\n </transition>\n <div\n v-if=\"\n (activeMessage?.role === 'user' && !isSpokenWaitAnchored) ||\n isError\n \"\n class=\"pk-chatbot-messages__spacer\"\n :style=\"{ minHeight: `${height}px` }\"></div>\n </div>\n <Transition name=\"pk-chatbot-messages-fab\">\n <button\n v-if=\"showScrollToBottom && (userScrolledUp || !isAtBottom)\"\n type=\"button\"\n class=\"pk-chatbot-messages__scroll-to-bottom\"\n :title=\"$t('action.scrollToBottom')\"\n :aria-label=\"$t('action.scrollToBottom')\"\n @click=\"onScrollToBottomClick\">\n <VvIcon name=\"ri:arrow-down-line\" />\n </button>\n </Transition>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-messages {\n overflow-y: auto;\n flex: 1;\n min-width: 0;\n font-size: var(--text-14);\n\n &__wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-16);\n min-width: 0;\n }\n\n // Conversation outline rail: sticky shell pinned to the vertical\n // middle of the scroll area, at the right edge. Zero-sized and\n // first in flow so neither it nor its absolutely-positioned pieces\n // inflate scrollHeight. Desktop only, like ChatGPT: in the widget and\n // the panel the chat is as wide on a phone as on a desktop, so the\n // viewport is the only thing that tells them apart. The fullscreen\n // layout measures the chat itself instead (PkChatbot).\n &__outline {\n display: none;\n position: sticky;\n top: 50%;\n z-index: 2;\n flex: none;\n align-self: flex-end;\n height: 0;\n\n @include media-breakpoint-up('md', $breakpoints) {\n display: block;\n }\n\n .pk-chatbot-outline {\n position: absolute;\n top: 0;\n right: 0;\n transform: translateY(-50%);\n }\n }\n\n // Placeholder while the answer has not started yet: settles like\n // the message spacer when released. Only the release is animated\n // (`--settling`): animating the growth would keep `scrollHeight`\n // behind while the anchor scroll reads it, landing short of the\n // anchor with no second chance to catch up.\n &--settling {\n .pk-chatbot-messages__spacer,\n .pk-chatbot-message {\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n }\n }\n\n // Floating \"back to bottom\" button, sticky inside the scroll area\n &__scroll-to-bottom {\n position: sticky;\n bottom: var(--spacing-8);\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n // The scroll container is a flex column: without this the\n // overflowing content shrinks the button, ignoring its height\n flex: none;\n width: var(--spacing-36);\n height: var(--spacing-36);\n // Net-zero layout contribution: mounting/unmounting the button\n // must not change scrollHeight, or reaching the bottom (which\n // hides it) would clamp scrollTop and cause a visible jump\n margin-block-start: calc(-1 * var(--spacing-36));\n margin-inline: auto;\n border: 1px solid var(--color-surface-3);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-1);\n box-shadow: var(--shadow-md);\n color: var(--color-word-1);\n cursor: pointer;\n transition: var(--transition-colors);\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n .pk-chatbot-messages-fab-enter-active,\n .pk-chatbot-messages-fab-leave-active {\n transition: opacity 150ms var(--ease-out);\n }\n\n .pk-chatbot-messages-fab-enter-from,\n .pk-chatbot-messages-fab-leave-to {\n opacity: 0;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .pk-chatbot-messages-fab-enter-active,\n .pk-chatbot-messages-fab-leave-active {\n transition: none;\n }\n }\n\n // A wakeup in the feed (spec agent-wakeup): a check the agent ran by\n // itself, or a run of them that found nothing. Quiet like the day divider.\n .pk-chatbot-wakeup {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-xs);\n color: var(--color-word-4);\n font-size: var(--text-12);\n text-align: center;\n }\n\n .pk-chatbot-message {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-sm);\n min-width: 0;\n align-items: flex-start;\n // `clip` instead of `hidden` so the reaction chip, which deliberately\n // overhangs the bubble's bottom edge, is not cut off. The margin has\n // to cover half the chip plus its shadow.\n overflow: clip;\n overflow-clip-margin: var(--spacing-16);\n max-width: 100%;\n // The anchor spacer (inline min-height on the last assistant message)\n // settles smoothly when released at the end of the stream: the\n // transition lives on `.pk-chatbot-messages--settling` above\n\n &__text {\n position: relative;\n color: var(--color-word-2);\n background-color: var(--color-surface);\n line-height: var(--leading-normal);\n border-width: var(--spacing-px);\n border-style: solid;\n border-color: var(--color-surface-3);\n padding: var(--spacing-sm);\n border-radius: var(--rounded-xl) var(--rounded-xl) var(--rounded-xl)\n 0;\n max-width: 100%;\n text-overflow: ellipsis;\n // Same reason as the block above: the chip is absolutely\n // positioned against this bubble and hangs below its edge.\n overflow: clip;\n overflow-clip-margin: var(--spacing-16);\n }\n\n // Read-only feedback: a circular vote chip that overlaps the bubble's\n // bottom-right corner (messaging-app style). White ring lifts it off\n // the bubble; solid accent makes the rating unmistakable.\n &__reaction {\n position: absolute;\n right: var(--spacing-8);\n bottom: 0;\n transform: translateY(50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--spacing-24);\n height: var(--spacing-24);\n border-radius: var(--rounded-full, 9999px);\n color: var(--color-white, #fff);\n font-size: var(--text-12);\n box-shadow: var(--shadow-md);\n\n &--positive {\n background-color: var(--color-success);\n color: var(--color-success-contrast);\n }\n\n &--negative {\n background-color: var(--color-danger);\n color: var(--color-danger-contrast);\n }\n\n // Real emoji (e.g. WhatsApp): the glyph carries its own colour, so\n // the chip stays a neutral surface pill.\n &--neutral {\n background-color: var(--color-surface-1);\n }\n }\n\n &__reaction-emoji {\n // Smaller than the chip so the glyph doesn't touch the ring.\n font-size: var(--text-12);\n line-height: 1;\n }\n\n &__loading-info {\n line-height: var(--leading-normal);\n border-width: var(--spacing-px);\n border-style: solid;\n border-color: var(--color-surface-3);\n padding: var(--spacing-sm);\n border-radius: var(--rounded-xl) var(--rounded-xl) var(--rounded-xl)\n 0;\n font-size: var(--text-12);\n color: var(--color-word-3);\n background-color: var(--color-surface);\n }\n\n &__footer {\n width: 100%;\n display: flex;\n gap: var(--spacing-8);\n align-items: center;\n }\n\n &__hover-meta {\n display: flex;\n min-width: 0;\n align-items: center;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n color: var(--color-word-3);\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n opacity: 0;\n transition: opacity 0.2s ease;\n }\n\n &:hover &__hover-meta,\n &:focus-within &__hover-meta {\n opacity: 1;\n }\n\n // Hover doesn't exist on touch devices: keep the metadata visible\n @media (hover: none) {\n &__hover-meta {\n opacity: 1;\n }\n }\n\n &--user {\n align-items: flex-end;\n\n .pk-chatbot-message__text {\n background-color: var(\n --chatbot-main-color,\n var(--color-surface-1)\n );\n border-color: var(--chatbot-main-color, var(--color-surface-3));\n color: var(--chatbot-contrast-color, var(--color-word-1));\n border-radius: var(--rounded-xl) var(--rounded-xl) 0\n var(--rounded-xl);\n }\n }\n\n &--assistant {\n width: 100%;\n gap: var(--spacing-16);\n\n .pk-chatbot-message__text {\n width: 100%;\n }\n }\n\n &--system {\n align-items: flex-end;\n\n .pk-chatbot-message__text {\n display: flex;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n padding: var(--spacing-8) var(--spacing-sm);\n align-items: center;\n border-color: var(--chatbot-main-color, var(--color-surface-3));\n color: var(--chatbot-main-color, var(--color-word-1));\n border-radius: var(--rounded-xl) var(--rounded-xl) 0\n var(--rounded-xl);\n }\n }\n }\n\n .pk-chatbot-part-enter-active {\n transition:\n opacity 0.25s ease,\n transform 0.25s ease;\n }\n\n .pk-chatbot-part-enter-from {\n opacity: 0;\n transform: translateY(8px);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .pk-chatbot-part-enter-active {\n transition: none;\n }\n\n .pk-chatbot-part-enter-from {\n transform: none;\n }\n }\n</style>\n","<script lang=\"ts\" setup>\n import type {\n ChatMessageActions,\n MessageFeedback,\n RevisedAnswer,\n UIChatMessage,\n } from 'models'\n import { isAudioFile, isWakeupSeed } from 'models'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkChatbotError from './PkChatbotError.vue'\n import PkChatbotFeedbackForm from './PkChatbotFeedbackForm.vue'\n import PkChatbotMessageFeedback from './PkChatbotMessageFeedback.vue'\n import PkChatbotFilePreview from './PkChatbotFilePreview.vue'\n import PkChatbotAudioTranscript from './PkChatbotAudioTranscript.vue'\n import PkRelativeTime from '../PkRelativeTime.vue'\n import { useI18n } from 'vue-i18n'\n import {\n useTemplateRef,\n ref,\n shallowRef,\n watch,\n computed,\n nextTick,\n onMounted,\n useSlots,\n } from 'vue'\n import { useResizeObserver } from '@vueuse/core'\n import { toolComponentMap } from './toolComponentMap'\n import PkToolApprovalCard from './PkToolApprovalCard.vue'\n import PkChatbotSteps from './PkChatbotSteps.vue'\n import {\n resolveContrastColor,\n getPartState,\n isTextPart,\n isFilePart,\n isToolPart,\n isReasoningPart,\n isStreamingPart,\n isToolConfirmPart,\n isConfirmationCardPart,\n getToolApprovalId,\n getPartIcon,\n getPartTypeLabel,\n getToolPartLabel,\n mergeConsecutiveTextParts,\n dropCorrectedWidgetErrors,\n moveFileOutputsLast,\n feedPartKey,\n formatDuration,\n LIVE_REASONING_TAIL_LENGTH,\n } from './utils'\n import {\n getFilePartMedia,\n getFilePartTranscript,\n getToolPartName,\n } from 'utils'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotOutlineRail from './PkChatbotOutlineRail.vue'\n import { useChatScroll } from './useChatScroll'\n import { buildWakeupFeed } from './wakeupFeed'\n\n const props = defineProps<{\n status?: 'submitted' | 'streaming' | 'ready' | 'error'\n messages?: UIChatMessage[]\n error?: Error\n actions?: ChatMessageActions[]\n mainColor?: string\n textColor?: 'auto' | 'white' | 'black'\n revisedAnswers?: RevisedAnswer[]\n messageFeedbacks?: MessageFeedback[]\n /**\n * Render the vote/feedback action buttons as read-only state indicators\n * (e.g. admin chat-logs view): icons reflect the user's feedback but the\n * buttons are not interactive.\n */\n readonlyFeedback?: boolean\n disableHeightAdjustment?: boolean\n showMessageDateTime?: boolean\n showMessageTokensCount?: boolean\n /**\n * Shows each answer's position in the chat, from 1, the numbering the\n * conversation analysis cites (\"message 62\").\n */\n showMessageNumber?: boolean\n showAllMessageParts?: boolean\n /**\n * Lets the user answer the tool requests rendered in the feed (approval\n * and confirm cards). Off by default: the log viewers render the same\n * messages read-only and have nothing to answer with.\n */\n interactive?: boolean\n showExtendedSteps?: boolean\n /**\n * A call is waiting for its answer. The turn runs outside this client\n * (the voice provider drives it), so `status` says nothing about it and\n * the feed would sit still for the seconds it takes.\n */\n awaitingSpokenAnswer?: boolean\n /**\n * Verdicts given ON A CALL, by toolCallId. A confirm answered during a\n * call goes back to the agent as a spoken message, so its part never\n * leaves the render-ack state: without this the card kept its buttons\n * after the answer, and every extra tap said \"Confermo\" again\n * (measured 2026-08-07). Only successful sends are recorded, so a tap\n * that never reached the provider leaves the card usable.\n */\n confirmVerdictsOnCall?: Map<string, boolean>\n isDark?: boolean\n /** Show a floating \"back to bottom\" button when the user scrolls up (fullscreen layout) */\n showScrollToBottom?: boolean\n // TODO: move feedback in a separate component to avoid passing these props\n feedbackMessageId?: string\n feedbackLoading?: boolean\n feedbackSubmitted?: boolean\n feedbackError?: string\n }>()\n\n const emit = defineEmits<{\n (e: 'show-info', message: UIChatMessage): void\n (e: 'regenerate'): void\n (e: 'revise', message: UIChatMessage): void\n (e: 'upvote', message: UIChatMessage): void\n (e: 'downvote', message: UIChatMessage): void\n (e: 'feedback', message: UIChatMessage): void\n (e: 'feedback-submit', comment: string): void\n (e: 'feedback-close'): void\n (e: 'scroll-up'): void\n (e: 'scroll-down'): void\n (e: 'auto-retry'): void\n (e: 'reset-chat'): void\n (\n e: 'approval-respond',\n payload: { id: string; approved: boolean },\n ): void\n (\n e: 'confirm-respond',\n payload: { toolCallId: string; confirmed: boolean },\n ): void\n }>()\n\n const {\n t: $t,\n te: $te,\n d: $d,\n n: $n,\n } = useI18n({\n useScope: 'global',\n })\n\n const slots = useSlots()\n\n /**\n * Forwards the user's verdict from the confirmation card. The two kinds of\n * request travel back on different channels: an approval answers the SDK\n * approval it carries the id of, a `requestConfirm` resolves as a client\n * tool output. Without the id there is nothing to answer, so the click is\n * dropped instead of sending an unmatchable response.\n */\n const onCardRespond = (\n part: UIChatMessage['parts'][number],\n approved: boolean,\n ) => {\n if (isToolConfirmPart(part)) {\n const toolCallId = (part as { toolCallId?: string }).toolCallId\n if (toolCallId) {\n emit('confirm-respond', { toolCallId, confirmed: approved })\n }\n return\n }\n const id = getToolApprovalId(part)\n if (!id) {\n return\n }\n emit('approval-respond', { id, approved })\n }\n\n const scrollEl = useTemplateRef<HTMLDivElement>('scrollEl')\n const wrapperEl = useTemplateRef<HTMLDivElement>('wrapperEl')\n const contrastColor = computed(() =>\n resolveContrastColor(props.textColor, props.mainColor),\n )\n\n // --- Scroll ---\n\n const { handleScroll, scrollToBottom, userScrolledUp, isAtBottom } =\n useChatScroll({\n scrollEl,\n status: () => props.status,\n messagesLength: () => props.messages?.length,\n lastMessageRole: () =>\n props.messages?.[props.messages.length - 1]?.role,\n contentEl: () => wrapperEl.value,\n beforeNewMessageScroll: () => updateAnchorSpacer(),\n onScrollUp: () => emit('scroll-up'),\n onScrollDown: () => emit('scroll-down'),\n })\n\n const onScrollToBottomClick = () => {\n // scrollToBottom() is a no-op while userScrolledUp is set\n userScrolledUp.value = false\n scrollToBottom()\n }\n\n // --- Conversation outline (fullscreen, desktop) ---\n\n // A wakeup's seed is not a question the user asked, and it is not drawn\n // as a user bubble either, which the 1:1 mapping below relies on.\n const outlineItems = computed(() =>\n (props.messages ?? [])\n .filter(\n (message) => message.role === 'user' && !isWakeupSeed(message),\n )\n .map((message) => ({\n id: message.id,\n preview:\n message.parts.find(isTextPart)?.text.trim().slice(0, 120) ||\n '…',\n })),\n )\n const activeOutlineId = ref<string>()\n\n // The rail entries map 1:1 onto the rendered user messages\n const getUserMessageEls = () =>\n scrollEl.value?.querySelectorAll<HTMLElement>(\n '.pk-chatbot-message--user',\n ) ?? []\n\n // Scroll-spy: active is the last user message above the middle of the\n // viewport (the exchange currently being read)\n const updateActiveOutline = () => {\n const scroller = scrollEl.value\n if (!scroller || !props.showScrollToBottom) {\n return\n }\n const line =\n scroller.getBoundingClientRect().top + scroller.clientHeight / 2\n let active = outlineItems.value[0]?.id\n getUserMessageEls().forEach((el, index) => {\n if (el.getBoundingClientRect().top <= line) {\n active = outlineItems.value[index]?.id\n }\n })\n activeOutlineId.value = active\n }\n\n watch(outlineItems, async () => {\n await nextTick()\n updateActiveOutline()\n })\n\n const onScroll = () => {\n handleScroll()\n updateActiveOutline()\n }\n\n const scrollToUserMessage = (id: string) => {\n const scroller = scrollEl.value\n const index = outlineItems.value.findIndex((item) => item.id === id)\n const messageEl = getUserMessageEls()[index]\n if (!scroller || !messageEl) {\n return\n }\n // Intentional jump away from the live tail: the wheel/touch intent\n // detection cannot see programmatic scrolls, pause the auto-follow\n // explicitly (reaching the bottom again re-enables it)\n userScrolledUp.value = true\n // Same resting line as a freshly sent message: top of the content area\n scroller.scrollTo({\n top:\n scroller.scrollTop +\n messageEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top -\n Number.parseFloat(getComputedStyle(scroller).paddingTop),\n behavior: 'smooth',\n })\n }\n\n // The answer is still being worked on: the spacer below holds while it is,\n // and so do the timeline and the activity of the last message.\n const isLoading = computed(() => {\n return props.status === 'submitted' || props.status === 'streaming'\n })\n\n // --- Height adjustment ---\n\n const height = ref<number>(0)\n // The min-height transition only serves the release (see below): while the\n // spacer grows it would hide the new height from the anchor scroll, which\n // reads `scrollHeight` once and would land short of the anchor.\n const isSpacerSettling = ref(false)\n\n // The spacer serves the exchange in flight from its first measure to its\n // release, at zero too (a question taller than the area, a measure\n // skipped while hidden): the height alone cannot tell held from released.\n let isSpacerHeld = false\n // The last user message the spacer was measured against, observed below.\n const anchoredUserMessageEl = shallowRef<HTMLElement>()\n\n // Recalculate when a message is added or the conversation changes, and\n // when one of the two sizes it is computed from changes (see the resize\n // observer below). Idempotent: also called by the scroll composable right\n // before the anchor scroll, so the spacer is always in place first.\n const updateAnchorSpacer = async () => {\n const messages = props.messages\n if (!messages?.length || props.disableHeightAdjustment) {\n return\n }\n // The spacer only serves the in-flight exchange, letting the\n // fresh user message anchor at the top while the answer streams.\n // On plain loads (e.g. opening an old conversation) it would\n // just leave a large void after the last message.\n const isExchangeInFlight =\n messages[messages.length - 1].role === 'user' || isLoading.value\n if (!isExchangeInFlight) {\n return\n }\n isSpacerHeld = true\n isSpacerSettling.value = false\n await nextTick()\n if (!scrollEl.value) {\n return\n }\n // Spacer sized so the freshly sent user message rests at the top\n // of the content area once scrolled to bottom: subtract both\n // paddings (clientHeight includes them) so any extra top\n // clearance (e.g. the fullscreen overlay header) is respected,\n // and the wrapper row gap separating the message from the spacer\n const { paddingTop, paddingBottom } = getComputedStyle(scrollEl.value)\n const scrollElHeight =\n scrollEl.value.clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n const wrapperGap = wrapperEl.value\n ? Number.parseFloat(getComputedStyle(wrapperEl.value).rowGap) || 0\n : 0\n // Measured on the rendered DOM (same lookup as the outline rail):\n // indexing the template-refs array with the `messages` index breaks\n // as soon as a message renders nothing (`v-if` on empty parts), the\n // refs array skips it and the heights shift by one.\n const userMessageEls = getUserMessageEls()\n anchoredUserMessageEl.value = userMessageEls[userMessageEls.length - 1]\n const lastUserMessageHeight =\n anchoredUserMessageEl.value?.clientHeight ?? 0\n const newHeight = scrollElHeight - lastUserMessageHeight - wrapperGap\n // A question taller than the content area leaves nothing to reserve:\n // the spacer goes to zero, never back to the value of the previous\n // exchange. A stale, larger spacer is the very failure the anchor\n // exists to prevent, the scroll landing past the message. Skipped\n // when the element measures nothing (rendered while hidden): there\n // the height already in place is still the good one.\n if (scrollElHeight > 0) {\n height.value = Math.max(newHeight, 0)\n }\n await nextTick()\n }\n\n // Two sources compared one by one: the store recreates `messages` on\n // every token, and a getter returning `[length, id]` would hand back a\n // new array each time and run this at every token. An answer that resumes\n // after a pause (an approval, a form, a tool the client runs) keeps its\n // length and its id, so a spacer released at the pause stays released:\n // bringing it back on the next token threw the view down, and the next\n // pause threw it up again.\n watch(\n [\n () => props.messages?.length,\n () => props.messages?.[props.messages.length - 1]?.id,\n ],\n () => updateAnchorSpacer(),\n )\n\n // The two sizes the spacer is computed from can change while it is held:\n // the question (the transcript of an audio, a box the user opens) and the\n // chat itself (a bar docking above the input, a phone keyboard closing).\n // Followed by observing them rather than by measuring at every token.\n // A released spacer is left alone: an answer resumed after a pause must\n // not bring it back.\n useResizeObserver([scrollEl, anchoredUserMessageEl], () => {\n if (isSpacerHeld) {\n updateAnchorSpacer()\n }\n })\n\n // Mounted mid-send (fullscreen first message: the messages view replaces\n // the empty state after the message is already in): the length watch\n // never fires for that message, apply the anchor spacer now. A first\n // spoken line mounts the same way and never moves `status`, hence the\n // second guard. Plain history loads (mounted at 'ready', nothing in\n // flight) stay untouched.\n onMounted(() => {\n if (isLoading.value || props.awaitingSpokenAnswer) {\n updateAnchorSpacer()\n }\n })\n\n // Release the spacer once the answer stops loading: it would otherwise\n // leave a large empty area after the last message. The min-height\n // transition settles the layout smoothly (the browser clamps the\n // scroll progressively when the viewport sat inside the removed area).\n // Keyed on `isLoading`, not on `status`: whatever keeps the answer\n // loading keeps the spacer too.\n watch(isLoading, (loading) => {\n if (!loading) {\n isSpacerHeld = false\n isSpacerSettling.value = true\n height.value = 0\n }\n })\n\n const isRevised = (messageId: string) => {\n return props.revisedAnswers?.some((r) => r.messageId === messageId)\n }\n\n const getMessageFeedback = (messageId: string) =>\n props.messageFeedbacks?.find((f) => f.messageId === messageId)\n\n // A form the visitor fills in live is, in a feed nobody answers from\n // (the chat logs), the record of what the agent showed: no fields to\n // type in, no button that looks like it sends.\n const RECORD_TOOLS = new Set(['showContactForm'])\n const recordToolProps = (part: Parameters<typeof getToolPartName>[0]) =>\n !props.interactive && RECORD_TOOLS.has(getToolPartName(part))\n ? { record: true }\n : {}\n\n // Interactive vote/feedback buttons. In read-only mode (admin chat-logs)\n // the vote is shown on the message itself (coloured bubble border + an\n // \"active\" thumb in the footer) and the comment, if any, below it.\n const isFeedbackButtonVisible = (\n kind: 'upvote' | 'downvote' | 'feedback',\n index: number,\n ) => !props.readonlyFeedback && isActionButtonVisible(kind, index)\n\n // Read-only feedback for a message (drives the reaction chip anchored to\n // the bubble). undefined when not in read-only mode.\n const readonlyFeedbackOf = (messageId: string) =>\n props.readonlyFeedback ? getMessageFeedback(messageId) : undefined\n const feedbackVote = (messageId: string) =>\n readonlyFeedbackOf(messageId)?.vote\n // The real reaction glyph, shown ONLY for unmapped reactions (no vote):\n // 👍/❤️/👎 keep the classic thumb icon, while e.g. WhatsApp \"😍\" shows the\n // actual emoji. Platforms that send a type word (Teams \"like\") have a vote\n // and therefore use the icon too.\n const feedbackEmoji = (messageId: string) => {\n const feedback = readonlyFeedbackOf(messageId)\n if (feedback?.vote) {\n return undefined\n }\n const raw = feedback?.metadata?.reactionEmoji\n return typeof raw === 'string' && /\\p{Extended_Pictographic}/u.test(raw)\n ? raw\n : undefined\n }\n const hasReaction = (messageId: string) =>\n Boolean(feedbackVote(messageId) || feedbackEmoji(messageId))\n const reactionToneClass = (messageId: string) => {\n // A real emoji carries its own colour → neutral pill. Only the\n // thumb-icon fallback uses the sentiment colour.\n if (feedbackEmoji(messageId)) {\n return 'pk-chatbot-message__reaction--neutral'\n }\n const vote = feedbackVote(messageId)\n if (vote === 'upvote') {\n return 'pk-chatbot-message__reaction--positive'\n }\n if (vote === 'downvote') {\n return 'pk-chatbot-message__reaction--negative'\n }\n return 'pk-chatbot-message__reaction--neutral'\n }\n const reactionTitle = (messageId: string) => {\n const vote = feedbackVote(messageId)\n if (vote === 'upvote') {\n return $t('hint.userFeedbackPositive')\n }\n if (vote === 'downvote') {\n return $t('hint.userFeedbackNegative')\n }\n return $t('hint.userFeedbackReaction')\n }\n\n // Index of the last text part: the reaction chip anchors to that bubble so\n // it renders once even when the message has several text parts.\n const lastTextPartIndex = (message: UIChatMessage) => {\n const parts = getMergedParts(message)\n for (let index = parts.length - 1; index >= 0; index -= 1) {\n if (isTextPart(parts[index])) {\n return index\n }\n }\n return -1\n }\n\n const activeMessage = computed(() => {\n return props.messages?.[props.messages.length - 1]\n })\n const activeMessageLastPart = computed(() => {\n const active = activeMessage.value\n if (!active) {\n return null\n }\n return active.parts[active.parts.length - 1]\n })\n\n const mergedPartsMap = computed(() => {\n const map = new Map<string, UIChatMessage['parts']>()\n for (const message of props.messages ?? []) {\n if (message.role === 'assistant') {\n map.set(\n message.id,\n moveFileOutputsLast(\n dropCorrectedWidgetErrors(\n mergeConsecutiveTextParts(message.parts),\n ),\n ),\n )\n }\n }\n return map\n })\n const getMergedParts = (message: UIChatMessage) => {\n return mergedPartsMap.value.get(message.id) ?? message.parts\n }\n const isError = computed(() => props.status === 'error')\n\n /**\n * The spoken wait shows only while nothing else is working: the wait\n * survives the hang-up (the answer can be written minutes later) and the\n * caller can type meanwhile, and two working indicators stacked would say\n * the same thing twice. It comes back when the written turn is over.\n */\n const isSpokenWaitVisible = computed(\n () => props.awaitingSpokenAnswer === true && !isLoading.value,\n )\n\n /**\n * The wait indicator closes the feed, so it carries the anchor spacer\n * itself, the way the streaming assistant message does. Rendering the\n * standalone spacer as well would stack the two and push the question\n * above the top of the content area, out of view.\n *\n * Only when it is the last thing in the feed: an error card, or an\n * assistant answer already landed, keeps the spacer where it was.\n */\n const isSpokenWaitAnchored = computed(\n () =>\n isSpokenWaitVisible.value &&\n activeMessage.value?.role === 'user' &&\n !isError.value,\n )\n\n const activeMessageLastPartLabel = computed(() => {\n const part = activeMessageLastPart.value\n return getToolPartLabel($t, part)\n })\n const activeMessageLastPartIcon = computed(() => {\n const part = activeMessageLastPart.value\n return getPartIcon(part)\n })\n\n const getMessageIndexById = (messageId: string) => {\n return props.messages?.findIndex((m) => m.id === messageId)\n }\n const isLastMessage = (index?: number) => {\n if (index === undefined || !props.messages) {\n return false\n }\n return index === props.messages.length - 1\n }\n const isMessageRegenerateButtonVisible = (index: number) => {\n return (\n isLastMessage(index) &&\n props.status === 'ready' &&\n !!props.actions?.includes('regenerate')\n )\n }\n // On the last message the actions appear only once the answer is ready;\n // on previous messages they are always available.\n const isActionButtonVisible = (\n action: ChatMessageActions,\n index: number,\n ) => {\n return (\n (!isLastMessage(index) || props.status === 'ready') &&\n !!props.actions?.includes(action)\n )\n }\n const isLastTextPart = (message: UIChatMessage) => {\n const lastPart = message.parts[message.parts.length - 1]\n return isTextPart(lastPart)\n }\n const messageHasSteps = (message: UIChatMessage) =>\n message.parts.some((part) => isReasoningPart(part) || isToolPart(part))\n // The extended steps timeline sits above the answer as soon as the\n // message has at least one step to show.\n const showStepsTimeline = (message: UIChatMessage) =>\n Boolean(props.showExtendedSteps) &&\n isAssistant(message) &&\n messageHasSteps(message)\n // A tool part the feed draws itself: its own component, a slot filled by\n // the host, or the debug chip of `showAllMessageParts`.\n const isRenderedToolPart = (part: UIChatMessage['parts'][number]) =>\n isToolPart(part) &&\n !isStreamingPart(part) &&\n Boolean(\n toolComponentMap[getToolPartName(part)] ||\n slots[part.type] ||\n props.showAllMessageParts,\n )\n // Whether anything is drawn below the steps timeline yet, by the same\n // branches the template walks for each part. Until then the timeline is\n // the bottom of the message and carries the activity itself; from then\n // on it collapses and the compact indicator takes over at the bottom.\n const messageHasContent = (message: UIChatMessage) =>\n message.parts.some(\n (part) =>\n (isTextPart(part) && Boolean(part.text.trim())) ||\n isFilePart(part) ||\n (isConfirmationCardPart(part) && !isStreamingPart(part)) ||\n isRenderedToolPart(part),\n )\n const isAssistant = (message: UIChatMessage) => message.role === 'assistant'\n const isUser = (message: UIChatMessage) => message.role === 'user'\n const getPreviousAssistantMessage = (index: number) => {\n return props.messages\n ?.slice(0, index)\n .reverse()\n .find((message) => isAssistant(message))\n }\n const getNextAssistantMessage = (index: number) => {\n return props.messages\n ?.slice(index + 1)\n .find((message) => isAssistant(message))\n }\n // Returns the date to show in the day divider before the message at\n // `index`, or undefined when the surrounding assistant messages belong\n // to the same (local) day.\n const getDividerDate = (index: number) => {\n const previousCreatedAt =\n getPreviousAssistantMessage(index)?.metadata?.createdAt\n const nextCreatedAt =\n getNextAssistantMessage(index)?.metadata?.createdAt\n if (!previousCreatedAt || !nextCreatedAt) {\n return undefined\n }\n const previousDate = new Date(previousCreatedAt)\n const nextDate = new Date(nextCreatedAt)\n return previousDate.toDateString() !== nextDate.toDateString()\n ? nextDate\n : undefined\n }\n // Wakeups (spec agent-wakeup): the checks that found nothing read as one\n // line and a seed as a thin row, never as words the user wrote. An answer\n // that draws anything (a widget, an approval card) is never folded. The\n // log pages, which draw every part, keep them all.\n const wakeupFeed = computed(() =>\n props.showAllMessageParts\n ? buildWakeupFeed([])\n : buildWakeupFeed(props.messages ?? [], messageHasContent),\n )\n // The numbering the conversation analysis cites: a message with no parts\n // is not rendered and not counted there either.\n const messageNumbers = computed(() => {\n let count = 0\n return (props.messages ?? []).map((message) =>\n message.parts.length > 0 ? ++count : undefined,\n )\n })\n const showMessageFooter = (message: UIChatMessage, index: number) => {\n if (index === 0 || message.role !== 'assistant') {\n return false\n }\n return Boolean(\n props.actions?.length ||\n props.showMessageNumber ||\n (props.showMessageDateTime &&\n message.metadata?.createdAt !== undefined) ||\n message.metadata?.completedAt !== undefined ||\n (props.showMessageTokensCount &&\n message.metadata?.totalTokens !== undefined),\n )\n }\n // Hover-only metadata (time + duration), shown when the fixed variant\n // (showMessageDateTime) is off.\n const formatMessageTime = (createdAt: number) => {\n const date = new Date(createdAt)\n const isToday = date.toDateString() === new Date().toDateString()\n return $d(date, isToday ? 'time' : 'date-time')\n }\n const getMessageDuration = (message: UIChatMessage) => {\n const { createdAt, completedAt } = message.metadata ?? {}\n if (!createdAt || !completedAt) {\n return undefined\n }\n return formatDuration($n, completedAt - createdAt)\n }\n // feedback message auto scroll\n const feedbackMessageEl =\n useTemplateRef<InstanceType<typeof PkChatbotFeedbackForm>[]>(\n 'feedbackMessageEl',\n )\n watch(\n () => props.feedbackMessageId,\n async () => {\n await nextTick()\n if (!props.feedbackMessageId) {\n return\n }\n const el = feedbackMessageEl.value?.[0]?.$el\n if (!el) {\n return\n }\n if (isLastMessage(getMessageIndexById(props.feedbackMessageId))) {\n scrollToBottom()\n return\n }\n el.scrollIntoView({ behavior: 'smooth', block: 'center' })\n },\n )\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n class=\"pk-chatbot-messages scrollbar-thin\"\n :class=\"{ 'pk-chatbot-messages--settling': isSpacerSettling }\"\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n :aria-busy=\"isLoading\"\n :style=\"{\n '--chatbot-main-color': mainColor,\n '--chatbot-contrast-color': contrastColor,\n }\"\n @scroll=\"onScroll\">\n <!-- First in flow (content top): its absolutely-positioned children\n must never inflate the scrollable overflow past the real content -->\n <div\n v-if=\"showScrollToBottom && outlineItems.length > 1\"\n class=\"pk-chatbot-messages__outline\">\n <PkChatbotOutlineRail\n :items=\"outlineItems\"\n :active-id=\"activeOutlineId\"\n @select=\"scrollToUserMessage\" />\n </div>\n <div ref=\"wrapperEl\" class=\"pk-chatbot-messages__wrapper\">\n <template v-for=\"(message, index) in messages\" :key=\"message.id\">\n <!-- The pk-chatbot-divider classes carry no rule of ours: they\n stay as hooks for the custom CSS of an agent. -->\n <div\n v-if=\"isUser(message) && getDividerDate(index)\"\n class=\"pk-chatbot-divider vv-separator\">\n <span class=\"pk-chatbot-divider__label\">\n {{ $d(getDividerDate(index)!, 'short') }}\n </span>\n </div>\n <div\n v-if=\"wakeupFeed.quietRuns.has(message.id)\"\n class=\"pk-chatbot-wakeup\">\n <VvIcon name=\"ri:alarm-line\" class=\"shrink-0\" />\n <span>\n {{\n $t(\n 'label.wakeupQuietRun',\n {\n count: wakeupFeed.quietRuns.get(message.id)!\n .count,\n time: $d(\n wakeupFeed.quietRuns.get(message.id)!\n .lastAt ?? Date.now(),\n 'time',\n ),\n },\n wakeupFeed.quietRuns.get(message.id)!.count,\n )\n }}\n </span>\n </div>\n <div\n v-else-if=\"\n isWakeupSeed(message) &&\n !wakeupFeed.hidden.has(message.id) &&\n !showAllMessageParts\n \"\n class=\"pk-chatbot-wakeup\"\n :title=\"message.metadata?.wakeup?.reason\">\n <VvIcon name=\"ri:alarm-line\" class=\"shrink-0\" />\n <span>\n {{ $t('label.wakeupCheck') }}\n <template v-if=\"message.metadata?.createdAt\">\n · {{ $d(message.metadata.createdAt, 'time') }}\n </template>\n </span>\n </div>\n <div\n v-if=\"\n !(isWakeupSeed(message) && !showAllMessageParts) &&\n !(\n wakeupFeed.hidden.has(message.id) &&\n !(isLoading && isLastMessage(index))\n ) &&\n (message.parts.length ||\n (isLastMessage(index) &&\n isLoading &&\n isAssistant(message)))\n \"\n class=\"pk-chatbot-message\"\n :class=\"[\n `pk-chatbot-message--${message.role}`,\n {\n 'pk-chatbot-message--loading':\n isLoading &&\n isLastMessage(index) &&\n isAssistant(message),\n },\n ]\"\n :style=\"{\n minHeight:\n isLastMessage(index) &&\n isAssistant(message) &&\n !isError\n ? `${height}px`\n : undefined,\n }\">\n <transition appear name=\"pk-chatbot-part\">\n <PkChatbotSteps\n v-if=\"showStepsTimeline(message)\"\n :message\n :is-streaming=\"isLoading && isLastMessage(index)\"\n :has-content=\"messageHasContent(message)\"\n :is-dark />\n </transition>\n <template\n v-for=\"(part, partIndex) in getMergedParts(message)\"\n :key=\"feedPartKey(part, partIndex)\">\n <transition\n v-if=\"isTextPart(part) && part.text.trim()\"\n appear\n name=\"pk-chatbot-part\">\n <div class=\"pk-chatbot-message__text\">\n <slot\n v-bind=\"{\n message,\n part,\n index,\n isLoading,\n }\"\n name=\"text\">\n <PkStreamingMarkdown\n v-if=\"isAssistant(message)\"\n class=\"wysiwyg\"\n :markdown=\"part.text\"\n :is-dark\n :loading=\"\n isLastMessage(index) &&\n status === 'streaming'\n \" />\n <template v-else>\n {{ part.text }}\n </template>\n </slot>\n <!-- Read-only reaction chip overlapping the\n bubble corner, like a messaging app. Hidden\n when a comment is shown: the callout below\n then carries the reaction glyph instead. -->\n <span\n v-if=\"\n hasReaction(message.id) &&\n !getMessageFeedback(message.id)\n ?.comment &&\n partIndex === lastTextPartIndex(message)\n \"\n class=\"pk-chatbot-message__reaction\"\n :class=\"reactionToneClass(message.id)\"\n :title=\"reactionTitle(message.id)\">\n <span\n v-if=\"feedbackEmoji(message.id)\"\n class=\"pk-chatbot-message__reaction-emoji\">\n {{ feedbackEmoji(message.id) }}\n </span>\n <VvIcon\n v-else\n :name=\"\n feedbackVote(message.id) ===\n 'downvote'\n ? 'ri:thumb-down-fill'\n : 'ri:thumb-up-fill'\n \" />\n </span>\n </div>\n </transition>\n <transition\n v-else-if=\"isFilePart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <div\n v-if=\"\n isAudioFile(part.filename, part.mediaType)\n \"\n class=\"flex flex-col items-end gap-8 w-full\">\n <PkChatbotFilePreview\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\"\n :size=\"\n getFilePartMedia(part)?.bytesCount\n \" />\n <!-- Heard once when it arrives: until the\n transcript comes back the turn waits on it. -->\n <PkChatbotAudioTranscript\n :url=\"part.url\"\n :transcript=\"getFilePartTranscript(part)\"\n :pending=\"\n status === 'submitted' &&\n isLastMessage(index)\n \" />\n </div>\n <PkChatbotFilePreview\n v-else\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\"\n :size=\"getFilePartMedia(part)?.bytesCount\" />\n </transition>\n <!-- Anything asking the user for consent (a suspended\n tool call or a `requestConfirm`) shares one card,\n ahead of the per-tool components. An approval stays\n answerable only in the trailing message: the backend\n closes the abandoned ones as denied, while a\n `requestConfirm` can still be resolved later. -->\n <transition\n v-else-if=\"\n isConfirmationCardPart(part) &&\n !isStreamingPart(part)\n \"\n appear\n name=\"pk-chatbot-part\">\n <!-- Key WITHOUT the part state: the card must stay\n mounted across the approval lifecycle so its\n inner transitions play once; remounting per\n state replays the appear animation on every\n hop (requested → responded → output). -->\n <PkToolApprovalCard\n :key=\"`approval-${partIndex}`\"\n :part\n :actionable=\"\n interactive === true &&\n (isToolConfirmPart(part) ||\n isLastMessage(index))\n \"\n :call-verdict=\"\n confirmVerdictsOnCall?.get(\n (part as { toolCallId?: string })\n .toolCallId ?? '',\n )\n \"\n @respond=\"onCardRespond(part, $event)\" />\n </transition>\n <transition\n v-else-if=\"isRenderedToolPart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <component\n :is=\"toolComponentMap[getToolPartName(part)]\"\n v-if=\"\n !slots[part.type] &&\n toolComponentMap[getToolPartName(part)]\n \"\n :key=\"`component-${partIndex}-${getPartState(part)}`\"\n :part\n v-bind=\"recordToolProps(part)\" />\n <slot\n v-else\n :name=\"part.type\"\n v-bind=\"{\n message,\n part,\n index,\n isLoading,\n }\">\n <div\n v-if=\"showAllMessageParts\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon\n v-if=\"getPartIcon(part)\"\n :name=\"getPartIcon(part)!\"\n class=\"shrink-0\" />\n <span class=\"text-12\">\n {{\n getPartTypeLabel(part, {\n t: $t,\n te: $te,\n })\n }}\n </span>\n </div>\n </div>\n </slot>\n </transition>\n <!-- Read-only comment: anchored right after the message\n text (same bubble as the reaction chip) so the\n feedback stays attached to the message body. -->\n <transition mode=\"out-in\">\n <PkChatbotMessageFeedback\n v-if=\"\n partIndex === lastTextPartIndex(message) &&\n readonlyFeedback &&\n getMessageFeedback(message.id)?.comment\n \"\n :feedback=\"getMessageFeedback(message.id)!\" />\n </transition>\n </template>\n <!-- The activity always reads at the bottom of the answer.\n With the steps timeline it shows only once something\n is drawn below the timeline: before that the timeline\n is the bottom, and both would name the same step. -->\n <transition name=\"fade-in\" mode=\"out-in\">\n <div\n v-if=\"\n isLoading &&\n isLastMessage(index) &&\n !isLastTextPart(message) &&\n isAssistant(message) &&\n (!showStepsTimeline(message) ||\n messageHasContent(message))\n \"\n :key=\"`loading-info-${message.id}`\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon name=\"loading-loop\" class=\"shrink-0\" />\n <transition mode=\"out-in\">\n <VvIcon\n v-if=\"activeMessageLastPartIcon\"\n :key=\"activeMessageLastPartIcon\"\n class=\"shrink-0\"\n :name=\"activeMessageLastPartIcon\" />\n </transition>\n <transition mode=\"out-in\">\n <span\n v-if=\"activeMessageLastPartLabel\"\n :key=\"activeMessageLastPartLabel\"\n class=\"text-12\">\n {{ activeMessageLastPartLabel }}\n </span>\n </transition>\n </div>\n <!-- With the timeline the reasoning lives in its\n step, which opens on demand. -->\n <transition name=\"fade-in\" mode=\"out-in\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"\n !showStepsTimeline(message) &&\n activeMessageLastPart &&\n 'text' in activeMessageLastPart &&\n activeMessageLastPart.text.trim()\n \"\n :markdown=\"activeMessageLastPart.text\"\n :is-dark\n :tail-length=\"LIVE_REASONING_TAIL_LENGTH\"\n inner-class=\"wysiwyg\"\n class=\"border border-surface-3 rounded-md p-4 mt-8 bg-surface-1 max-h-64 text-12 w-full\" />\n </transition>\n </div>\n </transition>\n <transition mode=\"out-in\">\n <div\n v-if=\"showMessageFooter(message, index)\"\n class=\"pk-chatbot-message__footer\">\n <VvButtonGroup\n modifiers=\"compact\"\n class=\"mr-auto shrink-0\">\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isMessageRegenerateButtonVisible(\n index,\n )\n \"\n icon=\"ri:reset-right-line\"\n modifiers=\"action-quiet-small\"\n :title=\"$t('action.regenerate')\"\n :aria-label=\"$t('action.regenerate')\"\n @click.stop=\"$emit('regenerate')\" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\n 'show-info',\n index,\n )\n \"\n icon=\"ri:information-line\"\n :title=\"$t('action.getMoreInfo')\"\n :aria-label=\"$t('action.getMoreInfo')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('show-info', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\n 'revise',\n index,\n )\n \"\n :icon=\"\n isRevised(message.id)\n ? 'ri:file-edit-fill'\n : 'ri:file-edit-line'\n \"\n :title=\"\n isRevised(message.id)\n ? $t('action.editRevise')\n : $t('action.createRevise')\n \"\n :aria-label=\"\n isRevised(message.id)\n ? $t('action.editRevise')\n : $t('action.createRevise')\n \"\n modifiers=\"action-quiet-small\"\n :class=\"{\n 'text-brand-graphic': isRevised(\n message.id,\n ),\n }\"\n @click.stop=\"\n $emit('revise', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'upvote',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.vote === 'upvote'\n ? 'ri:thumb-up-fill'\n : 'ri:thumb-up-line'\n \"\n :title=\"$t('action.upvote')\"\n :aria-label=\"$t('action.upvote')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('upvote', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'downvote',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.vote === 'downvote'\n ? 'ri:thumb-down-fill'\n : 'ri:thumb-down-line'\n \"\n :title=\"$t('action.downvote')\"\n :aria-label=\"$t('action.downvote')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('downvote', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'feedback',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.comment\n ? 'ri:feedback-fill'\n : 'ri:feedback-line'\n \"\n :title=\"$t('action.feedback')\"\n :aria-label=\"$t('action.feedback')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('feedback', message)\n \" />\n </transition>\n </VvButtonGroup>\n <div\n v-if=\"\n !showMessageDateTime &&\n message.metadata?.createdAt &&\n message.metadata?.completedAt\n \"\n class=\"pk-chatbot-message__hover-meta\">\n <time\n :datetime=\"\n new Date(\n message.metadata.createdAt,\n ).toISOString()\n \"\n :title=\"\n $d(\n new Date(\n message.metadata.createdAt,\n ),\n 'date-time',\n )\n \"\n class=\"flex items-center gap-4 min-w-0\">\n <VvIcon\n name=\"ri:time-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\">{{\n formatMessageTime(\n message.metadata.createdAt,\n )\n }}</span>\n </time>\n <span\n v-if=\"getMessageDuration(message)\"\n class=\"flex items-center gap-4 min-w-0\">\n <VvIcon\n name=\"ri:hourglass-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\">{{\n getMessageDuration(message)\n }}</span>\n </span>\n </div>\n <span\n v-if=\"showMessageNumber\"\n :title=\"\n $t('label.messageNumber', {\n n: messageNumbers[index],\n })\n \"\n class=\"text-12 text-word-3 tabular-nums\">\n #{{ messageNumbers[index] }}\n </span>\n <span\n v-if=\"\n showMessageTokensCount &&\n message.metadata?.totalTokens\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon\n name=\"ri:ai-generate-2-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\"\n >{{\n $n(\n message.metadata.totalTokens,\n 'integer',\n )\n }}\n {{ $t('label.tokens') }}\n </span>\n </span>\n <time\n v-if=\"\n showMessageDateTime &&\n message.metadata?.createdAt\n \"\n :datetime=\"\n new Date(\n message.metadata?.createdAt,\n ).toISOString()\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon name=\"ri:time-line\" class=\"shrink-0\" />\n <span class=\"truncate\">{{\n $d(\n new Date(message.metadata?.createdAt),\n 'date-time',\n )\n }}</span>\n </time>\n <div\n v-if=\"\n showMessageDateTime &&\n message?.metadata?.completedAt &&\n message?.metadata?.createdAt\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon\n name=\"ri:hourglass-line\"\n class=\"shrink-0\" />\n <PkRelativeTime\n class=\"truncate\"\n :date=\"message.metadata?.createdAt\"\n :end-date=\"message.metadata?.completedAt\" />\n </div>\n </div>\n </transition>\n <transition mode=\"out-in\">\n <PkChatbotFeedbackForm\n v-if=\"message.id === feedbackMessageId\"\n ref=\"feedbackMessageEl\"\n :loading=\"feedbackLoading\"\n :submitted=\"feedbackSubmitted\"\n :error=\"feedbackError\"\n @submit=\"$emit('feedback-submit', $event)\"\n @close=\"$emit('feedback-close')\" />\n </transition>\n </div>\n </template>\n <!-- A spoken turn streams to the provider, not here: nothing in\n this list moves between the question and the answer landing.\n Its own indicator, because the status-driven one hangs off an\n assistant message the SDK never creates for a call. When to\n show it and when it carries the anchor spacer: see\n `isSpokenWaitVisible` and `isSpokenWaitAnchored`. -->\n <transition appear name=\"pk-chatbot-part\">\n <div\n v-if=\"isSpokenWaitVisible\"\n class=\"pk-chatbot-message pk-chatbot-message--assistant pk-chatbot-message--loading\"\n :style=\"{\n minHeight: isSpokenWaitAnchored\n ? `${height}px`\n : undefined,\n }\">\n <div class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon name=\"loading-loop\" class=\"shrink-0\" />\n <span class=\"text-12\">\n {{ $t('label.answeringOnTheCall') }}\n </span>\n </div>\n </div>\n </div>\n </transition>\n <transition appear name=\"pk-chatbot-part\">\n <PkChatbotError\n v-if=\"isError\"\n :error\n @retry=\"$emit('auto-retry')\"\n @reset=\"$emit('reset-chat')\" />\n </transition>\n <div\n v-if=\"\n (activeMessage?.role === 'user' && !isSpokenWaitAnchored) ||\n isError\n \"\n class=\"pk-chatbot-messages__spacer\"\n :style=\"{ minHeight: `${height}px` }\"></div>\n </div>\n <Transition name=\"pk-chatbot-messages-fab\">\n <button\n v-if=\"showScrollToBottom && (userScrolledUp || !isAtBottom)\"\n type=\"button\"\n class=\"pk-chatbot-messages__scroll-to-bottom\"\n :title=\"$t('action.scrollToBottom')\"\n :aria-label=\"$t('action.scrollToBottom')\"\n @click=\"onScrollToBottomClick\">\n <VvIcon name=\"ri:arrow-down-line\" />\n </button>\n </Transition>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-messages {\n overflow-y: auto;\n flex: 1;\n min-width: 0;\n font-size: var(--text-14);\n\n &__wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-16);\n min-width: 0;\n }\n\n // Conversation outline rail: sticky shell pinned to the vertical\n // middle of the scroll area, at the right edge. Zero-sized and\n // first in flow so neither it nor its absolutely-positioned pieces\n // inflate scrollHeight. Desktop only, like ChatGPT: in the widget and\n // the panel the chat is as wide on a phone as on a desktop, so the\n // viewport is the only thing that tells them apart. The fullscreen\n // layout measures the chat itself instead (PkChatbot).\n &__outline {\n display: none;\n position: sticky;\n top: 50%;\n z-index: 2;\n flex: none;\n align-self: flex-end;\n height: 0;\n\n @include media-breakpoint-up('md', $breakpoints) {\n display: block;\n }\n\n .pk-chatbot-outline {\n position: absolute;\n top: 0;\n right: 0;\n transform: translateY(-50%);\n }\n }\n\n // Placeholder while the answer has not started yet: settles like\n // the message spacer when released. Only the release is animated\n // (`--settling`): animating the growth would keep `scrollHeight`\n // behind while the anchor scroll reads it, landing short of the\n // anchor with no second chance to catch up.\n &--settling {\n .pk-chatbot-messages__spacer,\n .pk-chatbot-message {\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n }\n }\n\n // Floating \"back to bottom\" button, sticky inside the scroll area\n &__scroll-to-bottom {\n position: sticky;\n bottom: var(--spacing-8);\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n // The scroll container is a flex column: without this the\n // overflowing content shrinks the button, ignoring its height\n flex: none;\n width: var(--spacing-36);\n height: var(--spacing-36);\n // Net-zero layout contribution: mounting/unmounting the button\n // must not change scrollHeight, or reaching the bottom (which\n // hides it) would clamp scrollTop and cause a visible jump\n margin-block-start: calc(-1 * var(--spacing-36));\n margin-inline: auto;\n border: 1px solid var(--color-surface-3);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-1);\n box-shadow: var(--shadow-md);\n color: var(--color-word-1);\n cursor: pointer;\n transition: var(--transition-colors);\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n .pk-chatbot-messages-fab-enter-active,\n .pk-chatbot-messages-fab-leave-active {\n transition: opacity 150ms var(--ease-out);\n }\n\n .pk-chatbot-messages-fab-enter-from,\n .pk-chatbot-messages-fab-leave-to {\n opacity: 0;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .pk-chatbot-messages-fab-enter-active,\n .pk-chatbot-messages-fab-leave-active {\n transition: none;\n }\n }\n\n // A wakeup in the feed (spec agent-wakeup): a check the agent ran by\n // itself, or a run of them that found nothing. Quiet like the day divider.\n .pk-chatbot-wakeup {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-xs);\n color: var(--color-word-4);\n font-size: var(--text-12);\n text-align: center;\n }\n\n .pk-chatbot-message {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-sm);\n min-width: 0;\n align-items: flex-start;\n // `clip` instead of `hidden` so the reaction chip, which deliberately\n // overhangs the bubble's bottom edge, is not cut off. The margin has\n // to cover half the chip plus its shadow.\n overflow: clip;\n overflow-clip-margin: var(--spacing-16);\n max-width: 100%;\n // The anchor spacer (inline min-height on the last assistant message)\n // settles smoothly when released at the end of the stream: the\n // transition lives on `.pk-chatbot-messages--settling` above\n\n &__text {\n position: relative;\n color: var(--color-word-2);\n background-color: var(--color-surface);\n line-height: var(--leading-normal);\n border-width: var(--spacing-px);\n border-style: solid;\n border-color: var(--color-surface-3);\n padding: var(--spacing-sm);\n border-radius: var(--rounded-xl) var(--rounded-xl) var(--rounded-xl)\n 0;\n max-width: 100%;\n text-overflow: ellipsis;\n // Same reason as the block above: the chip is absolutely\n // positioned against this bubble and hangs below its edge.\n overflow: clip;\n overflow-clip-margin: var(--spacing-16);\n }\n\n // Read-only feedback: a circular vote chip that overlaps the bubble's\n // bottom-right corner (messaging-app style). White ring lifts it off\n // the bubble; solid accent makes the rating unmistakable.\n &__reaction {\n position: absolute;\n right: var(--spacing-8);\n bottom: 0;\n transform: translateY(50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--spacing-24);\n height: var(--spacing-24);\n border-radius: var(--rounded-full, 9999px);\n color: var(--color-white, #fff);\n font-size: var(--text-12);\n box-shadow: var(--shadow-md);\n\n &--positive {\n background-color: var(--color-success);\n color: var(--color-success-contrast);\n }\n\n &--negative {\n background-color: var(--color-danger);\n color: var(--color-danger-contrast);\n }\n\n // Real emoji (e.g. WhatsApp): the glyph carries its own colour, so\n // the chip stays a neutral surface pill.\n &--neutral {\n background-color: var(--color-surface-1);\n }\n }\n\n &__reaction-emoji {\n // Smaller than the chip so the glyph doesn't touch the ring.\n font-size: var(--text-12);\n line-height: 1;\n }\n\n &__loading-info {\n line-height: var(--leading-normal);\n border-width: var(--spacing-px);\n border-style: solid;\n border-color: var(--color-surface-3);\n padding: var(--spacing-sm);\n border-radius: var(--rounded-xl) var(--rounded-xl) var(--rounded-xl)\n 0;\n font-size: var(--text-12);\n color: var(--color-word-3);\n background-color: var(--color-surface);\n }\n\n &__footer {\n width: 100%;\n display: flex;\n gap: var(--spacing-8);\n align-items: center;\n }\n\n &__hover-meta {\n display: flex;\n min-width: 0;\n align-items: center;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n color: var(--color-word-3);\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n opacity: 0;\n transition: opacity 0.2s ease;\n }\n\n &:hover &__hover-meta,\n &:focus-within &__hover-meta {\n opacity: 1;\n }\n\n // Hover doesn't exist on touch devices: keep the metadata visible\n @media (hover: none) {\n &__hover-meta {\n opacity: 1;\n }\n }\n\n &--user {\n align-items: flex-end;\n\n .pk-chatbot-message__text {\n background-color: var(\n --chatbot-main-color,\n var(--color-surface-1)\n );\n border-color: var(--chatbot-main-color, var(--color-surface-3));\n color: var(--chatbot-contrast-color, var(--color-word-1));\n border-radius: var(--rounded-xl) var(--rounded-xl) 0\n var(--rounded-xl);\n }\n }\n\n &--assistant {\n width: 100%;\n gap: var(--spacing-16);\n\n .pk-chatbot-message__text {\n width: 100%;\n }\n }\n\n &--system {\n align-items: flex-end;\n\n .pk-chatbot-message__text {\n display: flex;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n padding: var(--spacing-8) var(--spacing-sm);\n align-items: center;\n border-color: var(--chatbot-main-color, var(--color-surface-3));\n color: var(--chatbot-main-color, var(--color-word-1));\n border-radius: var(--rounded-xl) var(--rounded-xl) 0\n var(--rounded-xl);\n }\n }\n }\n\n .pk-chatbot-part-enter-active {\n transition:\n opacity 0.25s ease,\n transform 0.25s ease;\n }\n\n .pk-chatbot-part-enter-from {\n opacity: 0;\n transform: translateY(8px);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .pk-chatbot-part-enter-active {\n transition: none;\n }\n\n .pk-chatbot-part-enter-from {\n transform: none;\n }\n }\n</style>\n"],"ignoreList":[0],"mappings":";;;;;;;;;;;;;;;;;;;;;AAGA,SAAgB6G,GAAS,GAAQ;CAC7B,OAAO7G,EAAkBoE,GAAgB0C,CAAM;AACnD;ACwB8C7G,EAAS;CACnD,UAAUC,EAAS;CACnB,UAAUA,EAAS;AACvB,CAAC;AAMD,IAAa6G,KAAsB9G,EAAS;CACxC,QAAQE,EAAO;CACf,OAAOC,GAAe;CACtB,QAAQF,EAAS;AACrB,CAAC;AAGoCD,EAAS,EAC1C,eAAe8G,GAAoB,SAAS,EAChD,CAAC,GAIkC9G,EAAS,EACxC,QAAQE,EAAO,EACnB,CAAC;AAQD,IAAa6G,KAAgB,MACzBC,GAAS,SAAS,UAAU,EAAQA,EAAQ,UAAU,QAGpDC,qBAAsB,IAAI,IAAI;CAChC;CACA;CACA;AACJ,CAAC,GAOKC,qBAAkC,IAAI,IAAI;CAC5C,GAAG3C;CACH,GAAGE;CACH;AACJ,CAAC;AAMmD,CAChD,GAAGA,GACHD,EAAiB,eACrB;AAIA,IAAM2C,KAAW,MACZC,EAA4B,OAE3BC,MAAc,MAChBD,EAAK,KAAK,WAAW,OAAO,IAAIA,EAAK,KAAK,MAAM,CAAc,IAAI,KAAA,GASzDE,MAAqB,MAC9BN,GAAS,SAAS,eAClBA,EAAQ,MAAM,MAAM,MAAS;CACzB,IAAII,EAAK,SAAS,QACd,OAAOA,EAAK,KAAK,KAAK,CAAC,CAAC,SAAS;CAErC,IAAIA,EAAK,SAAS,QACd,OAAO;CAEX,IAAMG,IAAQJ,EAAQC,CAAI;CAC1B,IAAIG,KAASN,GAAoB,IAAIM,CAAK,GACtC,OAAO;CAEX,IAAMC,IAAWH,GAAWD,CAAI;CAChC,OACII,MAAa,KAAA,KACbN,GAAW,IAAIM,CAAQ,KACvBD,MAAU;AAElB,CAAC,GA+CQE,MAAuB,MAAqC;CACrE,IAAIT,GAAS,SAAS,eAAeM,GAAkBN,CAAO,GAC1D,OAAO;CAEX,IAAIU,IAAO;CACX,KAAK,IAAMN,KAAQJ,EAAQ,OACvB,AAAII,EAAK,SAAS,0BACd,IACID,EAAQC,CAAI,MAAM,sBACjBA,EAAwC,OAAO,SAAS;CAGrE,OAAOM;AACX,GC9KaC,KAGT;CACA,wBAAwBC,QACd,OAAO,6CACjB;CACA,cAAcA,QACJ,OAAO,mCACjB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACA,mBAAmBD,QACT,OAAO,wCACjB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACA,gBAAgBD,QACN,OAAO,qCACjB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACA,iBAAiBD,QACP,OAAO,sCACjB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACA,aAAaD,QAA2B,OAAO,kCAA0B;CACzE,WAAWA,QAA2B,OAAO,gCAAwB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACrE,kBAAkBD,QACR,OAAO,uCACjB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACA,cAAcD,QACJ,OAAO,mCACjB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACA,aAAaD,QAA2B,OAAO,kCAA0B,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACzE,UAAUD,QAA2B,OAAO,+BAAuB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACnE,oBAAoBD,QACV,OAAO,+BACjB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACA,iBAAiBD,QACP,OAAO,sCACjB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACA,YAAYD,QAA2B,OAAO,iCAAyB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACvE,aAAaD,QAA2B,OAAO,kCAA0B,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACzE,oBAAoBD,QACV,OAAO,yCACjB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;CACA,WAAWD,QAA2B,OAAO,gCAAwB;CACrE,aAAaA,QAA2B,OAAO,kCAA0B;CACzE,cAAcA,QACJ,OAAO,mCACjB,CAAA,MAAA,MAAAC,EAAA,CAAA,CAAA;AACJ;;;;;;;ECjDI,IAAMI,IAAQC,GAIRlB,IAAUmB,QAAe;GAC3B,IAAMC,IAAQH,EAAM;GACpB,OAAO,OAAOG,GAAO,WAAY,WAAWA,EAAM,UAAU;EAChE,CAAC;mBAIQpB,EAAA,SAAT3G,EAAA,GAAAC,EAEI,KAFJF,IAEIG,EADGyG,EAAA,KAAO,GAAA,CAAA,KAAAwB,EAAA,IAAA,EAAA;;IELLC,IAAkD,GAE1DvC,IAAoBxF,GACzB;;;;;;;;;;;;ECQI,IAAMuH,IAAQC,GAmBRW,IAAOD,GAIP,EAAE,GAAGE,GAAI,IAAIC,MAAQC,GAAQ,EAAE,UAAU,SAAS,CAAC,GAEnDC,IAAWd,QAETF,EAAM,IAQd,GAEMT,IAAWW,QACbtD,EAAgBoD,EAAM,IAA6C,CACvE,GAEMiB,IAAYf,QAAeX,EAAS,UAAUtB,CAAiB,GAQ/DiD,IAAShB,QACXe,EAAU,SACHD,EAAS,MAAM,UAAU,qBACtB3E,EAAiB2E,EAAS,MAAM,MAAM,MAC1ChB,EAAM,gBAAgB,KAAA,IACtBgB,EAAS,MAAM,UAAU,oBACnC,GAGMG,IAAYjB,QAAegB,EAAO,SAASlB,EAAM,eAAe,EAAI,GAGpEoB,IAAYlB,QAAegB,EAAO,SAASlB,EAAM,eAAe,EAAI,GAEpEqB,IAAanB,QACfe,EAAU,QACJD,EAAS,MAAM,WAAW,eAC1BhB,EAAM,gBAAgB,KACtBgB,EAAS,MAAM,UAAU,aAAa,EAChD,GAEMM,IAAWpB,QACbe,EAAU,QACJD,EAAS,MAAM,WAAW,eAC1BhB,EAAM,gBAAgB,KACtBgB,EAAS,MAAM,UAAU,aAAa,MACtCA,EAAS,MAAM,UAAU,eACnC,GAEMO,IAAUrB,QAAeM,EAAqBjB,EAAS,MAAM,GAQ7DiC,IAAStB,QACXqB,EAAQ,QACFV,EAAG,qBAAqB,IACxBA,EAAG,4BAA4B,EAC3B,MAAM1D,GAAUoC,EAAS,OAAO;GAAE,GAAGsB;GAAI,IAAIC;EAAI,CAAC,EACtD,CAAC,CACX,GAQMW,IAAUvB,QAAe;GAC3B,IAAIkB,EAAU,OACV,OAAO;IAAE,MAAM;IAAgB,SAASP,EAAG,sBAAsB;GAAE;GAEvE,IAAIQ,EAAW,OACX,OAAO;IACH,MAAM;IACN,SAASR,EACLI,EAAU,QACJ,2BACA,uBACV;GACJ;GAEJ,IAAIK,EAAS,OACT,OAAO;IACH,MAAM;IACN,SAAST,EACLI,EAAU,QACJ,2BACA,qBACV;GACJ;EAGR,CAAC,GAOKS,IAASxB,QAAe,EAAQuB,EAAQ,SAAU,CAACF,EAAQ,KAAK;;;GAUtE,OAAAzI,EAAA,GAAAC,EAkDM,OAAA,EAjDF,OAAKC,EAAA,CAAC,sDAAoD,EACmB,sDAAA,CAAA0I,EAAA,MAAA,CAAA,CAAA,EAAA,GAAA;IAQ7EzI,EAMaC,GAAA,MAAA;KALT,SAAAC,QAII,CAHO,CAAA8H,EAAA,SAAaC,EAAA,SADxBpI,EAAA,GAAAC,EAII,KAJJL,GAIIU,EADGoI,EAAA,KAAM,GAAA,CAAA,KAAAjB,EAAA,IAAA,EAAA,CAAA,CAAA;;;IAIcgB,EAAA,SAA/BzI,EAAA,GAAAQ,EAAwEC,GAAxDgI,EAAA,KAAO,GAAA;;KAAkB,MAAAtB,EAAA;KAAM,OAAOe,EAAA,MAAS;;IAM/D/H,EAyBaC,GAAA,EAzBD,MAAK,SAAQ,GAAA;KACrB,SAAAC,QAagB,CAbMsI,EAAA,SAiBtB3I,EAAA,GAAAC,EAMI,KAAA;;MAJA,OAAKC,EAAA,CAAC,mDAAiD,EAAA,SACpCuI,EAAA,MAAO,CAAA,CAAA;KAC1BtI,GAAAA,CAAAA,EAAgD6I,GAAA;MAAvC,MAAML,EAAA,MAAQ;MAAM,OAAM;KAAa/H,GAAAA,MAAAA,GAAAA,CAAAA,MAAAA,CAAAA,GAAAA,GAAA,MAChDN,EAAGqI,EAAA,MAAQ,OAAO,GAAA,CAAA,CAAA,GAAA,CAAA,MAtBtB3I,EAAA,GAAAQ,EAagBsI,GAAA,EAAA,KAAA,EAAA,GAAA;MAZZ,SAAAzI,QAK0C,CAL1CF,EAK0C0I,GAAA;OAJtC,WAAU;OACT,UAAQ,CAAGR,EAAA;OACZ,MAAK;OACJ,OAAO3H,EAAAqH,CAAA,CAAE,CAAA,gBAAA;OACT,SAAK,AAAA2B,EAAA,OAAA/I,GAAA,MAAOmH,EAAI,WAAA,EAAA,GAAA,CAAA,MAAA,CAAA;MACrB3H,GAAAA,MAAAA,GAAAA,CAAAA,YAAAA,OAAAA,CAAAA,GAAAA,EAK2C0I,GAAA;OAJvC,WAAU;OACT,UAAQ,CAAGR,EAAA;OACZ,MAAK;OACJ,OAAO3H,EAAAqH,CAAA,CAAE,CAAA,eAAA;OACT,SAAK,AAAA2B,EAAA,OAAA/I,GAAA,MAAOmH,EAAI,WAAA,EAAA,GAAA,CAAA,MAAA,CAAA;;;KAKzB9H,CAAAA,EAKsB,CAAA;;;;;;IEnN5B4J,KAAwB,IACxBC,KAAyB;AAyB/B,SAAgBC,GAAc,GAA+B;CACzD,IAAM,EACF,aACA,WACA,mBACA,oBACA,cACA,2BACA,eACA,oBACAC,GAEEC,IAAiBC,EAAI,EAAK,GAC1BC,IAAaD,EAAI,EAAI,GACvBE,IAAkB,IAClBC,IAA4C,MAC5CC,IAAwC,MACxCC,IAAgD,MAChDC,IAA+C,MAC/CC,IAAgB,GAIdC,KAAkB,IAA2B,aAAa;EAC5D,AAAI,CAACC,EAAS,SAASV,EAAe,QAGlC,IAAkB,MAGtB,IAAkB,IAClBU,EAAS,MAAM,SAAS;GACpB,KAAKA,EAAS,MAAM;GACpB;EACJ,CAAC,GAED,iBAAiB;GACb,IAAkB;EACtB,GAHoBC,MAAa,YAAY,IAAId,EAGnC;CAClB,GAQMe,KAAW,MAAsB;EACnC,AAAIC,EAAM,SAAS,MACf,EAAe,QAAQ;CAE/B,GAEIC,IAAa,GACXC,MAAgB,MAAsB;EACxC,IAAaF,EAAM,QAAQ,EAAE,EAAE,WAAW;CAC9C,GACMG,KAAe,MAAsB;EACvC,IAAMC,IAAIJ,EAAM,QAAQ,EAAE,EAAE,WAAW;EAKvC,AAHII,IAAIH,MACJ,EAAe,QAAQ,KAE3B,IAAaG;CACjB,GAEMC,UAAqB;EACvB,IAAI,CAACR,EAAS,OACV;EAEJ,IAAM,EACF,WAAWS,GACX,iBACA,oBACAT,EAAS;EAIb,AAHA,EAAW,QACPU,IAAeC,IAAeF,IAC9BvB,IACAO,MAKAgB,IAAmBX,KACnBc,IAAa,GAGbH,IAAmBX,MAEfN,EAAW,UACX,EAAe,QAAQ,KAE3BqB,IAAe,KAGnB,IAAgBJ;CACpB,GASMK,WAAmB;EACrB,IAAIrB,KAAmBH,EAAe,SAAS,CAACU,EAAS,OACrD;EAEJ,IAAMe,IAAKf,EAAS,OACdgB,IAASD,EAAG,eAAeA,EAAG;EACpC,AAAIC,KAAUD,EAAG,cAGjB,EAAW,QAAQ,IACnB,IAAkB,IAClB,EAAG,YAAYC,GACf,4BAA4B;GACxB,IAAkB;EACtB,CAAC;CACL,GAEMC,UAA4B;EAC9B,AAAI,MAAqBjB,EAAS,UAGlC,IAAmB,IAAI,iBAAiBc,EAAU,GAClDpB,EAAiB,QAAQM,EAAS,OAAO;GACrC,WAAW;GACX,SAAS;GACT,eAAe;EACnB,CAAC;CACL,GAEMkB,WAA2B;EAE7B,AADAxB,GAAkB,WAAW,GAC7B,IAAmB;CACvB;CAkBA,AAdAyB,EAAMC,IAAS,GAAW,MAAc;EACpC,AAAIC,MAAc,eAAeA,MAAc,cAC3CJ,EAAoB,KAGpBI,MAAc,WAAWC,MAAc,gBAI3CJ,GAAmB;CACvB,CAAC,GAIDC,EAAMI,GAAgB,YAAY;EAC9B,IAAKA,EAAe,KAGhBC,EAAgB,MAAM,aAM1B;GAIA,AAJAC,EAAwB,GACxB,EAAe,QAAQ,IAGvB,IAAkB;GAClB,IAAI;IAOA,AANA,MAAMC,EAAS,GAKf,MAAMC,IAAyB,GAC/B,MAAMD,EAAS;GACnB,UAAU;IAGN,EAAe;GACnB;EAjBwB;CAkB5B,CAAC;CAID,IAAIE,IAA2D,MAGzDC,UAAyB;EAC3B,AAAK7B,EAAS,UAGVA,EAAS,MAAM,gBAAgBA,EAAS,MAAM,iBAGlD,EAAS,MAAM,YAAYA,EAAS,MAAM,cAGtC4B,KACA,aAAaA,CAAkB,GAEnC,IAAqB,WACjBH,GACA,GACJ;CACJ,GAEMA,UAAgC;EAKlC,AAJA9B,GAAgB,WAAW,GAC3B,IAAiB,MACjBC,GAAsB,WAAW,GACjC,IAAuB,MACvB,AAEIgC,OADA,aAAaA,CAAkB,GACV;CAE7B;CA2DA,OAzDAE,SAAgB;EACZ,IAAI,CAAC9B,EAAS,OACV;EAMJ,AAJAA,EAAS,MAAM,iBAAiB,SAASE,GAAS,EAAE,SAAS,GAAK,CAAC,GACnEF,EAAS,MAAM,iBAAiB,cAAcK,IAAc,EACxD,SAAS,GACb,CAAC,GACDL,EAAS,MAAM,iBAAiB,aAAaM,GAAa,EACtD,SAAS,GACb,CAAC;EAMD,IAAMyB,IACFX,EAAO,MAAM,eAAeA,EAAO,MAAM;EAC7C,AAAIW,IACAd,EAAoB,KAEpB,IAAiB,IAAI,eAAeY,CAAgB,GACpDlC,EAAe,QAAQK,EAAS,KAAK,GAErC,IAAuB,IAAI,iBAAiB6B,CAAgB,GAC5DjC,EAAqB,QAAQI,EAAS,OAAO;GACzC,WAAW;GACX,SAAS;EACb,CAAC;EAQL,IAAMgC,IAAUC,IAAY,KAAKjC,EAAS,MAAM;EAMhD,AALIgC,MACA,IAAwB,IAAI,eAAelB,EAAU,GACrDjB,EAAsB,QAAQmC,CAAO,IAGpCD,KACDF,EAAiB;CAEzB,CAAC,GAEDK,SAAsB;EAOlB,AANAlC,EAAS,OAAO,oBAAoB,SAASE,CAAO,GACpDF,EAAS,OAAO,oBAAoB,cAAcK,EAAY,GAC9DL,EAAS,OAAO,oBAAoB,aAAaM,CAAW,GAC5DT,GAAuB,WAAW,GAClC,IAAwB,MACxBqB,GAAmB,GACnBO,EAAwB;CAC5B,CAAC,GAEM;EACH;EACA;EACA;EACA;CACJ;AACJ;;;AC7RA,SAAgBU,GACZ,GACA,UAAmD,IACzC;CACV,IAAMC,KAAW,MACbpG,GAAoBT,CAAO,KAAK,CAAC8G,EAAM9G,CAAwB,GAC7D+G,oBAAS,IAAI,IAAY,GACzBC,oBAAY,IAAI,IAA4B,GAC9CC,IAAQ;CACZ,OAAOA,IAAQC,EAAS,SAAQ;EAC5B,IAAMlH,IAAUkH,EAASD;EACzB,IAAIlH,EAAaC,CAAO,KAAK6G,EAAQK,EAASD,IAAQ,EAAE,GAAG;GACvD,IAAME,IAAQnH,EAAQ,IAClBoH,IAAQ,GACRC;GACJ,OACItH,EAAamH,EAASD,EAAM,KAC5BJ,EAAQK,EAASD,IAAQ,EAAE,IAM3B,AAJA,KAAS,GACT,IAASC,EAASD,EAAM,CAAC,UAAU,aAAaI,GAChDN,EAAO,IAAIG,EAASD,EAAM,CAAC,EAAE,GAC7BF,EAAO,IAAIG,EAASD,IAAQ,EAAE,CAAC,EAAE,GACjC,KAAS;GAEb,EAAU,IAAIE,GAAO;IAAE;IAAO;GAAO,CAAC;EAE1C,OAIA,AAHIN,EAAQ7G,CAAO,KACf+G,EAAO,IAAI/G,EAAQ,EAAE,GAEzB,KAAS;CACb;CACA,OAAO;EAAE;EAAQ;CAAU;AAC/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECAI,IAAMiB,IAAQC,GAwDRW,IAAOD,GAuBP,EACF,GAAGE,GACH,IAAIC,GACJ,GAAGsH,GACH,GAAGC,MACHtH,GAAQ,EACR,UAAU,SACd,CAAC,GAEKuH,IAAQC,GAAS,GASjBC,MACF,GACA,MACC;GACD,IAAIpL,EAAkB+B,CAAI,GAAG;IACzB,IAAMsJ,IAActJ,EAAiC;IACrD,AAAIsJ,KACA7H,EAAK,mBAAmB;KAAE;KAAY,WAAW8H;IAAS,CAAC;IAE/D;GACJ;GACA,IAAMC,IAAK/K,GAAkBuB,CAAI;GACjC,AAAKwJ,KAGL/H,EAAK,oBAAoB;IAAE;IAAI;GAAS,CAAC;EAC7C,GAEM4C,IAAWoF,GAA+B,UAAU,GACpDC,IAAYD,GAA+B,WAAW,GACtDE,KAAgB5I,QAClB5C,GAAqB0C,EAAM,WAAWA,EAAM,SAAS,CACzD,GAIM,EAAE,kBAAc,oBAAgB,oBAAgB,mBAClD4C,GAAc;GACV;GACA,cAAc5C,EAAM;GACpB,sBAAsBA,EAAM,UAAU;GACtC,uBACIA,EAAM,WAAWA,EAAM,SAAS,SAAS,EAAE,EAAE;GACjD,iBAAiB6I,EAAU;GAC3B,8BAA8BE,GAAmB;GACjD,kBAAkBnI,EAAK,WAAW;GAClC,oBAAoBA,EAAK,aAAa;EAC1C,CAAC,GAECoI,WAA8B;GAGhC,AADA,GAAe,QAAQ,IACvBzF,GAAe;EACnB,GAMM0F,IAAe/I,SAChBF,EAAM,YAAY,CAAC,EAAA,CACf,QACI,MAAYjB,EAAQ,SAAS,UAAU,CAACD,EAAaC,CAAO,CACjE,CAAA,CACC,KAAK,OAAa;GACf,IAAIA,EAAQ;GACZ,SACIA,EAAQ,MAAM,KAAKpC,CAAU,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,MAAM,GAAG,GAAG,KACxD;EACR,EAAE,CACV,GACMuM,KAAkBnG,EAAY,GAG9BoG,WACF3F,EAAS,OAAO,iBACZ,2BACJ,KAAK,CAAC,GAIJ4F,WAA4B;GAC9B,IAAMC,IAAW7F,EAAS;GAC1B,IAAI,CAAC6F,KAAY,CAACrJ,EAAM,oBACpB;GAEJ,IAAMsJ,IACFD,EAAS,sBAAsB,CAAC,CAAC,MAAMA,EAAS,eAAe,GAC/DE,IAASN,EAAa,MAAM,EAAE,EAAE;GAMpC,AALAE,GAAkB,CAAC,CAAC,SAAS,GAAI,MAAU;IACvC,AAAI5E,EAAG,sBAAsB,CAAC,CAAC,OAAO+E,MAClC,IAASL,EAAa,MAAMjD,EAAM,EAAE;GAE5C,CAAC,GACD,GAAgB,QAAQuD;EAC5B;EAEA,EAAMN,GAAc,YAAY;GAE5B,AADA,MAAM/D,EAAS,GACfkE,GAAoB;EACxB,CAAC;EAED,IAAMI,WAAiB;GAEnB,AADAxF,GAAa,GACboF,GAAoB;EACxB,GAEMK,MAAuB,MAAe;GACxC,IAAMJ,IAAW7F,EAAS,OACpBwC,IAAQiD,EAAa,MAAM,WAAW,MAASS,EAAK,OAAOf,CAAE,GAC7DgB,IAAYR,GAAkB,CAAC,CAACnD;GACtC,AAAKqD,KAAaM,MAMlB,GAAe,QAAQ,IAEvBN,EAAS,SAAS;IACd,KACIA,EAAS,YACTM,EAAU,sBAAsB,CAAC,CAAC,MAClCN,EAAS,sBAAsB,CAAC,CAAC,MACjC,OAAO,WAAW,iBAAiBA,CAAQ,CAAC,CAAC,UAAU;IAC3D,UAAU;GACd,CAAC;EACL,GAIMO,IAAY1J,QACPF,EAAM,WAAW,eAAeA,EAAM,WAAW,WAC3D,GAIK6J,IAAS9G,EAAY,CAAC,GAItB+G,KAAmB/G,EAAI,EAAK,GAK9BgH,KAAe,IAEbC,KAAwBC,GAAwB,GAMhDlB,KAAqB,YAAY;GACnC,IAAM9C,IAAWjG,EAAM;GAgBvB,IAfI,CAACiG,GAAU,UAAUjG,EAAM,2BAS3B,EADAiG,EAASA,EAAS,SAAS,EAAE,CAAC,SAAS,UAAU2D,EAAU,WAI/D,KAAe,IACf,GAAiB,QAAQ,IACzB,MAAM1E,EAAS,GACX,CAAC1B,EAAS,QACV;GAOJ,IAAM,EAAE,eAAY,qBAAkB,iBAAiBA,EAAS,KAAK,GAC/D0G,IACF1G,EAAS,MAAM,eACf,OAAO,WAAW2G,CAAU,IAC5B,OAAO,WAAWC,CAAa,GAC7BC,IAAaxB,EAAU,SACvB,OAAO,WAAW,iBAAiBA,EAAU,KAAK,CAAC,CAAC,MAAM,KAAK,GAM/DyB,IAAiBnB,GAAkB;GACzC,GAAsB,QAAQmB,EAAeA,EAAe,SAAS;GAGrE,IAAMC,IAAYL,KADdF,GAAsB,OAAO,gBAAgB,KACUK;GAU3D,AAHIH,IAAiB,MACjB,EAAO,QAAQ,KAAK,IAAIK,GAAW,CAAC,IAExC,MAAMrF,EAAS;EACnB;EA+CA,AAtCAP,EACI,OACU3E,EAAM,UAAU,cAChBA,EAAM,WAAWA,EAAM,SAAS,SAAS,EAAE,EAAE,EACvD,SACM+I,GAAmB,CAC7B,GAQAyB,GAAkB,CAAChH,GAAUwG,EAAqB,SAAS;GACvD,AAAID,MACAhB,GAAmB;EAE3B,CAAC,GAQDzD,SAAgB;GACZ,CAAIsE,EAAU,SAAS5J,EAAM,yBACzB+I,GAAmB;EAE3B,CAAC,GAQDpE,EAAMiF,IAAY,MAAY;GAC1B,AAAKa,MACD,KAAe,IACf,GAAiB,QAAQ,IACzB,EAAO,QAAQ;EAEvB,CAAC;EAED,IAAMC,MAAa,MACR1K,EAAM,gBAAgB,MAAM,MAAM2K,EAAE,cAAcC,CAAS,GAGhEC,KAAsB,MACxB7K,EAAM,kBAAkB,MAAM,MAAM8K,EAAE,cAAcF,CAAS,GAK3DG,qBAAe,IAAI,IAAI,CAAC,iBAAiB,CAAC,GAC1CC,MAAmB,MACrB,CAAChL,EAAM,eAAe+K,GAAa,IAAInO,EAAgBuC,CAAI,CAAC,IACtD,EAAE,QAAQ,GAAK,IACf,CAAC,GAKL8L,MACF,GACA,MACC,CAACjL,EAAM,oBAAoBkL,GAAsBC,GAAMnF,CAAK,GAI3DoF,MAAsB,MACxBpL,EAAM,mBAAmB6K,EAAmBD,CAAS,IAAI,KAAA,GACvDS,MAAgB,MAClBD,GAAmBR,CAAS,CAAC,EAAE,MAK7BU,MAAiB,MAAsB;GACzC,IAAMC,IAAWH,GAAmBR,CAAS;GAC7C,IAAIW,GAAU,MACV;GAEJ,IAAMC,IAAMD,GAAU,UAAU;GAChC,OAAO,OAAOC,KAAQ,YAAY,6BAA6B,KAAKA,CAAG,IACjEA,IACA,KAAA;EACV,GACMC,MAAe,MACjB,GAAQJ,GAAaT,CAAS,KAAKU,GAAcV,CAAS,IACxDc,MAAqB,MAAsB;GAG7C,IAAIJ,GAAcV,CAAS,GACvB,OAAO;GAEX,IAAMe,IAAON,GAAaT,CAAS;GAOnC,OANIe,MAAS,WACF,2CAEPA,MAAS,aACF,2CAEJ;EACX,GACMC,MAAiB,MAAsB;GACzC,IAAMD,IAAON,GAAaT,CAAS;GAOnC,OALW/J,EADP8K,MAAS,WACC,8BAEVA,MAAS,aACC,8BAEJ,2BAL+B;EAM7C,GAIME,MAAqB,MAA2B;GAClD,IAAMC,IAAQC,GAAehN,CAAO;GACpC,KAAK,IAAIiH,IAAQ8F,EAAM,SAAS,GAAG9F,KAAS,GAAG,KAC3C,IAAIrJ,EAAWmP,EAAM9F,EAAM,GACvB,OAAOA;GAGf,OAAO;EACX,GAEMgG,KAAgB9L,QACXF,EAAM,WAAWA,EAAM,SAAS,SAAS,EACnD,GACKiM,IAAwB/L,QAAe;GACzC,IAAMqJ,IAASyC,GAAc;GAI7B,OAHKzC,IAGEA,EAAO,MAAMA,EAAO,MAAM,SAAS,KAF/B;EAGf,CAAC,GAEK2C,KAAiBhM,QAAe;GAClC,IAAMiM,oBAAM,IAAI,IAAoC;GACpD,KAAK,IAAMpN,KAAWiB,EAAM,YAAY,CAAC,GACrC,AAAIjB,EAAQ,SAAS,eACjBoN,EAAI,IACApN,EAAQ,IACR1B,EACIG,EACID,GAA0BwB,EAAQ,KAAK,CAC3C,CACJ,CACJ;GAGR,OAAOoN;EACX,CAAC,GACKJ,MAAkB,MACbG,GAAe,MAAM,IAAInN,EAAQ,EAAE,KAAKA,EAAQ,OAErDqN,KAAUlM,QAAeF,EAAM,WAAW,OAAO,GAQjDqM,KAAsBnM,QAClBF,EAAM,yBAAyB,MAAQ,CAAC4J,EAAU,KAC5D,GAWM0C,KAAuBpM,QAErBmM,GAAoB,SACpBL,GAAc,OAAO,SAAS,UAC9B,CAACI,GAAQ,KACjB,GAEMG,KAA6BrM,QAAe;GAC9C,IAAMf,IAAO8M,EAAsB;GACnC,OAAOtO,GAAiBkD,GAAI1B,CAAI;EACpC,CAAC,GACKqN,KAA4BtM,QAAe;GAC7C,IAAMf,IAAO8M,EAAsB;GACnC,OAAOvO,EAAYyB,CAAI;EAC3B,CAAC,GAEKsN,MAAuB,MAClBzM,EAAM,UAAU,WAAW,MAAM0M,EAAE,OAAO9B,CAAS,GAExD+B,KAAiB,MACf3G,MAAU,KAAA,KAAa,CAAChG,EAAM,WACvB,KAEJgG,MAAUhG,EAAM,SAAS,SAAS,GAEvC4M,MAAoC,MAElCD,EAAc3G,CAAK,KACnBhG,EAAM,WAAW,WACjB,CAAC,CAACA,EAAM,SAAS,SAAS,YAAY,GAKxCkL,MACF,GACA,OAGK,CAACyB,EAAc3G,CAAK,KAAKhG,EAAM,WAAW,YAC3C,CAAC,CAACA,EAAM,SAAS,SAAS6M,CAAM,GAGlCC,MAAkB,MAA2B;GAC/C,IAAMC,IAAWhO,EAAQ,MAAMA,EAAQ,MAAM,SAAS;GACtD,OAAOpC,EAAWoQ,CAAQ;EAC9B,GACMC,MAAmB,MACrBjO,EAAQ,MAAM,MAAM,MAAS/B,EAAgBmC,CAAI,KAAKtC,EAAWsC,CAAI,CAAC,GAGpE8N,MAAqB,MACvB,EAAQjN,EAAM,qBACdkN,EAAYnO,CAAO,KACnBiO,GAAgBjO,CAAO,GAGrBoO,MAAsB,MACxBtQ,EAAWsC,CAAI,KACf,CAACiO,EAAgBjO,CAAI,KACrB,GACIO,GAAiB9C,EAAgBuC,CAAI,MACrCmJ,EAAMnJ,EAAK,SACXa,EAAM,sBAMRqN,MAAqB,MACvBtO,EAAQ,MAAM,MACT,MACIpC,EAAWwC,CAAI,KAAK,EAAQA,EAAK,KAAK,KAAK,KAC5CpC,EAAWoC,CAAI,KACd1B,EAAuB0B,CAAI,KAAK,CAACiO,EAAgBjO,CAAI,KACtDgO,GAAmBhO,CAAI,CAC/B,GACE+N,KAAe,MAA2BnO,EAAQ,SAAS,aAC3DuO,MAAU,MAA2BvO,EAAQ,SAAS,QACtDwO,MAA+B,MAC1BvN,EAAM,UACP,MAAM,GAAGgG,CAAK,CAAA,CACf,QAAQ,CAAA,CACR,MAAM,MAAYkH,EAAYnO,CAAO,CAAC,GAEzCyO,MAA2B,MACtBxN,EAAM,UACP,MAAMgG,IAAQ,CAAC,CAAA,CAChB,MAAM,MAAYkH,EAAYnO,CAAO,CAAC,GAKzC0O,MAAkB,MAAkB;GACtC,IAAMC,IACFH,GAA4BvH,CAAK,CAAC,EAAE,UAAU,WAC5C2H,IACFH,GAAwBxH,CAAK,CAAC,EAAE,UAAU;GAC9C,IAAI,CAAC0H,KAAqB,CAACC,GACvB;GAEJ,IAAMC,IAAe,IAAI,KAAKF,CAAiB,GACzCG,IAAW,IAAI,KAAKF,CAAa;GACvC,OAAOC,EAAa,aAAa,MAAMC,EAAS,aAAa,IAEvD,KAAA,IADAA;EAEV,GAKMC,IAAa5N,QACfF,EAAM,sBACA2F,GAAgB,CAAC,CAAC,IAClBA,GAAgB3F,EAAM,YAAY,CAAC,GAAGqN,EAAiB,CACjE,GAGMU,KAAiB7N,QAAe;GAClC,IAAIiG,IAAQ;GACZ,QAAQnG,EAAM,YAAY,CAAC,EAAA,CAAG,KAAK,MAC/BjB,EAAQ,MAAM,SAAS,IAAI,EAAEoH,IAAQ,KAAA,CACzC;EACJ,CAAC,GACK6H,MAAqB,GAAwB,MAC3ChI,MAAU,KAAKjH,EAAQ,SAAS,cACzB,KAEJ,GACHiB,EAAM,SAAS,UACfA,EAAM,qBACLA,EAAM,uBACHjB,EAAQ,UAAU,cAAc,KAAA,KACpCA,EAAQ,UAAU,gBAAgB,KAAA,KACjCiB,EAAM,0BACHjB,EAAQ,UAAU,gBAAgB,KAAA,IAKxCkP,MAAqB,MAAsB;GAC7C,IAAMC,IAAO,IAAI,KAAKC,CAAS,GACzBC,IAAUF,EAAK,aAAa,uBAAM,IAAI,KAAK,EAAA,CAAE,aAAa;GAChE,OAAO9F,EAAG8F,GAAME,IAAU,SAAS,WAAW;EAClD,GACMC,MAAsB,MAA2B;GACnD,IAAM,EAAE,cAAW,mBAAgBtP,EAAQ,YAAY,CAAC;GACxD,IAAKoP,KAAcG,GAGnB,OAAOpQ,GAAemK,GAAIiG,IAAcH,CAAS;EACrD,GAEMI,KACF3F,GACI,mBACJ;SACJjE,QACU3E,EAAM,mBACZ,YAAY;GAER,IADA,MAAMkF,EAAS,GACX,CAAClF,EAAM,mBACP;GAEJ,IAAMuE,IAAKgK,GAAkB,QAAQ,EAAE,EAAE;GACzC,AAAKhK,MAGDoI,EAAcF,GAAoBzM,EAAM,iBAAiB,CAAC,IAC1DuD,GAAe,IAGnBgB,EAAG,eAAe;IAAE,UAAU;IAAU,OAAO;GAAS,CAAC;EAC7D,CACJ;;GAIA,OAAAvK,EAAA,GAAAC,EAsoBM,OAAA;IAroBE,SAAA;IAAJ,KAAIuJ;IACJ,OAAKtJ,EAAA,CAAC,sCAAoC,EAAA,iCACC4P,GAAA,MAAgB,CAAA,CAAA;IAC3D,MAAK;IACL,aAAU;IACV,iBAAc;IACb,aAAWF,EAAA;IACX,OAAKzP,GAAA;KAAwC,wBAAA8F,EAAA;KAAmD,4BAAA6I,GAAA;;IAIxF;;IAIC7I,EAAA,sBAAsBgJ,EAAA,MAAa,SAAM,KADnDjP,EAAA,GAAAC,EAOM,OAPNqM,IAOM,CAJFlM,EAGoCC,IAAA;KAF/B,OAAO4O,EAAA;KACP,aAAWC,GAAA;KACX,UAAQO;;IAEjBlP,EAmmBM,OAAA;KAnmBG,SAAA;KAAJ,KAAIsO;KAAY,OAAM;;MACvB7O,EAAA,EAAA,GAAAC,EA0jBWO,GAAA,MAAAC,GA1jB0BwF,EAAA,WAAnB,GAAS,OAA0BG,EAAA,GAAAC,EAAAqO,GAAA,EAAA,KAAA3P,EAAQ,GAAA,GAAA;MAI/CuO,GAAOvO,CAAO,KAAK0O,GAAezH,CAAK,KADjDhM,EAAA,GAAAC,EAMM,OANNsM,IAMM,CAHFhM,EAEO,QAFPiM,IAEO9L,EADAC,EAAAyN,CAAA,CAAE,CAACqF,GAAezH,CAAK,GAAA,OAAA,CAAA,GAAA,CAAA,CAAA,CAAA,KAAAzF,EAAA,IAAA,EAAA;MAIxBuN,EAAA,MAAW,UAAU,IAAI/O,EAAQ,EAAE,KAD7C/E,EAAA,GAAAC,EAqBM,OArBNwM,IAqBM,CAlBFrM,EAAgD0H,GAAA;OAAxC,MAAK;OAAgB,OAAM;MACnCvH,CAAAA,GAAAA,EAgBO,QAAA,MAAAG,EAdCC,EAAAkG,CAAA,CAAE,CAAA,wBAAA;OAAuI,OAAAiN,EAAA,MAAW,UAAU,IAAI/O,EAAQ,EAAE,CAAA,CAA4C;OAAiD,MAAApE,EAAAyN,CAAA,CAAE,CAA0C0F,EAAA,MAAW,UAAU,IAAI/O,EAAQ,EAAE,CAAA,CAAgD,UAAU,KAAK,IAAG,GAAA,MAAA;MAA8J,GAAA+O,EAAA,MAAW,UAAU,IAAI/O,EAAQ,EAAE,CAAA,CAAG,KAAA,CAAA,GAAA,CAAA,CAAA,CAAA,KAiBlkBpE,EAAAmE,CAAA,CAAY,CAACC,CAAO,KAA8B,CAAA+O,EAAA,MAAW,OAAO,IAAI/O,EAAQ,EAAE,KAA8B,CAAAkB,EAAA,uBADxJjG,EAAA,GAAAC,EAeM,OAAA;;OATF,OAAM;OACL,OAAO8E,EAAQ,UAAU,QAAQ;MAClC3E,GAAAA,CAAAA,EAAgD0H,GAAA;OAAxC,MAAK;OAAgB,OAAM;MACnCvH,CAAAA,GAAAA,EAKO,QAAA,MAAA,CAJAK,GAAAF,EAAAC,EAAAkG,CAAA,CAAE,CAAA,mBAAA,CAAA,IAAwB,KAC7B,CAAA,GAAgB9B,EAAQ,UAAU,aAAlC/E,EAAA,GAAAC,EAEWO,GAAA,EAAA,KAAA,EAAA,GAAA,CAFkCI,GAAA,QACvCF,EAAGC,EAAAyN,CAAA,CAAE,CAACrJ,EAAQ,SAAS,WAAS,MAAA,CAAA,GAAA,CAAA,CAAA,GAAA,EAAA,KAAAwB,EAAA,IAAA,EAAA,CAAA,CAAA,CAAA,GAAA,GAAAmG,EAAA,KAAAnG,EAAA,IAAA,EAAA;OAKT5F,GAAAmE,CAAA,CAAY,CAACC,CAAO,KAAMkB,EAAA,yBAA+E,GAAA,MAAW,OAAO,IAAIlB,EAAQ,EAAE,KAAmC6K,EAAA,SAAa+C,EAAc3G,CAAK,OAAyDjH,EAAQ,MAAM,UAAuC4N,EAAc3G,CAAK,KAAqC4D,EAAA,SAA6CsD,EAAYnO,CAAO,MADvd/E,EAAA,GAAAC,EAygBM,OAAA;;OA7fF,OAAKC,EAAA,CAAC,sBAAoB,CAC+B,uBAAA6E,EAAQ,QAAA,EAAA,+BAA6H6K,EAAA,SAA6C+C,EAAc3G,CAAK,KAAqCkH,EAAYnO,CAAO,EAAA,CAAA,CAAA,CAAA;OASrT,OAAK5E,GAAA,EAAA,WAAmEwS,EAAc3G,CAAK,KAAiCkH,EAAYnO,CAAO,KAAkC,CAAAqN,GAAA,QAA6C,GAAAvC,EAAA,MAAM,MAAuC,KAAA,EAAA,CAAA;;OAQ5QzP,EAOaS,GAAA;QAPD,QAAA;QAAO,MAAK;;QACpB,SAAAC,QAKe,CAJLmS,GAAkBlO,CAAO,KADnC/E,EAAA,GAAAe,EAKeC,IAAA;;SAHV;SACA,gBAAc4O,EAAA,SAAa+C,EAAc3G,CAAK;SAC9C,eAAaqH,GAAkBtO,CAAO;SACtC,WAAAkB,EAAA;;;;;;;;;QAETjG,EAAA,EAAA,GAAAC,EAoLWO,GAAA,MAAAC,GAnLqBsR,GAAehN,CAAO,IAA1C,GAAM,OACRqB,EAAA,GAAAC,EAAAqO,GAAA,EAAA,KAAA/T,EAAAoD,EAAA,CAAW,CAACoB,GAAMyP,CAAS,EAAA,GAAA,CAEvBjU,EAAAgC,CAAA,CAAU,CAACwC,CAAI,KAAKA,EAAK,KAAK,KAAI,KAD5CnF,EAAA,GAAAe,EAuDaF,GAAA;;QArDT,QAAA;QACA,MAAK;;QACL,SAAAC,QAkDM,CAlDNP,EAkDM,OAlDNoM,IAkDM,CAjDF1L,GAoBO6T,EAAA,QAAA,QApBP5T,GAoBO,EAAA,SAAA,GAAA,GAAA;SAnB+C;SAAiD;SAA8C;SAA+C,WAAA0O,EAAA;QADpM,CAAA,SAoBO,CAXOsD,EAAYnO,CAAO,KAD7B/E,EAAA,GAAAe,EAQQI,GAAA;;SANJ,OAAM;SACL,UAAUgE,EAAK;SACf,WAAAc,EAAA;SACA,SAAsD0M,EAAc3G,CAAK,KAAiD/F,EAAA,WAAM;;;;;QAIrIjG,CAAAA,MAAAA,EAAA,GAAAC,EAEWO,GAAA,EAAA,KAAA,EAAA,GAAA,CADJI,GAAAF,EAAAyE,EAAK,IAAI,GAAA,CAAA,CAAA,GAAA,EAAA,EAAA,CAAA,GAQ+BsM,GAAY1M,EAAQ,EAAE,KAA8C,CAAA8L,EAAmB9L,EAAQ,EAAE,CAAA,EAAgD,WAAmD6P,MAAc/C,GAAkB9M,CAAO,KAD9R/E,EAAA,GAAAC,EAuBO,QAAA;;SAhBH,OAAKC,EAAA,CAAC,gCACEwR,GAAkB3M,EAAQ,EAAE,CAAA,CAAA;SACnC,OAAO6M,GAAc7M,EAAQ,EAAE;QAEtB,GAAA,CAAAuM,GAAcvM,EAAQ,EAAE,KADlC/E,EAAA,GAAAC,EAIO,QAJP4M,IAIOnM,EADA4Q,GAAcvM,EAAQ,EAAE,CAAA,GAAA,CAAA,MAE/B/E,EAAA,GAAAe,EAOQ+G,GAAA;;SALH,MAAmDuJ,GAAatM,EAAQ,EAAE,MAAA,aAAA,uBAAA;;;OAU5EpE,GAAAA,IAAAA,KAAAA,EAAAoC,CAAA,CAAU,CAACoC,CAAI,KAD9BnF,EAAA,GAAAe,EAgCaF,GAAA;;QA9BT,QAAA;QACA,MAAK;;QACL,SAAAC,QAqBM,CApByCH,EAAAyB,CAAA,CAAW,CAAC+C,EAAK,UAAUA,EAAK,SAAS,KADxFnF,EAAA,GAAAC,EAqBM,OArBN6M,IAqBM,CAhBF1M,EAMQgB,IAAA;SALH,cAAY+D,EAAK;SACjB,KAAKA,EAAK;SACV,UAAUA,EAAK;SACf,MAA+CxE,EAAAsC,CAAA,CAAgB,CAACkC,CAAI,CAAA,EAAG;;;;;;QAK5E/E,CAAAA,GAAAA,EAMQiB,GAAA;SALH,KAAK8D,EAAK;SACV,YAAYxE,EAAAmC,CAAA,CAAqB,CAACqC,CAAI;SACtC,SAAkDc,EAAA,WAAM,eAA4D0M,EAAc3G,CAAK;;;;;QAKhJhM,CAAAA,CAAAA,CAAAA,MAAAA,EAAA,GAAAe,EAKiDK,IAAA;;SAH5C,cAAY+D,EAAK;SACjB,KAAKA,EAAK;SACV,UAAUA,EAAK;SACf,MAAMxE,EAAAsC,CAAA,CAAgB,CAACkC,CAAI,CAAA,EAAG;;;;;;;;OASSxE,GAAAA,IAAAA,KAAAA,EAAA8C,CAAA,CAAsB,CAAC0B,CAAI,KAAsC,CAAAxE,EAAAyS,CAAA,CAAe,CAACjO,CAAI,KADrInF,EAAA,GAAAe,EA2BaF,GAAA;;QAtBT,QAAA;QACA,MAAK;;QAML,SAAAC,QAc6C,EAd7Cd,EAAA,GAAAe,EAc6CO,IAAA;SAbxC,KAAG,YAAcsT;SACjB;SACA,YAAiD3O,EAAA,gBAAW,OAAkDtF,EAAAyC,CAAA,CAAiB,CAAC+B,CAAI,KAA6CwN,EAAc3G,CAAK;SAKpM,gBAAmD/F,EAAA,uBAAuB,IAA8Cd,EAA8E,cAAU,EAAA;SAMhN,YAAO,MAAEqJ,GAAcrJ,GAAM6P,CAAM;;;;;;;;OAG7B,GAAA,IAAA,KAAA7B,GAAmBhO,CAAI,KADtCnF,EAAA,GAAAe,EAyCaF,GAAA;;QAvCT,QAAA;QACA,MAAK;;QACL,SAAAC,QAQqC,CANW,CAAAH,EAAA2N,CAAA,CAAK,CAACnJ,EAAK,SAA6CxE,EAAA+E,EAAA,CAAgB,CAAC/E,EAAAiC,CAAA,CAAe,CAACuC,CAAI,MAF7InF,EAAA,GAAAe,EAQqCQ,GAP5BZ,EAAA+E,EAAA,CAAgB,CAAC/E,EAAAiC,CAAA,CAAe,CAACuC,CAAI,EAAA,GAD9CjE,GAQqC;SAFhC,KAAG,aAAe0T,EAAS,GAAIjU,EAAAqD,EAAA,CAAY,CAACmB,CAAI;SAChD;QACO,GAAA,EAAA,SAAA,GAAA,GAAA6L,GAAgB7L,CAAI,CAAA,GAAA,MAAA,IAAA,CAAA,MAAA,CAAA,KAChClE,GA2BO6T,EAAA,QAzBI3P,EAAK,MAFhBjE,GA2BO,EAAA,SAAA,GAAA,GAAA;SAxB2C;SAA6C;SAA0C;SAA2C,WAAA0O,EAAA;QAHpL,CAAA,SA2BO,CAjBO3J,EAAA,uBADVjG,EAAA,GAAAC,EAiBM,OAjBN8M,IAiBM,CAdFxM,EAaM,OAbNyM,IAaM,CAXQrM,EAAA+C,CAAA,CAAW,CAACyB,CAAI,KAD1BnF,EAAA,GAAAe,EAGuB+G,GAAA;;SADlB,MAAMnH,EAAA+C,CAAA,CAAW,CAACyB,CAAI;SACvB,OAAM;QACV5E,GAAAA,MAAAA,GAAAA,CAAAA,MAAAA,CAAAA,KAAAA,EAAAA,IAAAA,EAAAA,GAAAA,EAOO,QAPP0M,IAOOvM,EALCC,EAAAkD,EAAA,CAAgB,CAACsB,GAAI;SAA2D,GAAAxE,EAAAkG,CAAA;SAA4D,IAAAlG,EAAAmG,CAAA;;;OAapK1G,GAAAA,IAAAA,KAAAA,EAAAA,IAAAA,EAAAA,GAAAA,EAQaS,GAAA,EARD,MAAK,SAAQ,GAAA;QACrB,SAAAC,QAMkD,CALH8T,MAAc/C,GAAkB9M,CAAO,KAAyCkB,EAAA,oBAAwD4K,EAAmB9L,EAAQ,EAAE,CAAA,EAAG,WADvN/E,EAAA,GAAAe,EAMkDS,IAAA;;SAA7C,UAAUqP,EAAmB9L,EAAQ,EAAE;;;;OAOpD3E,EA+CaS,GAAA;QA/CD,MAAK;QAAU,MAAK;;QAC5B,SAAAC,QA6CM,CA5CqC8O,EAAA,SAA6C+C,EAAc3G,CAAK,KAAsC,CAAA8G,GAAe/N,CAAO,KAAqCmO,EAAYnO,CAAO,MAAuC,CAAAkO,GAAkBlO,CAAO,KAAyCsO,GAAkBtO,CAAO,MADjW/E,EAAA,GAAAC,EA6CM,OAAA;SApCD,KAAG,gBAAkB8E,EAAQ;SAC9B,OAAM;QACNxE,GAAAA,CAAAA,EAiBM,OAjBN2M,IAiBM;SAhBF9M,EAA+C0H,GAAA;UAAvC,MAAK;UAAe,OAAM;;SAClC1H,EAMaS,GAAA,EAND,MAAK,SAAQ,GAAA;UACrB,SAAAC,QAIwC,CAH9B0R,GAAA,SADVxS,EAAA,GAAAe,EAIwC+G,GAAA;WAFnC,KAAK0K,GAAA;WACN,OAAM;WACL,MAAMA,GAAA;;;;SAEfpS,EAOaS,GAAA,EAPD,MAAK,SAAQ,GAAA;UACrB,SAAAC,QAKO,CAJGyR,GAAA,SADVvS,EAAA,GAAAC,EAKO,QAAA;WAHF,KAAKsS,GAAA;WACN,OAAM;UACH,GAAA7R,EAAA6R,GAAA,KAA0B,GAAA,CAAA,KAAAhM,EAAA,IAAA,EAAA,CAAA,CAAA;;;QAMzCnG,CAAAA,GAAAA,EAaaS,GAAA;SAbD,MAAK;SAAU,MAAK;;SAC5B,SAAAC,QAW+F,CAV3C,CAAAmS,GAAkBlO,CAAO,KAA6CkN,EAAA,SAA2E,UAAAA,EAAA,SAAiEA,EAAA,MAAsB,KAAK,KAAI,KADrSjS,EAAA,GAAAe,EAW+FU,IAAA;;UAJ1F,UAAUwQ,EAAA,MAAsB;UAChC,WAAAhM,EAAA;UACA,eAAatF,EAAAmD,EAAA;UACd,eAAY;UACZ,OAAM;;;;;;;;;;OAItB1D,EA+OaS,GAAA,EA/OD,MAAK,SAAQ,GAAA;QACrB,SAAAC,QA6OM,CA5OIkT,GAAkBjP,GAASiH,CAAK,KAD1ChM,EAAA,GAAAC,EA6OM,OA7ONkN,IA6OM;SA1OF/M,EAgIgBwH,GAAA;UA/HZ,WAAU;UACV,OAAM;;UACN,SAAA9G,QAYa;WAZbV,EAYaS,GAAA,EAZD,MAAK,SAAQ,GAAA;YACrB,SAAAC,QAUwC,CATe8R,GAAkF5G,CAAA,KADzIhM,EAAA,GAAAe,EAUwC4G,GAAA;;aAJpC,MAAK;aACL,WAAU;aACT,OAAOhH,EAAAkG,CAAA,CAAE,CAAA,mBAAA;aACT,cAAYlG,EAAAkG,CAAA,CAAE,CAAA,mBAAA;aACd,SAAK,AAAA2B,EAAA,OAAA9G,GAAA,MAAO9B,EAAAA,MAAK,YAAA,GAAA,CAAA,MAAA,CAAA;;;;WAE1BQ,EAeaS,GAAA,EAfD,MAAK,SAAQ,GAAA;YACrB,SAAAC,QAaQ,CAZ+CoQ,GAAA,aAAoIlF,CAAA,KAD3LhM,EAAA,GAAAe,EAaQ4G,GAAA;;aANJ,MAAK;aACJ,OAAOhH,EAAAkG,CAAA,CAAE,CAAA,oBAAA;aACT,cAAYlG,EAAAkG,CAAA,CAAE,CAAA,oBAAA;aACf,WAAU;aACT,SAAKnF,GAAA,MAAoD9B,EAAAA,MAAK,aAAcmF,CAAO,GAAA,CAAA,MAAA,CAAA;;;;;;;;WAI5F3E,EAgCaS,GAAA,EAhCD,MAAK,SAAQ,GAAA;YACrB,SAAAC,QA8BQ,CA7B+CoQ,GAAA,UAAiIlF,CAAA,KADxLhM,EAAA,GAAAe,EA8BQ4G,GAAA;;aAvBH,MAAmD+I,GAAU3L,EAAQ,EAAE,IAAA,sBAAA;aAKvE,OAAoD2L,GAAU3L,EAAQ,EAAE,IAAoDpE,EAAAkG,CAAA,CAAE,CAAA,mBAAA,IAAwElG,EAAAkG,CAAA,CAAE,CAAA,qBAAA;aAKxM,cAAyD6J,GAAU3L,EAAQ,EAAE,IAAoDpE,EAAAkG,CAAA,CAAE,CAAA,mBAAA,IAAwElG,EAAAkG,CAAA,CAAE,CAAA,qBAAA;aAK9M,WAAU;aACT,OAAK3G,EAAA,EAAsE,sBAAAwQ,GAA2D3L,EAAQ,EAAA,EAAA,CAAA;aAK9I,SAAKrD,GAAA,MAAoD9B,EAAAA,MAAK,UAAWmF,CAAO,GAAA,CAAA,MAAA,CAAA;;;;;;;;;;WAIzF3E,EAoBaS,GAAA,EApBD,MAAK,SAAQ,GAAA;YACrB,SAAAC,QAkBQ,CAjB+CmQ,GAAA,UAAmIjF,CAAA,KAD1LhM,EAAA,GAAAe,EAkBQ4G,GAAA;;aAXH,MAAmDkJ,EAAmB9L,EAAQ,EAAE,CAAA,EAAoD,SAAI,WAAA,qBAAA;aAMxI,OAAOpE,EAAAkG,CAAA,CAAE,CAAA,eAAA;aACT,cAAYlG,EAAAkG,CAAA,CAAE,CAAA,eAAA;aACf,WAAU;aACT,SAAKnF,GAAA,MAAoD9B,EAAAA,MAAK,UAAWmF,CAAO,GAAA,CAAA,MAAA,CAAA;;;;;;;;;WAIzF3E,EAoBaS,GAAA,EApBD,MAAK,SAAQ,GAAA;YACrB,SAAAC,QAkBQ,CAjB+CmQ,GAAA,YAAqIjF,CAAA,KAD5LhM,EAAA,GAAAe,EAkBQ4G,GAAA;;aAXH,MAAmDkJ,EAAmB9L,EAAQ,EAAE,CAAA,EAAoD,SAAI,aAAA,uBAAA;aAMxI,OAAOpE,EAAAkG,CAAA,CAAE,CAAA,iBAAA;aACT,cAAYlG,EAAAkG,CAAA,CAAE,CAAA,iBAAA;aACf,WAAU;aACT,SAAKnF,GAAA,MAAoD9B,EAAAA,MAAK,YAAamF,CAAO,GAAA,CAAA,MAAA,CAAA;;;;;;;;;WAI3F3E,EAoBaS,GAAA,EApBD,MAAK,SAAQ,GAAA;YACrB,SAAAC,QAkBQ,CAjB+CmQ,GAAA,YAAqIjF,CAAA,KAD5LhM,EAAA,GAAAe,EAkBQ4G,GAAA;;aAXH,MAAmDkJ,EAAmB9L,EAAQ,EAAE,CAAA,EAAoD,UAAA,qBAAA;aAMpI,OAAOpE,EAAAkG,CAAA,CAAE,CAAA,iBAAA;aACT,cAAYlG,EAAAkG,CAAA,CAAE,CAAA,iBAAA;aACf,WAAU;aACT,SAAKnF,GAAA,MAAoD9B,EAAAA,MAAK,YAAamF,CAAO,GAAA,CAAA,MAAA,CAAA;;;;;;;;;;;;SAM/C,CAAAkB,EAAA,uBAA2DlB,EAAQ,UAAU,aAAiDA,EAAQ,UAAU,eADhM/E,EAAA,GAAAC,EAyCM,OAzCNmN,IAyCM,CAlCF7M,EAuBO,QAAA;UAtBF,UAAuD,IAAA,KAAkDwE,EAAQ,SAAS,SAAqD,CAAA,CAAA,YAAW;UAK1L,OAAgDpE,EAAAyN,CAAA,CAAE,CAAkD,IAAA,KAAsDrJ,EAAQ,SAAS,SAAA,GAAA,WAAA;UAQ5K,OAAM;SACN3E,GAAAA,CAAAA,EAEuB0H,GAAA;UADnB,MAAK;UACL,OAAM;SACVvH,CAAAA,GAAAA,EAIS,QAJT+M,IAIS5M,EAHLuT,GAA+DlP,EAAQ,SAAS,SAAA,CAAA,GAAA,CAAA,CAAA,GAAA,GAAAsI,EAAA,GAM9EgH,GAAmBtP,CAAO,KADpC/E,EAAA,GAAAC,EASO,QATPsN,IASO,CANHnN,EAEuB0H,GAAA;UADnB,MAAK;UACL,OAAM;SACVvH,CAAAA,GAAAA,EAES,QAFTiN,IAES9M,EADL2T,GAAmBtP,CAAO,CAAA,GAAA,CAAA,CAAA,CAAA,KAAAwB,EAAA,IAAA,EAAA,CAAA,CAAA,KAAAA,EAAA,IAAA,EAAA;SAK5BN,EAAA,qBADVjG,EAAA,GAAAC,EASO,QAAA;;UAPF,OAA4CU,EAAAkG,CAAA,CAAE,CAAA,uBAAA,EAAqE,GAAAkN,GAAA,MAAe/H,GAAA,CAAA;UAKnI,OAAM;SAAmC,GAAA,OACxCtL,EAAGqT,GAAA,MAAe/H,EAAK,GAAA,GAAAyB,EAAA,KAAAlH,EAAA,IAAA,EAAA;SAGmBN,EAAA,0BAA8DlB,EAAQ,UAAU,eAD/H/E,EAAA,GAAAC,EAkBO,QAlBPyN,IAkBO,CAZHtN,EAEuB0H,GAAA;UADnB,MAAK;UACL,OAAM;SACVvH,CAAAA,GAAAA,EAQO,QARPoN,IAQOjN,EANCC,EAAA0N,CAAA,CAAE,CAA8CtJ,EAAQ,SAAS,aAAA,SAInE,CAAA,IAAA,MACFrE,EAAGC,EAAAkG,CAAA,CAAE,CAAA,cAAA,CAAA,GAAA,CAAA,CAAA,CAAA,KAAAN,EAAA,IAAA,EAAA;SAIkCN,EAAA,uBAA2DlB,EAAQ,UAAU,aAD5H/E,EAAA,GAAAC,EAkBO,QAAA;;UAbF,UAAmD,IAAA,KAA8C8E,EAAQ,UAAU,SAAiD,CAAA,CAAA,YAAW;UAKhL,OAAM;SACN3E,GAAAA,CAAAA,EAA+C0H,GAAA;UAAvC,MAAK;UAAe,OAAM;SAClCvH,CAAAA,GAAAA,EAKS,QALTsN,IAKSnN,EAJLC,EAAAyN,CAAA,CAAE,CAA8C,IAAA,KAAKrJ,EAAQ,UAAU,SAAS,GAAA,WAAA,CAAA,GAAA,CAAA,CAAA,GAAA,GAAA6I,EAAA,KAAArH,EAAA,IAAA,EAAA;SAOzCN,EAAA,uBAA2DlB,GAAS,UAAU,eAAmDA,GAAS,UAAU,aADnM/E,EAAA,GAAAC,EAcM,OAdN6N,IAcM,CAPF1N,EAEuB0H,GAAA;UADnB,MAAK;UACL,OAAM;SACV1H,CAAAA,GAAAA,EAGgDuB,IAAA;UAF5C,OAAM;UACL,MAAMoD,EAAQ,UAAU;UACxB,YAAUA,EAAQ,UAAU;;;;;OAI7C3E,EASaS,GAAA,EATD,MAAK,SAAQ,GAAA;QACrB,SAAAC,QAOuC,CAN7BiE,EAAQ,OAAOkB,EAAA,qBADzBjG,EAAA,GAAAe,EAOuCa,IAAA;;;SAL/B,SAAA;SAAJ,KAAI2S;SACH,SAAStO,EAAA;SACT,WAAWA,EAAA;SACX,OAAOA,EAAA;SACP,UAAM,AAAAuC,EAAA,QAAA,MAAE5I,EAAAA,MAAK,mBAAoBoV,CAAM;SACvC,SAAK,AAAAxM,EAAA,QAAA,MAAE5I,EAAAA,MAAK,gBAAA;;;;;;;;;;KAU7BQ,EAkBaS,GAAA;MAlBD,QAAA;MAAO,MAAK;;MACpB,SAAAC,QAgBM,CAfIuR,GAAA,SADVrS,EAAA,GAAAC,EAgBM,OAAA;;OAdF,OAAM;OACL,OAAKE,GAAA,EAAuC,WAAAmS,GAAA,QAAsD,GAAAzC,EAAA,MAAM,MAAmC,KAAA,EAAA,CAAA;MAK5ItP,GAAAA,CAAAA,EAOM,OAPNwN,IAOM,CANFxN,EAKM,OALNyN,IAKM,CAJF5N,EAA+C0H,GAAA;OAAvC,MAAK;OAAe,OAAM;MAClCvH,CAAAA,GAAAA,EAEO,QAFP0N,IAEOvN,EADAC,EAAAkG,CAAA,CAAE,CAAA,0BAAA,CAAA,GAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAA,KAAAN,EAAA,IAAA,EAAA,CAAA,CAAA;;;KAMzBnG,EAMaS,GAAA;MAND,QAAA;MAAO,MAAK;;MACpB,SAAAC,QAImC,CAHzBsR,GAAA,SADVpS,EAAA,GAAAe,EAImCc,IAAA;;OAF9B,OAAAoE,EAAA;OACA,SAAK,AAAAuC,EAAA,QAAA,MAAE5I,EAAAA,MAAK,YAAA;OACZ,SAAK,AAAA4I,EAAA,QAAA,MAAE5I,EAAAA,MAAK,YAAA;;;;KAGWoS,GAAA,OAAe,SAAI,UAAA,CAAgBM,GAAA,SAA6CF,GAAA,SADhHpS,EAAA,GAAAC,EAMgD,OAAA;;MAD5C,OAAM;MACL,OAAKE,GAAA,EAAA,WAAA,GAAkB0P,EAAA,MAAM,IAAA,CAAA;;;IAEtCzP,EAUaS,GAAA,EAVD,MAAK,0BAAyB,GAAA;KACtC,SAAAC,QAQS,CAPCmF,EAAA,uBAAuBtF,EAAAmI,EAAA,KAAc,CAAKnI,EAAAqI,EAAA,MADpDhJ,EAAA,GAAAC,EAQS,UAAA;;MANL,MAAK;MACL,OAAM;MACL,OAAOU,EAAAkG,CAAA,CAAE,CAAA,uBAAA;MACT,cAAYlG,EAAAkG,CAAA,CAAE,CAAA,uBAAA;MACd,SAAOmI;KACR5O,GAAAA,CAAAA,EAAoC0H,GAAA,EAA5B,MAAK,qBAAoB,CAAA,CAAA,GAAA,GAAAoG,EAAA,KAAA3H,EAAA,IAAA,EAAA,CAAA,CAAA"}
1
+ {"version":3,"file":"PkChatbotMessages-Bni4EjEB.js","names":["core._isoDateTime","z.object","z.string","z.uuid","z.iso.datetime","_hoisted_1","_openBlock","_createElementBlock","_toDisplayString","_createCommentVNode","_sfc_main","PkToolApprovalPreviewMessage","_hoisted_1","__unplugin_components_0","__unplugin_components_1","__unplugin_components_2","_openBlock","_createElementBlock","_normalizeClass","_createVNode","_Transition","_withCtx","_toDisplayString","_createCommentVNode","_createBlock","_resolveDynamicComponent","_unref","_withModifiers","_createTextVNode","_sfc_main","$emit","__unplugin_components_0","__unplugin_components_1","__unplugin_components_2","_openBlock","_createElementBlock","_normalizeClass","_normalizeStyle","_createVNode","PkChatbotOutlineRail","_createCommentVNode","_createElementVNode","_Fragment","_renderList","_toDisplayString","_unref","_createTextVNode","_Transition","_withCtx","_createBlock","PkChatbotSteps","_renderSlot","_mergeProps","PkStreamingMarkdown","PkChatbotFilePreview","PkChatbotAudioTranscript","PkToolApprovalCard","_resolveDynamicComponent","PkChatbotMessageFeedback","PkStreamingMarkdownAutoscroll","_withModifiers","PkRelativeTime","PkChatbotFeedbackForm","PkChatbotError","_sfc_main","_isoDateTime","uuid","string","ZodISODateTime","object","isAudioFile","isVoiceRenderAck","VOICE_RENDERED_TOOLS","AgentBuiltInTool","USER_ANSWERED_TOOLS","VvButtonLocalized","PkStreamingMarkdown_default","isTextPart","getToolPartName","isToolPart","getFilePartTranscript","isFilePart","isReasoningPart","getFilePartMedia","PkChatbotAudioTranscript_default","toolLabel","isToolConfirmPart","moveFileOutputsLast","resolveContrastColor","mergeConsecutiveTextParts","dropCorrectedWidgetErrors","isConfirmationCardPart","getPartIcon","getToolPartLabel","getToolApprovalId","getPartTypeLabel","LIVE_REASONING_TAIL_LENGTH","feedPartKey","getPartState","CONFIRM_TOOL_NAME","formatDuration","PkChatbotError_default","PkRelativeTime_default","PkChatbotFilePreview_default","PkChatbotFeedbackForm_default","PkChatbotMessageFeedback_default","PkStreamingMarkdownAutoscroll_default","PkChatbotSteps_default","PkChatbotOutlineRail_default","datetime","params","PendingWakeupSchema","isWakeupSeed","message","PENDING_TOOL_STATES","CARD_TOOLS","stateOf","part","toolNameOf","wakeupAnswerShows","state","toolName","isQuietWakeupAnswer","noop","toolComponentMap","defineAsyncComponent","n","_hoisted_1$2","PkToolApprovalPreviewMessage_default","defineComponent","props","__props","computed","input","openBlock","createElementBlock","toDisplayString","createCommentVNode","toolApprovalPreviews","_hoisted_1$1","PkToolApprovalCard_default","__emit","emit","$t","$te","useI18n","toolPart","isConfirm","isOpen","isPending","isExpired","isApproved","isDenied","preview","prompt","outcome","isBare","_component_VvButton","_component_VvButtonGroup","VvButtonGroup","_component_VvIcon","VvIcon","normalizeClass","createVNode","Transition","withCtx","createBlock","resolveDynamicComponent","createTextVNode","unref","_cache","withModifiers","NEAR_BOTTOM_THRESHOLD","SMOOTH_SCROLL_DURATION","useChatScroll","options","userScrolledUp","ref","isAtBottom","isAutoScrolling","mutationObserver","resizeObserver","initMutationObserver","contentResizeObserver","lastScrollTop","scrollToBottom","scrollEl","behavior","onWheel","event","lastTouchY","onTouchStart","onTouchMove","y","handleScroll","currentScrollTop","scrollHeight","clientHeight","onScrollUp","onScrollDown","autoFollow","el","bottom","startMutationScroll","stopMutationScroll","watch","status","newStatus","oldStatus","messagesLength","lastMessageRole","cleanupInitialObservers","nextTick","beforeNewMessageScroll","initialScrollTimer","tryInitialScroll","onMounted","isSendInFlight","wrapper","contentEl","onBeforeUnmount","buildWakeupFeed","isQuiet","draws","hidden","quietRuns","index","messages","start","count","lastAt","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","_hoisted_27","_hoisted_28","_hoisted_29","_hoisted_30","PkChatbotMessages_default","$d","$n","slots","useSlots","onCardRespond","toolCallId","approved","id","useTemplateRef","wrapperEl","contrastColor","updateAnchorSpacer","onScrollToBottomClick","outlineItems","activeOutlineId","getUserMessageEls","updateActiveOutline","scroller","line","active","onScroll","scrollToUserMessage","item","messageEl","isLoading","height","isSpacerSettling","isSpacerHeld","anchoredUserMessageEl","shallowRef","scrollElHeight","paddingTop","paddingBottom","wrapperGap","userMessageEls","newHeight","useResizeObserver","loading","isRevised","r","messageId","getMessageFeedback","f","RECORD_TOOLS","recordToolProps","isFeedbackButtonVisible","isActionButtonVisible","kind","readonlyFeedbackOf","feedbackVote","feedbackEmoji","feedback","raw","hasReaction","reactionToneClass","vote","reactionTitle","lastTextPartIndex","parts","getMergedParts","activeMessage","activeMessageLastPart","mergedPartsMap","map","isError","isSpokenWaitVisible","isSpokenWaitAnchored","activeMessageLastPartLabel","activeMessageLastPartIcon","getMessageIndexById","m","isLastMessage","isMessageRegenerateButtonVisible","action","isLastTextPart","lastPart","messageHasSteps","showStepsTimeline","isAssistant","isRenderedToolPart","isStreamingPart","messageHasContent","isUser","getPreviousAssistantMessage","getNextAssistantMessage","getDividerDate","previousCreatedAt","nextCreatedAt","previousDate","nextDate","wakeupFeed","messageNumbers","showMessageFooter","formatMessageTime","date","createdAt","isToday","getMessageDuration","completedAt","feedbackMessageEl","normalizeStyle","createElementVNode","Fragment","renderList","partIndex","renderSlot","_ctx","mergeProps","$event"],"sources":["../../../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/iso.js","../../../../packages/models/src/schema/ChatWakeup.ts","../../../../packages/components/src/chat/toolComponentMap.ts","../../../../packages/components/src/chat/PkToolApprovalPreviewMessage.vue","../../../../packages/components/src/chat/PkToolApprovalPreviewMessage.vue","../../../../packages/components/src/chat/toolApprovalPreviews.ts","../../../../packages/components/src/chat/PkToolApprovalCard.vue","../../../../packages/components/src/chat/PkToolApprovalCard.vue","../../../../packages/components/src/chat/useChatScroll.ts","../../../../packages/components/src/chat/wakeupFeed.ts","../../../../packages/components/src/chat/PkChatbotMessages.vue","../../../../packages/components/src/chat/PkChatbotMessages.vue"],"sourcesContent":["import * as core from \"../core/index.js\";\nimport { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from \"./schemas.js\";\nexport { ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime } from \"./schemas.js\";\nexport function datetime(params) {\n return core._isoDateTime(ZodISODateTime, params);\n}\nexport function date(params) {\n return core._isoDate(ZodISODate, params);\n}\nexport function time(params) {\n return core._isoTime(ZodISOTime, params);\n}\nexport function duration(params) {\n return core._isoDuration(ZodISODuration, params);\n}\n","import * as z from 'zod'\nimport {\n AgentBuiltInTool,\n USER_ANSWERED_TOOLS,\n VOICE_RENDERED_TOOLS,\n} from './Agent'\nimport type { UIChatMessage } from './Chat'\n\n/**\n * Lifecycle of a wakeup (spec agent-wakeup). `running` to `skipped` follow the\n * run-ledger convention of the headless runs; `replaced` and `stopped` close a\n * wakeup that never ran.\n */\nexport const ChatWakeupStatus = {\n Scheduled: 'scheduled',\n Running: 'running',\n Succeeded: 'succeeded',\n Failed: 'failed',\n Skipped: 'skipped',\n /** A later call of the same chat took its place. */\n Replaced: 'replaced',\n /** `stop: true`, or the user's \"Stop\" in the chat. */\n Stopped: 'stopped',\n} as const\n\nexport type ChatWakeupStatus =\n (typeof ChatWakeupStatus)[keyof typeof ChatWakeupStatus]\n\n/** Where a platform wakeup posts its answer: the Chat SDK thread, not the channel. */\nexport const ChatWakeupDeliveryTargetSchema = z.object({\n platform: z.string(),\n threadId: z.string(),\n})\nexport type ChatWakeupDeliveryTarget = z.infer<\n typeof ChatWakeupDeliveryTargetSchema\n>\n\n/** The wakeup still waiting in a chat, as the chat UI needs it. */\nexport const PendingWakeupSchema = z.object({\n wakeId: z.uuid(),\n dueAt: z.iso.datetime(),\n reason: z.string(),\n})\nexport type PendingWakeup = z.infer<typeof PendingWakeupSchema>\n\nexport const ChatWakeupStateSchema = z.object({\n pendingWakeup: PendingWakeupSchema.nullable(),\n})\nexport type ChatWakeupState = z.infer<typeof ChatWakeupStateSchema>\n\n/** The body of a scheduled Service Bus message, and of the internal fire call. */\nexport const WakeupFireJobSchema = z.object({\n wakeId: z.uuid(),\n})\nexport type WakeupFireJob = z.infer<typeof WakeupFireJobSchema>\n\n/** Delays a wakeup may take, in seconds: the clamp of Claude Code's ScheduleWakeup. */\nexport const WAKEUP_MIN_DELAY_SECONDS = 60\nexport const WAKEUP_MAX_DELAY_SECONDS = 3600\n\n/** The user message a wakeup injected: the agent's own prompt coming back, not the user's words. */\nexport const isWakeupSeed = (message?: UIChatMessage): boolean =>\n message?.role === 'user' && Boolean(message.metadata?.wakeup)\n\n/** Tool states that still wait for something: the model's input or the user's approval. */\nconst PENDING_TOOL_STATES = new Set([\n 'input-streaming',\n 'input-available',\n 'approval-requested',\n])\n\n/**\n * Tools the chat draws as a card: the client widgets, the cards that ask the\n * user something, and `editSpreadsheet`, a server tool whose output is the\n * download card of the new version.\n */\nconst CARD_TOOLS: ReadonlySet<string> = new Set([\n ...VOICE_RENDERED_TOOLS,\n ...USER_ANSWERED_TOOLS,\n 'editSpreadsheet',\n])\n\n/**\n * Cards that ask the user something. The contact form is one too, although\n * its lead goes through an endpoint of its own and never answers the turn.\n */\nconst QUESTION_TOOLS: ReadonlySet<string> = new Set([\n ...USER_ANSWERED_TOOLS,\n AgentBuiltInTool.ShowContactForm,\n])\n\ntype MessagePart = UIChatMessage['parts'][number]\n\nconst stateOf = (part: MessagePart): string | undefined =>\n (part as { state?: string }).state\n\nconst toolNameOf = (part: MessagePart): string | undefined =>\n part.type.startsWith('tool-') ? part.type.slice('tool-'.length) : undefined\n\n/**\n * Whether a wakeup answer shows the user anything: words, a file, a card, or a\n * tool call still waiting, such as one held for the user's approval. It is\n * what makes a round news (spec agent-wakeup, Fase 1.1), whatever its `noop`\n * said: the model's history keeps the round, the feed draws it and the user\n * gets a notification.\n */\nexport const wakeupAnswerShows = (message?: UIChatMessage): boolean =>\n message?.role === 'assistant' &&\n message.parts.some((part) => {\n if (part.type === 'text') {\n return part.text.trim().length > 0\n }\n if (part.type === 'file') {\n return true\n }\n const state = stateOf(part)\n if (state && PENDING_TOOL_STATES.has(state)) {\n return true\n }\n const toolName = toolNameOf(part)\n return (\n toolName !== undefined &&\n CARD_TOOLS.has(toolName) &&\n state === 'output-available'\n )\n })\n\n/**\n * Whether a wakeup answer leaves the user a question: a card that asks\n * something, still unanswered. Such a round ends its loop, and the answer\n * opens a normal turn (spec agent-wakeup, \"Una domanda ferma il ciclo\").\n */\nexport const wakeupAnswerAsks = (message?: UIChatMessage): boolean =>\n message?.role === 'assistant' &&\n message.parts.some((part) => {\n const toolName = toolNameOf(part)\n return (\n toolName !== undefined &&\n QUESTION_TOOLS.has(toolName) &&\n stateOf(part) === 'input-available'\n )\n })\n\n/**\n * Whether an answer still waits for the user to answer it in the chat: a card\n * answered through the turn, or a call held for the user's approval. A wakeup\n * round must not run below it, because the answer resumes the last message\n * only, and a history with a newer turn records the approval as denied. The\n * contact form does not count: its lead goes through an endpoint of its own,\n * wherever its card is, and its part never leaves `input-available`.\n */\nexport const answerAwaitsUser = (message?: UIChatMessage): boolean =>\n message?.role === 'assistant' &&\n message.parts.some((part) => {\n const state = stateOf(part)\n if (state === 'approval-requested') {\n return true\n }\n const toolName = toolNameOf(part)\n return (\n toolName !== undefined &&\n USER_ANSWERED_TOOLS.has(toolName) &&\n state === 'input-available'\n )\n })\n\n/**\n * An answer that only told its loop nothing had changed: a finished\n * `scheduleWakeup` call with `noop`, and nothing shown to the user\n * ({@link wakeupAnswerShows}). The one rule shared by the model's history\n * (which drops such rounds) and the chat feed (which folds them).\n */\nexport const isQuietWakeupAnswer = (message?: UIChatMessage): boolean => {\n if (message?.role !== 'assistant' || wakeupAnswerShows(message)) {\n return false\n }\n let noop = false\n for (const part of message.parts) {\n if (part.type === 'tool-scheduleWakeup') {\n noop =\n stateOf(part) === 'output-available' &&\n (part as { input?: { noop?: boolean } }).input?.noop === true\n }\n }\n return noop\n}\n","import { defineAsyncComponent } from 'vue'\n\n/**\n * Maps tool `part.type` to the corresponding component for auto-rendering.\n * Only includes simple tools that accept only a `:part` prop.\n * Interactive tools (showContactForm, showSuggestedReply, showSources,\n * showMultipleChoice) must be wired explicitly in the parent with their\n * required callbacks/events. Consent requests (`requestConfirm` and the tool\n * calls suspended for approval) are not here either: `PkChatbotMessages`\n * renders them all with `PkToolApprovalCard`.\n */\nexport const toolComponentMap: Record<\n string,\n ReturnType<typeof defineAsyncComponent>\n> = {\n requestOAuthConnection: defineAsyncComponent(\n () => import('./PkToolRequestOAuthConnection.vue'),\n ),\n showArtifact: defineAsyncComponent(\n () => import('./PkToolShowArtifact.vue'),\n ),\n showCalendarEvent: defineAsyncComponent(\n () => import('./PkToolShowCalendarEvent.vue'),\n ),\n showComparison: defineAsyncComponent(\n () => import('./PkToolShowComparison.vue'),\n ),\n showContactForm: defineAsyncComponent(\n () => import('./PkToolShowContactForm.vue'),\n ),\n showDiagram: defineAsyncComponent(() => import('./PkToolShowDiagram.vue')),\n showEmail: defineAsyncComponent(() => import('./PkToolShowEmail.vue')),\n showImageGallery: defineAsyncComponent(\n () => import('./PkToolShowImageGallery.vue'),\n ),\n showLocation: defineAsyncComponent(\n () => import('./PkToolShowLocation.vue'),\n ),\n showMessage: defineAsyncComponent(() => import('./PkToolShowMessage.vue')),\n showForm: defineAsyncComponent(() => import('./PkToolShowForm.vue')),\n showMultipleChoice: defineAsyncComponent(\n () => import('./PkToolShowForm.vue'),\n ),\n showProductList: defineAsyncComponent(\n () => import('./PkToolShowProductList.vue'),\n ),\n showQrCode: defineAsyncComponent(() => import('./PkToolShowQrCode.vue')),\n showSources: defineAsyncComponent(() => import('./PkToolShowSources.vue')),\n showSuggestedReply: defineAsyncComponent(\n () => import('./PkToolShowSuggestedReply.vue'),\n ),\n showVideo: defineAsyncComponent(() => import('./PkToolShowVideo.vue')),\n showWeather: defineAsyncComponent(() => import('./PkToolShowWeather.vue')),\n showWebPages: defineAsyncComponent(\n () => import('./PkToolShowWebPages.vue'),\n ),\n}\n","<script setup lang=\"ts\">\n import { computed } from 'vue'\n\n /**\n * Preview for the tools whose input is already a sentence written for the\n * user (`requestConfirm`): it is shown as-is, with no key/value framing.\n */\n const props = defineProps<{\n input?: unknown\n }>()\n\n const message = computed(() => {\n const input = props.input as { message?: unknown } | undefined\n return typeof input?.message === 'string' ? input.message : ''\n })\n</script>\n\n<template>\n <p v-if=\"message\" class=\"text-12 text-word-3 m-0 mb-xs\">\n {{ message }}\n </p>\n</template>\n","<script setup lang=\"ts\">\n import { computed } from 'vue'\n\n /**\n * Preview for the tools whose input is already a sentence written for the\n * user (`requestConfirm`): it is shown as-is, with no key/value framing.\n */\n const props = defineProps<{\n input?: unknown\n }>()\n\n const message = computed(() => {\n const input = props.input as { message?: unknown } | undefined\n return typeof input?.message === 'string' ? input.message : ''\n })\n</script>\n\n<template>\n <p v-if=\"message\" class=\"text-12 text-word-3 m-0 mb-xs\">\n {{ message }}\n </p>\n</template>\n","import type { Component } from 'vue'\nimport PkToolApprovalPreviewMessage from './PkToolApprovalPreviewMessage.vue'\nimport { CONFIRM_TOOL_NAME } from './utils'\n\n/**\n * Maps a tool name to the component that renders a human-readable preview of\n * its input inside `PkToolApprovalCard`. The preview receives the tool call\n * input as its `input` prop.\n *\n * Registering a preview needs no change to the card: tools with no entry here\n * show only the localized confirmation prompt (no raw echo of the call input).\n * Reuse the component that already renders that kind of payload when there is\n * one (e.g. `sendEmail` will reuse `PkToolShowEmail`).\n */\nexport const toolApprovalPreviews: Record<string, Component> = {\n // The model already writes the sentence to confirm: show it verbatim.\n [CONFIRM_TOOL_NAME]: PkToolApprovalPreviewMessage,\n}\n","<script setup lang=\"ts\">\n import { computed } from 'vue'\n import { useI18n } from 'vue-i18n'\n import { getToolPartName } from 'utils'\n import { isVoiceRenderAck } from 'models'\n import { CONFIRM_TOOL_NAME, toolLabel } from './utils'\n import { toolApprovalPreviews } from './toolApprovalPreviews'\n\n /**\n * The single confirmation card of the chat. It renders two things that are\n * the same question for the user:\n *\n * - **approval**: a tool call the backend suspended waiting for consent\n * (`approval-requested`), plus its resolved states so the decision stays\n * visible in the transcript;\n * - **confirm**: a `requestConfirm` call, the client-side tool the model uses\n * to have an action it described approved. Same layout, its own wording\n * (Proceed / Cancel, as on the messaging channels).\n *\n * The body comes from `toolApprovalPreviews[toolName]` when the tool has a\n * dedicated preview (localized, human copy: e.g. `sendEmail` will reuse the\n * email renderer); tools without one show only the localized prompt, never\n * a raw echo of the call input. Adding a preview never requires touching\n * this component.\n */\n const props = defineProps<{\n part: unknown\n /**\n * Whether the user can still answer. False in read-only views (chat\n * logs) and, for approvals, once the conversation has moved past the\n * request: the backend closes an abandoned approval as denied, so\n * answering it later would not run the tool.\n */\n actionable?: boolean\n /**\n * The verdict given ON A CALL, when there is one. A confirm answered\n * during a call travels back as a spoken message and its part stays\n * on the render ack, so the outcome only exists client-side: this is\n * what closes the card and names the outcome instead of leaving the\n * buttons alive (or worse, showing the answer as \"expired\").\n */\n callVerdict?: boolean\n }>()\n\n const emit = defineEmits<{\n respond: [approved: boolean]\n }>()\n\n const { t: $t, te: $te } = useI18n({ useScope: 'global' })\n\n const toolPart = computed(\n () =>\n props.part as {\n type?: string\n state?: string\n toolCallId?: string\n input?: unknown\n output?: unknown\n approval?: { id?: string; approved?: boolean; reason?: string }\n },\n )\n\n const toolName = computed(() =>\n getToolPartName(props.part as Parameters<typeof getToolPartName>[0]),\n )\n\n const isConfirm = computed(() => toolName.value === CONFIRM_TOOL_NAME)\n\n /**\n * True while the request still waits for an answer. A confirm drawn\n * during a CALL arrives already resolved with the note that it was drawn\n * (see `isVoiceRenderAck`, same rule as the forms): the note is not the\n * user's verdict, so the card keeps its buttons.\n */\n const isOpen = computed(() =>\n isConfirm.value\n ? (toolPart.value.state === 'input-available' ||\n isVoiceRenderAck(toolPart.value.output)) &&\n props.callVerdict === undefined\n : toolPart.value.state === 'approval-requested',\n )\n\n /** Open and answerable: the only case where the buttons act. */\n const isPending = computed(() => isOpen.value && props.actionable === true)\n\n /** Open but not answerable: shown as an expired request. */\n const isExpired = computed(() => isOpen.value && props.actionable !== true)\n\n const isApproved = computed(() =>\n isConfirm.value\n ? toolPart.value.output === 'confirmed' ||\n props.callVerdict === true\n : toolPart.value.approval?.approved === true,\n )\n\n const isDenied = computed(() =>\n isConfirm.value\n ? toolPart.value.output === 'cancelled' ||\n props.callVerdict === false\n : toolPart.value.approval?.approved === false ||\n toolPart.value.state === 'output-denied',\n )\n\n const preview = computed(() => toolApprovalPreviews[toolName.value])\n\n /**\n * The ask above the buttons. A preview shows what is about to happen and\n * the sentence leads into it; without one the sentence is all the user\n * reads, so it names the tool instead of ending on a colon with nothing\n * after it.\n */\n const prompt = computed(() =>\n preview.value\n ? $t('toolApproval.prompt')\n : $t('toolApproval.promptNamed', {\n tool: toolLabel(toolName.value, { t: $t, te: $te }),\n }),\n )\n\n /**\n * Once the request is answered (or expired) the card slims down: prompt\n * and buttons disappear and this single icon + text line tells the\n * outcome. A confirm has its own wording (the model described an action,\n * no tool ran on its behalf).\n */\n const outcome = computed(() => {\n if (isExpired.value) {\n return { icon: 'ri:time-line', message: $t('toolApproval.expired') }\n }\n if (isApproved.value) {\n return {\n icon: 'ri:check-line',\n message: $t(\n isConfirm.value\n ? 'toolApproval.confirmed'\n : 'toolApproval.approved',\n ),\n }\n }\n if (isDenied.value) {\n return {\n icon: 'ri:close-line',\n message: $t(\n isConfirm.value\n ? 'toolApproval.cancelled'\n : 'toolApproval.denied',\n ),\n }\n }\n return undefined\n })\n\n /**\n * Resolved and nothing to preview: all the card would frame is the\n * outcome line, so the frame goes too and only the line stays in the\n * transcript.\n */\n const isBare = computed(() => Boolean(outcome.value) && !preview.value)\n</script>\n\n<template>\n <!-- Plain block container, NOT flex-col: vv-button-group is inline-flex\n with justify-content center, and a stretched flex child would center\n the buttons instead of keeping them left like the original confirm\n card. -->\n <!-- transition-all eases the frame away (padding, border and background\n to zero) when the resolved card goes bare. -->\n <div\n class=\"w-full overflow-hidden transition-all duration-300\"\n :class=\"{\n 'bg-surface p-sm rounded-xl border border-surface-3': !isBare,\n }\">\n <!-- A confirm carries its own sentence from the model; an approval gets\n one of ours: it leads into the preview when there is one, and names\n the tool when there is not (see `prompt`). -->\n <!-- The ask reads wrong on a resolved request: once answered, the\n outcome line alone carries the whole story. -->\n <transition>\n <p\n v-if=\"!isConfirm && isOpen\"\n class=\"text-12 text-word-3 m-0 mb-xs\">\n {{ prompt }}\n </p>\n </transition>\n\n <component :is=\"preview\" v-if=\"preview\" :part :input=\"toolPart.input\" />\n\n <!-- Buttons only while the answer is still awaited (disabled while the\n confirm input streams in). Once there is an outcome they leave the\n card, so the transcript keeps a slim record instead of a dead\n control. -->\n <transition mode=\"out-in\">\n <VvButtonGroup v-if=\"!outcome\">\n <VvButton\n modifiers=\"primary-small\"\n :disabled=\"!isPending\"\n icon=\"ri:check-line\"\n :label=\"$t('action.proceed')\"\n @click.stop=\"emit('respond', true)\" />\n <VvButton\n modifiers=\"action-quiet-small\"\n :disabled=\"!isPending\"\n icon=\"ri:close-line\"\n :label=\"$t('action.cancel')\"\n @click.stop=\"emit('respond', false)\" />\n </VvButtonGroup>\n\n <!-- The stored denial reason is model-facing copy (and English):\n the card always shows the localized message instead. -->\n <p\n v-else\n class=\"flex items-center gap-4 text-12 text-word-3 m-0\"\n :class=\"{ 'mt-xs': preview }\">\n <VvIcon :name=\"outcome.icon\" class=\"shrink-0\" />\n {{ outcome.message }}\n </p>\n </transition>\n </div>\n</template>\n","<script setup lang=\"ts\">\n import { computed } from 'vue'\n import { useI18n } from 'vue-i18n'\n import { getToolPartName } from 'utils'\n import { isVoiceRenderAck } from 'models'\n import { CONFIRM_TOOL_NAME, toolLabel } from './utils'\n import { toolApprovalPreviews } from './toolApprovalPreviews'\n\n /**\n * The single confirmation card of the chat. It renders two things that are\n * the same question for the user:\n *\n * - **approval**: a tool call the backend suspended waiting for consent\n * (`approval-requested`), plus its resolved states so the decision stays\n * visible in the transcript;\n * - **confirm**: a `requestConfirm` call, the client-side tool the model uses\n * to have an action it described approved. Same layout, its own wording\n * (Proceed / Cancel, as on the messaging channels).\n *\n * The body comes from `toolApprovalPreviews[toolName]` when the tool has a\n * dedicated preview (localized, human copy: e.g. `sendEmail` will reuse the\n * email renderer); tools without one show only the localized prompt, never\n * a raw echo of the call input. Adding a preview never requires touching\n * this component.\n */\n const props = defineProps<{\n part: unknown\n /**\n * Whether the user can still answer. False in read-only views (chat\n * logs) and, for approvals, once the conversation has moved past the\n * request: the backend closes an abandoned approval as denied, so\n * answering it later would not run the tool.\n */\n actionable?: boolean\n /**\n * The verdict given ON A CALL, when there is one. A confirm answered\n * during a call travels back as a spoken message and its part stays\n * on the render ack, so the outcome only exists client-side: this is\n * what closes the card and names the outcome instead of leaving the\n * buttons alive (or worse, showing the answer as \"expired\").\n */\n callVerdict?: boolean\n }>()\n\n const emit = defineEmits<{\n respond: [approved: boolean]\n }>()\n\n const { t: $t, te: $te } = useI18n({ useScope: 'global' })\n\n const toolPart = computed(\n () =>\n props.part as {\n type?: string\n state?: string\n toolCallId?: string\n input?: unknown\n output?: unknown\n approval?: { id?: string; approved?: boolean; reason?: string }\n },\n )\n\n const toolName = computed(() =>\n getToolPartName(props.part as Parameters<typeof getToolPartName>[0]),\n )\n\n const isConfirm = computed(() => toolName.value === CONFIRM_TOOL_NAME)\n\n /**\n * True while the request still waits for an answer. A confirm drawn\n * during a CALL arrives already resolved with the note that it was drawn\n * (see `isVoiceRenderAck`, same rule as the forms): the note is not the\n * user's verdict, so the card keeps its buttons.\n */\n const isOpen = computed(() =>\n isConfirm.value\n ? (toolPart.value.state === 'input-available' ||\n isVoiceRenderAck(toolPart.value.output)) &&\n props.callVerdict === undefined\n : toolPart.value.state === 'approval-requested',\n )\n\n /** Open and answerable: the only case where the buttons act. */\n const isPending = computed(() => isOpen.value && props.actionable === true)\n\n /** Open but not answerable: shown as an expired request. */\n const isExpired = computed(() => isOpen.value && props.actionable !== true)\n\n const isApproved = computed(() =>\n isConfirm.value\n ? toolPart.value.output === 'confirmed' ||\n props.callVerdict === true\n : toolPart.value.approval?.approved === true,\n )\n\n const isDenied = computed(() =>\n isConfirm.value\n ? toolPart.value.output === 'cancelled' ||\n props.callVerdict === false\n : toolPart.value.approval?.approved === false ||\n toolPart.value.state === 'output-denied',\n )\n\n const preview = computed(() => toolApprovalPreviews[toolName.value])\n\n /**\n * The ask above the buttons. A preview shows what is about to happen and\n * the sentence leads into it; without one the sentence is all the user\n * reads, so it names the tool instead of ending on a colon with nothing\n * after it.\n */\n const prompt = computed(() =>\n preview.value\n ? $t('toolApproval.prompt')\n : $t('toolApproval.promptNamed', {\n tool: toolLabel(toolName.value, { t: $t, te: $te }),\n }),\n )\n\n /**\n * Once the request is answered (or expired) the card slims down: prompt\n * and buttons disappear and this single icon + text line tells the\n * outcome. A confirm has its own wording (the model described an action,\n * no tool ran on its behalf).\n */\n const outcome = computed(() => {\n if (isExpired.value) {\n return { icon: 'ri:time-line', message: $t('toolApproval.expired') }\n }\n if (isApproved.value) {\n return {\n icon: 'ri:check-line',\n message: $t(\n isConfirm.value\n ? 'toolApproval.confirmed'\n : 'toolApproval.approved',\n ),\n }\n }\n if (isDenied.value) {\n return {\n icon: 'ri:close-line',\n message: $t(\n isConfirm.value\n ? 'toolApproval.cancelled'\n : 'toolApproval.denied',\n ),\n }\n }\n return undefined\n })\n\n /**\n * Resolved and nothing to preview: all the card would frame is the\n * outcome line, so the frame goes too and only the line stays in the\n * transcript.\n */\n const isBare = computed(() => Boolean(outcome.value) && !preview.value)\n</script>\n\n<template>\n <!-- Plain block container, NOT flex-col: vv-button-group is inline-flex\n with justify-content center, and a stretched flex child would center\n the buttons instead of keeping them left like the original confirm\n card. -->\n <!-- transition-all eases the frame away (padding, border and background\n to zero) when the resolved card goes bare. -->\n <div\n class=\"w-full overflow-hidden transition-all duration-300\"\n :class=\"{\n 'bg-surface p-sm rounded-xl border border-surface-3': !isBare,\n }\">\n <!-- A confirm carries its own sentence from the model; an approval gets\n one of ours: it leads into the preview when there is one, and names\n the tool when there is not (see `prompt`). -->\n <!-- The ask reads wrong on a resolved request: once answered, the\n outcome line alone carries the whole story. -->\n <transition>\n <p\n v-if=\"!isConfirm && isOpen\"\n class=\"text-12 text-word-3 m-0 mb-xs\">\n {{ prompt }}\n </p>\n </transition>\n\n <component :is=\"preview\" v-if=\"preview\" :part :input=\"toolPart.input\" />\n\n <!-- Buttons only while the answer is still awaited (disabled while the\n confirm input streams in). Once there is an outcome they leave the\n card, so the transcript keeps a slim record instead of a dead\n control. -->\n <transition mode=\"out-in\">\n <VvButtonGroup v-if=\"!outcome\">\n <VvButton\n modifiers=\"primary-small\"\n :disabled=\"!isPending\"\n icon=\"ri:check-line\"\n :label=\"$t('action.proceed')\"\n @click.stop=\"emit('respond', true)\" />\n <VvButton\n modifiers=\"action-quiet-small\"\n :disabled=\"!isPending\"\n icon=\"ri:close-line\"\n :label=\"$t('action.cancel')\"\n @click.stop=\"emit('respond', false)\" />\n </VvButtonGroup>\n\n <!-- The stored denial reason is model-facing copy (and English):\n the card always shows the localized message instead. -->\n <p\n v-else\n class=\"flex items-center gap-4 text-12 text-word-3 m-0\"\n :class=\"{ 'mt-xs': preview }\">\n <VvIcon :name=\"outcome.icon\" class=\"shrink-0\" />\n {{ outcome.message }}\n </p>\n </transition>\n </div>\n</template>\n","import type { Ref, ShallowRef } from 'vue'\nimport { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'\n\nconst NEAR_BOTTOM_THRESHOLD = 30\nconst SMOOTH_SCROLL_DURATION = 500\n\ninterface UseChatScrollOptions {\n scrollEl:\n | Readonly<ShallowRef<HTMLDivElement | null>>\n | Ref<HTMLDivElement | undefined>\n status: () => string | undefined\n messagesLength: () => number | undefined\n lastMessageRole: () => string | undefined\n /**\n * Element whose height tracks the content height, watched for late\n * growth (lazy tool outputs). Defaults to the scroll element's first\n * child.\n */\n contentEl?: () => HTMLElement | null\n /**\n * Awaited right before the new-exchange scroll. Lets the host apply the\n * layout that changes the content height (the anchor spacer) first, so\n * the scroll targets the final bottom instead of an intermediate one.\n */\n beforeNewMessageScroll?: () => void | Promise<void>\n onScrollUp?: () => void\n onScrollDown?: () => void\n}\n\nexport function useChatScroll(options: UseChatScrollOptions) {\n const {\n scrollEl,\n status,\n messagesLength,\n lastMessageRole,\n contentEl,\n beforeNewMessageScroll,\n onScrollUp,\n onScrollDown,\n } = options\n\n const userScrolledUp = ref(false)\n const isAtBottom = ref(true)\n let isAutoScrolling = false\n let mutationObserver: MutationObserver | null = null\n let resizeObserver: ResizeObserver | null = null\n let initMutationObserver: MutationObserver | null = null\n let contentResizeObserver: ResizeObserver | null = null\n let lastScrollTop = 0\n\n // --- Core scroll ---\n\n const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {\n if (!scrollEl.value || userScrolledUp.value) {\n // Callers may lock the auto-scroll before awaiting layout: release\n // it here or the lock would mute the auto-follow forever\n isAutoScrolling = false\n return\n }\n isAutoScrolling = true\n scrollEl.value.scrollTo({\n top: scrollEl.value.scrollHeight,\n behavior,\n })\n const unlockDelay = behavior === 'instant' ? 0 : SMOOTH_SCROLL_DURATION\n setTimeout(() => {\n isAutoScrolling = false\n }, unlockDelay)\n }\n\n // --- User scroll detection ---\n // Intent to scroll away comes from input events (wheel/touch): unlike\n // scroll deltas they are never produced by programmatic scrolls, layout\n // shifts or elastic overscroll bounces, so the auto-scroll never fights\n // the user nor disables itself spuriously.\n\n const onWheel = (event: WheelEvent) => {\n if (event.deltaY < 0) {\n userScrolledUp.value = true\n }\n }\n\n let lastTouchY = 0\n const onTouchStart = (event: TouchEvent) => {\n lastTouchY = event.touches[0]?.clientY ?? 0\n }\n const onTouchMove = (event: TouchEvent) => {\n const y = event.touches[0]?.clientY ?? 0\n // Finger moving down drags the content up\n if (y > lastTouchY) {\n userScrolledUp.value = true\n }\n lastTouchY = y\n }\n\n const handleScroll = () => {\n if (!scrollEl.value) {\n return\n }\n const {\n scrollTop: currentScrollTop,\n scrollHeight,\n clientHeight,\n } = scrollEl.value\n isAtBottom.value =\n scrollHeight - clientHeight - currentScrollTop <\n NEAR_BOTTOM_THRESHOLD\n if (isAutoScrolling) {\n lastScrollTop = currentScrollTop\n return\n }\n\n if (currentScrollTop < lastScrollTop) {\n onScrollUp?.()\n }\n\n if (currentScrollTop > lastScrollTop) {\n // Back at the bottom: re-enable auto-scroll\n if (isAtBottom.value) {\n userScrolledUp.value = false\n }\n onScrollDown?.()\n }\n\n lastScrollTop = currentScrollTop\n }\n\n // --- Streaming auto-follow ---\n // Tracks the growing content towards the bottom, never above the\n // current position. While the answer fits the space reserved by the\n // last-message spacer in PkChatbotMessages the content bottom does not\n // move, so the freshly sent user message stays anchored at the top;\n // once the answer overflows, the follow keeps the tail in view until\n // the user scrolls away (wheel/touch sets `userScrolledUp`).\n const autoFollow = () => {\n if (isAutoScrolling || userScrolledUp.value || !scrollEl.value) {\n return\n }\n const el = scrollEl.value\n const bottom = el.scrollHeight - el.clientHeight\n if (bottom <= el.scrollTop) {\n return\n }\n isAtBottom.value = true\n isAutoScrolling = true\n el.scrollTop = bottom\n requestAnimationFrame(() => {\n isAutoScrolling = false\n })\n }\n\n const startMutationScroll = () => {\n if (mutationObserver || !scrollEl.value) {\n return\n }\n mutationObserver = new MutationObserver(autoFollow)\n mutationObserver.observe(scrollEl.value, {\n childList: true,\n subtree: true,\n characterData: true,\n })\n }\n\n const stopMutationScroll = () => {\n mutationObserver?.disconnect()\n mutationObserver = null\n }\n\n // --- Status watcher ---\n\n watch(status, (newStatus, oldStatus) => {\n if (newStatus === 'streaming' || newStatus === 'submitted') {\n startMutationScroll()\n return\n }\n if (newStatus === 'ready' && oldStatus === 'streaming') {\n // Keep observer active to catch footer buttons rendering\n return\n }\n stopMutationScroll()\n })\n\n // --- New message watcher ---\n\n watch(messagesLength, async () => {\n if (!messagesLength()) {\n return\n }\n if (lastMessageRole() === 'assistant') {\n return\n }\n // A user send ends the initial-load phase: on a chat whose content\n // first overflows while the answer streams in, the initial observers\n // would otherwise slam the scroll to the bottom past the anchor\n cleanupInitialObservers()\n userScrolledUp.value = false\n // Lock while the layout settles: the anchor spacer growth would\n // otherwise make the resize auto-follow jump before the real scroll\n isAutoScrolling = true\n try {\n await nextTick()\n // The spacer must be in place first: scrolling to a\n // `scrollHeight` that still misses it lands short of the anchor\n // and nothing brings the view back down (the resize auto-follow\n // is locked out).\n await beforeNewMessageScroll?.()\n await nextTick()\n } finally {\n // Always reaches scrollToBottom: a throw above would otherwise\n // leave the lock set and mute the auto-follow forever\n scrollToBottom()\n }\n })\n\n // --- Initial scroll ---\n\n let initialScrollTimer: ReturnType<typeof setTimeout> | null = null\n const INITIAL_SCROLL_SETTLE_MS = 300\n\n const tryInitialScroll = () => {\n if (!scrollEl.value) {\n return\n }\n if (scrollEl.value.scrollHeight <= scrollEl.value.clientHeight) {\n return\n }\n scrollEl.value.scrollTop = scrollEl.value.scrollHeight\n\n // Reset the settle timer on each mutation — disconnect only after stability\n if (initialScrollTimer) {\n clearTimeout(initialScrollTimer)\n }\n initialScrollTimer = setTimeout(\n cleanupInitialObservers,\n INITIAL_SCROLL_SETTLE_MS,\n )\n }\n\n const cleanupInitialObservers = () => {\n resizeObserver?.disconnect()\n resizeObserver = null\n initMutationObserver?.disconnect()\n initMutationObserver = null\n if (initialScrollTimer) {\n clearTimeout(initialScrollTimer)\n initialScrollTimer = null\n }\n }\n\n onMounted(() => {\n if (!scrollEl.value) {\n return\n }\n scrollEl.value.addEventListener('wheel', onWheel, { passive: true })\n scrollEl.value.addEventListener('touchstart', onTouchStart, {\n passive: true,\n })\n scrollEl.value.addEventListener('touchmove', onTouchMove, {\n passive: true,\n })\n\n // Mounted mid-send (e.g. fullscreen first message: the messages view\n // replaces the empty state after the message is already in) is not a\n // history load: skip the initial bottom-jump and follow the stream\n // from the anchored position instead.\n const isSendInFlight =\n status() === 'submitted' || status() === 'streaming'\n if (isSendInFlight) {\n startMutationScroll()\n } else {\n resizeObserver = new ResizeObserver(tryInitialScroll)\n resizeObserver.observe(scrollEl.value)\n\n initMutationObserver = new MutationObserver(tryInitialScroll)\n initMutationObserver.observe(scrollEl.value, {\n childList: true,\n subtree: true,\n })\n }\n\n // Late content growth: tool outputs render lazily (async chunks,\n // dynamic imports: diagrams, images...) and can change the content\n // height long after the initial scroll settled. The wrapper height\n // tracks the content height (the scroll element itself never\n // resizes with it).\n const wrapper = contentEl?.() ?? scrollEl.value.firstElementChild\n if (wrapper) {\n contentResizeObserver = new ResizeObserver(autoFollow)\n contentResizeObserver.observe(wrapper)\n }\n\n if (!isSendInFlight) {\n tryInitialScroll()\n }\n })\n\n onBeforeUnmount(() => {\n scrollEl.value?.removeEventListener('wheel', onWheel)\n scrollEl.value?.removeEventListener('touchstart', onTouchStart)\n scrollEl.value?.removeEventListener('touchmove', onTouchMove)\n contentResizeObserver?.disconnect()\n contentResizeObserver = null\n stopMutationScroll()\n cleanupInitialObservers()\n })\n\n return {\n handleScroll,\n scrollToBottom,\n userScrolledUp,\n isAtBottom,\n }\n}\n","import { isQuietWakeupAnswer, isWakeupSeed, type UIChatMessage } from 'models'\n\nexport interface WakeupQuietRun {\n /** Checks in the run, each a seed and its quiet answer. */\n count: number\n /** When the last check of the run started, from its seed. */\n lastAt?: number\n}\n\nexport interface WakeupFeed {\n /** Messages the feed does not draw as bubbles. */\n hidden: Set<string>\n /** By the id of the first seed of a run of quiet checks: the line drawn in its place. */\n quietRuns: Map<string, WakeupQuietRun>\n}\n\n/**\n * How a chat with wakeups reads (spec agent-wakeup, \"Interfaccia\"): a run of\n * checks that found nothing is one line (\"nothing new in 5 checks, the last\n * at 14:20\") instead of five prompts the user never wrote and five empty\n * answers, and a quiet answer of the user's own turn draws nothing either.\n * A seed whose answer has news stays, drawn as a thin line by the feed.\n *\n * `draws` is the view's own test of whether a message shows anything (a\n * widget, an approval card): such an answer is never folded, whatever its\n * `scheduleWakeup` said.\n */\nexport function buildWakeupFeed(\n messages: UIChatMessage[],\n draws: (message: UIChatMessage) => boolean = () => false,\n): WakeupFeed {\n const isQuiet = (message?: UIChatMessage) =>\n isQuietWakeupAnswer(message) && !draws(message as UIChatMessage)\n const hidden = new Set<string>()\n const quietRuns = new Map<string, WakeupQuietRun>()\n let index = 0\n while (index < messages.length) {\n const message = messages[index]\n if (isWakeupSeed(message) && isQuiet(messages[index + 1])) {\n const start = message.id\n let count = 0\n let lastAt: number | undefined\n while (\n isWakeupSeed(messages[index]) &&\n isQuiet(messages[index + 1])\n ) {\n count += 1\n lastAt = messages[index].metadata?.createdAt ?? lastAt\n hidden.add(messages[index].id)\n hidden.add(messages[index + 1].id)\n index += 2\n }\n quietRuns.set(start, { count, lastAt })\n continue\n }\n if (isQuiet(message)) {\n hidden.add(message.id)\n }\n index += 1\n }\n return { hidden, quietRuns }\n}\n","<script lang=\"ts\" setup>\n import type {\n ChatMessageActions,\n MessageFeedback,\n RevisedAnswer,\n UIChatMessage,\n } from 'models'\n import { isAudioFile, isWakeupSeed } from 'models'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkChatbotError from './PkChatbotError.vue'\n import PkChatbotFeedbackForm from './PkChatbotFeedbackForm.vue'\n import PkChatbotMessageFeedback from './PkChatbotMessageFeedback.vue'\n import PkChatbotFilePreview from './PkChatbotFilePreview.vue'\n import PkChatbotAudioTranscript from './PkChatbotAudioTranscript.vue'\n import PkRelativeTime from '../PkRelativeTime.vue'\n import { useI18n } from 'vue-i18n'\n import {\n useTemplateRef,\n ref,\n shallowRef,\n watch,\n computed,\n nextTick,\n onMounted,\n useSlots,\n } from 'vue'\n import { useResizeObserver } from '@vueuse/core'\n import { toolComponentMap } from './toolComponentMap'\n import PkToolApprovalCard from './PkToolApprovalCard.vue'\n import PkChatbotSteps from './PkChatbotSteps.vue'\n import {\n resolveContrastColor,\n getPartState,\n isTextPart,\n isFilePart,\n isToolPart,\n isReasoningPart,\n isStreamingPart,\n isToolConfirmPart,\n isConfirmationCardPart,\n getToolApprovalId,\n getPartIcon,\n getPartTypeLabel,\n getToolPartLabel,\n mergeConsecutiveTextParts,\n dropCorrectedWidgetErrors,\n moveFileOutputsLast,\n feedPartKey,\n formatDuration,\n LIVE_REASONING_TAIL_LENGTH,\n } from './utils'\n import {\n getFilePartMedia,\n getFilePartTranscript,\n getToolPartName,\n } from 'utils'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotOutlineRail from './PkChatbotOutlineRail.vue'\n import { useChatScroll } from './useChatScroll'\n import { buildWakeupFeed } from './wakeupFeed'\n\n const props = defineProps<{\n status?: 'submitted' | 'streaming' | 'ready' | 'error'\n messages?: UIChatMessage[]\n error?: Error\n actions?: ChatMessageActions[]\n mainColor?: string\n textColor?: 'auto' | 'white' | 'black'\n revisedAnswers?: RevisedAnswer[]\n messageFeedbacks?: MessageFeedback[]\n /**\n * Render the vote/feedback action buttons as read-only state indicators\n * (e.g. admin chat-logs view): icons reflect the user's feedback but the\n * buttons are not interactive.\n */\n readonlyFeedback?: boolean\n disableHeightAdjustment?: boolean\n showMessageDateTime?: boolean\n showMessageTokensCount?: boolean\n /**\n * Shows each answer's position in the chat, from 1, the numbering the\n * conversation analysis cites (\"message 62\").\n */\n showMessageNumber?: boolean\n showAllMessageParts?: boolean\n /**\n * Lets the user answer the tool requests rendered in the feed (approval\n * and confirm cards). Off by default: the log viewers render the same\n * messages read-only and have nothing to answer with.\n */\n interactive?: boolean\n showExtendedSteps?: boolean\n /**\n * A call is waiting for its answer. The turn runs outside this client\n * (the voice provider drives it), so `status` says nothing about it and\n * the feed would sit still for the seconds it takes.\n */\n awaitingSpokenAnswer?: boolean\n /**\n * Verdicts given ON A CALL, by toolCallId. A confirm answered during a\n * call goes back to the agent as a spoken message, so its part never\n * leaves the render-ack state: without this the card kept its buttons\n * after the answer, and every extra tap said \"Confermo\" again\n * (measured 2026-08-07). Only successful sends are recorded, so a tap\n * that never reached the provider leaves the card usable.\n */\n confirmVerdictsOnCall?: Map<string, boolean>\n isDark?: boolean\n /** Show a floating \"back to bottom\" button when the user scrolls up (fullscreen layout) */\n showScrollToBottom?: boolean\n // TODO: move feedback in a separate component to avoid passing these props\n feedbackMessageId?: string\n feedbackLoading?: boolean\n feedbackSubmitted?: boolean\n feedbackError?: string\n }>()\n\n const emit = defineEmits<{\n (e: 'show-info', message: UIChatMessage): void\n (e: 'regenerate'): void\n (e: 'revise', message: UIChatMessage): void\n (e: 'upvote', message: UIChatMessage): void\n (e: 'downvote', message: UIChatMessage): void\n (e: 'feedback', message: UIChatMessage): void\n (e: 'feedback-submit', comment: string): void\n (e: 'feedback-close'): void\n (e: 'scroll-up'): void\n (e: 'scroll-down'): void\n (e: 'auto-retry'): void\n (e: 'reset-chat'): void\n (\n e: 'approval-respond',\n payload: { id: string; approved: boolean },\n ): void\n (\n e: 'confirm-respond',\n payload: { toolCallId: string; confirmed: boolean },\n ): void\n }>()\n\n const {\n t: $t,\n te: $te,\n d: $d,\n n: $n,\n } = useI18n({\n useScope: 'global',\n })\n\n const slots = useSlots()\n\n /**\n * Forwards the user's verdict from the confirmation card. The two kinds of\n * request travel back on different channels: an approval answers the SDK\n * approval it carries the id of, a `requestConfirm` resolves as a client\n * tool output. Without the id there is nothing to answer, so the click is\n * dropped instead of sending an unmatchable response.\n */\n const onCardRespond = (\n part: UIChatMessage['parts'][number],\n approved: boolean,\n ) => {\n if (isToolConfirmPart(part)) {\n const toolCallId = (part as { toolCallId?: string }).toolCallId\n if (toolCallId) {\n emit('confirm-respond', { toolCallId, confirmed: approved })\n }\n return\n }\n const id = getToolApprovalId(part)\n if (!id) {\n return\n }\n emit('approval-respond', { id, approved })\n }\n\n const scrollEl = useTemplateRef<HTMLDivElement>('scrollEl')\n const wrapperEl = useTemplateRef<HTMLDivElement>('wrapperEl')\n const contrastColor = computed(() =>\n resolveContrastColor(props.textColor, props.mainColor),\n )\n\n // --- Scroll ---\n\n const { handleScroll, scrollToBottom, userScrolledUp, isAtBottom } =\n useChatScroll({\n scrollEl,\n status: () => props.status,\n messagesLength: () => props.messages?.length,\n lastMessageRole: () =>\n props.messages?.[props.messages.length - 1]?.role,\n contentEl: () => wrapperEl.value,\n beforeNewMessageScroll: () => updateAnchorSpacer(),\n onScrollUp: () => emit('scroll-up'),\n onScrollDown: () => emit('scroll-down'),\n })\n\n const onScrollToBottomClick = () => {\n // scrollToBottom() is a no-op while userScrolledUp is set\n userScrolledUp.value = false\n scrollToBottom()\n }\n\n // --- Conversation outline (fullscreen, desktop) ---\n\n // A wakeup's seed is not a question the user asked, and it is not drawn\n // as a user bubble either, which the 1:1 mapping below relies on.\n const outlineItems = computed(() =>\n (props.messages ?? [])\n .filter(\n (message) => message.role === 'user' && !isWakeupSeed(message),\n )\n .map((message) => ({\n id: message.id,\n preview:\n message.parts.find(isTextPart)?.text.trim().slice(0, 120) ||\n '…',\n })),\n )\n const activeOutlineId = ref<string>()\n\n // The rail entries map 1:1 onto the rendered user messages\n const getUserMessageEls = () =>\n scrollEl.value?.querySelectorAll<HTMLElement>(\n '.pk-chatbot-message--user',\n ) ?? []\n\n // Scroll-spy: active is the last user message above the middle of the\n // viewport (the exchange currently being read)\n const updateActiveOutline = () => {\n const scroller = scrollEl.value\n if (!scroller || !props.showScrollToBottom) {\n return\n }\n const line =\n scroller.getBoundingClientRect().top + scroller.clientHeight / 2\n let active = outlineItems.value[0]?.id\n getUserMessageEls().forEach((el, index) => {\n if (el.getBoundingClientRect().top <= line) {\n active = outlineItems.value[index]?.id\n }\n })\n activeOutlineId.value = active\n }\n\n watch(outlineItems, async () => {\n await nextTick()\n updateActiveOutline()\n })\n\n const onScroll = () => {\n handleScroll()\n updateActiveOutline()\n }\n\n const scrollToUserMessage = (id: string) => {\n const scroller = scrollEl.value\n const index = outlineItems.value.findIndex((item) => item.id === id)\n const messageEl = getUserMessageEls()[index]\n if (!scroller || !messageEl) {\n return\n }\n // Intentional jump away from the live tail: the wheel/touch intent\n // detection cannot see programmatic scrolls, pause the auto-follow\n // explicitly (reaching the bottom again re-enables it)\n userScrolledUp.value = true\n // Same resting line as a freshly sent message: top of the content area\n scroller.scrollTo({\n top:\n scroller.scrollTop +\n messageEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top -\n Number.parseFloat(getComputedStyle(scroller).paddingTop),\n behavior: 'smooth',\n })\n }\n\n // The answer is still being worked on: the spacer below holds while it is,\n // and so do the timeline and the activity of the last message.\n const isLoading = computed(() => {\n return props.status === 'submitted' || props.status === 'streaming'\n })\n\n // --- Height adjustment ---\n\n const height = ref<number>(0)\n // The min-height transition only serves the release (see below): while the\n // spacer grows it would hide the new height from the anchor scroll, which\n // reads `scrollHeight` once and would land short of the anchor.\n const isSpacerSettling = ref(false)\n\n // The spacer serves the exchange in flight from its first measure to its\n // release, at zero too (a question taller than the area, a measure\n // skipped while hidden): the height alone cannot tell held from released.\n let isSpacerHeld = false\n // The last user message the spacer was measured against, observed below.\n const anchoredUserMessageEl = shallowRef<HTMLElement>()\n\n // Recalculate when a message is added or the conversation changes, and\n // when one of the two sizes it is computed from changes (see the resize\n // observer below). Idempotent: also called by the scroll composable right\n // before the anchor scroll, so the spacer is always in place first.\n const updateAnchorSpacer = async () => {\n const messages = props.messages\n if (!messages?.length || props.disableHeightAdjustment) {\n return\n }\n // The spacer only serves the in-flight exchange, letting the\n // fresh user message anchor at the top while the answer streams.\n // On plain loads (e.g. opening an old conversation) it would\n // just leave a large void after the last message.\n const isExchangeInFlight =\n messages[messages.length - 1].role === 'user' || isLoading.value\n if (!isExchangeInFlight) {\n return\n }\n isSpacerHeld = true\n isSpacerSettling.value = false\n await nextTick()\n if (!scrollEl.value) {\n return\n }\n // Spacer sized so the freshly sent user message rests at the top\n // of the content area once scrolled to bottom: subtract both\n // paddings (clientHeight includes them) so any extra top\n // clearance (e.g. the fullscreen overlay header) is respected,\n // and the wrapper row gap separating the message from the spacer\n const { paddingTop, paddingBottom } = getComputedStyle(scrollEl.value)\n const scrollElHeight =\n scrollEl.value.clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n const wrapperGap = wrapperEl.value\n ? Number.parseFloat(getComputedStyle(wrapperEl.value).rowGap) || 0\n : 0\n // Measured on the rendered DOM (same lookup as the outline rail):\n // indexing the template-refs array with the `messages` index breaks\n // as soon as a message renders nothing (`v-if` on empty parts), the\n // refs array skips it and the heights shift by one.\n const userMessageEls = getUserMessageEls()\n anchoredUserMessageEl.value = userMessageEls[userMessageEls.length - 1]\n const lastUserMessageHeight =\n anchoredUserMessageEl.value?.clientHeight ?? 0\n const newHeight = scrollElHeight - lastUserMessageHeight - wrapperGap\n // A question taller than the content area leaves nothing to reserve:\n // the spacer goes to zero, never back to the value of the previous\n // exchange. A stale, larger spacer is the very failure the anchor\n // exists to prevent, the scroll landing past the message. Skipped\n // when the element measures nothing (rendered while hidden): there\n // the height already in place is still the good one.\n if (scrollElHeight > 0) {\n height.value = Math.max(newHeight, 0)\n }\n await nextTick()\n }\n\n // Two sources compared one by one: the store recreates `messages` on\n // every token, and a getter returning `[length, id]` would hand back a\n // new array each time and run this at every token. An answer that resumes\n // after a pause (an approval, a form, a tool the client runs) keeps its\n // length and its id, so a spacer released at the pause stays released:\n // bringing it back on the next token threw the view down, and the next\n // pause threw it up again.\n watch(\n [\n () => props.messages?.length,\n () => props.messages?.[props.messages.length - 1]?.id,\n ],\n () => updateAnchorSpacer(),\n )\n\n // The two sizes the spacer is computed from can change while it is held:\n // the question (the transcript of an audio, a box the user opens) and the\n // chat itself (a bar docking above the input, a phone keyboard closing).\n // Followed by observing them rather than by measuring at every token.\n // A released spacer is left alone: an answer resumed after a pause must\n // not bring it back.\n useResizeObserver([scrollEl, anchoredUserMessageEl], () => {\n if (isSpacerHeld) {\n updateAnchorSpacer()\n }\n })\n\n // Mounted mid-send (fullscreen first message: the messages view replaces\n // the empty state after the message is already in): the length watch\n // never fires for that message, apply the anchor spacer now. A first\n // spoken line mounts the same way and never moves `status`, hence the\n // second guard. Plain history loads (mounted at 'ready', nothing in\n // flight) stay untouched.\n onMounted(() => {\n if (isLoading.value || props.awaitingSpokenAnswer) {\n updateAnchorSpacer()\n }\n })\n\n // Release the spacer once the answer stops loading: it would otherwise\n // leave a large empty area after the last message. The min-height\n // transition settles the layout smoothly (the browser clamps the\n // scroll progressively when the viewport sat inside the removed area).\n // Keyed on `isLoading`, not on `status`: whatever keeps the answer\n // loading keeps the spacer too.\n watch(isLoading, (loading) => {\n if (!loading) {\n isSpacerHeld = false\n isSpacerSettling.value = true\n height.value = 0\n }\n })\n\n const isRevised = (messageId: string) => {\n return props.revisedAnswers?.some((r) => r.messageId === messageId)\n }\n\n const getMessageFeedback = (messageId: string) =>\n props.messageFeedbacks?.find((f) => f.messageId === messageId)\n\n // A form the visitor fills in live is, in a feed nobody answers from\n // (the chat logs), the record of what the agent showed: no fields to\n // type in, no button that looks like it sends.\n const RECORD_TOOLS = new Set(['showContactForm'])\n const recordToolProps = (part: Parameters<typeof getToolPartName>[0]) =>\n !props.interactive && RECORD_TOOLS.has(getToolPartName(part))\n ? { record: true }\n : {}\n\n // Interactive vote/feedback buttons. In read-only mode (admin chat-logs)\n // the vote is shown on the message itself (coloured bubble border + an\n // \"active\" thumb in the footer) and the comment, if any, below it.\n const isFeedbackButtonVisible = (\n kind: 'upvote' | 'downvote' | 'feedback',\n index: number,\n ) => !props.readonlyFeedback && isActionButtonVisible(kind, index)\n\n // Read-only feedback for a message (drives the reaction chip anchored to\n // the bubble). undefined when not in read-only mode.\n const readonlyFeedbackOf = (messageId: string) =>\n props.readonlyFeedback ? getMessageFeedback(messageId) : undefined\n const feedbackVote = (messageId: string) =>\n readonlyFeedbackOf(messageId)?.vote\n // The real reaction glyph, shown ONLY for unmapped reactions (no vote):\n // 👍/❤️/👎 keep the classic thumb icon, while e.g. WhatsApp \"😍\" shows the\n // actual emoji. Platforms that send a type word (Teams \"like\") have a vote\n // and therefore use the icon too.\n const feedbackEmoji = (messageId: string) => {\n const feedback = readonlyFeedbackOf(messageId)\n if (feedback?.vote) {\n return undefined\n }\n const raw = feedback?.metadata?.reactionEmoji\n return typeof raw === 'string' && /\\p{Extended_Pictographic}/u.test(raw)\n ? raw\n : undefined\n }\n const hasReaction = (messageId: string) =>\n Boolean(feedbackVote(messageId) || feedbackEmoji(messageId))\n const reactionToneClass = (messageId: string) => {\n // A real emoji carries its own colour → neutral pill. Only the\n // thumb-icon fallback uses the sentiment colour.\n if (feedbackEmoji(messageId)) {\n return 'pk-chatbot-message__reaction--neutral'\n }\n const vote = feedbackVote(messageId)\n if (vote === 'upvote') {\n return 'pk-chatbot-message__reaction--positive'\n }\n if (vote === 'downvote') {\n return 'pk-chatbot-message__reaction--negative'\n }\n return 'pk-chatbot-message__reaction--neutral'\n }\n const reactionTitle = (messageId: string) => {\n const vote = feedbackVote(messageId)\n if (vote === 'upvote') {\n return $t('hint.userFeedbackPositive')\n }\n if (vote === 'downvote') {\n return $t('hint.userFeedbackNegative')\n }\n return $t('hint.userFeedbackReaction')\n }\n\n // Index of the last text part: the reaction chip anchors to that bubble so\n // it renders once even when the message has several text parts.\n const lastTextPartIndex = (message: UIChatMessage) => {\n const parts = getMergedParts(message)\n for (let index = parts.length - 1; index >= 0; index -= 1) {\n if (isTextPart(parts[index])) {\n return index\n }\n }\n return -1\n }\n\n const activeMessage = computed(() => {\n return props.messages?.[props.messages.length - 1]\n })\n const activeMessageLastPart = computed(() => {\n const active = activeMessage.value\n if (!active) {\n return null\n }\n return active.parts[active.parts.length - 1]\n })\n\n const mergedPartsMap = computed(() => {\n const map = new Map<string, UIChatMessage['parts']>()\n for (const message of props.messages ?? []) {\n if (message.role === 'assistant') {\n map.set(\n message.id,\n moveFileOutputsLast(\n dropCorrectedWidgetErrors(\n mergeConsecutiveTextParts(message.parts),\n ),\n ),\n )\n }\n }\n return map\n })\n const getMergedParts = (message: UIChatMessage) => {\n return mergedPartsMap.value.get(message.id) ?? message.parts\n }\n const isError = computed(() => props.status === 'error')\n\n /**\n * The spoken wait shows only while nothing else is working: the wait\n * survives the hang-up (the answer can be written minutes later) and the\n * caller can type meanwhile, and two working indicators stacked would say\n * the same thing twice. It comes back when the written turn is over.\n */\n const isSpokenWaitVisible = computed(\n () => props.awaitingSpokenAnswer === true && !isLoading.value,\n )\n\n /**\n * The wait indicator closes the feed, so it carries the anchor spacer\n * itself, the way the streaming assistant message does. Rendering the\n * standalone spacer as well would stack the two and push the question\n * above the top of the content area, out of view.\n *\n * Only when it is the last thing in the feed: an error card, or an\n * assistant answer already landed, keeps the spacer where it was.\n */\n const isSpokenWaitAnchored = computed(\n () =>\n isSpokenWaitVisible.value &&\n activeMessage.value?.role === 'user' &&\n !isError.value,\n )\n\n const activeMessageLastPartLabel = computed(() => {\n const part = activeMessageLastPart.value\n return getToolPartLabel($t, part)\n })\n const activeMessageLastPartIcon = computed(() => {\n const part = activeMessageLastPart.value\n return getPartIcon(part)\n })\n\n const getMessageIndexById = (messageId: string) => {\n return props.messages?.findIndex((m) => m.id === messageId)\n }\n const isLastMessage = (index?: number) => {\n if (index === undefined || !props.messages) {\n return false\n }\n return index === props.messages.length - 1\n }\n const isMessageRegenerateButtonVisible = (index: number) => {\n return (\n isLastMessage(index) &&\n props.status === 'ready' &&\n !!props.actions?.includes('regenerate')\n )\n }\n // On the last message the actions appear only once the answer is ready;\n // on previous messages they are always available.\n const isActionButtonVisible = (\n action: ChatMessageActions,\n index: number,\n ) => {\n return (\n (!isLastMessage(index) || props.status === 'ready') &&\n !!props.actions?.includes(action)\n )\n }\n const isLastTextPart = (message: UIChatMessage) => {\n const lastPart = message.parts[message.parts.length - 1]\n return isTextPart(lastPart)\n }\n const messageHasSteps = (message: UIChatMessage) =>\n message.parts.some((part) => isReasoningPart(part) || isToolPart(part))\n // The extended steps timeline sits above the answer as soon as the\n // message has at least one step to show.\n const showStepsTimeline = (message: UIChatMessage) =>\n Boolean(props.showExtendedSteps) &&\n isAssistant(message) &&\n messageHasSteps(message)\n // A tool part the feed draws itself: its own component, a slot filled by\n // the host, or the debug chip of `showAllMessageParts`.\n const isRenderedToolPart = (part: UIChatMessage['parts'][number]) =>\n isToolPart(part) &&\n !isStreamingPart(part) &&\n Boolean(\n toolComponentMap[getToolPartName(part)] ||\n slots[part.type] ||\n props.showAllMessageParts,\n )\n // Whether anything is drawn below the steps timeline yet, by the same\n // branches the template walks for each part. Until then the timeline is\n // the bottom of the message and carries the activity itself; from then\n // on it collapses and the compact indicator takes over at the bottom.\n const messageHasContent = (message: UIChatMessage) =>\n message.parts.some(\n (part) =>\n (isTextPart(part) && Boolean(part.text.trim())) ||\n isFilePart(part) ||\n (isConfirmationCardPart(part) && !isStreamingPart(part)) ||\n isRenderedToolPart(part),\n )\n const isAssistant = (message: UIChatMessage) => message.role === 'assistant'\n const isUser = (message: UIChatMessage) => message.role === 'user'\n const getPreviousAssistantMessage = (index: number) => {\n return props.messages\n ?.slice(0, index)\n .reverse()\n .find((message) => isAssistant(message))\n }\n const getNextAssistantMessage = (index: number) => {\n return props.messages\n ?.slice(index + 1)\n .find((message) => isAssistant(message))\n }\n // Returns the date to show in the day divider before the message at\n // `index`, or undefined when the surrounding assistant messages belong\n // to the same (local) day.\n const getDividerDate = (index: number) => {\n const previousCreatedAt =\n getPreviousAssistantMessage(index)?.metadata?.createdAt\n const nextCreatedAt =\n getNextAssistantMessage(index)?.metadata?.createdAt\n if (!previousCreatedAt || !nextCreatedAt) {\n return undefined\n }\n const previousDate = new Date(previousCreatedAt)\n const nextDate = new Date(nextCreatedAt)\n return previousDate.toDateString() !== nextDate.toDateString()\n ? nextDate\n : undefined\n }\n // Wakeups (spec agent-wakeup): the checks that found nothing read as one\n // line and a seed as a thin row, never as words the user wrote. An answer\n // that draws anything (a widget, an approval card) is never folded. The\n // log pages, which draw every part, keep them all.\n const wakeupFeed = computed(() =>\n props.showAllMessageParts\n ? buildWakeupFeed([])\n : buildWakeupFeed(props.messages ?? [], messageHasContent),\n )\n // The numbering the conversation analysis cites: a message with no parts\n // is not rendered and not counted there either.\n const messageNumbers = computed(() => {\n let count = 0\n return (props.messages ?? []).map((message) =>\n message.parts.length > 0 ? ++count : undefined,\n )\n })\n const showMessageFooter = (message: UIChatMessage, index: number) => {\n if (index === 0 || message.role !== 'assistant') {\n return false\n }\n return Boolean(\n props.actions?.length ||\n props.showMessageNumber ||\n (props.showMessageDateTime &&\n message.metadata?.createdAt !== undefined) ||\n message.metadata?.completedAt !== undefined ||\n (props.showMessageTokensCount &&\n message.metadata?.totalTokens !== undefined),\n )\n }\n // Hover-only metadata (time + duration), shown when the fixed variant\n // (showMessageDateTime) is off.\n const formatMessageTime = (createdAt: number) => {\n const date = new Date(createdAt)\n const isToday = date.toDateString() === new Date().toDateString()\n return $d(date, isToday ? 'time' : 'date-time')\n }\n const getMessageDuration = (message: UIChatMessage) => {\n const { createdAt, completedAt } = message.metadata ?? {}\n if (!createdAt || !completedAt) {\n return undefined\n }\n return formatDuration($n, completedAt - createdAt)\n }\n // feedback message auto scroll\n const feedbackMessageEl =\n useTemplateRef<InstanceType<typeof PkChatbotFeedbackForm>[]>(\n 'feedbackMessageEl',\n )\n watch(\n () => props.feedbackMessageId,\n async () => {\n await nextTick()\n if (!props.feedbackMessageId) {\n return\n }\n const el = feedbackMessageEl.value?.[0]?.$el\n if (!el) {\n return\n }\n if (isLastMessage(getMessageIndexById(props.feedbackMessageId))) {\n scrollToBottom()\n return\n }\n el.scrollIntoView({ behavior: 'smooth', block: 'center' })\n },\n )\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n class=\"pk-chatbot-messages scrollbar-thin\"\n :class=\"{ 'pk-chatbot-messages--settling': isSpacerSettling }\"\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n :aria-busy=\"isLoading\"\n :style=\"{\n '--chatbot-main-color': mainColor,\n '--chatbot-contrast-color': contrastColor,\n }\"\n @scroll=\"onScroll\">\n <!-- First in flow (content top): its absolutely-positioned children\n must never inflate the scrollable overflow past the real content -->\n <div\n v-if=\"showScrollToBottom && outlineItems.length > 1\"\n class=\"pk-chatbot-messages__outline\">\n <PkChatbotOutlineRail\n :items=\"outlineItems\"\n :active-id=\"activeOutlineId\"\n @select=\"scrollToUserMessage\" />\n </div>\n <div ref=\"wrapperEl\" class=\"pk-chatbot-messages__wrapper\">\n <template v-for=\"(message, index) in messages\" :key=\"message.id\">\n <!-- The pk-chatbot-divider classes carry no rule of ours: they\n stay as hooks for the custom CSS of an agent. -->\n <div\n v-if=\"isUser(message) && getDividerDate(index)\"\n class=\"pk-chatbot-divider vv-separator\">\n <span class=\"pk-chatbot-divider__label\">\n {{ $d(getDividerDate(index)!, 'short') }}\n </span>\n </div>\n <div\n v-if=\"wakeupFeed.quietRuns.has(message.id)\"\n class=\"pk-chatbot-wakeup\">\n <VvIcon name=\"ri:alarm-line\" class=\"shrink-0\" />\n <span>\n {{\n $t(\n 'label.wakeupQuietRun',\n {\n count: wakeupFeed.quietRuns.get(message.id)!\n .count,\n time: $d(\n wakeupFeed.quietRuns.get(message.id)!\n .lastAt ?? Date.now(),\n 'time',\n ),\n },\n wakeupFeed.quietRuns.get(message.id)!.count,\n )\n }}\n </span>\n </div>\n <div\n v-else-if=\"\n isWakeupSeed(message) &&\n !wakeupFeed.hidden.has(message.id) &&\n !showAllMessageParts\n \"\n class=\"pk-chatbot-wakeup\"\n :title=\"message.metadata?.wakeup?.reason\">\n <VvIcon name=\"ri:alarm-line\" class=\"shrink-0\" />\n <span>\n {{ $t('label.wakeupCheck') }}\n <template v-if=\"message.metadata?.createdAt\">\n · {{ $d(message.metadata.createdAt, 'time') }}\n </template>\n </span>\n </div>\n <div\n v-if=\"\n !(isWakeupSeed(message) && !showAllMessageParts) &&\n !(\n wakeupFeed.hidden.has(message.id) &&\n !(isLoading && isLastMessage(index))\n ) &&\n (message.parts.length ||\n (isLastMessage(index) &&\n isLoading &&\n isAssistant(message)))\n \"\n class=\"pk-chatbot-message\"\n :class=\"[\n `pk-chatbot-message--${message.role}`,\n {\n 'pk-chatbot-message--loading':\n isLoading &&\n isLastMessage(index) &&\n isAssistant(message),\n },\n ]\"\n :style=\"{\n minHeight:\n isLastMessage(index) &&\n isAssistant(message) &&\n !isError\n ? `${height}px`\n : undefined,\n }\">\n <transition appear name=\"pk-chatbot-part\">\n <PkChatbotSteps\n v-if=\"showStepsTimeline(message)\"\n :message\n :is-streaming=\"isLoading && isLastMessage(index)\"\n :has-content=\"messageHasContent(message)\"\n :is-dark />\n </transition>\n <template\n v-for=\"(part, partIndex) in getMergedParts(message)\"\n :key=\"feedPartKey(part, partIndex)\">\n <transition\n v-if=\"isTextPart(part) && part.text.trim()\"\n appear\n name=\"pk-chatbot-part\">\n <div class=\"pk-chatbot-message__text\">\n <slot\n v-bind=\"{\n message,\n part,\n index,\n isLoading,\n }\"\n name=\"text\">\n <PkStreamingMarkdown\n v-if=\"isAssistant(message)\"\n class=\"wysiwyg\"\n :markdown=\"part.text\"\n :is-dark\n :loading=\"\n isLastMessage(index) &&\n status === 'streaming'\n \" />\n <template v-else>\n {{ part.text }}\n </template>\n </slot>\n <!-- Read-only reaction chip overlapping the\n bubble corner, like a messaging app. Hidden\n when a comment is shown: the callout below\n then carries the reaction glyph instead. -->\n <span\n v-if=\"\n hasReaction(message.id) &&\n !getMessageFeedback(message.id)\n ?.comment &&\n partIndex === lastTextPartIndex(message)\n \"\n class=\"pk-chatbot-message__reaction\"\n :class=\"reactionToneClass(message.id)\"\n :title=\"reactionTitle(message.id)\">\n <span\n v-if=\"feedbackEmoji(message.id)\"\n class=\"pk-chatbot-message__reaction-emoji\">\n {{ feedbackEmoji(message.id) }}\n </span>\n <VvIcon\n v-else\n :name=\"\n feedbackVote(message.id) ===\n 'downvote'\n ? 'ri:thumb-down-fill'\n : 'ri:thumb-up-fill'\n \" />\n </span>\n </div>\n </transition>\n <transition\n v-else-if=\"isFilePart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <div\n v-if=\"\n isAudioFile(part.filename, part.mediaType)\n \"\n class=\"flex flex-col items-end gap-8 w-full\">\n <PkChatbotFilePreview\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\"\n :size=\"\n getFilePartMedia(part)?.bytesCount\n \" />\n <!-- Heard once when it arrives: until the\n transcript comes back the turn waits on it. -->\n <PkChatbotAudioTranscript\n :url=\"part.url\"\n :transcript=\"getFilePartTranscript(part)\"\n :pending=\"\n status === 'submitted' &&\n isLastMessage(index)\n \" />\n </div>\n <PkChatbotFilePreview\n v-else\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\"\n :size=\"getFilePartMedia(part)?.bytesCount\" />\n </transition>\n <!-- Anything asking the user for consent (a suspended\n tool call or a `requestConfirm`) shares one card,\n ahead of the per-tool components. An approval stays\n answerable only in the trailing message: the backend\n closes the abandoned ones as denied, while a\n `requestConfirm` can still be resolved later. -->\n <transition\n v-else-if=\"\n isConfirmationCardPart(part) &&\n !isStreamingPart(part)\n \"\n appear\n name=\"pk-chatbot-part\">\n <!-- Key WITHOUT the part state: the card must stay\n mounted across the approval lifecycle so its\n inner transitions play once; remounting per\n state replays the appear animation on every\n hop (requested → responded → output). -->\n <PkToolApprovalCard\n :key=\"`approval-${partIndex}`\"\n :part\n :actionable=\"\n interactive === true &&\n (isToolConfirmPart(part) ||\n isLastMessage(index))\n \"\n :call-verdict=\"\n confirmVerdictsOnCall?.get(\n (part as { toolCallId?: string })\n .toolCallId ?? '',\n )\n \"\n @respond=\"onCardRespond(part, $event)\" />\n </transition>\n <transition\n v-else-if=\"isRenderedToolPart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <component\n :is=\"toolComponentMap[getToolPartName(part)]\"\n v-if=\"\n !slots[part.type] &&\n toolComponentMap[getToolPartName(part)]\n \"\n :key=\"`component-${partIndex}-${getPartState(part)}`\"\n :part\n v-bind=\"recordToolProps(part)\" />\n <slot\n v-else\n :name=\"part.type\"\n v-bind=\"{\n message,\n part,\n index,\n isLoading,\n }\">\n <div\n v-if=\"showAllMessageParts\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon\n v-if=\"getPartIcon(part)\"\n :name=\"getPartIcon(part)!\"\n class=\"shrink-0\" />\n <span class=\"text-12\">\n {{\n getPartTypeLabel(part, {\n t: $t,\n te: $te,\n })\n }}\n </span>\n </div>\n </div>\n </slot>\n </transition>\n <!-- Read-only comment: anchored right after the message\n text (same bubble as the reaction chip) so the\n feedback stays attached to the message body. -->\n <transition mode=\"out-in\">\n <PkChatbotMessageFeedback\n v-if=\"\n partIndex === lastTextPartIndex(message) &&\n readonlyFeedback &&\n getMessageFeedback(message.id)?.comment\n \"\n :feedback=\"getMessageFeedback(message.id)!\" />\n </transition>\n </template>\n <!-- The activity always reads at the bottom of the answer.\n With the steps timeline it shows only once something\n is drawn below the timeline: before that the timeline\n is the bottom, and both would name the same step. -->\n <transition name=\"fade-in\" mode=\"out-in\">\n <div\n v-if=\"\n isLoading &&\n isLastMessage(index) &&\n !isLastTextPart(message) &&\n isAssistant(message) &&\n (!showStepsTimeline(message) ||\n messageHasContent(message))\n \"\n :key=\"`loading-info-${message.id}`\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon name=\"loading-loop\" class=\"shrink-0\" />\n <transition mode=\"out-in\">\n <VvIcon\n v-if=\"activeMessageLastPartIcon\"\n :key=\"activeMessageLastPartIcon\"\n class=\"shrink-0\"\n :name=\"activeMessageLastPartIcon\" />\n </transition>\n <transition mode=\"out-in\">\n <span\n v-if=\"activeMessageLastPartLabel\"\n :key=\"activeMessageLastPartLabel\"\n class=\"text-12\">\n {{ activeMessageLastPartLabel }}\n </span>\n </transition>\n </div>\n <!-- With the timeline the reasoning lives in its\n step, which opens on demand. -->\n <transition name=\"fade-in\" mode=\"out-in\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"\n !showStepsTimeline(message) &&\n activeMessageLastPart &&\n 'text' in activeMessageLastPart &&\n activeMessageLastPart.text.trim()\n \"\n :markdown=\"activeMessageLastPart.text\"\n :is-dark\n :tail-length=\"LIVE_REASONING_TAIL_LENGTH\"\n inner-class=\"wysiwyg\"\n class=\"border border-surface-3 rounded-md p-4 mt-8 bg-surface-1 max-h-64 text-12 w-full\" />\n </transition>\n </div>\n </transition>\n <transition mode=\"out-in\">\n <div\n v-if=\"showMessageFooter(message, index)\"\n class=\"pk-chatbot-message__footer\">\n <VvButtonGroup\n modifiers=\"compact\"\n class=\"mr-auto shrink-0\">\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isMessageRegenerateButtonVisible(\n index,\n )\n \"\n icon=\"ri:reset-right-line\"\n modifiers=\"action-quiet-small\"\n :title=\"$t('action.regenerate')\"\n :aria-label=\"$t('action.regenerate')\"\n @click.stop=\"$emit('regenerate')\" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\n 'show-info',\n index,\n )\n \"\n icon=\"ri:information-line\"\n :title=\"$t('action.getMoreInfo')\"\n :aria-label=\"$t('action.getMoreInfo')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('show-info', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\n 'revise',\n index,\n )\n \"\n :icon=\"\n isRevised(message.id)\n ? 'ri:file-edit-fill'\n : 'ri:file-edit-line'\n \"\n :title=\"\n isRevised(message.id)\n ? $t('action.editRevise')\n : $t('action.createRevise')\n \"\n :aria-label=\"\n isRevised(message.id)\n ? $t('action.editRevise')\n : $t('action.createRevise')\n \"\n modifiers=\"action-quiet-small\"\n :class=\"{\n 'text-brand-graphic': isRevised(\n message.id,\n ),\n }\"\n @click.stop=\"\n $emit('revise', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'upvote',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.vote === 'upvote'\n ? 'ri:thumb-up-fill'\n : 'ri:thumb-up-line'\n \"\n :title=\"$t('action.upvote')\"\n :aria-label=\"$t('action.upvote')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('upvote', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'downvote',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.vote === 'downvote'\n ? 'ri:thumb-down-fill'\n : 'ri:thumb-down-line'\n \"\n :title=\"$t('action.downvote')\"\n :aria-label=\"$t('action.downvote')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('downvote', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'feedback',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.comment\n ? 'ri:feedback-fill'\n : 'ri:feedback-line'\n \"\n :title=\"$t('action.feedback')\"\n :aria-label=\"$t('action.feedback')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('feedback', message)\n \" />\n </transition>\n </VvButtonGroup>\n <div\n v-if=\"\n !showMessageDateTime &&\n message.metadata?.createdAt &&\n message.metadata?.completedAt\n \"\n class=\"pk-chatbot-message__hover-meta\">\n <time\n :datetime=\"\n new Date(\n message.metadata.createdAt,\n ).toISOString()\n \"\n :title=\"\n $d(\n new Date(\n message.metadata.createdAt,\n ),\n 'date-time',\n )\n \"\n class=\"flex items-center gap-4 min-w-0\">\n <VvIcon\n name=\"ri:time-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\">{{\n formatMessageTime(\n message.metadata.createdAt,\n )\n }}</span>\n </time>\n <span\n v-if=\"getMessageDuration(message)\"\n class=\"flex items-center gap-4 min-w-0\">\n <VvIcon\n name=\"ri:hourglass-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\">{{\n getMessageDuration(message)\n }}</span>\n </span>\n </div>\n <span\n v-if=\"showMessageNumber\"\n :title=\"\n $t('label.messageNumber', {\n n: messageNumbers[index],\n })\n \"\n class=\"text-12 text-word-3 tabular-nums\">\n #{{ messageNumbers[index] }}\n </span>\n <span\n v-if=\"\n showMessageTokensCount &&\n message.metadata?.totalTokens\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon\n name=\"ri:ai-generate-2-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\"\n >{{\n $n(\n message.metadata.totalTokens,\n 'integer',\n )\n }}\n {{ $t('label.tokens') }}\n </span>\n </span>\n <time\n v-if=\"\n showMessageDateTime &&\n message.metadata?.createdAt\n \"\n :datetime=\"\n new Date(\n message.metadata?.createdAt,\n ).toISOString()\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon name=\"ri:time-line\" class=\"shrink-0\" />\n <span class=\"truncate\">{{\n $d(\n new Date(message.metadata?.createdAt),\n 'date-time',\n )\n }}</span>\n </time>\n <div\n v-if=\"\n showMessageDateTime &&\n message?.metadata?.completedAt &&\n message?.metadata?.createdAt\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon\n name=\"ri:hourglass-line\"\n class=\"shrink-0\" />\n <PkRelativeTime\n class=\"truncate\"\n :date=\"message.metadata?.createdAt\"\n :end-date=\"message.metadata?.completedAt\" />\n </div>\n </div>\n </transition>\n <transition mode=\"out-in\">\n <PkChatbotFeedbackForm\n v-if=\"message.id === feedbackMessageId\"\n ref=\"feedbackMessageEl\"\n :loading=\"feedbackLoading\"\n :submitted=\"feedbackSubmitted\"\n :error=\"feedbackError\"\n @submit=\"$emit('feedback-submit', $event)\"\n @close=\"$emit('feedback-close')\" />\n </transition>\n </div>\n </template>\n <!-- A spoken turn streams to the provider, not here: nothing in\n this list moves between the question and the answer landing.\n Its own indicator, because the status-driven one hangs off an\n assistant message the SDK never creates for a call. When to\n show it and when it carries the anchor spacer: see\n `isSpokenWaitVisible` and `isSpokenWaitAnchored`. -->\n <transition appear name=\"pk-chatbot-part\">\n <div\n v-if=\"isSpokenWaitVisible\"\n class=\"pk-chatbot-message pk-chatbot-message--assistant pk-chatbot-message--loading\"\n :style=\"{\n minHeight: isSpokenWaitAnchored\n ? `${height}px`\n : undefined,\n }\">\n <div class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon name=\"loading-loop\" class=\"shrink-0\" />\n <span class=\"text-12\">\n {{ $t('label.answeringOnTheCall') }}\n </span>\n </div>\n </div>\n </div>\n </transition>\n <transition appear name=\"pk-chatbot-part\">\n <PkChatbotError\n v-if=\"isError\"\n :error\n @retry=\"$emit('auto-retry')\"\n @reset=\"$emit('reset-chat')\" />\n </transition>\n <div\n v-if=\"\n (activeMessage?.role === 'user' && !isSpokenWaitAnchored) ||\n isError\n \"\n class=\"pk-chatbot-messages__spacer\"\n :style=\"{ minHeight: `${height}px` }\"></div>\n </div>\n <Transition name=\"pk-chatbot-messages-fab\">\n <button\n v-if=\"showScrollToBottom && (userScrolledUp || !isAtBottom)\"\n type=\"button\"\n class=\"pk-chatbot-messages__scroll-to-bottom\"\n :title=\"$t('action.scrollToBottom')\"\n :aria-label=\"$t('action.scrollToBottom')\"\n @click=\"onScrollToBottomClick\">\n <VvIcon name=\"ri:arrow-down-line\" />\n </button>\n </Transition>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-messages {\n overflow-y: auto;\n flex: 1;\n min-width: 0;\n font-size: var(--text-14);\n\n &__wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-16);\n min-width: 0;\n }\n\n // Conversation outline rail: sticky shell pinned to the vertical\n // middle of the scroll area, at the right edge. Zero-sized and\n // first in flow so neither it nor its absolutely-positioned pieces\n // inflate scrollHeight. Desktop only, like ChatGPT: in the widget and\n // the panel the chat is as wide on a phone as on a desktop, so the\n // viewport is the only thing that tells them apart. The fullscreen\n // layout measures the chat itself instead (PkChatbot).\n &__outline {\n display: none;\n position: sticky;\n top: 50%;\n z-index: 2;\n flex: none;\n align-self: flex-end;\n height: 0;\n\n @include media-breakpoint-up('md', $breakpoints) {\n display: block;\n }\n\n .pk-chatbot-outline {\n position: absolute;\n top: 0;\n right: 0;\n transform: translateY(-50%);\n }\n }\n\n // Placeholder while the answer has not started yet: settles like\n // the message spacer when released. Only the release is animated\n // (`--settling`): animating the growth would keep `scrollHeight`\n // behind while the anchor scroll reads it, landing short of the\n // anchor with no second chance to catch up.\n &--settling {\n .pk-chatbot-messages__spacer,\n .pk-chatbot-message {\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n }\n }\n\n // Floating \"back to bottom\" button, sticky inside the scroll area\n &__scroll-to-bottom {\n position: sticky;\n bottom: var(--spacing-8);\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n // The scroll container is a flex column: without this the\n // overflowing content shrinks the button, ignoring its height\n flex: none;\n width: var(--spacing-36);\n height: var(--spacing-36);\n // Net-zero layout contribution: mounting/unmounting the button\n // must not change scrollHeight, or reaching the bottom (which\n // hides it) would clamp scrollTop and cause a visible jump\n margin-block-start: calc(-1 * var(--spacing-36));\n margin-inline: auto;\n border: 1px solid var(--color-surface-3);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-1);\n box-shadow: var(--shadow-md);\n color: var(--color-word-1);\n cursor: pointer;\n transition: var(--transition-colors);\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n .pk-chatbot-messages-fab-enter-active,\n .pk-chatbot-messages-fab-leave-active {\n transition: opacity 150ms var(--ease-out);\n }\n\n .pk-chatbot-messages-fab-enter-from,\n .pk-chatbot-messages-fab-leave-to {\n opacity: 0;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .pk-chatbot-messages-fab-enter-active,\n .pk-chatbot-messages-fab-leave-active {\n transition: none;\n }\n }\n\n // A wakeup in the feed (spec agent-wakeup): a check the agent ran by\n // itself, or a run of them that found nothing. Quiet like the day divider.\n .pk-chatbot-wakeup {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-xs);\n color: var(--color-word-4);\n font-size: var(--text-12);\n text-align: center;\n }\n\n .pk-chatbot-message {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-sm);\n min-width: 0;\n align-items: flex-start;\n // `clip` instead of `hidden` so the reaction chip, which deliberately\n // overhangs the bubble's bottom edge, is not cut off. The margin has\n // to cover half the chip plus its shadow.\n overflow: clip;\n overflow-clip-margin: var(--spacing-16);\n max-width: 100%;\n // The anchor spacer (inline min-height on the last assistant message)\n // settles smoothly when released at the end of the stream: the\n // transition lives on `.pk-chatbot-messages--settling` above\n\n &__text {\n position: relative;\n color: var(--color-word-2);\n background-color: var(--color-surface);\n line-height: var(--leading-normal);\n border-width: var(--spacing-px);\n border-style: solid;\n border-color: var(--color-surface-3);\n padding: var(--spacing-sm);\n border-radius: var(--rounded-xl) var(--rounded-xl) var(--rounded-xl)\n 0;\n max-width: 100%;\n text-overflow: ellipsis;\n // Same reason as the block above: the chip is absolutely\n // positioned against this bubble and hangs below its edge.\n overflow: clip;\n overflow-clip-margin: var(--spacing-16);\n }\n\n // Read-only feedback: a circular vote chip that overlaps the bubble's\n // bottom-right corner (messaging-app style). White ring lifts it off\n // the bubble; solid accent makes the rating unmistakable.\n &__reaction {\n position: absolute;\n right: var(--spacing-8);\n bottom: 0;\n transform: translateY(50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--spacing-24);\n height: var(--spacing-24);\n border-radius: var(--rounded-full, 9999px);\n color: var(--color-white, #fff);\n font-size: var(--text-12);\n box-shadow: var(--shadow-md);\n\n &--positive {\n background-color: var(--color-success);\n color: var(--color-success-contrast);\n }\n\n &--negative {\n background-color: var(--color-danger);\n color: var(--color-danger-contrast);\n }\n\n // Real emoji (e.g. WhatsApp): the glyph carries its own colour, so\n // the chip stays a neutral surface pill.\n &--neutral {\n background-color: var(--color-surface-1);\n }\n }\n\n &__reaction-emoji {\n // Smaller than the chip so the glyph doesn't touch the ring.\n font-size: var(--text-12);\n line-height: 1;\n }\n\n &__loading-info {\n line-height: var(--leading-normal);\n border-width: var(--spacing-px);\n border-style: solid;\n border-color: var(--color-surface-3);\n padding: var(--spacing-sm);\n border-radius: var(--rounded-xl) var(--rounded-xl) var(--rounded-xl)\n 0;\n font-size: var(--text-12);\n color: var(--color-word-3);\n background-color: var(--color-surface);\n }\n\n &__footer {\n width: 100%;\n display: flex;\n gap: var(--spacing-8);\n align-items: center;\n }\n\n &__hover-meta {\n display: flex;\n min-width: 0;\n align-items: center;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n color: var(--color-word-3);\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n opacity: 0;\n transition: opacity 0.2s ease;\n }\n\n &:hover &__hover-meta,\n &:focus-within &__hover-meta {\n opacity: 1;\n }\n\n // Hover doesn't exist on touch devices: keep the metadata visible\n @media (hover: none) {\n &__hover-meta {\n opacity: 1;\n }\n }\n\n &--user {\n align-items: flex-end;\n\n .pk-chatbot-message__text {\n background-color: var(\n --chatbot-main-color,\n var(--color-surface-1)\n );\n border-color: var(--chatbot-main-color, var(--color-surface-3));\n color: var(--chatbot-contrast-color, var(--color-word-1));\n border-radius: var(--rounded-xl) var(--rounded-xl) 0\n var(--rounded-xl);\n }\n }\n\n &--assistant {\n width: 100%;\n gap: var(--spacing-16);\n\n .pk-chatbot-message__text {\n width: 100%;\n }\n }\n\n &--system {\n align-items: flex-end;\n\n .pk-chatbot-message__text {\n display: flex;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n padding: var(--spacing-8) var(--spacing-sm);\n align-items: center;\n border-color: var(--chatbot-main-color, var(--color-surface-3));\n color: var(--chatbot-main-color, var(--color-word-1));\n border-radius: var(--rounded-xl) var(--rounded-xl) 0\n var(--rounded-xl);\n }\n }\n }\n\n .pk-chatbot-part-enter-active {\n transition:\n opacity 0.25s ease,\n transform 0.25s ease;\n }\n\n .pk-chatbot-part-enter-from {\n opacity: 0;\n transform: translateY(8px);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .pk-chatbot-part-enter-active {\n transition: none;\n }\n\n .pk-chatbot-part-enter-from {\n transform: none;\n }\n }\n</style>\n","<script lang=\"ts\" setup>\n import type {\n ChatMessageActions,\n MessageFeedback,\n RevisedAnswer,\n UIChatMessage,\n } from 'models'\n import { isAudioFile, isWakeupSeed } from 'models'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkChatbotError from './PkChatbotError.vue'\n import PkChatbotFeedbackForm from './PkChatbotFeedbackForm.vue'\n import PkChatbotMessageFeedback from './PkChatbotMessageFeedback.vue'\n import PkChatbotFilePreview from './PkChatbotFilePreview.vue'\n import PkChatbotAudioTranscript from './PkChatbotAudioTranscript.vue'\n import PkRelativeTime from '../PkRelativeTime.vue'\n import { useI18n } from 'vue-i18n'\n import {\n useTemplateRef,\n ref,\n shallowRef,\n watch,\n computed,\n nextTick,\n onMounted,\n useSlots,\n } from 'vue'\n import { useResizeObserver } from '@vueuse/core'\n import { toolComponentMap } from './toolComponentMap'\n import PkToolApprovalCard from './PkToolApprovalCard.vue'\n import PkChatbotSteps from './PkChatbotSteps.vue'\n import {\n resolveContrastColor,\n getPartState,\n isTextPart,\n isFilePart,\n isToolPart,\n isReasoningPart,\n isStreamingPart,\n isToolConfirmPart,\n isConfirmationCardPart,\n getToolApprovalId,\n getPartIcon,\n getPartTypeLabel,\n getToolPartLabel,\n mergeConsecutiveTextParts,\n dropCorrectedWidgetErrors,\n moveFileOutputsLast,\n feedPartKey,\n formatDuration,\n LIVE_REASONING_TAIL_LENGTH,\n } from './utils'\n import {\n getFilePartMedia,\n getFilePartTranscript,\n getToolPartName,\n } from 'utils'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotOutlineRail from './PkChatbotOutlineRail.vue'\n import { useChatScroll } from './useChatScroll'\n import { buildWakeupFeed } from './wakeupFeed'\n\n const props = defineProps<{\n status?: 'submitted' | 'streaming' | 'ready' | 'error'\n messages?: UIChatMessage[]\n error?: Error\n actions?: ChatMessageActions[]\n mainColor?: string\n textColor?: 'auto' | 'white' | 'black'\n revisedAnswers?: RevisedAnswer[]\n messageFeedbacks?: MessageFeedback[]\n /**\n * Render the vote/feedback action buttons as read-only state indicators\n * (e.g. admin chat-logs view): icons reflect the user's feedback but the\n * buttons are not interactive.\n */\n readonlyFeedback?: boolean\n disableHeightAdjustment?: boolean\n showMessageDateTime?: boolean\n showMessageTokensCount?: boolean\n /**\n * Shows each answer's position in the chat, from 1, the numbering the\n * conversation analysis cites (\"message 62\").\n */\n showMessageNumber?: boolean\n showAllMessageParts?: boolean\n /**\n * Lets the user answer the tool requests rendered in the feed (approval\n * and confirm cards). Off by default: the log viewers render the same\n * messages read-only and have nothing to answer with.\n */\n interactive?: boolean\n showExtendedSteps?: boolean\n /**\n * A call is waiting for its answer. The turn runs outside this client\n * (the voice provider drives it), so `status` says nothing about it and\n * the feed would sit still for the seconds it takes.\n */\n awaitingSpokenAnswer?: boolean\n /**\n * Verdicts given ON A CALL, by toolCallId. A confirm answered during a\n * call goes back to the agent as a spoken message, so its part never\n * leaves the render-ack state: without this the card kept its buttons\n * after the answer, and every extra tap said \"Confermo\" again\n * (measured 2026-08-07). Only successful sends are recorded, so a tap\n * that never reached the provider leaves the card usable.\n */\n confirmVerdictsOnCall?: Map<string, boolean>\n isDark?: boolean\n /** Show a floating \"back to bottom\" button when the user scrolls up (fullscreen layout) */\n showScrollToBottom?: boolean\n // TODO: move feedback in a separate component to avoid passing these props\n feedbackMessageId?: string\n feedbackLoading?: boolean\n feedbackSubmitted?: boolean\n feedbackError?: string\n }>()\n\n const emit = defineEmits<{\n (e: 'show-info', message: UIChatMessage): void\n (e: 'regenerate'): void\n (e: 'revise', message: UIChatMessage): void\n (e: 'upvote', message: UIChatMessage): void\n (e: 'downvote', message: UIChatMessage): void\n (e: 'feedback', message: UIChatMessage): void\n (e: 'feedback-submit', comment: string): void\n (e: 'feedback-close'): void\n (e: 'scroll-up'): void\n (e: 'scroll-down'): void\n (e: 'auto-retry'): void\n (e: 'reset-chat'): void\n (\n e: 'approval-respond',\n payload: { id: string; approved: boolean },\n ): void\n (\n e: 'confirm-respond',\n payload: { toolCallId: string; confirmed: boolean },\n ): void\n }>()\n\n const {\n t: $t,\n te: $te,\n d: $d,\n n: $n,\n } = useI18n({\n useScope: 'global',\n })\n\n const slots = useSlots()\n\n /**\n * Forwards the user's verdict from the confirmation card. The two kinds of\n * request travel back on different channels: an approval answers the SDK\n * approval it carries the id of, a `requestConfirm` resolves as a client\n * tool output. Without the id there is nothing to answer, so the click is\n * dropped instead of sending an unmatchable response.\n */\n const onCardRespond = (\n part: UIChatMessage['parts'][number],\n approved: boolean,\n ) => {\n if (isToolConfirmPart(part)) {\n const toolCallId = (part as { toolCallId?: string }).toolCallId\n if (toolCallId) {\n emit('confirm-respond', { toolCallId, confirmed: approved })\n }\n return\n }\n const id = getToolApprovalId(part)\n if (!id) {\n return\n }\n emit('approval-respond', { id, approved })\n }\n\n const scrollEl = useTemplateRef<HTMLDivElement>('scrollEl')\n const wrapperEl = useTemplateRef<HTMLDivElement>('wrapperEl')\n const contrastColor = computed(() =>\n resolveContrastColor(props.textColor, props.mainColor),\n )\n\n // --- Scroll ---\n\n const { handleScroll, scrollToBottom, userScrolledUp, isAtBottom } =\n useChatScroll({\n scrollEl,\n status: () => props.status,\n messagesLength: () => props.messages?.length,\n lastMessageRole: () =>\n props.messages?.[props.messages.length - 1]?.role,\n contentEl: () => wrapperEl.value,\n beforeNewMessageScroll: () => updateAnchorSpacer(),\n onScrollUp: () => emit('scroll-up'),\n onScrollDown: () => emit('scroll-down'),\n })\n\n const onScrollToBottomClick = () => {\n // scrollToBottom() is a no-op while userScrolledUp is set\n userScrolledUp.value = false\n scrollToBottom()\n }\n\n // --- Conversation outline (fullscreen, desktop) ---\n\n // A wakeup's seed is not a question the user asked, and it is not drawn\n // as a user bubble either, which the 1:1 mapping below relies on.\n const outlineItems = computed(() =>\n (props.messages ?? [])\n .filter(\n (message) => message.role === 'user' && !isWakeupSeed(message),\n )\n .map((message) => ({\n id: message.id,\n preview:\n message.parts.find(isTextPart)?.text.trim().slice(0, 120) ||\n '…',\n })),\n )\n const activeOutlineId = ref<string>()\n\n // The rail entries map 1:1 onto the rendered user messages\n const getUserMessageEls = () =>\n scrollEl.value?.querySelectorAll<HTMLElement>(\n '.pk-chatbot-message--user',\n ) ?? []\n\n // Scroll-spy: active is the last user message above the middle of the\n // viewport (the exchange currently being read)\n const updateActiveOutline = () => {\n const scroller = scrollEl.value\n if (!scroller || !props.showScrollToBottom) {\n return\n }\n const line =\n scroller.getBoundingClientRect().top + scroller.clientHeight / 2\n let active = outlineItems.value[0]?.id\n getUserMessageEls().forEach((el, index) => {\n if (el.getBoundingClientRect().top <= line) {\n active = outlineItems.value[index]?.id\n }\n })\n activeOutlineId.value = active\n }\n\n watch(outlineItems, async () => {\n await nextTick()\n updateActiveOutline()\n })\n\n const onScroll = () => {\n handleScroll()\n updateActiveOutline()\n }\n\n const scrollToUserMessage = (id: string) => {\n const scroller = scrollEl.value\n const index = outlineItems.value.findIndex((item) => item.id === id)\n const messageEl = getUserMessageEls()[index]\n if (!scroller || !messageEl) {\n return\n }\n // Intentional jump away from the live tail: the wheel/touch intent\n // detection cannot see programmatic scrolls, pause the auto-follow\n // explicitly (reaching the bottom again re-enables it)\n userScrolledUp.value = true\n // Same resting line as a freshly sent message: top of the content area\n scroller.scrollTo({\n top:\n scroller.scrollTop +\n messageEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top -\n Number.parseFloat(getComputedStyle(scroller).paddingTop),\n behavior: 'smooth',\n })\n }\n\n // The answer is still being worked on: the spacer below holds while it is,\n // and so do the timeline and the activity of the last message.\n const isLoading = computed(() => {\n return props.status === 'submitted' || props.status === 'streaming'\n })\n\n // --- Height adjustment ---\n\n const height = ref<number>(0)\n // The min-height transition only serves the release (see below): while the\n // spacer grows it would hide the new height from the anchor scroll, which\n // reads `scrollHeight` once and would land short of the anchor.\n const isSpacerSettling = ref(false)\n\n // The spacer serves the exchange in flight from its first measure to its\n // release, at zero too (a question taller than the area, a measure\n // skipped while hidden): the height alone cannot tell held from released.\n let isSpacerHeld = false\n // The last user message the spacer was measured against, observed below.\n const anchoredUserMessageEl = shallowRef<HTMLElement>()\n\n // Recalculate when a message is added or the conversation changes, and\n // when one of the two sizes it is computed from changes (see the resize\n // observer below). Idempotent: also called by the scroll composable right\n // before the anchor scroll, so the spacer is always in place first.\n const updateAnchorSpacer = async () => {\n const messages = props.messages\n if (!messages?.length || props.disableHeightAdjustment) {\n return\n }\n // The spacer only serves the in-flight exchange, letting the\n // fresh user message anchor at the top while the answer streams.\n // On plain loads (e.g. opening an old conversation) it would\n // just leave a large void after the last message.\n const isExchangeInFlight =\n messages[messages.length - 1].role === 'user' || isLoading.value\n if (!isExchangeInFlight) {\n return\n }\n isSpacerHeld = true\n isSpacerSettling.value = false\n await nextTick()\n if (!scrollEl.value) {\n return\n }\n // Spacer sized so the freshly sent user message rests at the top\n // of the content area once scrolled to bottom: subtract both\n // paddings (clientHeight includes them) so any extra top\n // clearance (e.g. the fullscreen overlay header) is respected,\n // and the wrapper row gap separating the message from the spacer\n const { paddingTop, paddingBottom } = getComputedStyle(scrollEl.value)\n const scrollElHeight =\n scrollEl.value.clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n const wrapperGap = wrapperEl.value\n ? Number.parseFloat(getComputedStyle(wrapperEl.value).rowGap) || 0\n : 0\n // Measured on the rendered DOM (same lookup as the outline rail):\n // indexing the template-refs array with the `messages` index breaks\n // as soon as a message renders nothing (`v-if` on empty parts), the\n // refs array skips it and the heights shift by one.\n const userMessageEls = getUserMessageEls()\n anchoredUserMessageEl.value = userMessageEls[userMessageEls.length - 1]\n const lastUserMessageHeight =\n anchoredUserMessageEl.value?.clientHeight ?? 0\n const newHeight = scrollElHeight - lastUserMessageHeight - wrapperGap\n // A question taller than the content area leaves nothing to reserve:\n // the spacer goes to zero, never back to the value of the previous\n // exchange. A stale, larger spacer is the very failure the anchor\n // exists to prevent, the scroll landing past the message. Skipped\n // when the element measures nothing (rendered while hidden): there\n // the height already in place is still the good one.\n if (scrollElHeight > 0) {\n height.value = Math.max(newHeight, 0)\n }\n await nextTick()\n }\n\n // Two sources compared one by one: the store recreates `messages` on\n // every token, and a getter returning `[length, id]` would hand back a\n // new array each time and run this at every token. An answer that resumes\n // after a pause (an approval, a form, a tool the client runs) keeps its\n // length and its id, so a spacer released at the pause stays released:\n // bringing it back on the next token threw the view down, and the next\n // pause threw it up again.\n watch(\n [\n () => props.messages?.length,\n () => props.messages?.[props.messages.length - 1]?.id,\n ],\n () => updateAnchorSpacer(),\n )\n\n // The two sizes the spacer is computed from can change while it is held:\n // the question (the transcript of an audio, a box the user opens) and the\n // chat itself (a bar docking above the input, a phone keyboard closing).\n // Followed by observing them rather than by measuring at every token.\n // A released spacer is left alone: an answer resumed after a pause must\n // not bring it back.\n useResizeObserver([scrollEl, anchoredUserMessageEl], () => {\n if (isSpacerHeld) {\n updateAnchorSpacer()\n }\n })\n\n // Mounted mid-send (fullscreen first message: the messages view replaces\n // the empty state after the message is already in): the length watch\n // never fires for that message, apply the anchor spacer now. A first\n // spoken line mounts the same way and never moves `status`, hence the\n // second guard. Plain history loads (mounted at 'ready', nothing in\n // flight) stay untouched.\n onMounted(() => {\n if (isLoading.value || props.awaitingSpokenAnswer) {\n updateAnchorSpacer()\n }\n })\n\n // Release the spacer once the answer stops loading: it would otherwise\n // leave a large empty area after the last message. The min-height\n // transition settles the layout smoothly (the browser clamps the\n // scroll progressively when the viewport sat inside the removed area).\n // Keyed on `isLoading`, not on `status`: whatever keeps the answer\n // loading keeps the spacer too.\n watch(isLoading, (loading) => {\n if (!loading) {\n isSpacerHeld = false\n isSpacerSettling.value = true\n height.value = 0\n }\n })\n\n const isRevised = (messageId: string) => {\n return props.revisedAnswers?.some((r) => r.messageId === messageId)\n }\n\n const getMessageFeedback = (messageId: string) =>\n props.messageFeedbacks?.find((f) => f.messageId === messageId)\n\n // A form the visitor fills in live is, in a feed nobody answers from\n // (the chat logs), the record of what the agent showed: no fields to\n // type in, no button that looks like it sends.\n const RECORD_TOOLS = new Set(['showContactForm'])\n const recordToolProps = (part: Parameters<typeof getToolPartName>[0]) =>\n !props.interactive && RECORD_TOOLS.has(getToolPartName(part))\n ? { record: true }\n : {}\n\n // Interactive vote/feedback buttons. In read-only mode (admin chat-logs)\n // the vote is shown on the message itself (coloured bubble border + an\n // \"active\" thumb in the footer) and the comment, if any, below it.\n const isFeedbackButtonVisible = (\n kind: 'upvote' | 'downvote' | 'feedback',\n index: number,\n ) => !props.readonlyFeedback && isActionButtonVisible(kind, index)\n\n // Read-only feedback for a message (drives the reaction chip anchored to\n // the bubble). undefined when not in read-only mode.\n const readonlyFeedbackOf = (messageId: string) =>\n props.readonlyFeedback ? getMessageFeedback(messageId) : undefined\n const feedbackVote = (messageId: string) =>\n readonlyFeedbackOf(messageId)?.vote\n // The real reaction glyph, shown ONLY for unmapped reactions (no vote):\n // 👍/❤️/👎 keep the classic thumb icon, while e.g. WhatsApp \"😍\" shows the\n // actual emoji. Platforms that send a type word (Teams \"like\") have a vote\n // and therefore use the icon too.\n const feedbackEmoji = (messageId: string) => {\n const feedback = readonlyFeedbackOf(messageId)\n if (feedback?.vote) {\n return undefined\n }\n const raw = feedback?.metadata?.reactionEmoji\n return typeof raw === 'string' && /\\p{Extended_Pictographic}/u.test(raw)\n ? raw\n : undefined\n }\n const hasReaction = (messageId: string) =>\n Boolean(feedbackVote(messageId) || feedbackEmoji(messageId))\n const reactionToneClass = (messageId: string) => {\n // A real emoji carries its own colour → neutral pill. Only the\n // thumb-icon fallback uses the sentiment colour.\n if (feedbackEmoji(messageId)) {\n return 'pk-chatbot-message__reaction--neutral'\n }\n const vote = feedbackVote(messageId)\n if (vote === 'upvote') {\n return 'pk-chatbot-message__reaction--positive'\n }\n if (vote === 'downvote') {\n return 'pk-chatbot-message__reaction--negative'\n }\n return 'pk-chatbot-message__reaction--neutral'\n }\n const reactionTitle = (messageId: string) => {\n const vote = feedbackVote(messageId)\n if (vote === 'upvote') {\n return $t('hint.userFeedbackPositive')\n }\n if (vote === 'downvote') {\n return $t('hint.userFeedbackNegative')\n }\n return $t('hint.userFeedbackReaction')\n }\n\n // Index of the last text part: the reaction chip anchors to that bubble so\n // it renders once even when the message has several text parts.\n const lastTextPartIndex = (message: UIChatMessage) => {\n const parts = getMergedParts(message)\n for (let index = parts.length - 1; index >= 0; index -= 1) {\n if (isTextPart(parts[index])) {\n return index\n }\n }\n return -1\n }\n\n const activeMessage = computed(() => {\n return props.messages?.[props.messages.length - 1]\n })\n const activeMessageLastPart = computed(() => {\n const active = activeMessage.value\n if (!active) {\n return null\n }\n return active.parts[active.parts.length - 1]\n })\n\n const mergedPartsMap = computed(() => {\n const map = new Map<string, UIChatMessage['parts']>()\n for (const message of props.messages ?? []) {\n if (message.role === 'assistant') {\n map.set(\n message.id,\n moveFileOutputsLast(\n dropCorrectedWidgetErrors(\n mergeConsecutiveTextParts(message.parts),\n ),\n ),\n )\n }\n }\n return map\n })\n const getMergedParts = (message: UIChatMessage) => {\n return mergedPartsMap.value.get(message.id) ?? message.parts\n }\n const isError = computed(() => props.status === 'error')\n\n /**\n * The spoken wait shows only while nothing else is working: the wait\n * survives the hang-up (the answer can be written minutes later) and the\n * caller can type meanwhile, and two working indicators stacked would say\n * the same thing twice. It comes back when the written turn is over.\n */\n const isSpokenWaitVisible = computed(\n () => props.awaitingSpokenAnswer === true && !isLoading.value,\n )\n\n /**\n * The wait indicator closes the feed, so it carries the anchor spacer\n * itself, the way the streaming assistant message does. Rendering the\n * standalone spacer as well would stack the two and push the question\n * above the top of the content area, out of view.\n *\n * Only when it is the last thing in the feed: an error card, or an\n * assistant answer already landed, keeps the spacer where it was.\n */\n const isSpokenWaitAnchored = computed(\n () =>\n isSpokenWaitVisible.value &&\n activeMessage.value?.role === 'user' &&\n !isError.value,\n )\n\n const activeMessageLastPartLabel = computed(() => {\n const part = activeMessageLastPart.value\n return getToolPartLabel($t, part)\n })\n const activeMessageLastPartIcon = computed(() => {\n const part = activeMessageLastPart.value\n return getPartIcon(part)\n })\n\n const getMessageIndexById = (messageId: string) => {\n return props.messages?.findIndex((m) => m.id === messageId)\n }\n const isLastMessage = (index?: number) => {\n if (index === undefined || !props.messages) {\n return false\n }\n return index === props.messages.length - 1\n }\n const isMessageRegenerateButtonVisible = (index: number) => {\n return (\n isLastMessage(index) &&\n props.status === 'ready' &&\n !!props.actions?.includes('regenerate')\n )\n }\n // On the last message the actions appear only once the answer is ready;\n // on previous messages they are always available.\n const isActionButtonVisible = (\n action: ChatMessageActions,\n index: number,\n ) => {\n return (\n (!isLastMessage(index) || props.status === 'ready') &&\n !!props.actions?.includes(action)\n )\n }\n const isLastTextPart = (message: UIChatMessage) => {\n const lastPart = message.parts[message.parts.length - 1]\n return isTextPart(lastPart)\n }\n const messageHasSteps = (message: UIChatMessage) =>\n message.parts.some((part) => isReasoningPart(part) || isToolPart(part))\n // The extended steps timeline sits above the answer as soon as the\n // message has at least one step to show.\n const showStepsTimeline = (message: UIChatMessage) =>\n Boolean(props.showExtendedSteps) &&\n isAssistant(message) &&\n messageHasSteps(message)\n // A tool part the feed draws itself: its own component, a slot filled by\n // the host, or the debug chip of `showAllMessageParts`.\n const isRenderedToolPart = (part: UIChatMessage['parts'][number]) =>\n isToolPart(part) &&\n !isStreamingPart(part) &&\n Boolean(\n toolComponentMap[getToolPartName(part)] ||\n slots[part.type] ||\n props.showAllMessageParts,\n )\n // Whether anything is drawn below the steps timeline yet, by the same\n // branches the template walks for each part. Until then the timeline is\n // the bottom of the message and carries the activity itself; from then\n // on it collapses and the compact indicator takes over at the bottom.\n const messageHasContent = (message: UIChatMessage) =>\n message.parts.some(\n (part) =>\n (isTextPart(part) && Boolean(part.text.trim())) ||\n isFilePart(part) ||\n (isConfirmationCardPart(part) && !isStreamingPart(part)) ||\n isRenderedToolPart(part),\n )\n const isAssistant = (message: UIChatMessage) => message.role === 'assistant'\n const isUser = (message: UIChatMessage) => message.role === 'user'\n const getPreviousAssistantMessage = (index: number) => {\n return props.messages\n ?.slice(0, index)\n .reverse()\n .find((message) => isAssistant(message))\n }\n const getNextAssistantMessage = (index: number) => {\n return props.messages\n ?.slice(index + 1)\n .find((message) => isAssistant(message))\n }\n // Returns the date to show in the day divider before the message at\n // `index`, or undefined when the surrounding assistant messages belong\n // to the same (local) day.\n const getDividerDate = (index: number) => {\n const previousCreatedAt =\n getPreviousAssistantMessage(index)?.metadata?.createdAt\n const nextCreatedAt =\n getNextAssistantMessage(index)?.metadata?.createdAt\n if (!previousCreatedAt || !nextCreatedAt) {\n return undefined\n }\n const previousDate = new Date(previousCreatedAt)\n const nextDate = new Date(nextCreatedAt)\n return previousDate.toDateString() !== nextDate.toDateString()\n ? nextDate\n : undefined\n }\n // Wakeups (spec agent-wakeup): the checks that found nothing read as one\n // line and a seed as a thin row, never as words the user wrote. An answer\n // that draws anything (a widget, an approval card) is never folded. The\n // log pages, which draw every part, keep them all.\n const wakeupFeed = computed(() =>\n props.showAllMessageParts\n ? buildWakeupFeed([])\n : buildWakeupFeed(props.messages ?? [], messageHasContent),\n )\n // The numbering the conversation analysis cites: a message with no parts\n // is not rendered and not counted there either.\n const messageNumbers = computed(() => {\n let count = 0\n return (props.messages ?? []).map((message) =>\n message.parts.length > 0 ? ++count : undefined,\n )\n })\n const showMessageFooter = (message: UIChatMessage, index: number) => {\n if (index === 0 || message.role !== 'assistant') {\n return false\n }\n return Boolean(\n props.actions?.length ||\n props.showMessageNumber ||\n (props.showMessageDateTime &&\n message.metadata?.createdAt !== undefined) ||\n message.metadata?.completedAt !== undefined ||\n (props.showMessageTokensCount &&\n message.metadata?.totalTokens !== undefined),\n )\n }\n // Hover-only metadata (time + duration), shown when the fixed variant\n // (showMessageDateTime) is off.\n const formatMessageTime = (createdAt: number) => {\n const date = new Date(createdAt)\n const isToday = date.toDateString() === new Date().toDateString()\n return $d(date, isToday ? 'time' : 'date-time')\n }\n const getMessageDuration = (message: UIChatMessage) => {\n const { createdAt, completedAt } = message.metadata ?? {}\n if (!createdAt || !completedAt) {\n return undefined\n }\n return formatDuration($n, completedAt - createdAt)\n }\n // feedback message auto scroll\n const feedbackMessageEl =\n useTemplateRef<InstanceType<typeof PkChatbotFeedbackForm>[]>(\n 'feedbackMessageEl',\n )\n watch(\n () => props.feedbackMessageId,\n async () => {\n await nextTick()\n if (!props.feedbackMessageId) {\n return\n }\n const el = feedbackMessageEl.value?.[0]?.$el\n if (!el) {\n return\n }\n if (isLastMessage(getMessageIndexById(props.feedbackMessageId))) {\n scrollToBottom()\n return\n }\n el.scrollIntoView({ behavior: 'smooth', block: 'center' })\n },\n )\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n class=\"pk-chatbot-messages scrollbar-thin\"\n :class=\"{ 'pk-chatbot-messages--settling': isSpacerSettling }\"\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n :aria-busy=\"isLoading\"\n :style=\"{\n '--chatbot-main-color': mainColor,\n '--chatbot-contrast-color': contrastColor,\n }\"\n @scroll=\"onScroll\">\n <!-- First in flow (content top): its absolutely-positioned children\n must never inflate the scrollable overflow past the real content -->\n <div\n v-if=\"showScrollToBottom && outlineItems.length > 1\"\n class=\"pk-chatbot-messages__outline\">\n <PkChatbotOutlineRail\n :items=\"outlineItems\"\n :active-id=\"activeOutlineId\"\n @select=\"scrollToUserMessage\" />\n </div>\n <div ref=\"wrapperEl\" class=\"pk-chatbot-messages__wrapper\">\n <template v-for=\"(message, index) in messages\" :key=\"message.id\">\n <!-- The pk-chatbot-divider classes carry no rule of ours: they\n stay as hooks for the custom CSS of an agent. -->\n <div\n v-if=\"isUser(message) && getDividerDate(index)\"\n class=\"pk-chatbot-divider vv-separator\">\n <span class=\"pk-chatbot-divider__label\">\n {{ $d(getDividerDate(index)!, 'short') }}\n </span>\n </div>\n <div\n v-if=\"wakeupFeed.quietRuns.has(message.id)\"\n class=\"pk-chatbot-wakeup\">\n <VvIcon name=\"ri:alarm-line\" class=\"shrink-0\" />\n <span>\n {{\n $t(\n 'label.wakeupQuietRun',\n {\n count: wakeupFeed.quietRuns.get(message.id)!\n .count,\n time: $d(\n wakeupFeed.quietRuns.get(message.id)!\n .lastAt ?? Date.now(),\n 'time',\n ),\n },\n wakeupFeed.quietRuns.get(message.id)!.count,\n )\n }}\n </span>\n </div>\n <div\n v-else-if=\"\n isWakeupSeed(message) &&\n !wakeupFeed.hidden.has(message.id) &&\n !showAllMessageParts\n \"\n class=\"pk-chatbot-wakeup\"\n :title=\"message.metadata?.wakeup?.reason\">\n <VvIcon name=\"ri:alarm-line\" class=\"shrink-0\" />\n <span>\n {{ $t('label.wakeupCheck') }}\n <template v-if=\"message.metadata?.createdAt\">\n · {{ $d(message.metadata.createdAt, 'time') }}\n </template>\n </span>\n </div>\n <div\n v-if=\"\n !(isWakeupSeed(message) && !showAllMessageParts) &&\n !(\n wakeupFeed.hidden.has(message.id) &&\n !(isLoading && isLastMessage(index))\n ) &&\n (message.parts.length ||\n (isLastMessage(index) &&\n isLoading &&\n isAssistant(message)))\n \"\n class=\"pk-chatbot-message\"\n :class=\"[\n `pk-chatbot-message--${message.role}`,\n {\n 'pk-chatbot-message--loading':\n isLoading &&\n isLastMessage(index) &&\n isAssistant(message),\n },\n ]\"\n :style=\"{\n minHeight:\n isLastMessage(index) &&\n isAssistant(message) &&\n !isError\n ? `${height}px`\n : undefined,\n }\">\n <transition appear name=\"pk-chatbot-part\">\n <PkChatbotSteps\n v-if=\"showStepsTimeline(message)\"\n :message\n :is-streaming=\"isLoading && isLastMessage(index)\"\n :has-content=\"messageHasContent(message)\"\n :is-dark />\n </transition>\n <template\n v-for=\"(part, partIndex) in getMergedParts(message)\"\n :key=\"feedPartKey(part, partIndex)\">\n <transition\n v-if=\"isTextPart(part) && part.text.trim()\"\n appear\n name=\"pk-chatbot-part\">\n <div class=\"pk-chatbot-message__text\">\n <slot\n v-bind=\"{\n message,\n part,\n index,\n isLoading,\n }\"\n name=\"text\">\n <PkStreamingMarkdown\n v-if=\"isAssistant(message)\"\n class=\"wysiwyg\"\n :markdown=\"part.text\"\n :is-dark\n :loading=\"\n isLastMessage(index) &&\n status === 'streaming'\n \" />\n <template v-else>\n {{ part.text }}\n </template>\n </slot>\n <!-- Read-only reaction chip overlapping the\n bubble corner, like a messaging app. Hidden\n when a comment is shown: the callout below\n then carries the reaction glyph instead. -->\n <span\n v-if=\"\n hasReaction(message.id) &&\n !getMessageFeedback(message.id)\n ?.comment &&\n partIndex === lastTextPartIndex(message)\n \"\n class=\"pk-chatbot-message__reaction\"\n :class=\"reactionToneClass(message.id)\"\n :title=\"reactionTitle(message.id)\">\n <span\n v-if=\"feedbackEmoji(message.id)\"\n class=\"pk-chatbot-message__reaction-emoji\">\n {{ feedbackEmoji(message.id) }}\n </span>\n <VvIcon\n v-else\n :name=\"\n feedbackVote(message.id) ===\n 'downvote'\n ? 'ri:thumb-down-fill'\n : 'ri:thumb-up-fill'\n \" />\n </span>\n </div>\n </transition>\n <transition\n v-else-if=\"isFilePart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <div\n v-if=\"\n isAudioFile(part.filename, part.mediaType)\n \"\n class=\"flex flex-col items-end gap-8 w-full\">\n <PkChatbotFilePreview\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\"\n :size=\"\n getFilePartMedia(part)?.bytesCount\n \" />\n <!-- Heard once when it arrives: until the\n transcript comes back the turn waits on it. -->\n <PkChatbotAudioTranscript\n :url=\"part.url\"\n :transcript=\"getFilePartTranscript(part)\"\n :pending=\"\n status === 'submitted' &&\n isLastMessage(index)\n \" />\n </div>\n <PkChatbotFilePreview\n v-else\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\"\n :size=\"getFilePartMedia(part)?.bytesCount\" />\n </transition>\n <!-- Anything asking the user for consent (a suspended\n tool call or a `requestConfirm`) shares one card,\n ahead of the per-tool components. An approval stays\n answerable only in the trailing message: the backend\n closes the abandoned ones as denied, while a\n `requestConfirm` can still be resolved later. -->\n <transition\n v-else-if=\"\n isConfirmationCardPart(part) &&\n !isStreamingPart(part)\n \"\n appear\n name=\"pk-chatbot-part\">\n <!-- Key WITHOUT the part state: the card must stay\n mounted across the approval lifecycle so its\n inner transitions play once; remounting per\n state replays the appear animation on every\n hop (requested → responded → output). -->\n <PkToolApprovalCard\n :key=\"`approval-${partIndex}`\"\n :part\n :actionable=\"\n interactive === true &&\n (isToolConfirmPart(part) ||\n isLastMessage(index))\n \"\n :call-verdict=\"\n confirmVerdictsOnCall?.get(\n (part as { toolCallId?: string })\n .toolCallId ?? '',\n )\n \"\n @respond=\"onCardRespond(part, $event)\" />\n </transition>\n <transition\n v-else-if=\"isRenderedToolPart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <component\n :is=\"toolComponentMap[getToolPartName(part)]\"\n v-if=\"\n !slots[part.type] &&\n toolComponentMap[getToolPartName(part)]\n \"\n :key=\"`component-${partIndex}-${getPartState(part)}`\"\n :part\n v-bind=\"recordToolProps(part)\" />\n <slot\n v-else\n :name=\"part.type\"\n v-bind=\"{\n message,\n part,\n index,\n isLoading,\n }\">\n <div\n v-if=\"showAllMessageParts\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon\n v-if=\"getPartIcon(part)\"\n :name=\"getPartIcon(part)!\"\n class=\"shrink-0\" />\n <span class=\"text-12\">\n {{\n getPartTypeLabel(part, {\n t: $t,\n te: $te,\n })\n }}\n </span>\n </div>\n </div>\n </slot>\n </transition>\n <!-- Read-only comment: anchored right after the message\n text (same bubble as the reaction chip) so the\n feedback stays attached to the message body. -->\n <transition mode=\"out-in\">\n <PkChatbotMessageFeedback\n v-if=\"\n partIndex === lastTextPartIndex(message) &&\n readonlyFeedback &&\n getMessageFeedback(message.id)?.comment\n \"\n :feedback=\"getMessageFeedback(message.id)!\" />\n </transition>\n </template>\n <!-- The activity always reads at the bottom of the answer.\n With the steps timeline it shows only once something\n is drawn below the timeline: before that the timeline\n is the bottom, and both would name the same step. -->\n <transition name=\"fade-in\" mode=\"out-in\">\n <div\n v-if=\"\n isLoading &&\n isLastMessage(index) &&\n !isLastTextPart(message) &&\n isAssistant(message) &&\n (!showStepsTimeline(message) ||\n messageHasContent(message))\n \"\n :key=\"`loading-info-${message.id}`\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon name=\"loading-loop\" class=\"shrink-0\" />\n <transition mode=\"out-in\">\n <VvIcon\n v-if=\"activeMessageLastPartIcon\"\n :key=\"activeMessageLastPartIcon\"\n class=\"shrink-0\"\n :name=\"activeMessageLastPartIcon\" />\n </transition>\n <transition mode=\"out-in\">\n <span\n v-if=\"activeMessageLastPartLabel\"\n :key=\"activeMessageLastPartLabel\"\n class=\"text-12\">\n {{ activeMessageLastPartLabel }}\n </span>\n </transition>\n </div>\n <!-- With the timeline the reasoning lives in its\n step, which opens on demand. -->\n <transition name=\"fade-in\" mode=\"out-in\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"\n !showStepsTimeline(message) &&\n activeMessageLastPart &&\n 'text' in activeMessageLastPart &&\n activeMessageLastPart.text.trim()\n \"\n :markdown=\"activeMessageLastPart.text\"\n :is-dark\n :tail-length=\"LIVE_REASONING_TAIL_LENGTH\"\n inner-class=\"wysiwyg\"\n class=\"border border-surface-3 rounded-md p-4 mt-8 bg-surface-1 max-h-64 text-12 w-full\" />\n </transition>\n </div>\n </transition>\n <transition mode=\"out-in\">\n <div\n v-if=\"showMessageFooter(message, index)\"\n class=\"pk-chatbot-message__footer\">\n <VvButtonGroup\n modifiers=\"compact\"\n class=\"mr-auto shrink-0\">\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isMessageRegenerateButtonVisible(\n index,\n )\n \"\n icon=\"ri:reset-right-line\"\n modifiers=\"action-quiet-small\"\n :title=\"$t('action.regenerate')\"\n :aria-label=\"$t('action.regenerate')\"\n @click.stop=\"$emit('regenerate')\" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\n 'show-info',\n index,\n )\n \"\n icon=\"ri:information-line\"\n :title=\"$t('action.getMoreInfo')\"\n :aria-label=\"$t('action.getMoreInfo')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('show-info', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\n 'revise',\n index,\n )\n \"\n :icon=\"\n isRevised(message.id)\n ? 'ri:file-edit-fill'\n : 'ri:file-edit-line'\n \"\n :title=\"\n isRevised(message.id)\n ? $t('action.editRevise')\n : $t('action.createRevise')\n \"\n :aria-label=\"\n isRevised(message.id)\n ? $t('action.editRevise')\n : $t('action.createRevise')\n \"\n modifiers=\"action-quiet-small\"\n :class=\"{\n 'text-brand-graphic': isRevised(\n message.id,\n ),\n }\"\n @click.stop=\"\n $emit('revise', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'upvote',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.vote === 'upvote'\n ? 'ri:thumb-up-fill'\n : 'ri:thumb-up-line'\n \"\n :title=\"$t('action.upvote')\"\n :aria-label=\"$t('action.upvote')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('upvote', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'downvote',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.vote === 'downvote'\n ? 'ri:thumb-down-fill'\n : 'ri:thumb-down-line'\n \"\n :title=\"$t('action.downvote')\"\n :aria-label=\"$t('action.downvote')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('downvote', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isFeedbackButtonVisible(\n 'feedback',\n index,\n )\n \"\n :icon=\"\n getMessageFeedback(message.id)\n ?.comment\n ? 'ri:feedback-fill'\n : 'ri:feedback-line'\n \"\n :title=\"$t('action.feedback')\"\n :aria-label=\"$t('action.feedback')\"\n modifiers=\"action-quiet-small\"\n @click.stop=\"\n $emit('feedback', message)\n \" />\n </transition>\n </VvButtonGroup>\n <div\n v-if=\"\n !showMessageDateTime &&\n message.metadata?.createdAt &&\n message.metadata?.completedAt\n \"\n class=\"pk-chatbot-message__hover-meta\">\n <time\n :datetime=\"\n new Date(\n message.metadata.createdAt,\n ).toISOString()\n \"\n :title=\"\n $d(\n new Date(\n message.metadata.createdAt,\n ),\n 'date-time',\n )\n \"\n class=\"flex items-center gap-4 min-w-0\">\n <VvIcon\n name=\"ri:time-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\">{{\n formatMessageTime(\n message.metadata.createdAt,\n )\n }}</span>\n </time>\n <span\n v-if=\"getMessageDuration(message)\"\n class=\"flex items-center gap-4 min-w-0\">\n <VvIcon\n name=\"ri:hourglass-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\">{{\n getMessageDuration(message)\n }}</span>\n </span>\n </div>\n <span\n v-if=\"showMessageNumber\"\n :title=\"\n $t('label.messageNumber', {\n n: messageNumbers[index],\n })\n \"\n class=\"text-12 text-word-3 tabular-nums\">\n #{{ messageNumbers[index] }}\n </span>\n <span\n v-if=\"\n showMessageTokensCount &&\n message.metadata?.totalTokens\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon\n name=\"ri:ai-generate-2-line\"\n class=\"shrink-0\" />\n <span class=\"truncate\"\n >{{\n $n(\n message.metadata.totalTokens,\n 'integer',\n )\n }}\n {{ $t('label.tokens') }}\n </span>\n </span>\n <time\n v-if=\"\n showMessageDateTime &&\n message.metadata?.createdAt\n \"\n :datetime=\"\n new Date(\n message.metadata?.createdAt,\n ).toISOString()\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon name=\"ri:time-line\" class=\"shrink-0\" />\n <span class=\"truncate\">{{\n $d(\n new Date(message.metadata?.createdAt),\n 'date-time',\n )\n }}</span>\n </time>\n <div\n v-if=\"\n showMessageDateTime &&\n message?.metadata?.completedAt &&\n message?.metadata?.createdAt\n \"\n class=\"flex items-center gap-4 text-12 text-word-3\">\n <VvIcon\n name=\"ri:hourglass-line\"\n class=\"shrink-0\" />\n <PkRelativeTime\n class=\"truncate\"\n :date=\"message.metadata?.createdAt\"\n :end-date=\"message.metadata?.completedAt\" />\n </div>\n </div>\n </transition>\n <transition mode=\"out-in\">\n <PkChatbotFeedbackForm\n v-if=\"message.id === feedbackMessageId\"\n ref=\"feedbackMessageEl\"\n :loading=\"feedbackLoading\"\n :submitted=\"feedbackSubmitted\"\n :error=\"feedbackError\"\n @submit=\"$emit('feedback-submit', $event)\"\n @close=\"$emit('feedback-close')\" />\n </transition>\n </div>\n </template>\n <!-- A spoken turn streams to the provider, not here: nothing in\n this list moves between the question and the answer landing.\n Its own indicator, because the status-driven one hangs off an\n assistant message the SDK never creates for a call. When to\n show it and when it carries the anchor spacer: see\n `isSpokenWaitVisible` and `isSpokenWaitAnchored`. -->\n <transition appear name=\"pk-chatbot-part\">\n <div\n v-if=\"isSpokenWaitVisible\"\n class=\"pk-chatbot-message pk-chatbot-message--assistant pk-chatbot-message--loading\"\n :style=\"{\n minHeight: isSpokenWaitAnchored\n ? `${height}px`\n : undefined,\n }\">\n <div class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon name=\"loading-loop\" class=\"shrink-0\" />\n <span class=\"text-12\">\n {{ $t('label.answeringOnTheCall') }}\n </span>\n </div>\n </div>\n </div>\n </transition>\n <transition appear name=\"pk-chatbot-part\">\n <PkChatbotError\n v-if=\"isError\"\n :error\n @retry=\"$emit('auto-retry')\"\n @reset=\"$emit('reset-chat')\" />\n </transition>\n <div\n v-if=\"\n (activeMessage?.role === 'user' && !isSpokenWaitAnchored) ||\n isError\n \"\n class=\"pk-chatbot-messages__spacer\"\n :style=\"{ minHeight: `${height}px` }\"></div>\n </div>\n <Transition name=\"pk-chatbot-messages-fab\">\n <button\n v-if=\"showScrollToBottom && (userScrolledUp || !isAtBottom)\"\n type=\"button\"\n class=\"pk-chatbot-messages__scroll-to-bottom\"\n :title=\"$t('action.scrollToBottom')\"\n :aria-label=\"$t('action.scrollToBottom')\"\n @click=\"onScrollToBottomClick\">\n <VvIcon name=\"ri:arrow-down-line\" />\n </button>\n </Transition>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-messages {\n overflow-y: auto;\n flex: 1;\n min-width: 0;\n font-size: var(--text-14);\n\n &__wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-16);\n min-width: 0;\n }\n\n // Conversation outline rail: sticky shell pinned to the vertical\n // middle of the scroll area, at the right edge. Zero-sized and\n // first in flow so neither it nor its absolutely-positioned pieces\n // inflate scrollHeight. Desktop only, like ChatGPT: in the widget and\n // the panel the chat is as wide on a phone as on a desktop, so the\n // viewport is the only thing that tells them apart. The fullscreen\n // layout measures the chat itself instead (PkChatbot).\n &__outline {\n display: none;\n position: sticky;\n top: 50%;\n z-index: 2;\n flex: none;\n align-self: flex-end;\n height: 0;\n\n @include media-breakpoint-up('md', $breakpoints) {\n display: block;\n }\n\n .pk-chatbot-outline {\n position: absolute;\n top: 0;\n right: 0;\n transform: translateY(-50%);\n }\n }\n\n // Placeholder while the answer has not started yet: settles like\n // the message spacer when released. Only the release is animated\n // (`--settling`): animating the growth would keep `scrollHeight`\n // behind while the anchor scroll reads it, landing short of the\n // anchor with no second chance to catch up.\n &--settling {\n .pk-chatbot-messages__spacer,\n .pk-chatbot-message {\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n }\n }\n\n // Floating \"back to bottom\" button, sticky inside the scroll area\n &__scroll-to-bottom {\n position: sticky;\n bottom: var(--spacing-8);\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n // The scroll container is a flex column: without this the\n // overflowing content shrinks the button, ignoring its height\n flex: none;\n width: var(--spacing-36);\n height: var(--spacing-36);\n // Net-zero layout contribution: mounting/unmounting the button\n // must not change scrollHeight, or reaching the bottom (which\n // hides it) would clamp scrollTop and cause a visible jump\n margin-block-start: calc(-1 * var(--spacing-36));\n margin-inline: auto;\n border: 1px solid var(--color-surface-3);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-1);\n box-shadow: var(--shadow-md);\n color: var(--color-word-1);\n cursor: pointer;\n transition: var(--transition-colors);\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n .pk-chatbot-messages-fab-enter-active,\n .pk-chatbot-messages-fab-leave-active {\n transition: opacity 150ms var(--ease-out);\n }\n\n .pk-chatbot-messages-fab-enter-from,\n .pk-chatbot-messages-fab-leave-to {\n opacity: 0;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .pk-chatbot-messages-fab-enter-active,\n .pk-chatbot-messages-fab-leave-active {\n transition: none;\n }\n }\n\n // A wakeup in the feed (spec agent-wakeup): a check the agent ran by\n // itself, or a run of them that found nothing. Quiet like the day divider.\n .pk-chatbot-wakeup {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-xs);\n color: var(--color-word-4);\n font-size: var(--text-12);\n text-align: center;\n }\n\n .pk-chatbot-message {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-sm);\n min-width: 0;\n align-items: flex-start;\n // `clip` instead of `hidden` so the reaction chip, which deliberately\n // overhangs the bubble's bottom edge, is not cut off. The margin has\n // to cover half the chip plus its shadow.\n overflow: clip;\n overflow-clip-margin: var(--spacing-16);\n max-width: 100%;\n // The anchor spacer (inline min-height on the last assistant message)\n // settles smoothly when released at the end of the stream: the\n // transition lives on `.pk-chatbot-messages--settling` above\n\n &__text {\n position: relative;\n color: var(--color-word-2);\n background-color: var(--color-surface);\n line-height: var(--leading-normal);\n border-width: var(--spacing-px);\n border-style: solid;\n border-color: var(--color-surface-3);\n padding: var(--spacing-sm);\n border-radius: var(--rounded-xl) var(--rounded-xl) var(--rounded-xl)\n 0;\n max-width: 100%;\n text-overflow: ellipsis;\n // Same reason as the block above: the chip is absolutely\n // positioned against this bubble and hangs below its edge.\n overflow: clip;\n overflow-clip-margin: var(--spacing-16);\n }\n\n // Read-only feedback: a circular vote chip that overlaps the bubble's\n // bottom-right corner (messaging-app style). White ring lifts it off\n // the bubble; solid accent makes the rating unmistakable.\n &__reaction {\n position: absolute;\n right: var(--spacing-8);\n bottom: 0;\n transform: translateY(50%);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--spacing-24);\n height: var(--spacing-24);\n border-radius: var(--rounded-full, 9999px);\n color: var(--color-white, #fff);\n font-size: var(--text-12);\n box-shadow: var(--shadow-md);\n\n &--positive {\n background-color: var(--color-success);\n color: var(--color-success-contrast);\n }\n\n &--negative {\n background-color: var(--color-danger);\n color: var(--color-danger-contrast);\n }\n\n // Real emoji (e.g. WhatsApp): the glyph carries its own colour, so\n // the chip stays a neutral surface pill.\n &--neutral {\n background-color: var(--color-surface-1);\n }\n }\n\n &__reaction-emoji {\n // Smaller than the chip so the glyph doesn't touch the ring.\n font-size: var(--text-12);\n line-height: 1;\n }\n\n &__loading-info {\n line-height: var(--leading-normal);\n border-width: var(--spacing-px);\n border-style: solid;\n border-color: var(--color-surface-3);\n padding: var(--spacing-sm);\n border-radius: var(--rounded-xl) var(--rounded-xl) var(--rounded-xl)\n 0;\n font-size: var(--text-12);\n color: var(--color-word-3);\n background-color: var(--color-surface);\n }\n\n &__footer {\n width: 100%;\n display: flex;\n gap: var(--spacing-8);\n align-items: center;\n }\n\n &__hover-meta {\n display: flex;\n min-width: 0;\n align-items: center;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n color: var(--color-word-3);\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n opacity: 0;\n transition: opacity 0.2s ease;\n }\n\n &:hover &__hover-meta,\n &:focus-within &__hover-meta {\n opacity: 1;\n }\n\n // Hover doesn't exist on touch devices: keep the metadata visible\n @media (hover: none) {\n &__hover-meta {\n opacity: 1;\n }\n }\n\n &--user {\n align-items: flex-end;\n\n .pk-chatbot-message__text {\n background-color: var(\n --chatbot-main-color,\n var(--color-surface-1)\n );\n border-color: var(--chatbot-main-color, var(--color-surface-3));\n color: var(--chatbot-contrast-color, var(--color-word-1));\n border-radius: var(--rounded-xl) var(--rounded-xl) 0\n var(--rounded-xl);\n }\n }\n\n &--assistant {\n width: 100%;\n gap: var(--spacing-16);\n\n .pk-chatbot-message__text {\n width: 100%;\n }\n }\n\n &--system {\n align-items: flex-end;\n\n .pk-chatbot-message__text {\n display: flex;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n padding: var(--spacing-8) var(--spacing-sm);\n align-items: center;\n border-color: var(--chatbot-main-color, var(--color-surface-3));\n color: var(--chatbot-main-color, var(--color-word-1));\n border-radius: var(--rounded-xl) var(--rounded-xl) 0\n var(--rounded-xl);\n }\n }\n }\n\n .pk-chatbot-part-enter-active {\n transition:\n opacity 0.25s ease,\n transform 0.25s ease;\n }\n\n .pk-chatbot-part-enter-from {\n opacity: 0;\n transform: translateY(8px);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .pk-chatbot-part-enter-active {\n transition: none;\n }\n\n .pk-chatbot-part-enter-from {\n transform: none;\n }\n }\n</style>\n"],"ignoreList":[0],"mappings":"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"}