@8wave/ai-elements 0.86.0 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/_chunks/{PkToolShowForm-Cn-H4cT2.js → PkToolShowForm-DxInI-SU.js} +2 -2
  2. package/dist/_chunks/PkToolShowForm-DxInI-SU.js.map +1 -0
  3. package/dist/ai-elements.es.js +3111 -3088
  4. package/dist/ai-elements.es.js.map +1 -1
  5. package/dist-vue/PkChatbot.js +1 -1
  6. package/dist-vue/PkChatbotMessages.js +1 -1
  7. package/dist-vue/PkChatbotViewChat.js +1 -1
  8. package/dist-vue/PkChatbotViewConversations.js +1 -1
  9. package/dist-vue/PkChatbotViewProfile.js +1 -1
  10. package/dist-vue/_chunks/{PkChatbot-BIMT0wBz.js → PkChatbot-obI_7VAa.js} +5 -5
  11. package/dist-vue/_chunks/{PkChatbot-BIMT0wBz.js.map → PkChatbot-obI_7VAa.js.map} +1 -1
  12. package/dist-vue/_chunks/{PkChatbotMessages-Dc480HRd.js → PkChatbotMessages-BTVFyrnS.js} +16 -16
  13. package/dist-vue/_chunks/{PkChatbotMessages-Dc480HRd.js.map → PkChatbotMessages-BTVFyrnS.js.map} +1 -1
  14. package/dist-vue/_chunks/{PkChatbotViewChat-5XcFKLsL.js → PkChatbotViewChat-DdY7Xuqa.js} +4 -4
  15. package/dist-vue/_chunks/{PkChatbotViewChat-5XcFKLsL.js.map → PkChatbotViewChat-DdY7Xuqa.js.map} +1 -1
  16. package/dist-vue/_chunks/{PkChatbotViewConversations-DtMC16ye.js → PkChatbotViewConversations-C8hV9Mwm.js} +2 -2
  17. package/dist-vue/_chunks/{PkChatbotViewConversations-DtMC16ye.js.map → PkChatbotViewConversations-C8hV9Mwm.js.map} +1 -1
  18. package/dist-vue/_chunks/{PkChatbotViewProfile-rwxE8oAz.js → PkChatbotViewProfile-Dk02VeJS.js} +2 -2
  19. package/dist-vue/_chunks/{PkChatbotViewProfile-rwxE8oAz.js.map → PkChatbotViewProfile-Dk02VeJS.js.map} +1 -1
  20. package/dist-vue/_chunks/{PkToolShowArtifact-CO29-4g-.js → PkToolShowArtifact-LA-xP42x.js} +2 -2
  21. package/dist-vue/_chunks/{PkToolShowArtifact-CO29-4g-.js.map → PkToolShowArtifact-LA-xP42x.js.map} +1 -1
  22. package/dist-vue/_chunks/{PkToolShowCalendarEvent-D6pBcKlC.js → PkToolShowCalendarEvent-B0fvvNqq.js} +2 -2
  23. package/dist-vue/_chunks/{PkToolShowCalendarEvent-D6pBcKlC.js.map → PkToolShowCalendarEvent-B0fvvNqq.js.map} +1 -1
  24. package/dist-vue/_chunks/{PkToolShowComparison-9sZ-wZur.js → PkToolShowComparison-CkxbcdHx.js} +2 -2
  25. package/dist-vue/_chunks/{PkToolShowComparison-9sZ-wZur.js.map → PkToolShowComparison-CkxbcdHx.js.map} +1 -1
  26. package/dist-vue/_chunks/{PkToolShowContactForm-Dpqd5tro.js → PkToolShowContactForm-Q-zWz2QT.js} +2 -2
  27. package/dist-vue/_chunks/{PkToolShowContactForm-Dpqd5tro.js.map → PkToolShowContactForm-Q-zWz2QT.js.map} +1 -1
  28. package/dist-vue/_chunks/{PkToolShowEmail-BzMs4yST.js → PkToolShowEmail-DcV3KIBI.js} +2 -2
  29. package/dist-vue/_chunks/{PkToolShowEmail-BzMs4yST.js.map → PkToolShowEmail-DcV3KIBI.js.map} +1 -1
  30. package/dist-vue/_chunks/{PkToolShowForm-BwHOBNU6.js → PkToolShowForm-YwhD8noA.js} +3 -3
  31. package/dist-vue/_chunks/PkToolShowForm-YwhD8noA.js.map +1 -0
  32. package/dist-vue/_chunks/{PkToolShowImageGallery-AY3RDKm1.js → PkToolShowImageGallery-C1r8jvlG.js} +2 -2
  33. package/dist-vue/_chunks/{PkToolShowImageGallery-AY3RDKm1.js.map → PkToolShowImageGallery-C1r8jvlG.js.map} +1 -1
  34. package/dist-vue/_chunks/{PkToolShowLocation-BNSkfQzK.js → PkToolShowLocation-BvKZaaJS.js} +2 -2
  35. package/dist-vue/_chunks/{PkToolShowLocation-BNSkfQzK.js.map → PkToolShowLocation-BvKZaaJS.js.map} +1 -1
  36. package/dist-vue/_chunks/{PkToolShowMessage-BkeZrC7V.js → PkToolShowMessage-J5IWwUjF.js} +2 -2
  37. package/dist-vue/_chunks/{PkToolShowMessage-BkeZrC7V.js.map → PkToolShowMessage-J5IWwUjF.js.map} +1 -1
  38. package/dist-vue/_chunks/{PkToolShowProductList-DSKpE_xV.js → PkToolShowProductList-D4Fap8dC.js} +2 -2
  39. package/dist-vue/_chunks/{PkToolShowProductList-DSKpE_xV.js.map → PkToolShowProductList-D4Fap8dC.js.map} +1 -1
  40. package/dist-vue/_chunks/{PkToolShowQrCode-4VvbbzaY.js → PkToolShowQrCode-BUH5vIS8.js} +2 -2
  41. package/dist-vue/_chunks/{PkToolShowQrCode-4VvbbzaY.js.map → PkToolShowQrCode-BUH5vIS8.js.map} +1 -1
  42. package/dist-vue/_chunks/{PkToolShowSources-OrX42JvT.js → PkToolShowSources-ChkWKhFd.js} +2 -2
  43. package/dist-vue/_chunks/{PkToolShowSources-OrX42JvT.js.map → PkToolShowSources-ChkWKhFd.js.map} +1 -1
  44. package/dist-vue/_chunks/{PkToolShowSuggestedReply-DwuQo5jJ.js → PkToolShowSuggestedReply-VVg-OVtH.js} +2 -2
  45. package/dist-vue/_chunks/{PkToolShowSuggestedReply-DwuQo5jJ.js.map → PkToolShowSuggestedReply-VVg-OVtH.js.map} +1 -1
  46. package/dist-vue/_chunks/{PkToolShowWebPages-DB2ZAV_5.js → PkToolShowWebPages-CbdH6FZQ.js} +2 -2
  47. package/dist-vue/_chunks/{PkToolShowWebPages-DB2ZAV_5.js.map → PkToolShowWebPages-CbdH6FZQ.js.map} +1 -1
  48. package/dist-vue/_chunks/{createChatbotApiClient-nfzYJAR8.js → createChatbotApiClient-DWRtOu7t.js} +386 -383
  49. package/dist-vue/_chunks/createChatbotApiClient-DWRtOu7t.js.map +1 -0
  50. package/dist-vue/_chunks/{dist-cI6n0Ysp.js → dist-BN5P-Pmm.js} +2 -2
  51. package/dist-vue/_chunks/{dist-cI6n0Ysp.js.map → dist-BN5P-Pmm.js.map} +1 -1
  52. package/dist-vue/_chunks/{dist-CsJGDQx2.js → dist-Bv_EQP56.js} +2 -2
  53. package/dist-vue/_chunks/{dist-CsJGDQx2.js.map → dist-Bv_EQP56.js.map} +1 -1
  54. package/dist-vue/_chunks/{dist-DSYfmLHg.js → dist-C2-7Fze7.js} +2 -2
  55. package/dist-vue/_chunks/{dist-DSYfmLHg.js.map → dist-C2-7Fze7.js.map} +1 -1
  56. package/dist-vue/_chunks/{dist-CR5js-m0.js → dist-CYAK1sKO.js} +2 -2
  57. package/dist-vue/_chunks/{dist-CR5js-m0.js.map → dist-CYAK1sKO.js.map} +1 -1
  58. package/dist-vue/_chunks/{dist-CmgWe1rp.js → dist-Cact3-tk.js} +2 -2
  59. package/dist-vue/_chunks/{dist-CmgWe1rp.js.map → dist-Cact3-tk.js.map} +1 -1
  60. package/dist-vue/_chunks/{dist-mzC92jO7.js → dist-D7NafeHu.js} +4 -4
  61. package/dist-vue/_chunks/{dist-mzC92jO7.js.map → dist-D7NafeHu.js.map} +1 -1
  62. package/dist-vue/_chunks/{dist-DFanv2QX.js → dist-DHQ8itnF.js} +2 -2
  63. package/dist-vue/_chunks/{dist-DFanv2QX.js.map → dist-DHQ8itnF.js.map} +1 -1
  64. package/dist-vue/_chunks/{dist-BNvqaw4Y.js → dist-DTPBebYZ.js} +3 -3
  65. package/dist-vue/_chunks/{dist-BNvqaw4Y.js.map → dist-DTPBebYZ.js.map} +1 -1
  66. package/dist-vue/_chunks/{dist-DW3ekwuQ.js → dist-DlXJzThT.js} +2 -2
  67. package/dist-vue/_chunks/{dist-DW3ekwuQ.js.map → dist-DlXJzThT.js.map} +1 -1
  68. package/dist-vue/_chunks/{dist-CiIh8vh_.js → dist-_Aw9VPtK.js} +3 -3
  69. package/dist-vue/_chunks/{dist-CiIh8vh_.js.map → dist-_Aw9VPtK.js.map} +1 -1
  70. package/dist-vue/_chunks/{useChatbotStore-Or_R6a3R.js → useChatbotStore-VxGMdCch.js} +1079 -1082
  71. package/dist-vue/_chunks/{useChatbotStore-Or_R6a3R.js.map → useChatbotStore-VxGMdCch.js.map} +1 -1
  72. package/dist-vue/api.js +1 -1
  73. package/dist-vue/apps/web-component/src/components/EmbeddedChatWidget.ce.d.ts +3 -0
  74. package/dist-vue/apps/web-component/src/composables/useChatbotAuth.d.ts +4 -4
  75. package/dist-vue/apps/web-component/src/lib.d.ts +1 -0
  76. package/dist-vue/composables.js +2 -2
  77. package/dist-vue/index.js +4644 -8185
  78. package/dist-vue/index.js.map +1 -1
  79. package/dist-vue/locales.js +4 -0
  80. package/dist-vue/packages/ability/src/index.d.ts +1 -2
  81. package/dist-vue/packages/ability/src/types.d.ts +10 -1
  82. package/dist-vue/packages/auth/src/index.d.ts +2 -2
  83. package/dist-vue/packages/components/src/chat/PkChatSidebarConversationItem.d.ts +2 -2
  84. package/dist-vue/packages/components/src/chat/PkChatbotAuth.d.ts +10 -10
  85. package/dist-vue/packages/composable/src/chatbot/useChatbotStore.d.ts +1 -1
  86. package/dist-vue/packages/composable/src/useJsonSchemaEditor.d.ts +3 -0
  87. package/dist-vue/packages/models/src/schema/Agent.d.ts +14 -0
  88. package/dist-vue/style.css +1 -1
  89. package/package.json +4 -5
  90. package/dist/_chunks/PkToolShowForm-Cn-H4cT2.js.map +0 -1
  91. package/dist-vue/_chunks/PkToolShowForm-BwHOBNU6.js.map +0 -1
  92. package/dist-vue/_chunks/createChatbotApiClient-nfzYJAR8.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"PkChatbotMessages-Dc480HRd.js","names":["$emit"],"sources":["../../../../packages/components/src/chat/toolComponentMap.ts","../../../../packages/components/src/chat/useChatScroll.ts","../../../../packages/components/src/chat/PkChatbotMessages.vue","../../../../packages/components/src/chat/PkChatbotMessages.vue"],"sourcesContent":["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 (requestConfirm, showContactForm, showSuggestedReply,\n * showSources, showMultipleChoice) must be wired explicitly in the parent\n * with their required callbacks/events.\n */\nexport const toolComponentMap: Record<\n string,\n ReturnType<typeof defineAsyncComponent>\n> = {\n requestConfirm: defineAsyncComponent(\n () => import('./PkToolRequestConfirm.vue'),\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 showWeather: defineAsyncComponent(() => import('./PkToolShowWeather.vue')),\n showWebPages: defineAsyncComponent(\n () => import('./PkToolShowWebPages.vue'),\n ),\n}\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 * Max scrollTop the streaming auto-follow may reach (e.g. the freshly\n * sent user message resting at the top of the viewport). Scrolling past\n * it remains possible (user gesture or scrollToBottom), and once past it\n * the auto-follow tracks the bottom again.\n */\n autoScrollLimit?: () => number | 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 onScrollUp?: () => void\n onScrollDown?: () => void\n}\n\nexport function useChatScroll(options: UseChatScrollOptions) {\n const {\n scrollEl,\n status,\n messagesLength,\n lastMessageRole,\n autoScrollLimit,\n contentEl,\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 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, but never above the\n // current position nor past `autoScrollLimit` (the freshly sent user\n // message anchored at the top of the viewport — the counterpart of the\n // last-message spacer in PkChatbotMessages). Once the user moves past\n // the limit, the follow tracks the bottom again.\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 const limit = autoScrollLimit?.()\n const target =\n limit !== undefined && el.scrollTop <= limit + 1\n ? Math.min(bottom, limit)\n : bottom\n isAtBottom.value = bottom - target < NEAR_BOTTOM_THRESHOLD\n if (target <= el.scrollTop) {\n return\n }\n isAutoScrolling = true\n el.scrollTop = target\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 isAutoScrolling = true\n await nextTick()\n scrollToBottom()\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","<script lang=\"ts\" setup>\n import type {\n ChatMessageActions,\n MessageFeedback,\n RevisedAnswer,\n UIChatMessage,\n } from 'models'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkChatbotError from './PkChatbotError.vue'\n import PkChatbotFeedbackForm from './PkChatbotFeedbackForm.vue'\n import PkChatbotFilePreview from './PkChatbotFilePreview.vue'\n import PkRelativeTime from '../PkRelativeTime.vue'\n import { useI18n } from 'vue-i18n'\n import {\n useTemplateRef,\n ref,\n watch,\n computed,\n nextTick,\n useSlots,\n } from 'vue'\n import { toolComponentMap } from './toolComponentMap'\n import PkChatbotSteps from './PkChatbotSteps.vue'\n import {\n resolveContrastColor,\n getPartState,\n isTextPart,\n isFilePart,\n isToolPart,\n isReasoningPart,\n isStreamingPart,\n getPartIcon,\n getToolPartLabel,\n mergeConsecutiveTextParts,\n formatDuration,\n } from './utils'\n import { getToolPartName, toKebabCase } from 'utils'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotOutlineRail from './PkChatbotOutlineRail.vue'\n import { useChatScroll } from './useChatScroll'\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 disableHeightAdjustment?: boolean\n showMessageDateTime?: boolean\n showMessageTokensCount?: boolean\n showAllMessageParts?: boolean\n showExtendedSteps?: 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\n const {\n t: $t,\n d: $d,\n n: $n,\n } = useI18n({\n useScope: 'global',\n })\n\n const slots = useSlots()\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 messagesElRefs = useTemplateRef<HTMLDivElement[]>('messagesEl')\n\n // ScrollTop that rests the last user message at the top of the content\n // area: the streaming auto-follow stops there so the freshly sent\n // message never slides under the page header. Only applied when the\n // scroll-to-bottom button is available as the affordance to go past it.\n const lastUserMessageScrollLimit = () => {\n const scroller = scrollEl.value\n const lastUserMessageIndex = props.messages\n ?.map((message) => message.role)\n .lastIndexOf('user')\n const messageEl = messagesElRefs.value?.[lastUserMessageIndex ?? -1]\n if (!props.showScrollToBottom || !scroller || !messageEl) {\n return undefined\n }\n return (\n scroller.scrollTop +\n messageEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top -\n Number.parseFloat(getComputedStyle(scroller).paddingTop)\n )\n }\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 autoScrollLimit: lastUserMessageScrollLimit,\n contentEl: () => wrapperEl.value,\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 const outlineItems = computed(() =>\n (props.messages ?? [])\n .filter((message) => message.role === 'user')\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('.pk-chatbot-message--user') ?? []\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 // --- Height adjustment ---\n\n const height = ref<number>(0)\n\n // Recalculate when a message is added or the conversation changes (the\n // store recreates `messages` on every streamed token, so watching the\n // array reference would re-run this on each character). The height only\n // depends on the last user message height, which is stable while\n // streaming.\n watch(\n () => [\n props.messages?.length,\n props.messages?.[props.messages.length - 1]?.id,\n ],\n 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' ||\n props.status === 'submitted' ||\n props.status === 'streaming'\n if (!isExchangeInFlight) {\n return\n }\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 const { paddingTop, paddingBottom } = getComputedStyle(\n scrollEl.value,\n )\n const scrollElHeight =\n scrollEl.value.clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n const lastUserMessageIndex = messages\n .map((message) => message.role)\n .lastIndexOf('user')\n const lastUserMessageHeight =\n messagesElRefs.value?.[lastUserMessageIndex]?.clientHeight ?? 0\n const newHeight = scrollElHeight - lastUserMessageHeight\n if (newHeight > 0) {\n height.value = newHeight\n }\n },\n )\n\n // Release the spacer once the exchange is over: 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 watch(\n () => props.status,\n (status) => {\n if (status === 'ready' || status === 'error') {\n height.value = 0\n }\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 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(message.id, mergeConsecutiveTextParts(message.parts))\n }\n }\n return map\n })\n const getMergedParts = (message: UIChatMessage) => {\n return mergedPartsMap.value.get(message.id) ?? message.parts\n }\n const isLoading = computed(() => {\n return props.status === 'submitted' || props.status === 'streaming'\n })\n const isError = computed(() => props.status === 'error')\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 replaces the compact activity indicator as\n // soon as the message has at least one step to show.\n const showStepsTimeline = (message: UIChatMessage) =>\n Boolean(props.showExtendedSteps) &&\n isAssistant(message) &&\n messageHasSteps(message)\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 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.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\"\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 <div\n v-if=\"isUser(message) && getDividerDate(index)\"\n class=\"pk-chatbot-divider\">\n <span class=\"pk-chatbot-divider__label\">\n {{ $d(getDividerDate(index)!, 'short') }}\n </span>\n </div>\n <div\n v-if=\"\n message.parts.length ||\n (isLastMessage(index) &&\n isLoading &&\n isAssistant(message))\n \"\n ref=\"messagesEl\"\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 :is-dark />\n </transition>\n <template\n v-for=\"(part, partIndex) in getMergedParts(message)\"\n :key=\"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 </div>\n </transition>\n <transition\n v-else-if=\"isFilePart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <PkChatbotFilePreview\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\" />\n </transition>\n <transition\n v-else-if=\"\n isToolPart(part) &&\n !isStreamingPart(part) &&\n (toolComponentMap[getToolPartName(part)] ||\n slots[part.type] ||\n showAllMessageParts)\n \"\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 <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 <code class=\"font-mono rounded text-12\">\n {{ toKebabCase(part.type) }}\n </code>\n </div>\n </div>\n </slot>\n </transition>\n </template>\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 \"\n :key=\"`loading-info-${message.id}`\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon\n name=\"line-md:loading-loop\"\n 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 <transition name=\"fade-in\" mode=\"out-in\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"\n activeMessageLastPart &&\n 'text' in activeMessageLastPart &&\n activeMessageLastPart.text.trim()\n \"\n :markdown=\"activeMessageLastPart.text\"\n :is-dark\n inner-class=\"wysiwyg\"\n class=\"border border-surface-4 rounded 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 modifiers=\"compact\" class=\"mr-auto\">\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': isRevised(message.id),\n }\"\n @click.stop=\"\n $emit('revise', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\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 isActionButtonVisible(\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 isActionButtonVisible(\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\">\n <VvIcon name=\"ri:time-line\" />\n {{\n formatMessageTime(\n message.metadata.createdAt,\n )\n }}\n </time>\n <span\n v-if=\"getMessageDuration(message)\"\n class=\"flex items-center gap-4\">\n <VvIcon name=\"ri:hourglass-line\" />\n {{ getMessageDuration(message) }}\n </span>\n </div>\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 name=\"ri:ai-generate-2-line\" />\n {{\n $n(message.metadata.totalTokens, 'integer')\n }}\n {{ $t('label.tokens') }}\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\" />\n {{\n $d(\n new Date(message.metadata?.createdAt),\n 'date-time',\n )\n }}\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 name=\"ri:hourglass-line\" />\n <PkRelativeTime\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 <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=\"activeMessage?.role === 'user' || isError\"\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 scrollbar-width: thin;\n scrollbar-color: var(--color-word-5) var(--color-surface-1);\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)\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\n &__spacer {\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\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 .pk-chatbot-divider {\n position: relative;\n border-bottom: 1px solid var(--color-surface-3);\n\n &__label {\n position: absolute;\n padding-inline: var(--spacing-8);\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background-color: var(--color-surface);\n color: var(--color-word-4);\n font-size: var(--text-12);\n }\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 overflow: hidden;\n max-width: 100%;\n background-color: var(--color-surface);\n // The anchor spacer (inline min-height on the last assistant\n // message) settles smoothly when released at the end of the stream\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n\n &__text {\n color: var(--color-word-2);\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 }\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 }\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 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-4));\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-4));\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 PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkChatbotError from './PkChatbotError.vue'\n import PkChatbotFeedbackForm from './PkChatbotFeedbackForm.vue'\n import PkChatbotFilePreview from './PkChatbotFilePreview.vue'\n import PkRelativeTime from '../PkRelativeTime.vue'\n import { useI18n } from 'vue-i18n'\n import {\n useTemplateRef,\n ref,\n watch,\n computed,\n nextTick,\n useSlots,\n } from 'vue'\n import { toolComponentMap } from './toolComponentMap'\n import PkChatbotSteps from './PkChatbotSteps.vue'\n import {\n resolveContrastColor,\n getPartState,\n isTextPart,\n isFilePart,\n isToolPart,\n isReasoningPart,\n isStreamingPart,\n getPartIcon,\n getToolPartLabel,\n mergeConsecutiveTextParts,\n formatDuration,\n } from './utils'\n import { getToolPartName, toKebabCase } from 'utils'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotOutlineRail from './PkChatbotOutlineRail.vue'\n import { useChatScroll } from './useChatScroll'\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 disableHeightAdjustment?: boolean\n showMessageDateTime?: boolean\n showMessageTokensCount?: boolean\n showAllMessageParts?: boolean\n showExtendedSteps?: 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\n const {\n t: $t,\n d: $d,\n n: $n,\n } = useI18n({\n useScope: 'global',\n })\n\n const slots = useSlots()\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 messagesElRefs = useTemplateRef<HTMLDivElement[]>('messagesEl')\n\n // ScrollTop that rests the last user message at the top of the content\n // area: the streaming auto-follow stops there so the freshly sent\n // message never slides under the page header. Only applied when the\n // scroll-to-bottom button is available as the affordance to go past it.\n const lastUserMessageScrollLimit = () => {\n const scroller = scrollEl.value\n const lastUserMessageIndex = props.messages\n ?.map((message) => message.role)\n .lastIndexOf('user')\n const messageEl = messagesElRefs.value?.[lastUserMessageIndex ?? -1]\n if (!props.showScrollToBottom || !scroller || !messageEl) {\n return undefined\n }\n return (\n scroller.scrollTop +\n messageEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top -\n Number.parseFloat(getComputedStyle(scroller).paddingTop)\n )\n }\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 autoScrollLimit: lastUserMessageScrollLimit,\n contentEl: () => wrapperEl.value,\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 const outlineItems = computed(() =>\n (props.messages ?? [])\n .filter((message) => message.role === 'user')\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('.pk-chatbot-message--user') ?? []\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 // --- Height adjustment ---\n\n const height = ref<number>(0)\n\n // Recalculate when a message is added or the conversation changes (the\n // store recreates `messages` on every streamed token, so watching the\n // array reference would re-run this on each character). The height only\n // depends on the last user message height, which is stable while\n // streaming.\n watch(\n () => [\n props.messages?.length,\n props.messages?.[props.messages.length - 1]?.id,\n ],\n 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' ||\n props.status === 'submitted' ||\n props.status === 'streaming'\n if (!isExchangeInFlight) {\n return\n }\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 const { paddingTop, paddingBottom } = getComputedStyle(\n scrollEl.value,\n )\n const scrollElHeight =\n scrollEl.value.clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n const lastUserMessageIndex = messages\n .map((message) => message.role)\n .lastIndexOf('user')\n const lastUserMessageHeight =\n messagesElRefs.value?.[lastUserMessageIndex]?.clientHeight ?? 0\n const newHeight = scrollElHeight - lastUserMessageHeight\n if (newHeight > 0) {\n height.value = newHeight\n }\n },\n )\n\n // Release the spacer once the exchange is over: 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 watch(\n () => props.status,\n (status) => {\n if (status === 'ready' || status === 'error') {\n height.value = 0\n }\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 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(message.id, mergeConsecutiveTextParts(message.parts))\n }\n }\n return map\n })\n const getMergedParts = (message: UIChatMessage) => {\n return mergedPartsMap.value.get(message.id) ?? message.parts\n }\n const isLoading = computed(() => {\n return props.status === 'submitted' || props.status === 'streaming'\n })\n const isError = computed(() => props.status === 'error')\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 replaces the compact activity indicator as\n // soon as the message has at least one step to show.\n const showStepsTimeline = (message: UIChatMessage) =>\n Boolean(props.showExtendedSteps) &&\n isAssistant(message) &&\n messageHasSteps(message)\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 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.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\"\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 <div\n v-if=\"isUser(message) && getDividerDate(index)\"\n class=\"pk-chatbot-divider\">\n <span class=\"pk-chatbot-divider__label\">\n {{ $d(getDividerDate(index)!, 'short') }}\n </span>\n </div>\n <div\n v-if=\"\n message.parts.length ||\n (isLastMessage(index) &&\n isLoading &&\n isAssistant(message))\n \"\n ref=\"messagesEl\"\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 :is-dark />\n </transition>\n <template\n v-for=\"(part, partIndex) in getMergedParts(message)\"\n :key=\"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 </div>\n </transition>\n <transition\n v-else-if=\"isFilePart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <PkChatbotFilePreview\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\" />\n </transition>\n <transition\n v-else-if=\"\n isToolPart(part) &&\n !isStreamingPart(part) &&\n (toolComponentMap[getToolPartName(part)] ||\n slots[part.type] ||\n showAllMessageParts)\n \"\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 <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 <code class=\"font-mono rounded text-12\">\n {{ toKebabCase(part.type) }}\n </code>\n </div>\n </div>\n </slot>\n </transition>\n </template>\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 \"\n :key=\"`loading-info-${message.id}`\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon\n name=\"line-md:loading-loop\"\n 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 <transition name=\"fade-in\" mode=\"out-in\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"\n activeMessageLastPart &&\n 'text' in activeMessageLastPart &&\n activeMessageLastPart.text.trim()\n \"\n :markdown=\"activeMessageLastPart.text\"\n :is-dark\n inner-class=\"wysiwyg\"\n class=\"border border-surface-4 rounded 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 modifiers=\"compact\" class=\"mr-auto\">\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': isRevised(message.id),\n }\"\n @click.stop=\"\n $emit('revise', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\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 isActionButtonVisible(\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 isActionButtonVisible(\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\">\n <VvIcon name=\"ri:time-line\" />\n {{\n formatMessageTime(\n message.metadata.createdAt,\n )\n }}\n </time>\n <span\n v-if=\"getMessageDuration(message)\"\n class=\"flex items-center gap-4\">\n <VvIcon name=\"ri:hourglass-line\" />\n {{ getMessageDuration(message) }}\n </span>\n </div>\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 name=\"ri:ai-generate-2-line\" />\n {{\n $n(message.metadata.totalTokens, 'integer')\n }}\n {{ $t('label.tokens') }}\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\" />\n {{\n $d(\n new Date(message.metadata?.createdAt),\n 'date-time',\n )\n }}\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 name=\"ri:hourglass-line\" />\n <PkRelativeTime\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 <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=\"activeMessage?.role === 'user' || isError\"\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 scrollbar-width: thin;\n scrollbar-color: var(--color-word-5) var(--color-surface-1);\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)\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\n &__spacer {\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\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 .pk-chatbot-divider {\n position: relative;\n border-bottom: 1px solid var(--color-surface-3);\n\n &__label {\n position: absolute;\n padding-inline: var(--spacing-8);\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background-color: var(--color-surface);\n color: var(--color-word-4);\n font-size: var(--text-12);\n }\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 overflow: hidden;\n max-width: 100%;\n background-color: var(--color-surface);\n // The anchor spacer (inline min-height on the last assistant\n // message) settles smoothly when released at the end of the stream\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n\n &__text {\n color: var(--color-word-2);\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 }\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 }\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 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-4));\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-4));\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"],"mappings":"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"}
