@8wave/ai-elements 0.101.0-beta.4 → 0.101.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-elements.es.js +7 -5
- package/dist/ai-elements.es.js.map +1 -1
- package/dist-vue/PkChatbot.js +1 -1
- package/dist-vue/PkChatbotMessages.js +1 -1
- package/dist-vue/PkChatbotStepToolDetail.js +1 -1
- package/dist-vue/PkChatbotSteps.js +1 -1
- package/dist-vue/PkChatbotViewChat.js +1 -1
- package/dist-vue/_chunks/{PkChatbot-BdpdK0BB.js → PkChatbot-1Y7fGR-C.js} +3 -3
- package/dist-vue/_chunks/{PkChatbot-BdpdK0BB.js.map → PkChatbot-1Y7fGR-C.js.map} +1 -1
- package/dist-vue/_chunks/{PkChatbotMessages-D2hNzIaL.js → PkChatbotMessages-C6X92Igt.js} +4 -4
- package/dist-vue/_chunks/{PkChatbotMessages-D2hNzIaL.js.map → PkChatbotMessages-C6X92Igt.js.map} +1 -1
- package/dist-vue/_chunks/{PkChatbotStepToolDetail-CtGrjHO0.js → PkChatbotStepToolDetail-BBX4KL3d.js} +2 -2
- package/dist-vue/_chunks/{PkChatbotStepToolDetail-CtGrjHO0.js.map → PkChatbotStepToolDetail-BBX4KL3d.js.map} +1 -1
- package/dist-vue/_chunks/{PkChatbotSteps-Bbuh0hgL.js → PkChatbotSteps-DlZpwc_a.js} +3 -3
- package/dist-vue/_chunks/{PkChatbotSteps-Bbuh0hgL.js.map → PkChatbotSteps-DlZpwc_a.js.map} +1 -1
- package/dist-vue/_chunks/{PkChatbotViewChat-PYrrnkjN.js → PkChatbotViewChat-CBvcSg0J.js} +4 -4
- package/dist-vue/_chunks/{PkChatbotViewChat-PYrrnkjN.js.map → PkChatbotViewChat-CBvcSg0J.js.map} +1 -1
- package/dist-vue/_chunks/{PkToolShowDiagram-kbwsonk0.js → PkToolShowDiagram-BvOy4aeQ.js} +2 -2
- package/dist-vue/_chunks/{PkToolShowDiagram-kbwsonk0.js.map → PkToolShowDiagram-BvOy4aeQ.js.map} +1 -1
- package/dist-vue/_chunks/{utils-Bf73GiBm.js → utils-CgxndZld.js} +2 -2
- package/dist-vue/_chunks/{utils-Bf73GiBm.js.map → utils-CgxndZld.js.map} +1 -1
- package/dist-vue/composables.js +1 -1
- package/dist-vue/index.js +10 -20
- package/dist-vue/index.js.map +1 -1
- package/dist-vue/locales.js +2 -0
- package/package.json +1 -1
package/dist-vue/_chunks/{PkChatbotStepToolDetail-CtGrjHO0.js → PkChatbotStepToolDetail-BBX4KL3d.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as e, n as t } from "./utils-
|
|
1
|
+
import { _ as e, n as t } from "./utils-CgxndZld.js";
|
|
2
2
|
import { VvIcon as n } from "@volverjs/ui-vue/components";
|
|
3
3
|
import { Fragment as r, computed as i, createBlock as a, createCommentVNode as o, createElementBlock as s, createElementVNode as c, createVNode as l, defineComponent as u, normalizeStyle as d, openBlock as f, reactive as p, renderList as m, resolveDynamicComponent as h, toDisplayString as g, unref as _, withCtx as v, withModifiers as y } from "vue";
|
|
4
4
|
import { useI18n as ee } from "vue-i18n";
|
|
@@ -235,4 +235,4 @@ var te = { class: "pk-chatbot-step-detail" }, ne = {
|
|
|
235
235
|
//#endregion
|
|
236
236
|
export { R as t };
|
|
237
237
|
|
|
238
|
-
//# sourceMappingURL=PkChatbotStepToolDetail-
|
|
238
|
+
//# sourceMappingURL=PkChatbotStepToolDetail-BBX4KL3d.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PkChatbotStepToolDetail-CtGrjHO0.js","names":[],"sources":["../../../../packages/components/src/chat/PkChatbotStepToolDetail.vue","../../../../packages/components/src/chat/PkChatbotStepToolDetail.vue"],"sourcesContent":["<script lang=\"ts\" setup>\n import { computed, reactive } from 'vue'\n import { useI18n } from 'vue-i18n'\n import type { UIChatMessage } from 'models'\n import { stringifyPayload, WEB_SEARCH_TOOL_TYPES } from './utils'\n\n type MessagePart = UIChatMessage['parts'][number]\n\n interface DetailHit {\n key: string\n icon: string\n title: string\n url?: string\n meta?: string\n relevance?: number\n body?: string\n }\n\n interface Detail {\n chips?: string[]\n fact?: { icon: string; text: string }\n hits?: DetailHit[]\n showEmpty?: boolean\n passage?: { title?: string; meta?: string; content: string }\n rawInput?: string\n rawOutput?: string\n }\n\n const props = defineProps<{\n part: MessagePart\n }>()\n\n const { t: $t } = useI18n({\n useScope: 'global',\n })\n\n const asRecord = (value: unknown): Record<string, unknown> | undefined =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined\n\n const pickString = (\n record: Record<string, unknown> | undefined,\n ...keys: string[]\n ): string | undefined => {\n for (const key of keys) {\n const value = record?.[key]\n if (typeof value === 'string' && value.trim()) {\n return value\n }\n }\n return undefined\n }\n\n const formatCoordinates = (\n record?: Record<string, unknown>,\n ): string | undefined => {\n const latitude = record?.latitude\n const longitude = record?.longitude\n if (typeof latitude !== 'number' || typeof longitude !== 'number') {\n return undefined\n }\n return `${latitude.toFixed(4)}, ${longitude.toFixed(4)}`\n }\n\n const rawInputValue = computed(() =>\n 'input' in props.part ? props.part.input : undefined,\n )\n const rawOutputValue = computed(() =>\n 'output' in props.part ? props.part.output : undefined,\n )\n const input = computed(() => asRecord(rawInputValue.value))\n const output = computed(() => asRecord(rawOutputValue.value))\n\n // Search queries shown as chips (queries[], query or objective)\n const queryChips = computed<string[]>(() => {\n const raw =\n input.value?.queries ?? input.value?.query ?? input.value?.objective\n if (typeof raw === 'string' && raw.trim()) {\n return [raw]\n }\n if (Array.isArray(raw)) {\n return raw.filter(\n (query): query is string =>\n typeof query === 'string' && Boolean(query.trim()),\n )\n }\n return []\n })\n\n // Raw scores live on different scales depending on the agent search mode\n // (RRF ~0.03, cosine 0 to 1) and tend to cluster, so a plain ratio on the\n // max renders every bar as ~100%. Spread them on a min/max scale with a\n // floor so the ordering stays visible, and hide the bar entirely when it\n // carries no information (single hit or identical scores).\n const spreadRelevance = (scores: number[]): (number | undefined)[] => {\n const min = Math.min(...scores)\n const max = Math.max(...scores)\n if (scores.length < 2 || max <= min) {\n return scores.map(() => undefined)\n }\n const floor = 0.15\n return scores.map(\n (score) => floor + ((score - min) / (max - min)) * (1 - floor),\n )\n }\n\n // Normalizes a raw result array into records paired with a spread\n // relevance derived from each record's `similarity` score.\n const scoredRecords = (\n raw: unknown,\n ):\n | { record: Record<string, unknown>; relevance?: number }[]\n | undefined => {\n if (!Array.isArray(raw)) {\n return undefined\n }\n const records = raw\n .map((item) => asRecord(item))\n .filter((record): record is Record<string, unknown> =>\n Boolean(record),\n )\n const relevances = spreadRelevance(\n records.map((record) =>\n typeof record.similarity === 'number' ? record.similarity : 0,\n ),\n )\n return records.map((record, index) => ({\n record,\n relevance: relevances[index],\n }))\n }\n\n // searchInKnowledgeBase: group chunks by document, relevance compares the\n // best chunk score of each document within the result set.\n const knowledgeHits = computed<DetailHit[] | undefined>(() => {\n const results = output.value?.results\n if (!Array.isArray(results)) {\n return undefined\n }\n interface DocumentGroup {\n id: string\n type: string\n title: string\n url?: string\n similarity: number\n chunks: string[]\n }\n const groups = new Map<string, DocumentGroup>()\n for (const [index, raw] of results.entries()) {\n const result = asRecord(raw)\n if (!result) {\n continue\n }\n const document = asRecord(result.document)\n const source = asRecord(document?.source)\n const id =\n typeof document?.id === 'string' ? document.id : `doc-${index}`\n const similarity =\n typeof result.similarity === 'number' ? result.similarity : 0\n let group = groups.get(id)\n if (!group) {\n group = {\n id,\n type:\n typeof document?.type === 'string' ? document.type : '',\n title:\n pickString(document, 'name') ??\n pickString(source, 'filename', 'url') ??\n $t('messagePart.stepDocument'),\n url: pickString(source, 'url'),\n similarity,\n chunks: [],\n }\n groups.set(id, group)\n }\n group.similarity = Math.max(group.similarity, similarity)\n if (typeof result.content === 'string' && result.content.trim()) {\n group.chunks.push(result.content)\n }\n }\n const list = [...groups.values()]\n const relevances = spreadRelevance(list.map((g) => g.similarity))\n return list.map((group, index) => ({\n key: group.id,\n icon: group.type === 'url' ? 'ri:global-line' : 'ri:file-text-line',\n title: group.title,\n url: group.url,\n meta: $t('messagePart.stepExcerpts', group.chunks.length || 1),\n relevance: relevances[index],\n body: group.chunks.join('\\n\\n· · ·\\n\\n') || undefined,\n }))\n })\n\n // retrieveRevisedAnswers: one hit per matched question, the corrected\n // answer is the expandable body.\n const revisedAnswerHits = computed<DetailHit[] | undefined>(() =>\n scoredRecords(output.value?.results)?.map(\n ({ record, relevance }, index) => ({\n key:\n pickString(record, 'revisedAnswerId') ?? `revised-${index}`,\n icon: 'ri:file-edit-line',\n title:\n pickString(record, 'userQuestion') ??\n $t('messagePart.revisedAnswers'),\n relevance,\n body: pickString(record, 'revisedMessage'),\n }),\n ),\n )\n\n // recallUserMemory / saveUserMemory delete candidates: one hit per\n // memory, long facts expand to their full text.\n const memoryHits = (raw: unknown): DetailHit[] | undefined =>\n scoredRecords(raw)?.map(({ record, relevance }, index) => {\n const content = pickString(record, 'content') ?? ''\n return {\n key: pickString(record, 'id') ?? `memory-${index}`,\n icon: 'ri:brain-line',\n title: content,\n meta: pickString(record, 'updatedAt')?.slice(0, 10),\n relevance,\n body: content.length > 120 ? content : undefined,\n }\n })\n\n // Query chips over a list of hits, flagging an explicit empty state so the\n // \"no results\" message shows once the tool has run (hits defined, length 0).\n const hitsDetail = (hits: DetailHit[] | undefined): Detail => ({\n chips: queryChips.value,\n hits,\n showEmpty: hits !== undefined && !hits.length,\n })\n\n // Rich, tool-specific view of the step. Returns {} when the tool has\n // nothing meaningful to show yet (the raw fallback is applied by `detail`).\n const buildDetail = (): Detail => {\n const type = props.part.type\n if (type === 'tool-searchInKnowledgeBase') {\n return hitsDetail(knowledgeHits.value)\n }\n if (type === 'tool-retrieveRevisedAnswers') {\n return hitsDetail(revisedAnswerHits.value)\n }\n if (type === 'tool-expandChunkContext') {\n const content = pickString(output.value, 'content')\n if (!content) {\n return {}\n }\n const totalChunks = output.value?.totalChunksReturned\n return {\n passage: {\n title: pickString(output.value, 'documentName'),\n meta:\n typeof totalChunks === 'number'\n ? $t('messagePart.stepExcerpts', totalChunks)\n : undefined,\n content,\n },\n }\n }\n if (type === 'tool-readDocumentFile') {\n const name = pickString(output.value, 'name')\n if (!name) {\n return {}\n }\n const mimeType = pickString(output.value, 'mimeType')\n return {\n fact: {\n icon: 'ri:file-text-line',\n text: mimeType ? `${name} (${mimeType})` : name,\n },\n }\n }\n if (type === 'tool-getWeather') {\n const location =\n pickString(output.value, 'location') ??\n pickString(input.value, 'locationName') ??\n formatCoordinates(input.value)\n if (!location) {\n return {}\n }\n const startDate = pickString(input.value, 'startDate')\n const endDate = pickString(input.value, 'endDate')\n const dateRange = startDate\n ? endDate && endDate !== startDate\n ? ` · ${startDate} → ${endDate}`\n : ` · ${startDate}`\n : ''\n return {\n fact: {\n icon: 'ri:sun-cloudy-line',\n text: `${location}${dateRange}`,\n },\n }\n }\n if (type === 'tool-geocode') {\n const from =\n pickString(input.value, 'locationName') ??\n formatCoordinates(input.value)\n const to =\n pickString(\n output.value,\n 'name',\n 'displayName',\n 'locationName',\n 'label',\n ) ?? formatCoordinates(output.value)\n const text =\n from && to && from !== to ? `${from} → ${to}` : (from ?? to)\n if (!text) {\n return {}\n }\n return {\n fact: {\n icon: 'ri:map-pin-line',\n text,\n },\n }\n }\n if (type === 'tool-recallUserMemory') {\n return hitsDetail(memoryHits(output.value?.memories))\n }\n if (type === 'tool-saveUserMemory') {\n const candidates = memoryHits(output.value?.candidates)\n if (candidates) {\n return hitsDetail(candidates)\n }\n const text =\n pickString(input.value, 'content') ??\n pickString(output.value, 'message', 'error')\n if (!text) {\n return {}\n }\n return {\n fact: {\n icon:\n pickString(input.value, 'action') === 'delete'\n ? 'ri:delete-bin-line'\n : 'ri:save-3-line',\n text,\n },\n }\n }\n if (WEB_SEARCH_TOOL_TYPES.includes(type)) {\n return {\n chips: queryChips.value,\n rawOutput: stringifyPayload(rawOutputValue.value),\n }\n }\n return {}\n }\n\n const detail = computed<Detail>(() => {\n const result = buildDetail()\n // Nothing meaningful to show for this tool yet (e.g. still running):\n // fall back to the raw payloads so the step body is never empty.\n const hasRichContent =\n result.chips?.length ||\n result.fact ||\n result.passage ||\n result.showEmpty ||\n result.hits?.length\n if (!hasRichContent) {\n result.rawInput = stringifyPayload(rawInputValue.value)\n result.rawOutput ??= stringifyPayload(rawOutputValue.value)\n }\n return result\n })\n\n const expandedHits = reactive(new Set<string>())\n const toggleHit = (key: string) => {\n if (expandedHits.has(key)) {\n expandedHits.delete(key)\n } else {\n expandedHits.add(key)\n }\n }\n</script>\n\n<template>\n <div class=\"pk-chatbot-step-detail\">\n <ul v-if=\"detail.chips?.length\" class=\"pk-chatbot-step-detail__chips\">\n <li\n v-for=\"(chip, chipIndex) in detail.chips\"\n :key=\"`${chipIndex}-${chip}`\"\n class=\"pk-chatbot-step-detail__chip\">\n <VvIcon name=\"ri:search-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-step-detail__chip-text\">\n {{ chip }}\n </span>\n </li>\n </ul>\n <div v-if=\"detail.fact\" class=\"pk-chatbot-step-detail__fact\">\n <VvIcon :name=\"detail.fact.icon\" class=\"shrink-0\" />\n <span>{{ detail.fact.text }}</span>\n </div>\n <div v-if=\"detail.showEmpty\" class=\"pk-chatbot-step-detail__empty\">\n {{ $t('messagePart.stepNoResults') }}\n </div>\n <ul\n v-else-if=\"detail.hits?.length\"\n class=\"pk-chatbot-step-detail__hits\">\n <li v-for=\"hit in detail.hits\" :key=\"hit.key\">\n <component\n :is=\"hit.body ? 'button' : 'div'\"\n :type=\"hit.body ? 'button' : undefined\"\n class=\"pk-chatbot-step-detail__hit-header\"\n :aria-expanded=\"\n hit.body ? expandedHits.has(hit.key) : undefined\n \"\n @click=\"hit.body ? toggleHit(hit.key) : undefined\">\n <VvIcon\n :name=\"hit.icon\"\n class=\"pk-chatbot-step-detail__hit-icon\" />\n <span class=\"pk-chatbot-step-detail__hit-heading\">\n <span class=\"pk-chatbot-step-detail__hit-title\">\n {{ hit.title }}\n </span>\n <a\n v-if=\"hit.url\"\n :href=\"hit.url\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"pk-chatbot-step-detail__link\"\n @click.stop>\n <VvIcon name=\"ri:external-link-line\" />\n </a>\n </span>\n <span\n v-if=\"hit.meta\"\n class=\"pk-chatbot-step-detail__hit-meta\">\n {{ hit.meta }}\n </span>\n <span\n v-if=\"hit.relevance !== undefined\"\n class=\"pk-chatbot-step-detail__relevance\"\n aria-hidden=\"true\">\n <span\n class=\"pk-chatbot-step-detail__relevance-fill\"\n :style=\"{\n width: `${Math.round(hit.relevance * 100)}%`,\n }\" />\n </span>\n <VvIcon\n v-if=\"hit.body\"\n :name=\"\n expandedHits.has(hit.key)\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"shrink-0\" />\n </component>\n <div\n v-if=\"hit.body && expandedHits.has(hit.key)\"\n class=\"pk-chatbot-step-detail__hit-body\">\n {{ hit.body }}\n </div>\n </li>\n </ul>\n <div v-if=\"detail.passage\" class=\"pk-chatbot-step-detail__passage\">\n <div class=\"pk-chatbot-step-detail__passage-header\">\n <VvIcon name=\"ri:file-text-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-step-detail__hit-heading\">\n <span class=\"pk-chatbot-step-detail__hit-title\">\n {{\n detail.passage.title ??\n $t('messagePart.stepDocument')\n }}\n </span>\n </span>\n <span\n v-if=\"detail.passage.meta\"\n class=\"pk-chatbot-step-detail__hit-meta\">\n {{ detail.passage.meta }}\n </span>\n </div>\n <div class=\"pk-chatbot-step-detail__passage-content\">\n {{ detail.passage.content }}\n </div>\n </div>\n <div v-if=\"detail.rawInput\" class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepInput') }}\n </span>\n <pre>{{ detail.rawInput }}</pre>\n </div>\n <div v-if=\"detail.rawOutput\" class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepOutput') }}\n </span>\n <pre>{{ detail.rawOutput }}</pre>\n </div>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-step-detail {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n color: var(--color-word-3);\n\n &__chips {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-4);\n }\n\n &__chip {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-4);\n max-width: 100%;\n padding: var(--spacing-2) var(--spacing-8);\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-1);\n color: var(--color-word-2);\n }\n\n &__chip-text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n &__fact {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n color: var(--color-word-2);\n }\n\n &__empty {\n color: var(--color-word-4);\n font-style: italic;\n }\n\n &__hits {\n display: flex;\n flex-direction: column;\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n background-color: var(--color-surface-1);\n overflow: hidden;\n\n > li + li {\n border-top: var(--spacing-px) solid var(--color-surface-3);\n }\n }\n\n &__hit-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: 100%;\n min-height: var(--spacing-32);\n padding: var(--spacing-4) var(--spacing-8);\n text-align: left;\n\n &:is(button) {\n cursor: pointer;\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n &__hit-icon {\n flex-shrink: 0;\n color: var(--color-word-3);\n }\n\n // Grows to fill the row so meta, relevance and chevron stay aligned;\n // the title inside only shrinks, keeping the link icon next to the\n // title text instead of pushed to the right edge.\n &__hit-heading {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n flex: 1 1 0;\n min-width: 0;\n }\n\n &__hit-title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-2);\n }\n\n &__hit-meta {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__relevance {\n flex-shrink: 0;\n width: var(--spacing-40);\n height: var(--spacing-4);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-3);\n overflow: hidden;\n }\n\n &__relevance-fill {\n display: block;\n height: 100%;\n border-radius: inherit;\n background-color: var(--chatbot-main-color, var(--color-word-4));\n }\n\n &__link {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--color-word-4);\n\n &:hover {\n color: var(--color-word-1);\n }\n }\n\n &__hit-body,\n &__passage-content {\n padding: var(--spacing-8);\n max-height: var(--spacing-160);\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--color-word-3);\n }\n\n &__hit-body {\n border-top: var(--spacing-px) solid var(--color-surface-3);\n }\n\n &__passage {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n background-color: var(--color-surface-1);\n overflow: hidden;\n }\n\n &__passage-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n min-height: var(--spacing-32);\n padding: var(--spacing-4) var(--spacing-8);\n border-bottom: var(--spacing-px) solid var(--color-surface-3);\n }\n }\n</style>\n","<script lang=\"ts\" setup>\n import { computed, reactive } from 'vue'\n import { useI18n } from 'vue-i18n'\n import type { UIChatMessage } from 'models'\n import { stringifyPayload, WEB_SEARCH_TOOL_TYPES } from './utils'\n\n type MessagePart = UIChatMessage['parts'][number]\n\n interface DetailHit {\n key: string\n icon: string\n title: string\n url?: string\n meta?: string\n relevance?: number\n body?: string\n }\n\n interface Detail {\n chips?: string[]\n fact?: { icon: string; text: string }\n hits?: DetailHit[]\n showEmpty?: boolean\n passage?: { title?: string; meta?: string; content: string }\n rawInput?: string\n rawOutput?: string\n }\n\n const props = defineProps<{\n part: MessagePart\n }>()\n\n const { t: $t } = useI18n({\n useScope: 'global',\n })\n\n const asRecord = (value: unknown): Record<string, unknown> | undefined =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined\n\n const pickString = (\n record: Record<string, unknown> | undefined,\n ...keys: string[]\n ): string | undefined => {\n for (const key of keys) {\n const value = record?.[key]\n if (typeof value === 'string' && value.trim()) {\n return value\n }\n }\n return undefined\n }\n\n const formatCoordinates = (\n record?: Record<string, unknown>,\n ): string | undefined => {\n const latitude = record?.latitude\n const longitude = record?.longitude\n if (typeof latitude !== 'number' || typeof longitude !== 'number') {\n return undefined\n }\n return `${latitude.toFixed(4)}, ${longitude.toFixed(4)}`\n }\n\n const rawInputValue = computed(() =>\n 'input' in props.part ? props.part.input : undefined,\n )\n const rawOutputValue = computed(() =>\n 'output' in props.part ? props.part.output : undefined,\n )\n const input = computed(() => asRecord(rawInputValue.value))\n const output = computed(() => asRecord(rawOutputValue.value))\n\n // Search queries shown as chips (queries[], query or objective)\n const queryChips = computed<string[]>(() => {\n const raw =\n input.value?.queries ?? input.value?.query ?? input.value?.objective\n if (typeof raw === 'string' && raw.trim()) {\n return [raw]\n }\n if (Array.isArray(raw)) {\n return raw.filter(\n (query): query is string =>\n typeof query === 'string' && Boolean(query.trim()),\n )\n }\n return []\n })\n\n // Raw scores live on different scales depending on the agent search mode\n // (RRF ~0.03, cosine 0 to 1) and tend to cluster, so a plain ratio on the\n // max renders every bar as ~100%. Spread them on a min/max scale with a\n // floor so the ordering stays visible, and hide the bar entirely when it\n // carries no information (single hit or identical scores).\n const spreadRelevance = (scores: number[]): (number | undefined)[] => {\n const min = Math.min(...scores)\n const max = Math.max(...scores)\n if (scores.length < 2 || max <= min) {\n return scores.map(() => undefined)\n }\n const floor = 0.15\n return scores.map(\n (score) => floor + ((score - min) / (max - min)) * (1 - floor),\n )\n }\n\n // Normalizes a raw result array into records paired with a spread\n // relevance derived from each record's `similarity` score.\n const scoredRecords = (\n raw: unknown,\n ):\n | { record: Record<string, unknown>; relevance?: number }[]\n | undefined => {\n if (!Array.isArray(raw)) {\n return undefined\n }\n const records = raw\n .map((item) => asRecord(item))\n .filter((record): record is Record<string, unknown> =>\n Boolean(record),\n )\n const relevances = spreadRelevance(\n records.map((record) =>\n typeof record.similarity === 'number' ? record.similarity : 0,\n ),\n )\n return records.map((record, index) => ({\n record,\n relevance: relevances[index],\n }))\n }\n\n // searchInKnowledgeBase: group chunks by document, relevance compares the\n // best chunk score of each document within the result set.\n const knowledgeHits = computed<DetailHit[] | undefined>(() => {\n const results = output.value?.results\n if (!Array.isArray(results)) {\n return undefined\n }\n interface DocumentGroup {\n id: string\n type: string\n title: string\n url?: string\n similarity: number\n chunks: string[]\n }\n const groups = new Map<string, DocumentGroup>()\n for (const [index, raw] of results.entries()) {\n const result = asRecord(raw)\n if (!result) {\n continue\n }\n const document = asRecord(result.document)\n const source = asRecord(document?.source)\n const id =\n typeof document?.id === 'string' ? document.id : `doc-${index}`\n const similarity =\n typeof result.similarity === 'number' ? result.similarity : 0\n let group = groups.get(id)\n if (!group) {\n group = {\n id,\n type:\n typeof document?.type === 'string' ? document.type : '',\n title:\n pickString(document, 'name') ??\n pickString(source, 'filename', 'url') ??\n $t('messagePart.stepDocument'),\n url: pickString(source, 'url'),\n similarity,\n chunks: [],\n }\n groups.set(id, group)\n }\n group.similarity = Math.max(group.similarity, similarity)\n if (typeof result.content === 'string' && result.content.trim()) {\n group.chunks.push(result.content)\n }\n }\n const list = [...groups.values()]\n const relevances = spreadRelevance(list.map((g) => g.similarity))\n return list.map((group, index) => ({\n key: group.id,\n icon: group.type === 'url' ? 'ri:global-line' : 'ri:file-text-line',\n title: group.title,\n url: group.url,\n meta: $t('messagePart.stepExcerpts', group.chunks.length || 1),\n relevance: relevances[index],\n body: group.chunks.join('\\n\\n· · ·\\n\\n') || undefined,\n }))\n })\n\n // retrieveRevisedAnswers: one hit per matched question, the corrected\n // answer is the expandable body.\n const revisedAnswerHits = computed<DetailHit[] | undefined>(() =>\n scoredRecords(output.value?.results)?.map(\n ({ record, relevance }, index) => ({\n key:\n pickString(record, 'revisedAnswerId') ?? `revised-${index}`,\n icon: 'ri:file-edit-line',\n title:\n pickString(record, 'userQuestion') ??\n $t('messagePart.revisedAnswers'),\n relevance,\n body: pickString(record, 'revisedMessage'),\n }),\n ),\n )\n\n // recallUserMemory / saveUserMemory delete candidates: one hit per\n // memory, long facts expand to their full text.\n const memoryHits = (raw: unknown): DetailHit[] | undefined =>\n scoredRecords(raw)?.map(({ record, relevance }, index) => {\n const content = pickString(record, 'content') ?? ''\n return {\n key: pickString(record, 'id') ?? `memory-${index}`,\n icon: 'ri:brain-line',\n title: content,\n meta: pickString(record, 'updatedAt')?.slice(0, 10),\n relevance,\n body: content.length > 120 ? content : undefined,\n }\n })\n\n // Query chips over a list of hits, flagging an explicit empty state so the\n // \"no results\" message shows once the tool has run (hits defined, length 0).\n const hitsDetail = (hits: DetailHit[] | undefined): Detail => ({\n chips: queryChips.value,\n hits,\n showEmpty: hits !== undefined && !hits.length,\n })\n\n // Rich, tool-specific view of the step. Returns {} when the tool has\n // nothing meaningful to show yet (the raw fallback is applied by `detail`).\n const buildDetail = (): Detail => {\n const type = props.part.type\n if (type === 'tool-searchInKnowledgeBase') {\n return hitsDetail(knowledgeHits.value)\n }\n if (type === 'tool-retrieveRevisedAnswers') {\n return hitsDetail(revisedAnswerHits.value)\n }\n if (type === 'tool-expandChunkContext') {\n const content = pickString(output.value, 'content')\n if (!content) {\n return {}\n }\n const totalChunks = output.value?.totalChunksReturned\n return {\n passage: {\n title: pickString(output.value, 'documentName'),\n meta:\n typeof totalChunks === 'number'\n ? $t('messagePart.stepExcerpts', totalChunks)\n : undefined,\n content,\n },\n }\n }\n if (type === 'tool-readDocumentFile') {\n const name = pickString(output.value, 'name')\n if (!name) {\n return {}\n }\n const mimeType = pickString(output.value, 'mimeType')\n return {\n fact: {\n icon: 'ri:file-text-line',\n text: mimeType ? `${name} (${mimeType})` : name,\n },\n }\n }\n if (type === 'tool-getWeather') {\n const location =\n pickString(output.value, 'location') ??\n pickString(input.value, 'locationName') ??\n formatCoordinates(input.value)\n if (!location) {\n return {}\n }\n const startDate = pickString(input.value, 'startDate')\n const endDate = pickString(input.value, 'endDate')\n const dateRange = startDate\n ? endDate && endDate !== startDate\n ? ` · ${startDate} → ${endDate}`\n : ` · ${startDate}`\n : ''\n return {\n fact: {\n icon: 'ri:sun-cloudy-line',\n text: `${location}${dateRange}`,\n },\n }\n }\n if (type === 'tool-geocode') {\n const from =\n pickString(input.value, 'locationName') ??\n formatCoordinates(input.value)\n const to =\n pickString(\n output.value,\n 'name',\n 'displayName',\n 'locationName',\n 'label',\n ) ?? formatCoordinates(output.value)\n const text =\n from && to && from !== to ? `${from} → ${to}` : (from ?? to)\n if (!text) {\n return {}\n }\n return {\n fact: {\n icon: 'ri:map-pin-line',\n text,\n },\n }\n }\n if (type === 'tool-recallUserMemory') {\n return hitsDetail(memoryHits(output.value?.memories))\n }\n if (type === 'tool-saveUserMemory') {\n const candidates = memoryHits(output.value?.candidates)\n if (candidates) {\n return hitsDetail(candidates)\n }\n const text =\n pickString(input.value, 'content') ??\n pickString(output.value, 'message', 'error')\n if (!text) {\n return {}\n }\n return {\n fact: {\n icon:\n pickString(input.value, 'action') === 'delete'\n ? 'ri:delete-bin-line'\n : 'ri:save-3-line',\n text,\n },\n }\n }\n if (WEB_SEARCH_TOOL_TYPES.includes(type)) {\n return {\n chips: queryChips.value,\n rawOutput: stringifyPayload(rawOutputValue.value),\n }\n }\n return {}\n }\n\n const detail = computed<Detail>(() => {\n const result = buildDetail()\n // Nothing meaningful to show for this tool yet (e.g. still running):\n // fall back to the raw payloads so the step body is never empty.\n const hasRichContent =\n result.chips?.length ||\n result.fact ||\n result.passage ||\n result.showEmpty ||\n result.hits?.length\n if (!hasRichContent) {\n result.rawInput = stringifyPayload(rawInputValue.value)\n result.rawOutput ??= stringifyPayload(rawOutputValue.value)\n }\n return result\n })\n\n const expandedHits = reactive(new Set<string>())\n const toggleHit = (key: string) => {\n if (expandedHits.has(key)) {\n expandedHits.delete(key)\n } else {\n expandedHits.add(key)\n }\n }\n</script>\n\n<template>\n <div class=\"pk-chatbot-step-detail\">\n <ul v-if=\"detail.chips?.length\" class=\"pk-chatbot-step-detail__chips\">\n <li\n v-for=\"(chip, chipIndex) in detail.chips\"\n :key=\"`${chipIndex}-${chip}`\"\n class=\"pk-chatbot-step-detail__chip\">\n <VvIcon name=\"ri:search-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-step-detail__chip-text\">\n {{ chip }}\n </span>\n </li>\n </ul>\n <div v-if=\"detail.fact\" class=\"pk-chatbot-step-detail__fact\">\n <VvIcon :name=\"detail.fact.icon\" class=\"shrink-0\" />\n <span>{{ detail.fact.text }}</span>\n </div>\n <div v-if=\"detail.showEmpty\" class=\"pk-chatbot-step-detail__empty\">\n {{ $t('messagePart.stepNoResults') }}\n </div>\n <ul\n v-else-if=\"detail.hits?.length\"\n class=\"pk-chatbot-step-detail__hits\">\n <li v-for=\"hit in detail.hits\" :key=\"hit.key\">\n <component\n :is=\"hit.body ? 'button' : 'div'\"\n :type=\"hit.body ? 'button' : undefined\"\n class=\"pk-chatbot-step-detail__hit-header\"\n :aria-expanded=\"\n hit.body ? expandedHits.has(hit.key) : undefined\n \"\n @click=\"hit.body ? toggleHit(hit.key) : undefined\">\n <VvIcon\n :name=\"hit.icon\"\n class=\"pk-chatbot-step-detail__hit-icon\" />\n <span class=\"pk-chatbot-step-detail__hit-heading\">\n <span class=\"pk-chatbot-step-detail__hit-title\">\n {{ hit.title }}\n </span>\n <a\n v-if=\"hit.url\"\n :href=\"hit.url\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"pk-chatbot-step-detail__link\"\n @click.stop>\n <VvIcon name=\"ri:external-link-line\" />\n </a>\n </span>\n <span\n v-if=\"hit.meta\"\n class=\"pk-chatbot-step-detail__hit-meta\">\n {{ hit.meta }}\n </span>\n <span\n v-if=\"hit.relevance !== undefined\"\n class=\"pk-chatbot-step-detail__relevance\"\n aria-hidden=\"true\">\n <span\n class=\"pk-chatbot-step-detail__relevance-fill\"\n :style=\"{\n width: `${Math.round(hit.relevance * 100)}%`,\n }\" />\n </span>\n <VvIcon\n v-if=\"hit.body\"\n :name=\"\n expandedHits.has(hit.key)\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"shrink-0\" />\n </component>\n <div\n v-if=\"hit.body && expandedHits.has(hit.key)\"\n class=\"pk-chatbot-step-detail__hit-body\">\n {{ hit.body }}\n </div>\n </li>\n </ul>\n <div v-if=\"detail.passage\" class=\"pk-chatbot-step-detail__passage\">\n <div class=\"pk-chatbot-step-detail__passage-header\">\n <VvIcon name=\"ri:file-text-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-step-detail__hit-heading\">\n <span class=\"pk-chatbot-step-detail__hit-title\">\n {{\n detail.passage.title ??\n $t('messagePart.stepDocument')\n }}\n </span>\n </span>\n <span\n v-if=\"detail.passage.meta\"\n class=\"pk-chatbot-step-detail__hit-meta\">\n {{ detail.passage.meta }}\n </span>\n </div>\n <div class=\"pk-chatbot-step-detail__passage-content\">\n {{ detail.passage.content }}\n </div>\n </div>\n <div v-if=\"detail.rawInput\" class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepInput') }}\n </span>\n <pre>{{ detail.rawInput }}</pre>\n </div>\n <div v-if=\"detail.rawOutput\" class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepOutput') }}\n </span>\n <pre>{{ detail.rawOutput }}</pre>\n </div>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-step-detail {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n color: var(--color-word-3);\n\n &__chips {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-4);\n }\n\n &__chip {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-4);\n max-width: 100%;\n padding: var(--spacing-2) var(--spacing-8);\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-1);\n color: var(--color-word-2);\n }\n\n &__chip-text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n &__fact {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n color: var(--color-word-2);\n }\n\n &__empty {\n color: var(--color-word-4);\n font-style: italic;\n }\n\n &__hits {\n display: flex;\n flex-direction: column;\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n background-color: var(--color-surface-1);\n overflow: hidden;\n\n > li + li {\n border-top: var(--spacing-px) solid var(--color-surface-3);\n }\n }\n\n &__hit-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: 100%;\n min-height: var(--spacing-32);\n padding: var(--spacing-4) var(--spacing-8);\n text-align: left;\n\n &:is(button) {\n cursor: pointer;\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n &__hit-icon {\n flex-shrink: 0;\n color: var(--color-word-3);\n }\n\n // Grows to fill the row so meta, relevance and chevron stay aligned;\n // the title inside only shrinks, keeping the link icon next to the\n // title text instead of pushed to the right edge.\n &__hit-heading {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n flex: 1 1 0;\n min-width: 0;\n }\n\n &__hit-title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-2);\n }\n\n &__hit-meta {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__relevance {\n flex-shrink: 0;\n width: var(--spacing-40);\n height: var(--spacing-4);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-3);\n overflow: hidden;\n }\n\n &__relevance-fill {\n display: block;\n height: 100%;\n border-radius: inherit;\n background-color: var(--chatbot-main-color, var(--color-word-4));\n }\n\n &__link {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--color-word-4);\n\n &:hover {\n color: var(--color-word-1);\n }\n }\n\n &__hit-body,\n &__passage-content {\n padding: var(--spacing-8);\n max-height: var(--spacing-160);\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--color-word-3);\n }\n\n &__hit-body {\n border-top: var(--spacing-px) solid var(--color-surface-3);\n }\n\n &__passage {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n background-color: var(--color-surface-1);\n overflow: hidden;\n }\n\n &__passage-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n min-height: var(--spacing-32);\n padding: var(--spacing-4) var(--spacing-8);\n border-bottom: var(--spacing-px) solid var(--color-surface-3);\n }\n }\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PkChatbotStepToolDetail-BBX4KL3d.js","names":[],"sources":["../../../../packages/components/src/chat/PkChatbotStepToolDetail.vue","../../../../packages/components/src/chat/PkChatbotStepToolDetail.vue"],"sourcesContent":["<script lang=\"ts\" setup>\n import { computed, reactive } from 'vue'\n import { useI18n } from 'vue-i18n'\n import type { UIChatMessage } from 'models'\n import { stringifyPayload, WEB_SEARCH_TOOL_TYPES } from './utils'\n\n type MessagePart = UIChatMessage['parts'][number]\n\n interface DetailHit {\n key: string\n icon: string\n title: string\n url?: string\n meta?: string\n relevance?: number\n body?: string\n }\n\n interface Detail {\n chips?: string[]\n fact?: { icon: string; text: string }\n hits?: DetailHit[]\n showEmpty?: boolean\n passage?: { title?: string; meta?: string; content: string }\n rawInput?: string\n rawOutput?: string\n }\n\n const props = defineProps<{\n part: MessagePart\n }>()\n\n const { t: $t } = useI18n({\n useScope: 'global',\n })\n\n const asRecord = (value: unknown): Record<string, unknown> | undefined =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined\n\n const pickString = (\n record: Record<string, unknown> | undefined,\n ...keys: string[]\n ): string | undefined => {\n for (const key of keys) {\n const value = record?.[key]\n if (typeof value === 'string' && value.trim()) {\n return value\n }\n }\n return undefined\n }\n\n const formatCoordinates = (\n record?: Record<string, unknown>,\n ): string | undefined => {\n const latitude = record?.latitude\n const longitude = record?.longitude\n if (typeof latitude !== 'number' || typeof longitude !== 'number') {\n return undefined\n }\n return `${latitude.toFixed(4)}, ${longitude.toFixed(4)}`\n }\n\n const rawInputValue = computed(() =>\n 'input' in props.part ? props.part.input : undefined,\n )\n const rawOutputValue = computed(() =>\n 'output' in props.part ? props.part.output : undefined,\n )\n const input = computed(() => asRecord(rawInputValue.value))\n const output = computed(() => asRecord(rawOutputValue.value))\n\n // Search queries shown as chips (queries[], query or objective)\n const queryChips = computed<string[]>(() => {\n const raw =\n input.value?.queries ?? input.value?.query ?? input.value?.objective\n if (typeof raw === 'string' && raw.trim()) {\n return [raw]\n }\n if (Array.isArray(raw)) {\n return raw.filter(\n (query): query is string =>\n typeof query === 'string' && Boolean(query.trim()),\n )\n }\n return []\n })\n\n // Raw scores live on different scales depending on the agent search mode\n // (RRF ~0.03, cosine 0 to 1) and tend to cluster, so a plain ratio on the\n // max renders every bar as ~100%. Spread them on a min/max scale with a\n // floor so the ordering stays visible, and hide the bar entirely when it\n // carries no information (single hit or identical scores).\n const spreadRelevance = (scores: number[]): (number | undefined)[] => {\n const min = Math.min(...scores)\n const max = Math.max(...scores)\n if (scores.length < 2 || max <= min) {\n return scores.map(() => undefined)\n }\n const floor = 0.15\n return scores.map(\n (score) => floor + ((score - min) / (max - min)) * (1 - floor),\n )\n }\n\n // Normalizes a raw result array into records paired with a spread\n // relevance derived from each record's `similarity` score.\n const scoredRecords = (\n raw: unknown,\n ):\n | { record: Record<string, unknown>; relevance?: number }[]\n | undefined => {\n if (!Array.isArray(raw)) {\n return undefined\n }\n const records = raw\n .map((item) => asRecord(item))\n .filter((record): record is Record<string, unknown> =>\n Boolean(record),\n )\n const relevances = spreadRelevance(\n records.map((record) =>\n typeof record.similarity === 'number' ? record.similarity : 0,\n ),\n )\n return records.map((record, index) => ({\n record,\n relevance: relevances[index],\n }))\n }\n\n // searchInKnowledgeBase: group chunks by document, relevance compares the\n // best chunk score of each document within the result set.\n const knowledgeHits = computed<DetailHit[] | undefined>(() => {\n const results = output.value?.results\n if (!Array.isArray(results)) {\n return undefined\n }\n interface DocumentGroup {\n id: string\n type: string\n title: string\n url?: string\n similarity: number\n chunks: string[]\n }\n const groups = new Map<string, DocumentGroup>()\n for (const [index, raw] of results.entries()) {\n const result = asRecord(raw)\n if (!result) {\n continue\n }\n const document = asRecord(result.document)\n const source = asRecord(document?.source)\n const id =\n typeof document?.id === 'string' ? document.id : `doc-${index}`\n const similarity =\n typeof result.similarity === 'number' ? result.similarity : 0\n let group = groups.get(id)\n if (!group) {\n group = {\n id,\n type:\n typeof document?.type === 'string' ? document.type : '',\n title:\n pickString(document, 'name') ??\n pickString(source, 'filename', 'url') ??\n $t('messagePart.stepDocument'),\n url: pickString(source, 'url'),\n similarity,\n chunks: [],\n }\n groups.set(id, group)\n }\n group.similarity = Math.max(group.similarity, similarity)\n if (typeof result.content === 'string' && result.content.trim()) {\n group.chunks.push(result.content)\n }\n }\n const list = [...groups.values()]\n const relevances = spreadRelevance(list.map((g) => g.similarity))\n return list.map((group, index) => ({\n key: group.id,\n icon: group.type === 'url' ? 'ri:global-line' : 'ri:file-text-line',\n title: group.title,\n url: group.url,\n meta: $t('messagePart.stepExcerpts', group.chunks.length || 1),\n relevance: relevances[index],\n body: group.chunks.join('\\n\\n· · ·\\n\\n') || undefined,\n }))\n })\n\n // retrieveRevisedAnswers: one hit per matched question, the corrected\n // answer is the expandable body.\n const revisedAnswerHits = computed<DetailHit[] | undefined>(() =>\n scoredRecords(output.value?.results)?.map(\n ({ record, relevance }, index) => ({\n key:\n pickString(record, 'revisedAnswerId') ?? `revised-${index}`,\n icon: 'ri:file-edit-line',\n title:\n pickString(record, 'userQuestion') ??\n $t('messagePart.revisedAnswers'),\n relevance,\n body: pickString(record, 'revisedMessage'),\n }),\n ),\n )\n\n // recallUserMemory / saveUserMemory delete candidates: one hit per\n // memory, long facts expand to their full text.\n const memoryHits = (raw: unknown): DetailHit[] | undefined =>\n scoredRecords(raw)?.map(({ record, relevance }, index) => {\n const content = pickString(record, 'content') ?? ''\n return {\n key: pickString(record, 'id') ?? `memory-${index}`,\n icon: 'ri:brain-line',\n title: content,\n meta: pickString(record, 'updatedAt')?.slice(0, 10),\n relevance,\n body: content.length > 120 ? content : undefined,\n }\n })\n\n // Query chips over a list of hits, flagging an explicit empty state so the\n // \"no results\" message shows once the tool has run (hits defined, length 0).\n const hitsDetail = (hits: DetailHit[] | undefined): Detail => ({\n chips: queryChips.value,\n hits,\n showEmpty: hits !== undefined && !hits.length,\n })\n\n // Rich, tool-specific view of the step. Returns {} when the tool has\n // nothing meaningful to show yet (the raw fallback is applied by `detail`).\n const buildDetail = (): Detail => {\n const type = props.part.type\n if (type === 'tool-searchInKnowledgeBase') {\n return hitsDetail(knowledgeHits.value)\n }\n if (type === 'tool-retrieveRevisedAnswers') {\n return hitsDetail(revisedAnswerHits.value)\n }\n if (type === 'tool-expandChunkContext') {\n const content = pickString(output.value, 'content')\n if (!content) {\n return {}\n }\n const totalChunks = output.value?.totalChunksReturned\n return {\n passage: {\n title: pickString(output.value, 'documentName'),\n meta:\n typeof totalChunks === 'number'\n ? $t('messagePart.stepExcerpts', totalChunks)\n : undefined,\n content,\n },\n }\n }\n if (type === 'tool-readDocumentFile') {\n const name = pickString(output.value, 'name')\n if (!name) {\n return {}\n }\n const mimeType = pickString(output.value, 'mimeType')\n return {\n fact: {\n icon: 'ri:file-text-line',\n text: mimeType ? `${name} (${mimeType})` : name,\n },\n }\n }\n if (type === 'tool-getWeather') {\n const location =\n pickString(output.value, 'location') ??\n pickString(input.value, 'locationName') ??\n formatCoordinates(input.value)\n if (!location) {\n return {}\n }\n const startDate = pickString(input.value, 'startDate')\n const endDate = pickString(input.value, 'endDate')\n const dateRange = startDate\n ? endDate && endDate !== startDate\n ? ` · ${startDate} → ${endDate}`\n : ` · ${startDate}`\n : ''\n return {\n fact: {\n icon: 'ri:sun-cloudy-line',\n text: `${location}${dateRange}`,\n },\n }\n }\n if (type === 'tool-geocode') {\n const from =\n pickString(input.value, 'locationName') ??\n formatCoordinates(input.value)\n const to =\n pickString(\n output.value,\n 'name',\n 'displayName',\n 'locationName',\n 'label',\n ) ?? formatCoordinates(output.value)\n const text =\n from && to && from !== to ? `${from} → ${to}` : (from ?? to)\n if (!text) {\n return {}\n }\n return {\n fact: {\n icon: 'ri:map-pin-line',\n text,\n },\n }\n }\n if (type === 'tool-recallUserMemory') {\n return hitsDetail(memoryHits(output.value?.memories))\n }\n if (type === 'tool-saveUserMemory') {\n const candidates = memoryHits(output.value?.candidates)\n if (candidates) {\n return hitsDetail(candidates)\n }\n const text =\n pickString(input.value, 'content') ??\n pickString(output.value, 'message', 'error')\n if (!text) {\n return {}\n }\n return {\n fact: {\n icon:\n pickString(input.value, 'action') === 'delete'\n ? 'ri:delete-bin-line'\n : 'ri:save-3-line',\n text,\n },\n }\n }\n if (WEB_SEARCH_TOOL_TYPES.includes(type)) {\n return {\n chips: queryChips.value,\n rawOutput: stringifyPayload(rawOutputValue.value),\n }\n }\n return {}\n }\n\n const detail = computed<Detail>(() => {\n const result = buildDetail()\n // Nothing meaningful to show for this tool yet (e.g. still running):\n // fall back to the raw payloads so the step body is never empty.\n const hasRichContent =\n result.chips?.length ||\n result.fact ||\n result.passage ||\n result.showEmpty ||\n result.hits?.length\n if (!hasRichContent) {\n result.rawInput = stringifyPayload(rawInputValue.value)\n result.rawOutput ??= stringifyPayload(rawOutputValue.value)\n }\n return result\n })\n\n const expandedHits = reactive(new Set<string>())\n const toggleHit = (key: string) => {\n if (expandedHits.has(key)) {\n expandedHits.delete(key)\n } else {\n expandedHits.add(key)\n }\n }\n</script>\n\n<template>\n <div class=\"pk-chatbot-step-detail\">\n <ul v-if=\"detail.chips?.length\" class=\"pk-chatbot-step-detail__chips\">\n <li\n v-for=\"(chip, chipIndex) in detail.chips\"\n :key=\"`${chipIndex}-${chip}`\"\n class=\"pk-chatbot-step-detail__chip\">\n <VvIcon name=\"ri:search-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-step-detail__chip-text\">\n {{ chip }}\n </span>\n </li>\n </ul>\n <div v-if=\"detail.fact\" class=\"pk-chatbot-step-detail__fact\">\n <VvIcon :name=\"detail.fact.icon\" class=\"shrink-0\" />\n <span>{{ detail.fact.text }}</span>\n </div>\n <div v-if=\"detail.showEmpty\" class=\"pk-chatbot-step-detail__empty\">\n {{ $t('messagePart.stepNoResults') }}\n </div>\n <ul\n v-else-if=\"detail.hits?.length\"\n class=\"pk-chatbot-step-detail__hits\">\n <li v-for=\"hit in detail.hits\" :key=\"hit.key\">\n <component\n :is=\"hit.body ? 'button' : 'div'\"\n :type=\"hit.body ? 'button' : undefined\"\n class=\"pk-chatbot-step-detail__hit-header\"\n :aria-expanded=\"\n hit.body ? expandedHits.has(hit.key) : undefined\n \"\n @click=\"hit.body ? toggleHit(hit.key) : undefined\">\n <VvIcon\n :name=\"hit.icon\"\n class=\"pk-chatbot-step-detail__hit-icon\" />\n <span class=\"pk-chatbot-step-detail__hit-heading\">\n <span class=\"pk-chatbot-step-detail__hit-title\">\n {{ hit.title }}\n </span>\n <a\n v-if=\"hit.url\"\n :href=\"hit.url\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"pk-chatbot-step-detail__link\"\n @click.stop>\n <VvIcon name=\"ri:external-link-line\" />\n </a>\n </span>\n <span\n v-if=\"hit.meta\"\n class=\"pk-chatbot-step-detail__hit-meta\">\n {{ hit.meta }}\n </span>\n <span\n v-if=\"hit.relevance !== undefined\"\n class=\"pk-chatbot-step-detail__relevance\"\n aria-hidden=\"true\">\n <span\n class=\"pk-chatbot-step-detail__relevance-fill\"\n :style=\"{\n width: `${Math.round(hit.relevance * 100)}%`,\n }\" />\n </span>\n <VvIcon\n v-if=\"hit.body\"\n :name=\"\n expandedHits.has(hit.key)\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"shrink-0\" />\n </component>\n <div\n v-if=\"hit.body && expandedHits.has(hit.key)\"\n class=\"pk-chatbot-step-detail__hit-body\">\n {{ hit.body }}\n </div>\n </li>\n </ul>\n <div v-if=\"detail.passage\" class=\"pk-chatbot-step-detail__passage\">\n <div class=\"pk-chatbot-step-detail__passage-header\">\n <VvIcon name=\"ri:file-text-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-step-detail__hit-heading\">\n <span class=\"pk-chatbot-step-detail__hit-title\">\n {{\n detail.passage.title ??\n $t('messagePart.stepDocument')\n }}\n </span>\n </span>\n <span\n v-if=\"detail.passage.meta\"\n class=\"pk-chatbot-step-detail__hit-meta\">\n {{ detail.passage.meta }}\n </span>\n </div>\n <div class=\"pk-chatbot-step-detail__passage-content\">\n {{ detail.passage.content }}\n </div>\n </div>\n <div v-if=\"detail.rawInput\" class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepInput') }}\n </span>\n <pre>{{ detail.rawInput }}</pre>\n </div>\n <div v-if=\"detail.rawOutput\" class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepOutput') }}\n </span>\n <pre>{{ detail.rawOutput }}</pre>\n </div>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-step-detail {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n color: var(--color-word-3);\n\n &__chips {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-4);\n }\n\n &__chip {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-4);\n max-width: 100%;\n padding: var(--spacing-2) var(--spacing-8);\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-1);\n color: var(--color-word-2);\n }\n\n &__chip-text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n &__fact {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n color: var(--color-word-2);\n }\n\n &__empty {\n color: var(--color-word-4);\n font-style: italic;\n }\n\n &__hits {\n display: flex;\n flex-direction: column;\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n background-color: var(--color-surface-1);\n overflow: hidden;\n\n > li + li {\n border-top: var(--spacing-px) solid var(--color-surface-3);\n }\n }\n\n &__hit-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: 100%;\n min-height: var(--spacing-32);\n padding: var(--spacing-4) var(--spacing-8);\n text-align: left;\n\n &:is(button) {\n cursor: pointer;\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n &__hit-icon {\n flex-shrink: 0;\n color: var(--color-word-3);\n }\n\n // Grows to fill the row so meta, relevance and chevron stay aligned;\n // the title inside only shrinks, keeping the link icon next to the\n // title text instead of pushed to the right edge.\n &__hit-heading {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n flex: 1 1 0;\n min-width: 0;\n }\n\n &__hit-title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-2);\n }\n\n &__hit-meta {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__relevance {\n flex-shrink: 0;\n width: var(--spacing-40);\n height: var(--spacing-4);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-3);\n overflow: hidden;\n }\n\n &__relevance-fill {\n display: block;\n height: 100%;\n border-radius: inherit;\n background-color: var(--chatbot-main-color, var(--color-word-4));\n }\n\n &__link {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--color-word-4);\n\n &:hover {\n color: var(--color-word-1);\n }\n }\n\n &__hit-body,\n &__passage-content {\n padding: var(--spacing-8);\n max-height: var(--spacing-160);\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--color-word-3);\n }\n\n &__hit-body {\n border-top: var(--spacing-px) solid var(--color-surface-3);\n }\n\n &__passage {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n background-color: var(--color-surface-1);\n overflow: hidden;\n }\n\n &__passage-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n min-height: var(--spacing-32);\n padding: var(--spacing-4) var(--spacing-8);\n border-bottom: var(--spacing-px) solid var(--color-surface-3);\n }\n }\n</style>\n","<script lang=\"ts\" setup>\n import { computed, reactive } from 'vue'\n import { useI18n } from 'vue-i18n'\n import type { UIChatMessage } from 'models'\n import { stringifyPayload, WEB_SEARCH_TOOL_TYPES } from './utils'\n\n type MessagePart = UIChatMessage['parts'][number]\n\n interface DetailHit {\n key: string\n icon: string\n title: string\n url?: string\n meta?: string\n relevance?: number\n body?: string\n }\n\n interface Detail {\n chips?: string[]\n fact?: { icon: string; text: string }\n hits?: DetailHit[]\n showEmpty?: boolean\n passage?: { title?: string; meta?: string; content: string }\n rawInput?: string\n rawOutput?: string\n }\n\n const props = defineProps<{\n part: MessagePart\n }>()\n\n const { t: $t } = useI18n({\n useScope: 'global',\n })\n\n const asRecord = (value: unknown): Record<string, unknown> | undefined =>\n value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined\n\n const pickString = (\n record: Record<string, unknown> | undefined,\n ...keys: string[]\n ): string | undefined => {\n for (const key of keys) {\n const value = record?.[key]\n if (typeof value === 'string' && value.trim()) {\n return value\n }\n }\n return undefined\n }\n\n const formatCoordinates = (\n record?: Record<string, unknown>,\n ): string | undefined => {\n const latitude = record?.latitude\n const longitude = record?.longitude\n if (typeof latitude !== 'number' || typeof longitude !== 'number') {\n return undefined\n }\n return `${latitude.toFixed(4)}, ${longitude.toFixed(4)}`\n }\n\n const rawInputValue = computed(() =>\n 'input' in props.part ? props.part.input : undefined,\n )\n const rawOutputValue = computed(() =>\n 'output' in props.part ? props.part.output : undefined,\n )\n const input = computed(() => asRecord(rawInputValue.value))\n const output = computed(() => asRecord(rawOutputValue.value))\n\n // Search queries shown as chips (queries[], query or objective)\n const queryChips = computed<string[]>(() => {\n const raw =\n input.value?.queries ?? input.value?.query ?? input.value?.objective\n if (typeof raw === 'string' && raw.trim()) {\n return [raw]\n }\n if (Array.isArray(raw)) {\n return raw.filter(\n (query): query is string =>\n typeof query === 'string' && Boolean(query.trim()),\n )\n }\n return []\n })\n\n // Raw scores live on different scales depending on the agent search mode\n // (RRF ~0.03, cosine 0 to 1) and tend to cluster, so a plain ratio on the\n // max renders every bar as ~100%. Spread them on a min/max scale with a\n // floor so the ordering stays visible, and hide the bar entirely when it\n // carries no information (single hit or identical scores).\n const spreadRelevance = (scores: number[]): (number | undefined)[] => {\n const min = Math.min(...scores)\n const max = Math.max(...scores)\n if (scores.length < 2 || max <= min) {\n return scores.map(() => undefined)\n }\n const floor = 0.15\n return scores.map(\n (score) => floor + ((score - min) / (max - min)) * (1 - floor),\n )\n }\n\n // Normalizes a raw result array into records paired with a spread\n // relevance derived from each record's `similarity` score.\n const scoredRecords = (\n raw: unknown,\n ):\n | { record: Record<string, unknown>; relevance?: number }[]\n | undefined => {\n if (!Array.isArray(raw)) {\n return undefined\n }\n const records = raw\n .map((item) => asRecord(item))\n .filter((record): record is Record<string, unknown> =>\n Boolean(record),\n )\n const relevances = spreadRelevance(\n records.map((record) =>\n typeof record.similarity === 'number' ? record.similarity : 0,\n ),\n )\n return records.map((record, index) => ({\n record,\n relevance: relevances[index],\n }))\n }\n\n // searchInKnowledgeBase: group chunks by document, relevance compares the\n // best chunk score of each document within the result set.\n const knowledgeHits = computed<DetailHit[] | undefined>(() => {\n const results = output.value?.results\n if (!Array.isArray(results)) {\n return undefined\n }\n interface DocumentGroup {\n id: string\n type: string\n title: string\n url?: string\n similarity: number\n chunks: string[]\n }\n const groups = new Map<string, DocumentGroup>()\n for (const [index, raw] of results.entries()) {\n const result = asRecord(raw)\n if (!result) {\n continue\n }\n const document = asRecord(result.document)\n const source = asRecord(document?.source)\n const id =\n typeof document?.id === 'string' ? document.id : `doc-${index}`\n const similarity =\n typeof result.similarity === 'number' ? result.similarity : 0\n let group = groups.get(id)\n if (!group) {\n group = {\n id,\n type:\n typeof document?.type === 'string' ? document.type : '',\n title:\n pickString(document, 'name') ??\n pickString(source, 'filename', 'url') ??\n $t('messagePart.stepDocument'),\n url: pickString(source, 'url'),\n similarity,\n chunks: [],\n }\n groups.set(id, group)\n }\n group.similarity = Math.max(group.similarity, similarity)\n if (typeof result.content === 'string' && result.content.trim()) {\n group.chunks.push(result.content)\n }\n }\n const list = [...groups.values()]\n const relevances = spreadRelevance(list.map((g) => g.similarity))\n return list.map((group, index) => ({\n key: group.id,\n icon: group.type === 'url' ? 'ri:global-line' : 'ri:file-text-line',\n title: group.title,\n url: group.url,\n meta: $t('messagePart.stepExcerpts', group.chunks.length || 1),\n relevance: relevances[index],\n body: group.chunks.join('\\n\\n· · ·\\n\\n') || undefined,\n }))\n })\n\n // retrieveRevisedAnswers: one hit per matched question, the corrected\n // answer is the expandable body.\n const revisedAnswerHits = computed<DetailHit[] | undefined>(() =>\n scoredRecords(output.value?.results)?.map(\n ({ record, relevance }, index) => ({\n key:\n pickString(record, 'revisedAnswerId') ?? `revised-${index}`,\n icon: 'ri:file-edit-line',\n title:\n pickString(record, 'userQuestion') ??\n $t('messagePart.revisedAnswers'),\n relevance,\n body: pickString(record, 'revisedMessage'),\n }),\n ),\n )\n\n // recallUserMemory / saveUserMemory delete candidates: one hit per\n // memory, long facts expand to their full text.\n const memoryHits = (raw: unknown): DetailHit[] | undefined =>\n scoredRecords(raw)?.map(({ record, relevance }, index) => {\n const content = pickString(record, 'content') ?? ''\n return {\n key: pickString(record, 'id') ?? `memory-${index}`,\n icon: 'ri:brain-line',\n title: content,\n meta: pickString(record, 'updatedAt')?.slice(0, 10),\n relevance,\n body: content.length > 120 ? content : undefined,\n }\n })\n\n // Query chips over a list of hits, flagging an explicit empty state so the\n // \"no results\" message shows once the tool has run (hits defined, length 0).\n const hitsDetail = (hits: DetailHit[] | undefined): Detail => ({\n chips: queryChips.value,\n hits,\n showEmpty: hits !== undefined && !hits.length,\n })\n\n // Rich, tool-specific view of the step. Returns {} when the tool has\n // nothing meaningful to show yet (the raw fallback is applied by `detail`).\n const buildDetail = (): Detail => {\n const type = props.part.type\n if (type === 'tool-searchInKnowledgeBase') {\n return hitsDetail(knowledgeHits.value)\n }\n if (type === 'tool-retrieveRevisedAnswers') {\n return hitsDetail(revisedAnswerHits.value)\n }\n if (type === 'tool-expandChunkContext') {\n const content = pickString(output.value, 'content')\n if (!content) {\n return {}\n }\n const totalChunks = output.value?.totalChunksReturned\n return {\n passage: {\n title: pickString(output.value, 'documentName'),\n meta:\n typeof totalChunks === 'number'\n ? $t('messagePart.stepExcerpts', totalChunks)\n : undefined,\n content,\n },\n }\n }\n if (type === 'tool-readDocumentFile') {\n const name = pickString(output.value, 'name')\n if (!name) {\n return {}\n }\n const mimeType = pickString(output.value, 'mimeType')\n return {\n fact: {\n icon: 'ri:file-text-line',\n text: mimeType ? `${name} (${mimeType})` : name,\n },\n }\n }\n if (type === 'tool-getWeather') {\n const location =\n pickString(output.value, 'location') ??\n pickString(input.value, 'locationName') ??\n formatCoordinates(input.value)\n if (!location) {\n return {}\n }\n const startDate = pickString(input.value, 'startDate')\n const endDate = pickString(input.value, 'endDate')\n const dateRange = startDate\n ? endDate && endDate !== startDate\n ? ` · ${startDate} → ${endDate}`\n : ` · ${startDate}`\n : ''\n return {\n fact: {\n icon: 'ri:sun-cloudy-line',\n text: `${location}${dateRange}`,\n },\n }\n }\n if (type === 'tool-geocode') {\n const from =\n pickString(input.value, 'locationName') ??\n formatCoordinates(input.value)\n const to =\n pickString(\n output.value,\n 'name',\n 'displayName',\n 'locationName',\n 'label',\n ) ?? formatCoordinates(output.value)\n const text =\n from && to && from !== to ? `${from} → ${to}` : (from ?? to)\n if (!text) {\n return {}\n }\n return {\n fact: {\n icon: 'ri:map-pin-line',\n text,\n },\n }\n }\n if (type === 'tool-recallUserMemory') {\n return hitsDetail(memoryHits(output.value?.memories))\n }\n if (type === 'tool-saveUserMemory') {\n const candidates = memoryHits(output.value?.candidates)\n if (candidates) {\n return hitsDetail(candidates)\n }\n const text =\n pickString(input.value, 'content') ??\n pickString(output.value, 'message', 'error')\n if (!text) {\n return {}\n }\n return {\n fact: {\n icon:\n pickString(input.value, 'action') === 'delete'\n ? 'ri:delete-bin-line'\n : 'ri:save-3-line',\n text,\n },\n }\n }\n if (WEB_SEARCH_TOOL_TYPES.includes(type)) {\n return {\n chips: queryChips.value,\n rawOutput: stringifyPayload(rawOutputValue.value),\n }\n }\n return {}\n }\n\n const detail = computed<Detail>(() => {\n const result = buildDetail()\n // Nothing meaningful to show for this tool yet (e.g. still running):\n // fall back to the raw payloads so the step body is never empty.\n const hasRichContent =\n result.chips?.length ||\n result.fact ||\n result.passage ||\n result.showEmpty ||\n result.hits?.length\n if (!hasRichContent) {\n result.rawInput = stringifyPayload(rawInputValue.value)\n result.rawOutput ??= stringifyPayload(rawOutputValue.value)\n }\n return result\n })\n\n const expandedHits = reactive(new Set<string>())\n const toggleHit = (key: string) => {\n if (expandedHits.has(key)) {\n expandedHits.delete(key)\n } else {\n expandedHits.add(key)\n }\n }\n</script>\n\n<template>\n <div class=\"pk-chatbot-step-detail\">\n <ul v-if=\"detail.chips?.length\" class=\"pk-chatbot-step-detail__chips\">\n <li\n v-for=\"(chip, chipIndex) in detail.chips\"\n :key=\"`${chipIndex}-${chip}`\"\n class=\"pk-chatbot-step-detail__chip\">\n <VvIcon name=\"ri:search-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-step-detail__chip-text\">\n {{ chip }}\n </span>\n </li>\n </ul>\n <div v-if=\"detail.fact\" class=\"pk-chatbot-step-detail__fact\">\n <VvIcon :name=\"detail.fact.icon\" class=\"shrink-0\" />\n <span>{{ detail.fact.text }}</span>\n </div>\n <div v-if=\"detail.showEmpty\" class=\"pk-chatbot-step-detail__empty\">\n {{ $t('messagePart.stepNoResults') }}\n </div>\n <ul\n v-else-if=\"detail.hits?.length\"\n class=\"pk-chatbot-step-detail__hits\">\n <li v-for=\"hit in detail.hits\" :key=\"hit.key\">\n <component\n :is=\"hit.body ? 'button' : 'div'\"\n :type=\"hit.body ? 'button' : undefined\"\n class=\"pk-chatbot-step-detail__hit-header\"\n :aria-expanded=\"\n hit.body ? expandedHits.has(hit.key) : undefined\n \"\n @click=\"hit.body ? toggleHit(hit.key) : undefined\">\n <VvIcon\n :name=\"hit.icon\"\n class=\"pk-chatbot-step-detail__hit-icon\" />\n <span class=\"pk-chatbot-step-detail__hit-heading\">\n <span class=\"pk-chatbot-step-detail__hit-title\">\n {{ hit.title }}\n </span>\n <a\n v-if=\"hit.url\"\n :href=\"hit.url\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"pk-chatbot-step-detail__link\"\n @click.stop>\n <VvIcon name=\"ri:external-link-line\" />\n </a>\n </span>\n <span\n v-if=\"hit.meta\"\n class=\"pk-chatbot-step-detail__hit-meta\">\n {{ hit.meta }}\n </span>\n <span\n v-if=\"hit.relevance !== undefined\"\n class=\"pk-chatbot-step-detail__relevance\"\n aria-hidden=\"true\">\n <span\n class=\"pk-chatbot-step-detail__relevance-fill\"\n :style=\"{\n width: `${Math.round(hit.relevance * 100)}%`,\n }\" />\n </span>\n <VvIcon\n v-if=\"hit.body\"\n :name=\"\n expandedHits.has(hit.key)\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"shrink-0\" />\n </component>\n <div\n v-if=\"hit.body && expandedHits.has(hit.key)\"\n class=\"pk-chatbot-step-detail__hit-body\">\n {{ hit.body }}\n </div>\n </li>\n </ul>\n <div v-if=\"detail.passage\" class=\"pk-chatbot-step-detail__passage\">\n <div class=\"pk-chatbot-step-detail__passage-header\">\n <VvIcon name=\"ri:file-text-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-step-detail__hit-heading\">\n <span class=\"pk-chatbot-step-detail__hit-title\">\n {{\n detail.passage.title ??\n $t('messagePart.stepDocument')\n }}\n </span>\n </span>\n <span\n v-if=\"detail.passage.meta\"\n class=\"pk-chatbot-step-detail__hit-meta\">\n {{ detail.passage.meta }}\n </span>\n </div>\n <div class=\"pk-chatbot-step-detail__passage-content\">\n {{ detail.passage.content }}\n </div>\n </div>\n <div v-if=\"detail.rawInput\" class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepInput') }}\n </span>\n <pre>{{ detail.rawInput }}</pre>\n </div>\n <div v-if=\"detail.rawOutput\" class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepOutput') }}\n </span>\n <pre>{{ detail.rawOutput }}</pre>\n </div>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-step-detail {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-8);\n font-size: var(--text-12);\n color: var(--color-word-3);\n\n &__chips {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-4);\n }\n\n &__chip {\n display: inline-flex;\n align-items: center;\n gap: var(--spacing-4);\n max-width: 100%;\n padding: var(--spacing-2) var(--spacing-8);\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-1);\n color: var(--color-word-2);\n }\n\n &__chip-text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n &__fact {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n color: var(--color-word-2);\n }\n\n &__empty {\n color: var(--color-word-4);\n font-style: italic;\n }\n\n &__hits {\n display: flex;\n flex-direction: column;\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n background-color: var(--color-surface-1);\n overflow: hidden;\n\n > li + li {\n border-top: var(--spacing-px) solid var(--color-surface-3);\n }\n }\n\n &__hit-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: 100%;\n min-height: var(--spacing-32);\n padding: var(--spacing-4) var(--spacing-8);\n text-align: left;\n\n &:is(button) {\n cursor: pointer;\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n &__hit-icon {\n flex-shrink: 0;\n color: var(--color-word-3);\n }\n\n // Grows to fill the row so meta, relevance and chevron stay aligned;\n // the title inside only shrinks, keeping the link icon next to the\n // title text instead of pushed to the right edge.\n &__hit-heading {\n display: flex;\n align-items: center;\n gap: var(--spacing-4);\n flex: 1 1 0;\n min-width: 0;\n }\n\n &__hit-title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-2);\n }\n\n &__hit-meta {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__relevance {\n flex-shrink: 0;\n width: var(--spacing-40);\n height: var(--spacing-4);\n border-radius: var(--rounded-full);\n background-color: var(--color-surface-3);\n overflow: hidden;\n }\n\n &__relevance-fill {\n display: block;\n height: 100%;\n border-radius: inherit;\n background-color: var(--chatbot-main-color, var(--color-word-4));\n }\n\n &__link {\n flex-shrink: 0;\n display: inline-flex;\n color: var(--color-word-4);\n\n &:hover {\n color: var(--color-word-1);\n }\n }\n\n &__hit-body,\n &__passage-content {\n padding: var(--spacing-8);\n max-height: var(--spacing-160);\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--color-word-3);\n }\n\n &__hit-body {\n border-top: var(--spacing-px) solid var(--color-surface-3);\n }\n\n &__passage {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n background-color: var(--color-surface-1);\n overflow: hidden;\n }\n\n &__passage-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n min-height: var(--spacing-32);\n padding: var(--spacing-4) var(--spacing-8);\n border-bottom: var(--spacing-px) solid var(--color-surface-3);\n }\n }\n</style>\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as e } from "./PkStreamingMarkdown-BBni7OTn.js";
|
|
2
2
|
import { c as t, o as n } from "./message-RohfrdWS.js";
|
|
3
|
-
import { _ as r, a as i, c as a, i as o, l as s, o as c, u as l } from "./utils-
|
|
4
|
-
import { t as u } from "./PkChatbotStepToolDetail-
|
|
3
|
+
import { _ as r, a as i, c as a, i as o, l as s, o as c, u as l } from "./utils-CgxndZld.js";
|
|
4
|
+
import { t as u } from "./PkChatbotStepToolDetail-BBX4KL3d.js";
|
|
5
5
|
import { VvIcon as d } from "@volverjs/ui-vue/components";
|
|
6
6
|
import { Fragment as f, computed as p, createBlock as m, createCommentVNode as h, createElementBlock as g, createElementVNode as _, createTextVNode as v, createVNode as y, defineComponent as b, nextTick as x, normalizeClass as S, openBlock as C, reactive as ee, ref as w, renderList as T, resolveDynamicComponent as te, toDisplayString as E, unref as D, watch as O, withCtx as ne } from "vue";
|
|
7
7
|
import { useI18n as re } from "vue-i18n";
|
|
@@ -169,4 +169,4 @@ var k = /* @__PURE__ */ b({
|
|
|
169
169
|
//#endregion
|
|
170
170
|
export { k as n, W as t };
|
|
171
171
|
|
|
172
|
-
//# sourceMappingURL=PkChatbotSteps-
|
|
172
|
+
//# sourceMappingURL=PkChatbotSteps-DlZpwc_a.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PkChatbotSteps-Bbuh0hgL.js","names":[],"sources":["../../../../packages/components/src/chat/PkStreamingMarkdownAutoscroll.vue","../../../../packages/components/src/chat/PkStreamingMarkdownAutoscroll.vue","../../../../packages/components/src/chat/PkChatbotSteps.vue","../../../../packages/components/src/chat/PkChatbotSteps.vue"],"sourcesContent":["<script lang=\"ts\" setup>\n import { nextTick, ref, watch } from 'vue'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n\n const props = defineProps<{\n markdown?: string\n innerClass?: string\n isDark?: boolean\n }>()\n\n const scrollEl = ref<HTMLDivElement>()\n const lastScrollTop = ref<number>(0)\n const stopAutoScroll = ref<boolean>(false)\n\n const handleScroll = () => {\n if (!scrollEl.value) {\n return\n }\n const currentScrollTop = scrollEl.value.scrollTop\n if (currentScrollTop < lastScrollTop.value) {\n const delta = lastScrollTop.value - currentScrollTop\n if (delta < 50) {\n stopAutoScroll.value = true\n }\n }\n if (currentScrollTop > lastScrollTop.value) {\n if (\n currentScrollTop ===\n scrollEl.value.scrollHeight - scrollEl.value.clientHeight\n ) {\n stopAutoScroll.value = false\n }\n }\n lastScrollTop.value = currentScrollTop\n }\n\n watch(\n () => props.markdown,\n async () => {\n await nextTick()\n if (scrollEl.value && !stopAutoScroll.value) {\n scrollEl.value.scrollTop = scrollEl.value.scrollHeight\n }\n },\n { immediate: true },\n )\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n class=\"overflow-auto light-scrollbar\"\n @scroll=\"handleScroll\">\n <PkStreamingMarkdown :markdown :is-dark :class=\"innerClass\" />\n </div>\n</template>\n","<script lang=\"ts\" setup>\n import { nextTick, ref, watch } from 'vue'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n\n const props = defineProps<{\n markdown?: string\n innerClass?: string\n isDark?: boolean\n }>()\n\n const scrollEl = ref<HTMLDivElement>()\n const lastScrollTop = ref<number>(0)\n const stopAutoScroll = ref<boolean>(false)\n\n const handleScroll = () => {\n if (!scrollEl.value) {\n return\n }\n const currentScrollTop = scrollEl.value.scrollTop\n if (currentScrollTop < lastScrollTop.value) {\n const delta = lastScrollTop.value - currentScrollTop\n if (delta < 50) {\n stopAutoScroll.value = true\n }\n }\n if (currentScrollTop > lastScrollTop.value) {\n if (\n currentScrollTop ===\n scrollEl.value.scrollHeight - scrollEl.value.clientHeight\n ) {\n stopAutoScroll.value = false\n }\n }\n lastScrollTop.value = currentScrollTop\n }\n\n watch(\n () => props.markdown,\n async () => {\n await nextTick()\n if (scrollEl.value && !stopAutoScroll.value) {\n scrollEl.value.scrollTop = scrollEl.value.scrollHeight\n }\n },\n { immediate: true },\n )\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n class=\"overflow-auto light-scrollbar\"\n @scroll=\"handleScroll\">\n <PkStreamingMarkdown :markdown :is-dark :class=\"innerClass\" />\n </div>\n</template>\n","<script lang=\"ts\" setup>\n import { computed, reactive, ref, watch } from 'vue'\n import { useI18n } from 'vue-i18n'\n import type { UIChatMessage } from 'models'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotStepToolDetail from './PkChatbotStepToolDetail.vue'\n import {\n isReasoningPart,\n isToolPart,\n getPartIcon,\n getPartState,\n getToolPartLabel,\n getToolPartSummary,\n hasRichToolDetail,\n stringifyPayload,\n formatDuration as formatDurationWith,\n } from './utils'\n\n const props = defineProps<{\n message: UIChatMessage\n isStreaming?: boolean\n isDark?: boolean\n }>()\n\n const { t: $t, n: $n } = useI18n({\n useScope: 'global',\n })\n\n type MessagePart = UIChatMessage['parts'][number]\n\n interface Step {\n key: string\n part: MessagePart\n icon?: string\n label?: string\n summary?: string\n durationMs?: number\n isActive: boolean\n text?: string\n toolInput?: unknown\n toolOutput?: unknown\n }\n\n const steps = computed<Step[]>(() => {\n const parts = props.message.parts\n const partTimings = props.message.metadata?.partTimings\n const result: Step[] = []\n for (const [index, part] of parts.entries()) {\n const isReasoning = isReasoningPart(part)\n const isTool = isToolPart(part)\n if (!isReasoning && !isTool) {\n continue\n }\n const toolCallId =\n 'toolCallId' in part ? (part.toolCallId as string) : undefined\n result.push({\n key: toolCallId ?? `${part.type}-${index}`,\n part,\n icon: getPartIcon(part),\n label: getToolPartLabel($t, part),\n summary: getToolPartSummary(part),\n durationMs: toolCallId ? partTimings?.[toolCallId] : undefined,\n isActive:\n Boolean(props.isStreaming) && index === parts.length - 1,\n text: isReasoning && part.text.trim() ? part.text : undefined,\n toolInput: isTool && 'input' in part ? part.input : undefined,\n toolOutput:\n isTool && 'output' in part ? part.output : undefined,\n })\n }\n return result\n })\n\n const totalDurationMs = computed(() => {\n const { createdAt, completedAt } = props.message.metadata ?? {}\n if (!createdAt || !completedAt) {\n return undefined\n }\n return completedAt - createdAt\n })\n\n const formatDuration = (ms?: number) => formatDurationWith($n, ms)\n\n const getStepError = (step: Step) =>\n getPartState(step.part) === 'output-error' &&\n 'errorText' in step.part &&\n typeof step.part.errorText === 'string'\n ? step.part.errorText\n : undefined\n\n const hasBody = (step: Step) => {\n if (step.text || getStepError(step)) {\n return true\n }\n if (hasRichToolDetail(step.part)) {\n return step.toolInput !== undefined || step.toolOutput !== undefined\n }\n return Boolean(\n stringifyPayload(step.toolInput) ??\n stringifyPayload(step.toolOutput),\n )\n }\n\n // --- Expand / collapse ---\n // The active (streaming) step is auto-expanded; a manual toggle always\n // wins over the automatic behavior.\n const manualSteps = reactive(new Map<string, boolean>())\n const isStepExpanded = (step: Step) =>\n manualSteps.get(step.key) ?? step.isActive\n const toggleStep = (step: Step) => {\n manualSteps.set(step.key, !isStepExpanded(step))\n }\n\n // The group is open while streaming and collapses to its header once the\n // answer is complete, unless the user toggled it manually.\n const manualGroup = ref<boolean | undefined>(undefined)\n const isGroupExpanded = computed(\n () => manualGroup.value ?? Boolean(props.isStreaming),\n )\n // Reset the manual override when a new stream starts so the timeline\n // opens again on regenerate.\n watch(\n () => props.isStreaming,\n (streaming) => {\n if (streaming) {\n manualGroup.value = undefined\n manualSteps.clear()\n }\n },\n )\n</script>\n\n<template>\n <div v-if=\"steps.length\" class=\"pk-chatbot-steps\">\n <button\n type=\"button\"\n class=\"pk-chatbot-steps__header\"\n :aria-expanded=\"isGroupExpanded\"\n @click=\"manualGroup = !isGroupExpanded\">\n <VvIcon\n v-if=\"isStreaming\"\n name=\"line-md:loading-loop\"\n class=\"shrink-0\" />\n <VvIcon v-else name=\"ri:route-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-steps__title\">\n {{ $t('messagePart.steps') }}\n </span>\n <span class=\"pk-chatbot-steps__meta\">\n {{ $t('messagePart.stepCount', steps.length) }}\n <template v-if=\"formatDuration(totalDurationMs)\">\n · {{ formatDuration(totalDurationMs) }}\n </template>\n </span>\n <VvIcon\n :name=\"\n isGroupExpanded\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"pk-chatbot-steps__chevron shrink-0 ml-auto\" />\n </button>\n <ol v-if=\"isGroupExpanded\" class=\"pk-chatbot-steps__list\">\n <li\n v-for=\"step in steps\"\n :key=\"step.key\"\n class=\"pk-chatbot-steps__step\"\n :class=\"{\n 'pk-chatbot-steps__step--active': step.isActive,\n }\">\n <component\n :is=\"hasBody(step) ? 'button' : 'div'\"\n :type=\"hasBody(step) ? 'button' : undefined\"\n class=\"pk-chatbot-steps__step-header\"\n :aria-expanded=\"\n hasBody(step) ? isStepExpanded(step) : undefined\n \"\n @click=\"hasBody(step) ? toggleStep(step) : undefined\">\n <span\n class=\"pk-chatbot-steps-marker\"\n :class=\"{\n 'pk-chatbot-steps-marker--active': step.isActive,\n }\">\n <VvIcon\n v-if=\"step.isActive\"\n name=\"line-md:loading-loop\" />\n <VvIcon v-else-if=\"step.icon\" :name=\"step.icon\" />\n </span>\n <span class=\"pk-chatbot-steps__step-label\">\n {{ step.label }}\n </span>\n <span class=\"pk-chatbot-steps__step-summary\">\n {{ step.summary }}\n </span>\n <span\n v-if=\"formatDuration(step.durationMs)\"\n class=\"pk-chatbot-steps__step-duration\">\n {{ formatDuration(step.durationMs) }}\n </span>\n <VvIcon\n v-if=\"hasBody(step)\"\n :name=\"\n isStepExpanded(step)\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"pk-chatbot-steps__chevron shrink-0 ml-auto\" />\n </component>\n <div\n v-if=\"hasBody(step) && isStepExpanded(step)\"\n class=\"pk-chatbot-steps__step-body\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"step.text && step.isActive\"\n :markdown=\"step.text\"\n :is-dark\n inner-class=\"wysiwyg\"\n class=\"pk-chatbot-steps__step-content pk-chatbot-steps__step-content--scroll\" />\n <PkStreamingMarkdown\n v-else-if=\"step.text\"\n :markdown=\"step.text\"\n :is-dark\n class=\"wysiwyg pk-chatbot-steps__step-content\" />\n <div\n v-else-if=\"getStepError(step)\"\n class=\"pk-chatbot-steps__step-payload pk-chatbot-steps__step-payload--error\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepError') }}\n </span>\n <pre>{{ getStepError(step) }}</pre>\n </div>\n <PkChatbotStepToolDetail\n v-else-if=\"hasRichToolDetail(step.part)\"\n :part=\"step.part\" />\n <template v-else>\n <div\n v-if=\"stringifyPayload(step.toolInput)\"\n class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepInput') }}\n </span>\n <pre>{{ stringifyPayload(step.toolInput) }}</pre>\n </div>\n <div\n v-if=\"stringifyPayload(step.toolOutput)\"\n class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepOutput') }}\n </span>\n <pre>{{ stringifyPayload(step.toolOutput) }}</pre>\n </div>\n </template>\n </div>\n </li>\n </ol>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-steps {\n width: 100%;\n border: var(--spacing-px) solid var(--color-surface-3);\n border-radius: var(--rounded-lg);\n background-color: var(--color-surface-1);\n font-size: var(--text-12);\n color: var(--color-word-3);\n overflow: hidden;\n\n &__header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: 100%;\n min-height: var(--spacing-40);\n padding: var(--spacing-8) var(--spacing-sm);\n text-align: left;\n cursor: pointer;\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n\n &__chevron {\n font-size: var(--text-16);\n }\n\n &__title {\n font-weight: 600;\n color: var(--color-word-2);\n flex-grow: 1;\n }\n\n &__meta {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__list {\n background-color: var(--color-surface);\n border-top: var(--spacing-px) solid var(--color-surface-3);\n padding: var(--spacing-8) var(--spacing-sm);\n display: flex;\n flex-direction: column;\n }\n\n &__step {\n position: relative;\n\n // Vertical connector between step markers\n &:not(:last-child)::before {\n content: '';\n position: absolute;\n left: var(--spacing-10);\n top: var(--spacing-28);\n bottom: calc(var(--spacing-8) * -1);\n width: var(--spacing-px);\n background-color: var(--color-surface-4);\n }\n }\n\n &__step-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: calc(100% + var(--spacing-16));\n min-height: var(--spacing-36);\n margin-inline: calc(var(--spacing-8) * -1);\n padding: var(--spacing-4) var(--spacing-8);\n text-align: left;\n\n &:is(button) {\n cursor: pointer;\n border-radius: var(--rounded);\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n &-marker {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--spacing-20);\n height: var(--spacing-20);\n flex-shrink: 0;\n position: relative;\n z-index: 1;\n background-color: var(--color-surface-1);\n border-radius: var(--rounded-full);\n border: var(--spacing-px) solid var(--color-surface-4);\n\n &--active {\n background-color: var(\n --chatbot-main-color,\n var(--color-surface-4)\n );\n border-color: transparent;\n color: var(--chatbot-contrast-color, var(--color-word-1));\n }\n }\n\n &__step-label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-2);\n }\n\n &__step-summary {\n flex: 1 1 0;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-4);\n }\n\n &__step-duration {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__step--active &__step-label {\n color: var(--color-word-1);\n }\n\n &__step-body {\n margin-left: var(--spacing-28);\n margin-block: var(--spacing-4) var(--spacing-8);\n }\n\n &__step-content {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n padding: var(--spacing-8);\n background-color: var(--color-surface-1);\n\n &--scroll {\n max-height: var(--spacing-256);\n }\n }\n\n &__step-payload {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2);\n\n & + & {\n margin-top: var(--spacing-8);\n }\n\n pre {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n padding: var(--spacing-8);\n font-size: var(--text-10);\n max-height: var(--spacing-160);\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n font-family: var(--font-mono);\n background-color: var(--color-surface-1);\n }\n }\n\n &__step-payload-label {\n font-weight: 600;\n color: var(--color-word-3);\n text-transform: uppercase;\n font-size: var(--text-10);\n letter-spacing: 0.05em;\n }\n\n &__step-payload--error {\n .pk-chatbot-steps__step-payload-label,\n pre {\n color: var(--color-danger);\n }\n\n pre {\n border-color: var(--color-danger-lighten-5);\n }\n }\n }\n</style>\n","<script lang=\"ts\" setup>\n import { computed, reactive, ref, watch } from 'vue'\n import { useI18n } from 'vue-i18n'\n import type { UIChatMessage } from 'models'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotStepToolDetail from './PkChatbotStepToolDetail.vue'\n import {\n isReasoningPart,\n isToolPart,\n getPartIcon,\n getPartState,\n getToolPartLabel,\n getToolPartSummary,\n hasRichToolDetail,\n stringifyPayload,\n formatDuration as formatDurationWith,\n } from './utils'\n\n const props = defineProps<{\n message: UIChatMessage\n isStreaming?: boolean\n isDark?: boolean\n }>()\n\n const { t: $t, n: $n } = useI18n({\n useScope: 'global',\n })\n\n type MessagePart = UIChatMessage['parts'][number]\n\n interface Step {\n key: string\n part: MessagePart\n icon?: string\n label?: string\n summary?: string\n durationMs?: number\n isActive: boolean\n text?: string\n toolInput?: unknown\n toolOutput?: unknown\n }\n\n const steps = computed<Step[]>(() => {\n const parts = props.message.parts\n const partTimings = props.message.metadata?.partTimings\n const result: Step[] = []\n for (const [index, part] of parts.entries()) {\n const isReasoning = isReasoningPart(part)\n const isTool = isToolPart(part)\n if (!isReasoning && !isTool) {\n continue\n }\n const toolCallId =\n 'toolCallId' in part ? (part.toolCallId as string) : undefined\n result.push({\n key: toolCallId ?? `${part.type}-${index}`,\n part,\n icon: getPartIcon(part),\n label: getToolPartLabel($t, part),\n summary: getToolPartSummary(part),\n durationMs: toolCallId ? partTimings?.[toolCallId] : undefined,\n isActive:\n Boolean(props.isStreaming) && index === parts.length - 1,\n text: isReasoning && part.text.trim() ? part.text : undefined,\n toolInput: isTool && 'input' in part ? part.input : undefined,\n toolOutput:\n isTool && 'output' in part ? part.output : undefined,\n })\n }\n return result\n })\n\n const totalDurationMs = computed(() => {\n const { createdAt, completedAt } = props.message.metadata ?? {}\n if (!createdAt || !completedAt) {\n return undefined\n }\n return completedAt - createdAt\n })\n\n const formatDuration = (ms?: number) => formatDurationWith($n, ms)\n\n const getStepError = (step: Step) =>\n getPartState(step.part) === 'output-error' &&\n 'errorText' in step.part &&\n typeof step.part.errorText === 'string'\n ? step.part.errorText\n : undefined\n\n const hasBody = (step: Step) => {\n if (step.text || getStepError(step)) {\n return true\n }\n if (hasRichToolDetail(step.part)) {\n return step.toolInput !== undefined || step.toolOutput !== undefined\n }\n return Boolean(\n stringifyPayload(step.toolInput) ??\n stringifyPayload(step.toolOutput),\n )\n }\n\n // --- Expand / collapse ---\n // The active (streaming) step is auto-expanded; a manual toggle always\n // wins over the automatic behavior.\n const manualSteps = reactive(new Map<string, boolean>())\n const isStepExpanded = (step: Step) =>\n manualSteps.get(step.key) ?? step.isActive\n const toggleStep = (step: Step) => {\n manualSteps.set(step.key, !isStepExpanded(step))\n }\n\n // The group is open while streaming and collapses to its header once the\n // answer is complete, unless the user toggled it manually.\n const manualGroup = ref<boolean | undefined>(undefined)\n const isGroupExpanded = computed(\n () => manualGroup.value ?? Boolean(props.isStreaming),\n )\n // Reset the manual override when a new stream starts so the timeline\n // opens again on regenerate.\n watch(\n () => props.isStreaming,\n (streaming) => {\n if (streaming) {\n manualGroup.value = undefined\n manualSteps.clear()\n }\n },\n )\n</script>\n\n<template>\n <div v-if=\"steps.length\" class=\"pk-chatbot-steps\">\n <button\n type=\"button\"\n class=\"pk-chatbot-steps__header\"\n :aria-expanded=\"isGroupExpanded\"\n @click=\"manualGroup = !isGroupExpanded\">\n <VvIcon\n v-if=\"isStreaming\"\n name=\"line-md:loading-loop\"\n class=\"shrink-0\" />\n <VvIcon v-else name=\"ri:route-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-steps__title\">\n {{ $t('messagePart.steps') }}\n </span>\n <span class=\"pk-chatbot-steps__meta\">\n {{ $t('messagePart.stepCount', steps.length) }}\n <template v-if=\"formatDuration(totalDurationMs)\">\n · {{ formatDuration(totalDurationMs) }}\n </template>\n </span>\n <VvIcon\n :name=\"\n isGroupExpanded\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"pk-chatbot-steps__chevron shrink-0 ml-auto\" />\n </button>\n <ol v-if=\"isGroupExpanded\" class=\"pk-chatbot-steps__list\">\n <li\n v-for=\"step in steps\"\n :key=\"step.key\"\n class=\"pk-chatbot-steps__step\"\n :class=\"{\n 'pk-chatbot-steps__step--active': step.isActive,\n }\">\n <component\n :is=\"hasBody(step) ? 'button' : 'div'\"\n :type=\"hasBody(step) ? 'button' : undefined\"\n class=\"pk-chatbot-steps__step-header\"\n :aria-expanded=\"\n hasBody(step) ? isStepExpanded(step) : undefined\n \"\n @click=\"hasBody(step) ? toggleStep(step) : undefined\">\n <span\n class=\"pk-chatbot-steps-marker\"\n :class=\"{\n 'pk-chatbot-steps-marker--active': step.isActive,\n }\">\n <VvIcon\n v-if=\"step.isActive\"\n name=\"line-md:loading-loop\" />\n <VvIcon v-else-if=\"step.icon\" :name=\"step.icon\" />\n </span>\n <span class=\"pk-chatbot-steps__step-label\">\n {{ step.label }}\n </span>\n <span class=\"pk-chatbot-steps__step-summary\">\n {{ step.summary }}\n </span>\n <span\n v-if=\"formatDuration(step.durationMs)\"\n class=\"pk-chatbot-steps__step-duration\">\n {{ formatDuration(step.durationMs) }}\n </span>\n <VvIcon\n v-if=\"hasBody(step)\"\n :name=\"\n isStepExpanded(step)\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"pk-chatbot-steps__chevron shrink-0 ml-auto\" />\n </component>\n <div\n v-if=\"hasBody(step) && isStepExpanded(step)\"\n class=\"pk-chatbot-steps__step-body\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"step.text && step.isActive\"\n :markdown=\"step.text\"\n :is-dark\n inner-class=\"wysiwyg\"\n class=\"pk-chatbot-steps__step-content pk-chatbot-steps__step-content--scroll\" />\n <PkStreamingMarkdown\n v-else-if=\"step.text\"\n :markdown=\"step.text\"\n :is-dark\n class=\"wysiwyg pk-chatbot-steps__step-content\" />\n <div\n v-else-if=\"getStepError(step)\"\n class=\"pk-chatbot-steps__step-payload pk-chatbot-steps__step-payload--error\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepError') }}\n </span>\n <pre>{{ getStepError(step) }}</pre>\n </div>\n <PkChatbotStepToolDetail\n v-else-if=\"hasRichToolDetail(step.part)\"\n :part=\"step.part\" />\n <template v-else>\n <div\n v-if=\"stringifyPayload(step.toolInput)\"\n class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepInput') }}\n </span>\n <pre>{{ stringifyPayload(step.toolInput) }}</pre>\n </div>\n <div\n v-if=\"stringifyPayload(step.toolOutput)\"\n class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepOutput') }}\n </span>\n <pre>{{ stringifyPayload(step.toolOutput) }}</pre>\n </div>\n </template>\n </div>\n </li>\n </ol>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-steps {\n width: 100%;\n border: var(--spacing-px) solid var(--color-surface-3);\n border-radius: var(--rounded-lg);\n background-color: var(--color-surface-1);\n font-size: var(--text-12);\n color: var(--color-word-3);\n overflow: hidden;\n\n &__header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: 100%;\n min-height: var(--spacing-40);\n padding: var(--spacing-8) var(--spacing-sm);\n text-align: left;\n cursor: pointer;\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n\n &__chevron {\n font-size: var(--text-16);\n }\n\n &__title {\n font-weight: 600;\n color: var(--color-word-2);\n flex-grow: 1;\n }\n\n &__meta {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__list {\n background-color: var(--color-surface);\n border-top: var(--spacing-px) solid var(--color-surface-3);\n padding: var(--spacing-8) var(--spacing-sm);\n display: flex;\n flex-direction: column;\n }\n\n &__step {\n position: relative;\n\n // Vertical connector between step markers\n &:not(:last-child)::before {\n content: '';\n position: absolute;\n left: var(--spacing-10);\n top: var(--spacing-28);\n bottom: calc(var(--spacing-8) * -1);\n width: var(--spacing-px);\n background-color: var(--color-surface-4);\n }\n }\n\n &__step-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: calc(100% + var(--spacing-16));\n min-height: var(--spacing-36);\n margin-inline: calc(var(--spacing-8) * -1);\n padding: var(--spacing-4) var(--spacing-8);\n text-align: left;\n\n &:is(button) {\n cursor: pointer;\n border-radius: var(--rounded);\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n &-marker {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--spacing-20);\n height: var(--spacing-20);\n flex-shrink: 0;\n position: relative;\n z-index: 1;\n background-color: var(--color-surface-1);\n border-radius: var(--rounded-full);\n border: var(--spacing-px) solid var(--color-surface-4);\n\n &--active {\n background-color: var(\n --chatbot-main-color,\n var(--color-surface-4)\n );\n border-color: transparent;\n color: var(--chatbot-contrast-color, var(--color-word-1));\n }\n }\n\n &__step-label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-2);\n }\n\n &__step-summary {\n flex: 1 1 0;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-4);\n }\n\n &__step-duration {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__step--active &__step-label {\n color: var(--color-word-1);\n }\n\n &__step-body {\n margin-left: var(--spacing-28);\n margin-block: var(--spacing-4) var(--spacing-8);\n }\n\n &__step-content {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n padding: var(--spacing-8);\n background-color: var(--color-surface-1);\n\n &--scroll {\n max-height: var(--spacing-256);\n }\n }\n\n &__step-payload {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2);\n\n & + & {\n margin-top: var(--spacing-8);\n }\n\n pre {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n padding: var(--spacing-8);\n font-size: var(--text-10);\n max-height: var(--spacing-160);\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n font-family: var(--font-mono);\n background-color: var(--color-surface-1);\n }\n }\n\n &__step-payload-label {\n font-weight: 600;\n color: var(--color-word-3);\n text-transform: uppercase;\n font-size: var(--text-10);\n letter-spacing: 0.05em;\n }\n\n &__step-payload--error {\n .pk-chatbot-steps__step-payload-label,\n pre {\n color: var(--color-danger);\n }\n\n pre {\n border-color: var(--color-danger-lighten-5);\n }\n }\n }\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PkChatbotSteps-DlZpwc_a.js","names":[],"sources":["../../../../packages/components/src/chat/PkStreamingMarkdownAutoscroll.vue","../../../../packages/components/src/chat/PkStreamingMarkdownAutoscroll.vue","../../../../packages/components/src/chat/PkChatbotSteps.vue","../../../../packages/components/src/chat/PkChatbotSteps.vue"],"sourcesContent":["<script lang=\"ts\" setup>\n import { nextTick, ref, watch } from 'vue'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n\n const props = defineProps<{\n markdown?: string\n innerClass?: string\n isDark?: boolean\n }>()\n\n const scrollEl = ref<HTMLDivElement>()\n const lastScrollTop = ref<number>(0)\n const stopAutoScroll = ref<boolean>(false)\n\n const handleScroll = () => {\n if (!scrollEl.value) {\n return\n }\n const currentScrollTop = scrollEl.value.scrollTop\n if (currentScrollTop < lastScrollTop.value) {\n const delta = lastScrollTop.value - currentScrollTop\n if (delta < 50) {\n stopAutoScroll.value = true\n }\n }\n if (currentScrollTop > lastScrollTop.value) {\n if (\n currentScrollTop ===\n scrollEl.value.scrollHeight - scrollEl.value.clientHeight\n ) {\n stopAutoScroll.value = false\n }\n }\n lastScrollTop.value = currentScrollTop\n }\n\n watch(\n () => props.markdown,\n async () => {\n await nextTick()\n if (scrollEl.value && !stopAutoScroll.value) {\n scrollEl.value.scrollTop = scrollEl.value.scrollHeight\n }\n },\n { immediate: true },\n )\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n class=\"overflow-auto light-scrollbar\"\n @scroll=\"handleScroll\">\n <PkStreamingMarkdown :markdown :is-dark :class=\"innerClass\" />\n </div>\n</template>\n","<script lang=\"ts\" setup>\n import { nextTick, ref, watch } from 'vue'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n\n const props = defineProps<{\n markdown?: string\n innerClass?: string\n isDark?: boolean\n }>()\n\n const scrollEl = ref<HTMLDivElement>()\n const lastScrollTop = ref<number>(0)\n const stopAutoScroll = ref<boolean>(false)\n\n const handleScroll = () => {\n if (!scrollEl.value) {\n return\n }\n const currentScrollTop = scrollEl.value.scrollTop\n if (currentScrollTop < lastScrollTop.value) {\n const delta = lastScrollTop.value - currentScrollTop\n if (delta < 50) {\n stopAutoScroll.value = true\n }\n }\n if (currentScrollTop > lastScrollTop.value) {\n if (\n currentScrollTop ===\n scrollEl.value.scrollHeight - scrollEl.value.clientHeight\n ) {\n stopAutoScroll.value = false\n }\n }\n lastScrollTop.value = currentScrollTop\n }\n\n watch(\n () => props.markdown,\n async () => {\n await nextTick()\n if (scrollEl.value && !stopAutoScroll.value) {\n scrollEl.value.scrollTop = scrollEl.value.scrollHeight\n }\n },\n { immediate: true },\n )\n</script>\n\n<template>\n <div\n ref=\"scrollEl\"\n class=\"overflow-auto light-scrollbar\"\n @scroll=\"handleScroll\">\n <PkStreamingMarkdown :markdown :is-dark :class=\"innerClass\" />\n </div>\n</template>\n","<script lang=\"ts\" setup>\n import { computed, reactive, ref, watch } from 'vue'\n import { useI18n } from 'vue-i18n'\n import type { UIChatMessage } from 'models'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotStepToolDetail from './PkChatbotStepToolDetail.vue'\n import {\n isReasoningPart,\n isToolPart,\n getPartIcon,\n getPartState,\n getToolPartLabel,\n getToolPartSummary,\n hasRichToolDetail,\n stringifyPayload,\n formatDuration as formatDurationWith,\n } from './utils'\n\n const props = defineProps<{\n message: UIChatMessage\n isStreaming?: boolean\n isDark?: boolean\n }>()\n\n const { t: $t, n: $n } = useI18n({\n useScope: 'global',\n })\n\n type MessagePart = UIChatMessage['parts'][number]\n\n interface Step {\n key: string\n part: MessagePart\n icon?: string\n label?: string\n summary?: string\n durationMs?: number\n isActive: boolean\n text?: string\n toolInput?: unknown\n toolOutput?: unknown\n }\n\n const steps = computed<Step[]>(() => {\n const parts = props.message.parts\n const partTimings = props.message.metadata?.partTimings\n const result: Step[] = []\n for (const [index, part] of parts.entries()) {\n const isReasoning = isReasoningPart(part)\n const isTool = isToolPart(part)\n if (!isReasoning && !isTool) {\n continue\n }\n const toolCallId =\n 'toolCallId' in part ? (part.toolCallId as string) : undefined\n result.push({\n key: toolCallId ?? `${part.type}-${index}`,\n part,\n icon: getPartIcon(part),\n label: getToolPartLabel($t, part),\n summary: getToolPartSummary(part),\n durationMs: toolCallId ? partTimings?.[toolCallId] : undefined,\n isActive:\n Boolean(props.isStreaming) && index === parts.length - 1,\n text: isReasoning && part.text.trim() ? part.text : undefined,\n toolInput: isTool && 'input' in part ? part.input : undefined,\n toolOutput:\n isTool && 'output' in part ? part.output : undefined,\n })\n }\n return result\n })\n\n const totalDurationMs = computed(() => {\n const { createdAt, completedAt } = props.message.metadata ?? {}\n if (!createdAt || !completedAt) {\n return undefined\n }\n return completedAt - createdAt\n })\n\n const formatDuration = (ms?: number) => formatDurationWith($n, ms)\n\n const getStepError = (step: Step) =>\n getPartState(step.part) === 'output-error' &&\n 'errorText' in step.part &&\n typeof step.part.errorText === 'string'\n ? step.part.errorText\n : undefined\n\n const hasBody = (step: Step) => {\n if (step.text || getStepError(step)) {\n return true\n }\n if (hasRichToolDetail(step.part)) {\n return step.toolInput !== undefined || step.toolOutput !== undefined\n }\n return Boolean(\n stringifyPayload(step.toolInput) ??\n stringifyPayload(step.toolOutput),\n )\n }\n\n // --- Expand / collapse ---\n // The active (streaming) step is auto-expanded; a manual toggle always\n // wins over the automatic behavior.\n const manualSteps = reactive(new Map<string, boolean>())\n const isStepExpanded = (step: Step) =>\n manualSteps.get(step.key) ?? step.isActive\n const toggleStep = (step: Step) => {\n manualSteps.set(step.key, !isStepExpanded(step))\n }\n\n // The group is open while streaming and collapses to its header once the\n // answer is complete, unless the user toggled it manually.\n const manualGroup = ref<boolean | undefined>(undefined)\n const isGroupExpanded = computed(\n () => manualGroup.value ?? Boolean(props.isStreaming),\n )\n // Reset the manual override when a new stream starts so the timeline\n // opens again on regenerate.\n watch(\n () => props.isStreaming,\n (streaming) => {\n if (streaming) {\n manualGroup.value = undefined\n manualSteps.clear()\n }\n },\n )\n</script>\n\n<template>\n <div v-if=\"steps.length\" class=\"pk-chatbot-steps\">\n <button\n type=\"button\"\n class=\"pk-chatbot-steps__header\"\n :aria-expanded=\"isGroupExpanded\"\n @click=\"manualGroup = !isGroupExpanded\">\n <VvIcon\n v-if=\"isStreaming\"\n name=\"line-md:loading-loop\"\n class=\"shrink-0\" />\n <VvIcon v-else name=\"ri:route-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-steps__title\">\n {{ $t('messagePart.steps') }}\n </span>\n <span class=\"pk-chatbot-steps__meta\">\n {{ $t('messagePart.stepCount', steps.length) }}\n <template v-if=\"formatDuration(totalDurationMs)\">\n · {{ formatDuration(totalDurationMs) }}\n </template>\n </span>\n <VvIcon\n :name=\"\n isGroupExpanded\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"pk-chatbot-steps__chevron shrink-0 ml-auto\" />\n </button>\n <ol v-if=\"isGroupExpanded\" class=\"pk-chatbot-steps__list\">\n <li\n v-for=\"step in steps\"\n :key=\"step.key\"\n class=\"pk-chatbot-steps__step\"\n :class=\"{\n 'pk-chatbot-steps__step--active': step.isActive,\n }\">\n <component\n :is=\"hasBody(step) ? 'button' : 'div'\"\n :type=\"hasBody(step) ? 'button' : undefined\"\n class=\"pk-chatbot-steps__step-header\"\n :aria-expanded=\"\n hasBody(step) ? isStepExpanded(step) : undefined\n \"\n @click=\"hasBody(step) ? toggleStep(step) : undefined\">\n <span\n class=\"pk-chatbot-steps-marker\"\n :class=\"{\n 'pk-chatbot-steps-marker--active': step.isActive,\n }\">\n <VvIcon\n v-if=\"step.isActive\"\n name=\"line-md:loading-loop\" />\n <VvIcon v-else-if=\"step.icon\" :name=\"step.icon\" />\n </span>\n <span class=\"pk-chatbot-steps__step-label\">\n {{ step.label }}\n </span>\n <span class=\"pk-chatbot-steps__step-summary\">\n {{ step.summary }}\n </span>\n <span\n v-if=\"formatDuration(step.durationMs)\"\n class=\"pk-chatbot-steps__step-duration\">\n {{ formatDuration(step.durationMs) }}\n </span>\n <VvIcon\n v-if=\"hasBody(step)\"\n :name=\"\n isStepExpanded(step)\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"pk-chatbot-steps__chevron shrink-0 ml-auto\" />\n </component>\n <div\n v-if=\"hasBody(step) && isStepExpanded(step)\"\n class=\"pk-chatbot-steps__step-body\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"step.text && step.isActive\"\n :markdown=\"step.text\"\n :is-dark\n inner-class=\"wysiwyg\"\n class=\"pk-chatbot-steps__step-content pk-chatbot-steps__step-content--scroll\" />\n <PkStreamingMarkdown\n v-else-if=\"step.text\"\n :markdown=\"step.text\"\n :is-dark\n class=\"wysiwyg pk-chatbot-steps__step-content\" />\n <div\n v-else-if=\"getStepError(step)\"\n class=\"pk-chatbot-steps__step-payload pk-chatbot-steps__step-payload--error\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepError') }}\n </span>\n <pre>{{ getStepError(step) }}</pre>\n </div>\n <PkChatbotStepToolDetail\n v-else-if=\"hasRichToolDetail(step.part)\"\n :part=\"step.part\" />\n <template v-else>\n <div\n v-if=\"stringifyPayload(step.toolInput)\"\n class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepInput') }}\n </span>\n <pre>{{ stringifyPayload(step.toolInput) }}</pre>\n </div>\n <div\n v-if=\"stringifyPayload(step.toolOutput)\"\n class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepOutput') }}\n </span>\n <pre>{{ stringifyPayload(step.toolOutput) }}</pre>\n </div>\n </template>\n </div>\n </li>\n </ol>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-steps {\n width: 100%;\n border: var(--spacing-px) solid var(--color-surface-3);\n border-radius: var(--rounded-lg);\n background-color: var(--color-surface-1);\n font-size: var(--text-12);\n color: var(--color-word-3);\n overflow: hidden;\n\n &__header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: 100%;\n min-height: var(--spacing-40);\n padding: var(--spacing-8) var(--spacing-sm);\n text-align: left;\n cursor: pointer;\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n\n &__chevron {\n font-size: var(--text-16);\n }\n\n &__title {\n font-weight: 600;\n color: var(--color-word-2);\n flex-grow: 1;\n }\n\n &__meta {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__list {\n background-color: var(--color-surface);\n border-top: var(--spacing-px) solid var(--color-surface-3);\n padding: var(--spacing-8) var(--spacing-sm);\n display: flex;\n flex-direction: column;\n }\n\n &__step {\n position: relative;\n\n // Vertical connector between step markers\n &:not(:last-child)::before {\n content: '';\n position: absolute;\n left: var(--spacing-10);\n top: var(--spacing-28);\n bottom: calc(var(--spacing-8) * -1);\n width: var(--spacing-px);\n background-color: var(--color-surface-4);\n }\n }\n\n &__step-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: calc(100% + var(--spacing-16));\n min-height: var(--spacing-36);\n margin-inline: calc(var(--spacing-8) * -1);\n padding: var(--spacing-4) var(--spacing-8);\n text-align: left;\n\n &:is(button) {\n cursor: pointer;\n border-radius: var(--rounded);\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n &-marker {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--spacing-20);\n height: var(--spacing-20);\n flex-shrink: 0;\n position: relative;\n z-index: 1;\n background-color: var(--color-surface-1);\n border-radius: var(--rounded-full);\n border: var(--spacing-px) solid var(--color-surface-4);\n\n &--active {\n background-color: var(\n --chatbot-main-color,\n var(--color-surface-4)\n );\n border-color: transparent;\n color: var(--chatbot-contrast-color, var(--color-word-1));\n }\n }\n\n &__step-label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-2);\n }\n\n &__step-summary {\n flex: 1 1 0;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-4);\n }\n\n &__step-duration {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__step--active &__step-label {\n color: var(--color-word-1);\n }\n\n &__step-body {\n margin-left: var(--spacing-28);\n margin-block: var(--spacing-4) var(--spacing-8);\n }\n\n &__step-content {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n padding: var(--spacing-8);\n background-color: var(--color-surface-1);\n\n &--scroll {\n max-height: var(--spacing-256);\n }\n }\n\n &__step-payload {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2);\n\n & + & {\n margin-top: var(--spacing-8);\n }\n\n pre {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n padding: var(--spacing-8);\n font-size: var(--text-10);\n max-height: var(--spacing-160);\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n font-family: var(--font-mono);\n background-color: var(--color-surface-1);\n }\n }\n\n &__step-payload-label {\n font-weight: 600;\n color: var(--color-word-3);\n text-transform: uppercase;\n font-size: var(--text-10);\n letter-spacing: 0.05em;\n }\n\n &__step-payload--error {\n .pk-chatbot-steps__step-payload-label,\n pre {\n color: var(--color-danger);\n }\n\n pre {\n border-color: var(--color-danger-lighten-5);\n }\n }\n }\n</style>\n","<script lang=\"ts\" setup>\n import { computed, reactive, ref, watch } from 'vue'\n import { useI18n } from 'vue-i18n'\n import type { UIChatMessage } from 'models'\n import PkStreamingMarkdown from './PkStreamingMarkdown.vue'\n import PkStreamingMarkdownAutoscroll from './PkStreamingMarkdownAutoscroll.vue'\n import PkChatbotStepToolDetail from './PkChatbotStepToolDetail.vue'\n import {\n isReasoningPart,\n isToolPart,\n getPartIcon,\n getPartState,\n getToolPartLabel,\n getToolPartSummary,\n hasRichToolDetail,\n stringifyPayload,\n formatDuration as formatDurationWith,\n } from './utils'\n\n const props = defineProps<{\n message: UIChatMessage\n isStreaming?: boolean\n isDark?: boolean\n }>()\n\n const { t: $t, n: $n } = useI18n({\n useScope: 'global',\n })\n\n type MessagePart = UIChatMessage['parts'][number]\n\n interface Step {\n key: string\n part: MessagePart\n icon?: string\n label?: string\n summary?: string\n durationMs?: number\n isActive: boolean\n text?: string\n toolInput?: unknown\n toolOutput?: unknown\n }\n\n const steps = computed<Step[]>(() => {\n const parts = props.message.parts\n const partTimings = props.message.metadata?.partTimings\n const result: Step[] = []\n for (const [index, part] of parts.entries()) {\n const isReasoning = isReasoningPart(part)\n const isTool = isToolPart(part)\n if (!isReasoning && !isTool) {\n continue\n }\n const toolCallId =\n 'toolCallId' in part ? (part.toolCallId as string) : undefined\n result.push({\n key: toolCallId ?? `${part.type}-${index}`,\n part,\n icon: getPartIcon(part),\n label: getToolPartLabel($t, part),\n summary: getToolPartSummary(part),\n durationMs: toolCallId ? partTimings?.[toolCallId] : undefined,\n isActive:\n Boolean(props.isStreaming) && index === parts.length - 1,\n text: isReasoning && part.text.trim() ? part.text : undefined,\n toolInput: isTool && 'input' in part ? part.input : undefined,\n toolOutput:\n isTool && 'output' in part ? part.output : undefined,\n })\n }\n return result\n })\n\n const totalDurationMs = computed(() => {\n const { createdAt, completedAt } = props.message.metadata ?? {}\n if (!createdAt || !completedAt) {\n return undefined\n }\n return completedAt - createdAt\n })\n\n const formatDuration = (ms?: number) => formatDurationWith($n, ms)\n\n const getStepError = (step: Step) =>\n getPartState(step.part) === 'output-error' &&\n 'errorText' in step.part &&\n typeof step.part.errorText === 'string'\n ? step.part.errorText\n : undefined\n\n const hasBody = (step: Step) => {\n if (step.text || getStepError(step)) {\n return true\n }\n if (hasRichToolDetail(step.part)) {\n return step.toolInput !== undefined || step.toolOutput !== undefined\n }\n return Boolean(\n stringifyPayload(step.toolInput) ??\n stringifyPayload(step.toolOutput),\n )\n }\n\n // --- Expand / collapse ---\n // The active (streaming) step is auto-expanded; a manual toggle always\n // wins over the automatic behavior.\n const manualSteps = reactive(new Map<string, boolean>())\n const isStepExpanded = (step: Step) =>\n manualSteps.get(step.key) ?? step.isActive\n const toggleStep = (step: Step) => {\n manualSteps.set(step.key, !isStepExpanded(step))\n }\n\n // The group is open while streaming and collapses to its header once the\n // answer is complete, unless the user toggled it manually.\n const manualGroup = ref<boolean | undefined>(undefined)\n const isGroupExpanded = computed(\n () => manualGroup.value ?? Boolean(props.isStreaming),\n )\n // Reset the manual override when a new stream starts so the timeline\n // opens again on regenerate.\n watch(\n () => props.isStreaming,\n (streaming) => {\n if (streaming) {\n manualGroup.value = undefined\n manualSteps.clear()\n }\n },\n )\n</script>\n\n<template>\n <div v-if=\"steps.length\" class=\"pk-chatbot-steps\">\n <button\n type=\"button\"\n class=\"pk-chatbot-steps__header\"\n :aria-expanded=\"isGroupExpanded\"\n @click=\"manualGroup = !isGroupExpanded\">\n <VvIcon\n v-if=\"isStreaming\"\n name=\"line-md:loading-loop\"\n class=\"shrink-0\" />\n <VvIcon v-else name=\"ri:route-line\" class=\"shrink-0\" />\n <span class=\"pk-chatbot-steps__title\">\n {{ $t('messagePart.steps') }}\n </span>\n <span class=\"pk-chatbot-steps__meta\">\n {{ $t('messagePart.stepCount', steps.length) }}\n <template v-if=\"formatDuration(totalDurationMs)\">\n · {{ formatDuration(totalDurationMs) }}\n </template>\n </span>\n <VvIcon\n :name=\"\n isGroupExpanded\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"pk-chatbot-steps__chevron shrink-0 ml-auto\" />\n </button>\n <ol v-if=\"isGroupExpanded\" class=\"pk-chatbot-steps__list\">\n <li\n v-for=\"step in steps\"\n :key=\"step.key\"\n class=\"pk-chatbot-steps__step\"\n :class=\"{\n 'pk-chatbot-steps__step--active': step.isActive,\n }\">\n <component\n :is=\"hasBody(step) ? 'button' : 'div'\"\n :type=\"hasBody(step) ? 'button' : undefined\"\n class=\"pk-chatbot-steps__step-header\"\n :aria-expanded=\"\n hasBody(step) ? isStepExpanded(step) : undefined\n \"\n @click=\"hasBody(step) ? toggleStep(step) : undefined\">\n <span\n class=\"pk-chatbot-steps-marker\"\n :class=\"{\n 'pk-chatbot-steps-marker--active': step.isActive,\n }\">\n <VvIcon\n v-if=\"step.isActive\"\n name=\"line-md:loading-loop\" />\n <VvIcon v-else-if=\"step.icon\" :name=\"step.icon\" />\n </span>\n <span class=\"pk-chatbot-steps__step-label\">\n {{ step.label }}\n </span>\n <span class=\"pk-chatbot-steps__step-summary\">\n {{ step.summary }}\n </span>\n <span\n v-if=\"formatDuration(step.durationMs)\"\n class=\"pk-chatbot-steps__step-duration\">\n {{ formatDuration(step.durationMs) }}\n </span>\n <VvIcon\n v-if=\"hasBody(step)\"\n :name=\"\n isStepExpanded(step)\n ? 'ri:arrow-up-s-line'\n : 'ri:arrow-down-s-line'\n \"\n class=\"pk-chatbot-steps__chevron shrink-0 ml-auto\" />\n </component>\n <div\n v-if=\"hasBody(step) && isStepExpanded(step)\"\n class=\"pk-chatbot-steps__step-body\">\n <PkStreamingMarkdownAutoscroll\n v-if=\"step.text && step.isActive\"\n :markdown=\"step.text\"\n :is-dark\n inner-class=\"wysiwyg\"\n class=\"pk-chatbot-steps__step-content pk-chatbot-steps__step-content--scroll\" />\n <PkStreamingMarkdown\n v-else-if=\"step.text\"\n :markdown=\"step.text\"\n :is-dark\n class=\"wysiwyg pk-chatbot-steps__step-content\" />\n <div\n v-else-if=\"getStepError(step)\"\n class=\"pk-chatbot-steps__step-payload pk-chatbot-steps__step-payload--error\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepError') }}\n </span>\n <pre>{{ getStepError(step) }}</pre>\n </div>\n <PkChatbotStepToolDetail\n v-else-if=\"hasRichToolDetail(step.part)\"\n :part=\"step.part\" />\n <template v-else>\n <div\n v-if=\"stringifyPayload(step.toolInput)\"\n class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepInput') }}\n </span>\n <pre>{{ stringifyPayload(step.toolInput) }}</pre>\n </div>\n <div\n v-if=\"stringifyPayload(step.toolOutput)\"\n class=\"pk-chatbot-steps__step-payload\">\n <span class=\"pk-chatbot-steps__step-payload-label\">\n {{ $t('messagePart.stepOutput') }}\n </span>\n <pre>{{ stringifyPayload(step.toolOutput) }}</pre>\n </div>\n </template>\n </div>\n </li>\n </ol>\n </div>\n</template>\n\n<style lang=\"scss\">\n .pk-chatbot-steps {\n width: 100%;\n border: var(--spacing-px) solid var(--color-surface-3);\n border-radius: var(--rounded-lg);\n background-color: var(--color-surface-1);\n font-size: var(--text-12);\n color: var(--color-word-3);\n overflow: hidden;\n\n &__header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: 100%;\n min-height: var(--spacing-40);\n padding: var(--spacing-8) var(--spacing-sm);\n text-align: left;\n cursor: pointer;\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n\n &__chevron {\n font-size: var(--text-16);\n }\n\n &__title {\n font-weight: 600;\n color: var(--color-word-2);\n flex-grow: 1;\n }\n\n &__meta {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__list {\n background-color: var(--color-surface);\n border-top: var(--spacing-px) solid var(--color-surface-3);\n padding: var(--spacing-8) var(--spacing-sm);\n display: flex;\n flex-direction: column;\n }\n\n &__step {\n position: relative;\n\n // Vertical connector between step markers\n &:not(:last-child)::before {\n content: '';\n position: absolute;\n left: var(--spacing-10);\n top: var(--spacing-28);\n bottom: calc(var(--spacing-8) * -1);\n width: var(--spacing-px);\n background-color: var(--color-surface-4);\n }\n }\n\n &__step-header {\n display: flex;\n align-items: center;\n gap: var(--spacing-8);\n width: calc(100% + var(--spacing-16));\n min-height: var(--spacing-36);\n margin-inline: calc(var(--spacing-8) * -1);\n padding: var(--spacing-4) var(--spacing-8);\n text-align: left;\n\n &:is(button) {\n cursor: pointer;\n border-radius: var(--rounded);\n transition: background-color 0.2s ease;\n\n &:hover {\n background-color: var(--color-surface-2);\n }\n }\n }\n\n &-marker {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--spacing-20);\n height: var(--spacing-20);\n flex-shrink: 0;\n position: relative;\n z-index: 1;\n background-color: var(--color-surface-1);\n border-radius: var(--rounded-full);\n border: var(--spacing-px) solid var(--color-surface-4);\n\n &--active {\n background-color: var(\n --chatbot-main-color,\n var(--color-surface-4)\n );\n border-color: transparent;\n color: var(--chatbot-contrast-color, var(--color-word-1));\n }\n }\n\n &__step-label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-2);\n }\n\n &__step-summary {\n flex: 1 1 0;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--color-word-4);\n }\n\n &__step-duration {\n color: var(--color-word-4);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n }\n\n &__step--active &__step-label {\n color: var(--color-word-1);\n }\n\n &__step-body {\n margin-left: var(--spacing-28);\n margin-block: var(--spacing-4) var(--spacing-8);\n }\n\n &__step-content {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n padding: var(--spacing-8);\n background-color: var(--color-surface-1);\n\n &--scroll {\n max-height: var(--spacing-256);\n }\n }\n\n &__step-payload {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-2);\n\n & + & {\n margin-top: var(--spacing-8);\n }\n\n pre {\n border: var(--spacing-px) solid var(--color-surface-4);\n border-radius: var(--rounded);\n padding: var(--spacing-8);\n font-size: var(--text-10);\n max-height: var(--spacing-160);\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n font-family: var(--font-mono);\n background-color: var(--color-surface-1);\n }\n }\n\n &__step-payload-label {\n font-weight: 600;\n color: var(--color-word-3);\n text-transform: uppercase;\n font-size: var(--text-10);\n letter-spacing: 0.05em;\n }\n\n &__step-payload--error {\n .pk-chatbot-steps__step-payload-label,\n pre {\n color: var(--color-danger);\n }\n\n pre {\n border-color: var(--color-danger-lighten-5);\n }\n }\n }\n</style>\n"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { a as e, n as t, t as n } from "./useChatbotStore-DWLLr7sg.js";
|
|
2
2
|
import { t as r } from "./src-BbpxM17m.js";
|
|
3
|
-
import { t as i } from "./PkChatbotMessages-
|
|
3
|
+
import { t as i } from "./PkChatbotMessages-C6X92Igt.js";
|
|
4
4
|
import { t as a } from "./useLocalizedString-PfaKjIQY.js";
|
|
5
5
|
import { t as o } from "./PkAvatar-paEJByEA.js";
|
|
6
|
-
import { r as s } from "./utils-
|
|
6
|
+
import { r as s } from "./utils-CgxndZld.js";
|
|
7
7
|
import { n as c, t as l } from "./PkChatbotInput-CpSSAW2z.js";
|
|
8
8
|
import { t as u } from "./PkChatbotEmptyState-Brs9qhwj.js";
|
|
9
9
|
import { VvIcon as d } from "@volverjs/ui-vue/components";
|
|
@@ -20301,7 +20301,7 @@ var qh = (e) => {
|
|
|
20301
20301
|
},
|
|
20302
20302
|
emits: ["show-info", "revise"],
|
|
20303
20303
|
setup(r, { emit: c }) {
|
|
20304
|
-
let f = ie(() => import("./PkToolShowForm-BzzeoKss.js").then((e) => e.n)), re = ie(() => import("./PkToolShowContactForm-CMK4zR2J.js").then((e) => e.n)), ae = ie(() => import("./PkToolShowSuggestedReply-DSd1dnZM.js").then((e) => e.n)), se = ie(() => import("./PkToolShowSources-DdfHFaPA.js").then((e) => e.n)), ue = ie(() => import("./PkToolRequestGeolocation-B1K2fN7U.js")), de = ie(() => import("./PkToolRequestOAuthConnection-D-qoxuPc.js")), fe = ie(() => import("./PkToolShowLocation-BBdeY0Ae.js").then((e) => e.n)), me = ie(() => import("./PkToolShowDiagram-
|
|
20304
|
+
let f = ie(() => import("./PkToolShowForm-BzzeoKss.js").then((e) => e.n)), re = ie(() => import("./PkToolShowContactForm-CMK4zR2J.js").then((e) => e.n)), ae = ie(() => import("./PkToolShowSuggestedReply-DSd1dnZM.js").then((e) => e.n)), se = ie(() => import("./PkToolShowSources-DdfHFaPA.js").then((e) => e.n)), ue = ie(() => import("./PkToolRequestGeolocation-B1K2fN7U.js")), de = ie(() => import("./PkToolRequestOAuthConnection-D-qoxuPc.js")), fe = ie(() => import("./PkToolShowLocation-BBdeY0Ae.js").then((e) => e.n)), me = ie(() => import("./PkToolShowDiagram-BvOy4aeQ.js")), he = r, _e = c, Te = t(he.agentId), { agentInterface: Ee, agentFileUpload: De, agentVoice: Oe, actions: ke, revisedAnswers: je, messages: Me, chat: Ne, messageFeedbacks: Pe, feedbackDialogMessage: Fe, isFeedbackSubmitting: Ie, isFeedbackSubmitted: Le, feedbackSubmitError: Re, isLeadSubmitted: Be, isLoadingSubmitLead: Ve, submitLeadError: He, input: Ue, inputMessagePlaceholder: We, isConversationBlocked: Ge, baseUrl: Ke, pendingAttachments: qe, isAttachmentLimitReached: Je, fileLimitError: Ye, apiClient: Xe, isDark: Ze } = Ce(Te), { handleSubmit: b, stopGeneration: x, regenerate: Qe, onUpvote: $e, onDownvote: et, onFeedback: tt, onFeedbackSubmit: nt, onLeadSubmit: rt, startNewChat: it, addToolOutput: at, addToolApprovalResponse: ot, handleFileSelect: st, clearFileLimitError: ct, refreshMessages: lt, appendMessage: ut } = Te, { t: dt } = xe({ useScope: "global" }), ft = m(() => {
|
|
20305
20305
|
let e = Ye.value;
|
|
20306
20306
|
if (e) return e.reason === "per-chat" ? dt("message.fileLimitPerChat", { max: e.max }) : dt("message.fileLimitPerMessage", { max: e.max });
|
|
20307
20307
|
}), pt = a(() => Ee.value?.dismissableNotice), mt = m(() => Me.value.some((e) => e.role === "user")), ht = m(() => he.modifier === "fullscreen" && !mt.value), gt = we(Ee, () => he.userName), _t = ve("chatViewEl"), vt = async (e) => (await Xe.value.expandSourceContext(he.agentId, e.documentId, e.chunkIndex)).content, yt = async (e) => {
|
|
@@ -20662,4 +20662,4 @@ var qh = (e) => {
|
|
|
20662
20662
|
//#endregion
|
|
20663
20663
|
export { Ae as i, ze as n, Ne as r, ng as t };
|
|
20664
20664
|
|
|
20665
|
-
//# sourceMappingURL=PkChatbotViewChat-
|
|
20665
|
+
//# sourceMappingURL=PkChatbotViewChat-CBvcSg0J.js.map
|