1
+ {"version":3,"file":"PkChatbotMessages-BTVFyrnS.js","names":["$emit"],"sources":["../../../../packages/components/src/chat/toolComponentMap.ts","../../../../packages/components/src/chat/useChatScroll.ts","../../../../packages/components/src/chat/PkChatbotMessages.vue","../../../../packages/components/src/chat/PkChatbotMessages.vue"],"sourcesContent":["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 (requestConfirm, showContactForm, showSuggestedReply,\n * showSources, showMultipleChoice) must be wired explicitly in the parent\n * with their required callbacks/events.\n */\nexport const toolComponentMap: Record<\n string,\n ReturnType<typeof defineAsyncComponent>\n> = {\n requestConfirm: defineAsyncComponent(\n () => import('./PkToolRequestConfirm.vue'),\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 showWeather: defineAsyncComponent(() => import('./PkToolShowWeather.vue')),\n showWebPages: defineAsyncComponent(\n () => import('./PkToolShowWebPages.vue'),\n ),\n}\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 * Max scrollTop the streaming auto-follow may reach (e.g. the freshly\n * sent user message resting at the top of the viewport). Scrolling past\n * it remains possible (user gesture or scrollToBottom), and once past it\n * the auto-follow tracks the bottom again.\n */\n autoScrollLimit?: () => number | 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 onScrollUp?: () => void\n onScrollDown?: () => void\n}\n\nexport function useChatScroll(options: UseChatScrollOptions) {\n const {\n scrollEl,\n status,\n messagesLength,\n lastMessageRole,\n autoScrollLimit,\n contentEl,\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 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, but never above the\n // current position nor past `autoScrollLimit` (the freshly sent user\n // message anchored at the top of the viewport — the counterpart of the\n // last-message spacer in PkChatbotMessages). Once the user moves past\n // the limit, the follow tracks the bottom again.\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 const limit = autoScrollLimit?.()\n const target =\n limit !== undefined && el.scrollTop <= limit + 1\n ? Math.min(bottom, limit)\n : bottom\n isAtBottom.value = bottom - target < NEAR_BOTTOM_THRESHOLD\n if (target <= el.scrollTop) {\n return\n }\n isAutoScrolling = true\n el.scrollTop = target\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 isAutoScrolling = true\n await nextTick()\n scrollToBottom()\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","<script lang=\"ts\" setup>\n import type {\n ChatMessageActions,\n MessageFeedback,\n RevisedAnswer,\n UIChatMessage,\n } from 'models'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkChatbotError from './PkChatbotError.vue'\n import PkChatbotFeedbackForm from './PkChatbotFeedbackForm.vue'\n import PkChatbotFilePreview from './PkChatbotFilePreview.vue'\n import PkRelativeTime from '../PkRelativeTime.vue'\n import { useI18n } from 'vue-i18n'\n import {\n useTemplateRef,\n ref,\n watch,\n computed,\n nextTick,\n useSlots,\n } from 'vue'\n import { toolComponentMap } from './toolComponentMap'\n import PkChatbotSteps from './PkChatbotSteps.vue'\n import {\n resolveContrastColor,\n getPartState,\n isTextPart,\n isFilePart,\n isToolPart,\n isReasoningPart,\n isStreamingPart,\n getPartIcon,\n getToolPartLabel,\n mergeConsecutiveTextParts,\n formatDuration,\n } from './utils'\n import { getToolPartName, toKebabCase } from 'utils'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotOutlineRail from './PkChatbotOutlineRail.vue'\n import { useChatScroll } from './useChatScroll'\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 disableHeightAdjustment?: boolean\n showMessageDateTime?: boolean\n showMessageTokensCount?: boolean\n showAllMessageParts?: boolean\n showExtendedSteps?: 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\n const {\n t: $t,\n d: $d,\n n: $n,\n } = useI18n({\n useScope: 'global',\n })\n\n const slots = useSlots()\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 messagesElRefs = useTemplateRef<HTMLDivElement[]>('messagesEl')\n\n // ScrollTop that rests the last user message at the top of the content\n // area: the streaming auto-follow stops there so the freshly sent\n // message never slides under the page header. Only applied when the\n // scroll-to-bottom button is available as the affordance to go past it.\n const lastUserMessageScrollLimit = () => {\n const scroller = scrollEl.value\n const lastUserMessageIndex = props.messages\n ?.map((message) => message.role)\n .lastIndexOf('user')\n const messageEl = messagesElRefs.value?.[lastUserMessageIndex ?? -1]\n if (!props.showScrollToBottom || !scroller || !messageEl) {\n return undefined\n }\n return (\n scroller.scrollTop +\n messageEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top -\n Number.parseFloat(getComputedStyle(scroller).paddingTop)\n )\n }\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 autoScrollLimit: lastUserMessageScrollLimit,\n contentEl: () => wrapperEl.value,\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 const outlineItems = computed(() =>\n (props.messages ?? [])\n .filter((message) => message.role === 'user')\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('.pk-chatbot-message--user') ?? []\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 // --- Height adjustment ---\n\n const height = ref<number>(0)\n\n // Recalculate when a message is added or the conversation changes (the\n // store recreates `messages` on every streamed token, so watching the\n // array reference would re-run this on each character). The height only\n // depends on the last user message height, which is stable while\n // streaming.\n watch(\n () => [\n props.messages?.length,\n props.messages?.[props.messages.length - 1]?.id,\n ],\n 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' ||\n props.status === 'submitted' ||\n props.status === 'streaming'\n if (!isExchangeInFlight) {\n return\n }\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 const { paddingTop, paddingBottom } = getComputedStyle(\n scrollEl.value,\n )\n const scrollElHeight =\n scrollEl.value.clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n const lastUserMessageIndex = messages\n .map((message) => message.role)\n .lastIndexOf('user')\n const lastUserMessageHeight =\n messagesElRefs.value?.[lastUserMessageIndex]?.clientHeight ?? 0\n const newHeight = scrollElHeight - lastUserMessageHeight\n if (newHeight > 0) {\n height.value = newHeight\n }\n },\n )\n\n // Release the spacer once the exchange is over: 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 watch(\n () => props.status,\n (status) => {\n if (status === 'ready' || status === 'error') {\n height.value = 0\n }\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 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(message.id, mergeConsecutiveTextParts(message.parts))\n }\n }\n return map\n })\n const getMergedParts = (message: UIChatMessage) => {\n return mergedPartsMap.value.get(message.id) ?? message.parts\n }\n const isLoading = computed(() => {\n return props.status === 'submitted' || props.status === 'streaming'\n })\n const isError = computed(() => props.status === 'error')\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 replaces the compact activity indicator as\n // soon as the message has at least one step to show.\n const showStepsTimeline = (message: UIChatMessage) =>\n Boolean(props.showExtendedSteps) &&\n isAssistant(message) &&\n messageHasSteps(message)\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 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.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\"\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 <div\n v-if=\"isUser(message) && getDividerDate(index)\"\n class=\"pk-chatbot-divider\">\n <span class=\"pk-chatbot-divider__label\">\n {{ $d(getDividerDate(index)!, 'short') }}\n </span>\n </div>\n <div\n v-if=\"\n message.parts.length ||\n (isLastMessage(index) &&\n isLoading &&\n isAssistant(message))\n \"\n ref=\"messagesEl\"\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 :is-dark />\n </transition>\n <template\n v-for=\"(part, partIndex) in getMergedParts(message)\"\n :key=\"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 </div>\n </transition>\n <transition\n v-else-if=\"isFilePart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <PkChatbotFilePreview\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\" />\n </transition>\n <transition\n v-else-if=\"\n isToolPart(part) &&\n !isStreamingPart(part) &&\n (toolComponentMap[getToolPartName(part)] ||\n slots[part.type] ||\n showAllMessageParts)\n \"\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 <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 <code class=\"font-mono rounded text-12\">\n {{ toKebabCase(part.type) }}\n </code>\n </div>\n </div>\n </slot>\n </transition>\n </template>\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 \"\n :key=\"`loading-info-${message.id}`\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon\n name=\"line-md:loading-loop\"\n 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 <transition name=\"fade-in\" mode=\"out-in\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"\n activeMessageLastPart &&\n 'text' in activeMessageLastPart &&\n activeMessageLastPart.text.trim()\n \"\n :markdown=\"activeMessageLastPart.text\"\n :is-dark\n inner-class=\"wysiwyg\"\n class=\"border border-surface-4 rounded 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 modifiers=\"compact\" class=\"mr-auto\">\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': isRevised(message.id),\n }\"\n @click.stop=\"\n $emit('revise', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\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 isActionButtonVisible(\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 isActionButtonVisible(\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\">\n <VvIcon name=\"ri:time-line\" />\n {{\n formatMessageTime(\n message.metadata.createdAt,\n )\n }}\n </time>\n <span\n v-if=\"getMessageDuration(message)\"\n class=\"flex items-center gap-4\">\n <VvIcon name=\"ri:hourglass-line\" />\n {{ getMessageDuration(message) }}\n </span>\n </div>\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 name=\"ri:ai-generate-2-line\" />\n {{\n $n(message.metadata.totalTokens, 'integer')\n }}\n {{ $t('label.tokens') }}\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\" />\n {{\n $d(\n new Date(message.metadata?.createdAt),\n 'date-time',\n )\n }}\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 name=\"ri:hourglass-line\" />\n <PkRelativeTime\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 <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=\"activeMessage?.role === 'user' || isError\"\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 scrollbar-width: thin;\n scrollbar-color: var(--color-word-5) var(--color-surface-1);\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)\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\n &__spacer {\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\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 .pk-chatbot-divider {\n position: relative;\n border-bottom: 1px solid var(--color-surface-3);\n\n &__label {\n position: absolute;\n padding-inline: var(--spacing-8);\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background-color: var(--color-surface);\n color: var(--color-word-4);\n font-size: var(--text-12);\n }\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 overflow: hidden;\n max-width: 100%;\n background-color: var(--color-surface);\n // The anchor spacer (inline min-height on the last assistant\n // message) settles smoothly when released at the end of the stream\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n\n &__text {\n color: var(--color-word-2);\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 }\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 }\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 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-4));\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-4));\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 PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkChatbotError from './PkChatbotError.vue'\n import PkChatbotFeedbackForm from './PkChatbotFeedbackForm.vue'\n import PkChatbotFilePreview from './PkChatbotFilePreview.vue'\n import PkRelativeTime from '../PkRelativeTime.vue'\n import { useI18n } from 'vue-i18n'\n import {\n useTemplateRef,\n ref,\n watch,\n computed,\n nextTick,\n useSlots,\n } from 'vue'\n import { toolComponentMap } from './toolComponentMap'\n import PkChatbotSteps from './PkChatbotSteps.vue'\n import {\n resolveContrastColor,\n getPartState,\n isTextPart,\n isFilePart,\n isToolPart,\n isReasoningPart,\n isStreamingPart,\n getPartIcon,\n getToolPartLabel,\n mergeConsecutiveTextParts,\n formatDuration,\n } from './utils'\n import { getToolPartName, toKebabCase } from 'utils'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotOutlineRail from './PkChatbotOutlineRail.vue'\n import { useChatScroll } from './useChatScroll'\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 disableHeightAdjustment?: boolean\n showMessageDateTime?: boolean\n showMessageTokensCount?: boolean\n showAllMessageParts?: boolean\n showExtendedSteps?: 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\n const {\n t: $t,\n d: $d,\n n: $n,\n } = useI18n({\n useScope: 'global',\n })\n\n const slots = useSlots()\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 messagesElRefs = useTemplateRef<HTMLDivElement[]>('messagesEl')\n\n // ScrollTop that rests the last user message at the top of the content\n // area: the streaming auto-follow stops there so the freshly sent\n // message never slides under the page header. Only applied when the\n // scroll-to-bottom button is available as the affordance to go past it.\n const lastUserMessageScrollLimit = () => {\n const scroller = scrollEl.value\n const lastUserMessageIndex = props.messages\n ?.map((message) => message.role)\n .lastIndexOf('user')\n const messageEl = messagesElRefs.value?.[lastUserMessageIndex ?? -1]\n if (!props.showScrollToBottom || !scroller || !messageEl) {\n return undefined\n }\n return (\n scroller.scrollTop +\n messageEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top -\n Number.parseFloat(getComputedStyle(scroller).paddingTop)\n )\n }\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 autoScrollLimit: lastUserMessageScrollLimit,\n contentEl: () => wrapperEl.value,\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 const outlineItems = computed(() =>\n (props.messages ?? [])\n .filter((message) => message.role === 'user')\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('.pk-chatbot-message--user') ?? []\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 // --- Height adjustment ---\n\n const height = ref<number>(0)\n\n // Recalculate when a message is added or the conversation changes (the\n // store recreates `messages` on every streamed token, so watching the\n // array reference would re-run this on each character). The height only\n // depends on the last user message height, which is stable while\n // streaming.\n watch(\n () => [\n props.messages?.length,\n props.messages?.[props.messages.length - 1]?.id,\n ],\n 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' ||\n props.status === 'submitted' ||\n props.status === 'streaming'\n if (!isExchangeInFlight) {\n return\n }\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 const { paddingTop, paddingBottom } = getComputedStyle(\n scrollEl.value,\n )\n const scrollElHeight =\n scrollEl.value.clientHeight -\n Number.parseFloat(paddingTop) -\n Number.parseFloat(paddingBottom)\n const lastUserMessageIndex = messages\n .map((message) => message.role)\n .lastIndexOf('user')\n const lastUserMessageHeight =\n messagesElRefs.value?.[lastUserMessageIndex]?.clientHeight ?? 0\n const newHeight = scrollElHeight - lastUserMessageHeight\n if (newHeight > 0) {\n height.value = newHeight\n }\n },\n )\n\n // Release the spacer once the exchange is over: 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 watch(\n () => props.status,\n (status) => {\n if (status === 'ready' || status === 'error') {\n height.value = 0\n }\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 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(message.id, mergeConsecutiveTextParts(message.parts))\n }\n }\n return map\n })\n const getMergedParts = (message: UIChatMessage) => {\n return mergedPartsMap.value.get(message.id) ?? message.parts\n }\n const isLoading = computed(() => {\n return props.status === 'submitted' || props.status === 'streaming'\n })\n const isError = computed(() => props.status === 'error')\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 replaces the compact activity indicator as\n // soon as the message has at least one step to show.\n const showStepsTimeline = (message: UIChatMessage) =>\n Boolean(props.showExtendedSteps) &&\n isAssistant(message) &&\n messageHasSteps(message)\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 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.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\"\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 <div\n v-if=\"isUser(message) && getDividerDate(index)\"\n class=\"pk-chatbot-divider\">\n <span class=\"pk-chatbot-divider__label\">\n {{ $d(getDividerDate(index)!, 'short') }}\n </span>\n </div>\n <div\n v-if=\"\n message.parts.length ||\n (isLastMessage(index) &&\n isLoading &&\n isAssistant(message))\n \"\n ref=\"messagesEl\"\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 :is-dark />\n </transition>\n <template\n v-for=\"(part, partIndex) in getMergedParts(message)\"\n :key=\"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 </div>\n </transition>\n <transition\n v-else-if=\"isFilePart(part)\"\n appear\n name=\"pk-chatbot-part\">\n <PkChatbotFilePreview\n :media-type=\"part.mediaType\"\n :url=\"part.url\"\n :filename=\"part.filename\" />\n </transition>\n <transition\n v-else-if=\"\n isToolPart(part) &&\n !isStreamingPart(part) &&\n (toolComponentMap[getToolPartName(part)] ||\n slots[part.type] ||\n showAllMessageParts)\n \"\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 <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 <code class=\"font-mono rounded text-12\">\n {{ toKebabCase(part.type) }}\n </code>\n </div>\n </div>\n </slot>\n </transition>\n </template>\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 \"\n :key=\"`loading-info-${message.id}`\"\n class=\"pk-chatbot-message__loading-info\">\n <div class=\"flex gap-8 items-center\">\n <VvIcon\n name=\"line-md:loading-loop\"\n 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 <transition name=\"fade-in\" mode=\"out-in\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"\n activeMessageLastPart &&\n 'text' in activeMessageLastPart &&\n activeMessageLastPart.text.trim()\n \"\n :markdown=\"activeMessageLastPart.text\"\n :is-dark\n inner-class=\"wysiwyg\"\n class=\"border border-surface-4 rounded 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 modifiers=\"compact\" class=\"mr-auto\">\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': isRevised(message.id),\n }\"\n @click.stop=\"\n $emit('revise', message)\n \" />\n </transition>\n <transition mode=\"out-in\">\n <VvButton\n v-if=\"\n isActionButtonVisible(\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 isActionButtonVisible(\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 isActionButtonVisible(\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\">\n <VvIcon name=\"ri:time-line\" />\n {{\n formatMessageTime(\n message.metadata.createdAt,\n )\n }}\n </time>\n <span\n v-if=\"getMessageDuration(message)\"\n class=\"flex items-center gap-4\">\n <VvIcon name=\"ri:hourglass-line\" />\n {{ getMessageDuration(message) }}\n </span>\n </div>\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 name=\"ri:ai-generate-2-line\" />\n {{\n $n(message.metadata.totalTokens, 'integer')\n }}\n {{ $t('label.tokens') }}\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\" />\n {{\n $d(\n new Date(message.metadata?.createdAt),\n 'date-time',\n )\n }}\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 name=\"ri:hourglass-line\" />\n <PkRelativeTime\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 <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=\"activeMessage?.role === 'user' || isError\"\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 scrollbar-width: thin;\n scrollbar-color: var(--color-word-5) var(--color-surface-1);\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)\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\n &__spacer {\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\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 .pk-chatbot-divider {\n position: relative;\n border-bottom: 1px solid var(--color-surface-3);\n\n &__label {\n position: absolute;\n padding-inline: var(--spacing-8);\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background-color: var(--color-surface);\n color: var(--color-word-4);\n font-size: var(--text-12);\n }\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 overflow: hidden;\n max-width: 100%;\n background-color: var(--color-surface);\n // The anchor spacer (inline min-height on the last assistant\n // message) settles smoothly when released at the end of the stream\n transition: min-height 300ms var(--ease-out);\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n\n &__text {\n color: var(--color-word-2);\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 }\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 }\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 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-4));\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-4));\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"],"mappings":"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"}