|
@@ -1,9 +1,20 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="chat-content">
|
|
<div class="chat-content">
|
|
|
<div v-if="messages.length > 0" ref="scrollContainerRef" class="item-list">
|
|
<div v-if="messages.length > 0" ref="scrollContainerRef" class="item-list">
|
|
|
- <Bubble v-for="item in messages" :key="item.id || item.key" :content="item.content" :placement="item.placement"
|
|
|
|
|
- :loading="item.loading" :shape="item.shape" :variant="item.variant" :is-markdown="item.isMarkdown"
|
|
|
|
|
- :is-fog="item.isFog" :typing="item.typing" class="message-bubble" maxWidth="800px">
|
|
|
|
|
|
|
+ <Bubble
|
|
|
|
|
+ v-for="item in messages"
|
|
|
|
|
+ :key="item.id || item.key"
|
|
|
|
|
+ :content="item.content"
|
|
|
|
|
+ :placement="item.placement"
|
|
|
|
|
+ :loading="item.loading"
|
|
|
|
|
+ :shape="item.shape"
|
|
|
|
|
+ :variant="item.variant"
|
|
|
|
|
+ :is-markdown="item.isMarkdown"
|
|
|
|
|
+ :is-fog="item.isFog"
|
|
|
|
|
+ :typing="item.typing"
|
|
|
|
|
+ class="message-bubble"
|
|
|
|
|
+ maxWidth="800px"
|
|
|
|
|
+ >
|
|
|
<template #avatar>
|
|
<template #avatar>
|
|
|
<div class="avatar-wrapper">
|
|
<div class="avatar-wrapper">
|
|
|
<Icon v-if="item.role === 'ai'" icon="mingcute:ai-line" :width="22" />
|
|
<Icon v-if="item.role === 'ai'" icon="mingcute:ai-line" :width="22" />
|
|
@@ -13,36 +24,50 @@
|
|
|
|
|
|
|
|
<template #header>
|
|
<template #header>
|
|
|
<div class="header-wrapper">
|
|
<div class="header-wrapper">
|
|
|
- <div class="header-name">
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div class="header-name"></div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<template #content>
|
|
<template #content>
|
|
|
<div class="msg-content-wrapper">
|
|
<div class="msg-content-wrapper">
|
|
|
<div v-if="item.role === 'ai' && hasStructuredBlocks(item)" class="structured-blocks">
|
|
<div v-if="item.role === 'ai' && hasStructuredBlocks(item)" class="structured-blocks">
|
|
|
- <Thinking v-if="shouldShowThinking(item)" :content="getThinkingText(item)"
|
|
|
|
|
- :status="getThinkingStatus(item)" :model-value="!item.streamCompleted" auto-collapse max-width="100%"
|
|
|
|
|
- background-color="var(--bg-container)" color="var(--text-primary)" />
|
|
|
|
|
|
|
+ <Thinking
|
|
|
|
|
+ v-if="shouldShowThinking(item)"
|
|
|
|
|
+ :content="getThinkingText(item)"
|
|
|
|
|
+ :status="getThinkingStatus(item)"
|
|
|
|
|
+ :model-value="isThinkingOpen(item)"
|
|
|
|
|
+ auto-collapse
|
|
|
|
|
+ max-width="100%"
|
|
|
|
|
+ background-color="var(--bg-container)"
|
|
|
|
|
+ color="var(--text-primary)"
|
|
|
|
|
+ />
|
|
|
|
|
|
|
|
<div v-if="getToolCalls(item).length" class="structured-block">
|
|
<div v-if="getToolCalls(item).length" class="structured-block">
|
|
|
<div class="structured-block__head">
|
|
<div class="structured-block__head">
|
|
|
<span class="structured-block__title">{{ t('pages.chat.toolCallTitle') }}</span>
|
|
<span class="structured-block__title">{{ t('pages.chat.toolCallTitle') }}</span>
|
|
|
- <el-tag size="small" type="warning" effect="plain">{{ getToolCalls(item).length }}</el-tag>
|
|
|
|
|
|
|
+ <el-tag size="small" type="warning" effect="plain">{{
|
|
|
|
|
+ getToolCalls(item).length
|
|
|
|
|
+ }}</el-tag>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="tool-list">
|
|
<div class="tool-list">
|
|
|
- <div v-for="(toolCall, index) in getToolCalls(item)"
|
|
|
|
|
- :key="`${item.id || item.key}-tool-call-${index}`" class="tool-card">
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(toolCall, index) in getToolCalls(item)"
|
|
|
|
|
+ :key="`${item.id || item.key}-tool-call-${index}`"
|
|
|
|
|
+ class="tool-card"
|
|
|
|
|
+ >
|
|
|
<div class="tool-card__head">
|
|
<div class="tool-card__head">
|
|
|
<div class="tool-card__meta">
|
|
<div class="tool-card__meta">
|
|
|
<el-tag size="small" type="warning" effect="dark">
|
|
<el-tag size="small" type="warning" effect="dark">
|
|
|
{{ getToolName(toolCall.toolName) }}
|
|
{{ getToolName(toolCall.toolName) }}
|
|
|
</el-tag>
|
|
</el-tag>
|
|
|
- <span v-if="toolCall.toolCallId" class="tool-card__id">{{ toolCall.toolCallId }}</span>
|
|
|
|
|
|
|
+ <span v-if="toolCall.toolCallId" class="tool-card__id">{{
|
|
|
|
|
+ toolCall.toolCallId
|
|
|
|
|
+ }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <pre
|
|
|
|
|
- class="tool-card__content">{{ formatStructuredValue(toolCall.arguments || toolCall.content) }}</pre>
|
|
|
|
|
|
|
+ <pre class="tool-card__content">{{
|
|
|
|
|
+ formatStructuredValue(toolCall.arguments || toolCall.content)
|
|
|
|
|
+ }}</pre>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -50,18 +75,29 @@
|
|
|
<div v-if="getToolResults(item).length" class="structured-block">
|
|
<div v-if="getToolResults(item).length" class="structured-block">
|
|
|
<div class="structured-block__head">
|
|
<div class="structured-block__head">
|
|
|
<span class="structured-block__title">{{ t('pages.chat.toolResultTitle') }}</span>
|
|
<span class="structured-block__title">{{ t('pages.chat.toolResultTitle') }}</span>
|
|
|
- <el-tag size="small" type="success" effect="plain">{{ getToolResults(item).length }}</el-tag>
|
|
|
|
|
|
|
+ <el-tag size="small" type="success" effect="plain">{{
|
|
|
|
|
+ getToolResults(item).length
|
|
|
|
|
+ }}</el-tag>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="tool-list">
|
|
<div class="tool-list">
|
|
|
- <div v-for="(toolResult, index) in getToolResults(item)"
|
|
|
|
|
- :key="`${item.id || item.key}-tool-result-${index}`" class="tool-card">
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(toolResult, index) in getToolResults(item)"
|
|
|
|
|
+ :key="`${item.id || item.key}-tool-result-${index}`"
|
|
|
|
|
+ class="tool-card"
|
|
|
|
|
+ >
|
|
|
<div class="tool-card__head">
|
|
<div class="tool-card__head">
|
|
|
<div class="tool-card__meta">
|
|
<div class="tool-card__meta">
|
|
|
- <el-tag :type="toolResult.success === false ? 'danger' : 'success'" size="small" effect="dark">
|
|
|
|
|
|
|
+ <el-tag
|
|
|
|
|
+ :type="toolResult.success === false ? 'danger' : 'success'"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ effect="dark"
|
|
|
|
|
+ >
|
|
|
{{ toolResult.success === false ? t('pages.chat.failedTag') : 'OK' }}
|
|
{{ toolResult.success === false ? t('pages.chat.failedTag') : 'OK' }}
|
|
|
</el-tag>
|
|
</el-tag>
|
|
|
<span class="tool-card__name">{{ getToolName(toolResult.toolName) }}</span>
|
|
<span class="tool-card__name">{{ getToolName(toolResult.toolName) }}</span>
|
|
|
- <span v-if="toolResult.toolCallId" class="tool-card__id">{{ toolResult.toolCallId }}</span>
|
|
|
|
|
|
|
+ <span v-if="toolResult.toolCallId" class="tool-card__id">{{
|
|
|
|
|
+ toolResult.toolCallId
|
|
|
|
|
+ }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="toolResult.durationMs !== undefined" class="tool-card__duration">
|
|
<div v-if="toolResult.durationMs !== undefined" class="tool-card__duration">
|
|
|
{{ toolResult.durationMs }}ms
|
|
{{ toolResult.durationMs }}ms
|
|
@@ -69,39 +105,72 @@
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="hasToolResultThought(toolResult)" class="tool-card__section">
|
|
<div v-if="hasToolResultThought(toolResult)" class="tool-card__section">
|
|
|
<div class="tool-card__label">{{ t('pages.chat.thinkTitle') }}</div>
|
|
<div class="tool-card__label">{{ t('pages.chat.thinkTitle') }}</div>
|
|
|
- <pre class="tool-card__content">{{ getToolResultThought(toolResult) }}</pre>
|
|
|
|
|
|
|
+ <pre
|
|
|
|
|
+ class="tool-card__content"
|
|
|
|
|
+ ><XMarkdown :markdown="getToolResultThought(toolResult)"/></pre>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="hasToolResultOutput(toolResult)" class="tool-card__section">
|
|
<div v-if="hasToolResultOutput(toolResult)" class="tool-card__section">
|
|
|
<div class="tool-card__label">{{ t('pages.chat.toolResultTitle') }}</div>
|
|
<div class="tool-card__label">{{ t('pages.chat.toolResultTitle') }}</div>
|
|
|
- <pre class="tool-card__content">{{ getToolResultOutput(toolResult) }}</pre>
|
|
|
|
|
|
|
+ <pre class="tool-card__content">
|
|
|
|
|
+ <XMarkdown :markdown="getToolResultOutput(toolResult)"/>
|
|
|
|
|
+ </pre>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="hasToolResultItems(toolResult)" class="tool-card__section">
|
|
<div v-if="hasToolResultItems(toolResult)" class="tool-card__section">
|
|
|
<div class="tool-card__label">content_items</div>
|
|
<div class="tool-card__label">content_items</div>
|
|
|
- <pre class="tool-card__content">{{ formatStructuredValue(getToolResultItems(toolResult)) }}</pre>
|
|
|
|
|
|
|
+ <pre class="tool-card__content">
|
|
|
|
|
+ <XMarkdown :markdown="formatStructuredValue(getToolResultItems(toolResult))"/>
|
|
|
|
|
+ </pre>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="toolResult.error" class="tool-card__section">
|
|
<div v-if="toolResult.error" class="tool-card__section">
|
|
|
<div class="tool-card__label">{{ t('pages.chat.failedTag') }}</div>
|
|
<div class="tool-card__label">{{ t('pages.chat.failedTag') }}</div>
|
|
|
- <pre class="tool-card__content tool-card__content--error">{{ toolResult.error }}</pre>
|
|
|
|
|
|
|
+ <pre class="tool-card__content tool-card__content--error">{{
|
|
|
|
|
+ toolResult.error
|
|
|
|
|
+ }}</pre>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <XMarkdown v-if="item.content" class="msg-content-text" :markdown="item.content" enableLatex enableBreaks />
|
|
|
|
|
|
|
+ <XMarkdown
|
|
|
|
|
+ v-if="getDisplayContent(item)"
|
|
|
|
|
+ class="msg-content-text"
|
|
|
|
|
+ :markdown="getDisplayContent(item)"
|
|
|
|
|
+ enableLatex
|
|
|
|
|
+ enableBreaks
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<template #footer>
|
|
<template #footer>
|
|
|
<div v-if="getMessageImages(item).length" class="file-wrap">
|
|
<div v-if="getMessageImages(item).length" class="file-wrap">
|
|
|
- <el-image v-for="(src, index) in getMessageImages(item)" :key="`${item.id || item.key}-file-${index}`"
|
|
|
|
|
- :src="src" class="file-image" fit="cover" preview-teleported hide-on-click-modal
|
|
|
|
|
- :preview-src-list="getMessageImages(item)" />
|
|
|
|
|
|
|
+ <el-image
|
|
|
|
|
+ v-for="(src, index) in getMessageImages(item)"
|
|
|
|
|
+ :key="`${item.id || item.key}-file-${index}`"
|
|
|
|
|
+ :src="src"
|
|
|
|
|
+ class="file-image"
|
|
|
|
|
+ fit="cover"
|
|
|
|
|
+ preview-teleported
|
|
|
|
|
+ hide-on-click-modal
|
|
|
|
|
+ :preview-src-list="getMessageImages(item)"
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
<div class="footer-wrapper" v-if="item.role === 'ai' && item.streamCompleted">
|
|
<div class="footer-wrapper" v-if="item.role === 'ai' && item.streamCompleted">
|
|
|
<div class="footer-container">
|
|
<div class="footer-container">
|
|
|
- <el-button type="info" :icon="Refresh" size="small" circle @click="handleRetry(item)" />
|
|
|
|
|
- <el-button color="#626aef" :icon="DocumentCopy" size="small" circle @click="handleCopy(item)" />
|
|
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ type="info"
|
|
|
|
|
+ :icon="Refresh"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ circle
|
|
|
|
|
+ @click="handleRetry(item)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ color="#626aef"
|
|
|
|
|
+ :icon="DocumentCopy"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ circle
|
|
|
|
|
+ @click="handleCopy(item)"
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
<div class="footer-time">
|
|
<div class="footer-time">
|
|
|
{{ item.updateTime }}
|
|
{{ item.updateTime }}
|
|
@@ -117,6 +186,7 @@
|
|
|
</div>
|
|
</div>
|
|
|
<div class="empty-text">{{ t('pages.chat.emptyTitle') }}</div>
|
|
<div class="empty-text">{{ t('pages.chat.emptyTitle') }}</div>
|
|
|
<div class="empty-subtext">{{ t('pages.chat.emptySubtitle') }}</div>
|
|
<div class="empty-subtext">{{ t('pages.chat.emptySubtitle') }}</div>
|
|
|
|
|
+ <slot />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
@@ -127,7 +197,7 @@ import { Bubble, Thinking, XMarkdown } from 'vue-element-plus-x'
|
|
|
import { useI18n } from '@/composables/useI18n'
|
|
import { useI18n } from '@/composables/useI18n'
|
|
|
import { ElMessage } from 'element-plus'
|
|
import { ElMessage } from 'element-plus'
|
|
|
import { DocumentCopy, Refresh } from '@element-plus/icons-vue'
|
|
import { DocumentCopy, Refresh } from '@element-plus/icons-vue'
|
|
|
-import { Icon } from "@repo/ui"
|
|
|
|
|
|
|
+import { Icon } from '@repo/ui'
|
|
|
import type { BubbleMessage, ChatToolResult } from '../../views/chat/types'
|
|
import type { BubbleMessage, ChatToolResult } from '../../views/chat/types'
|
|
|
|
|
|
|
|
const { t } = useI18n()
|
|
const { t } = useI18n()
|
|
@@ -147,10 +217,38 @@ const getMessageImages = (message: BubbleMessage) => {
|
|
|
return ids.filter(Boolean).map((fileId) => `/File/GetImage?fileId=${fileId}`)
|
|
return ids.filter(Boolean).map((fileId) => `/File/GetImage?fileId=${fileId}`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const getThinkingText = (message: BubbleMessage) => `${message.thinking || ''}`.trim()
|
|
|
|
|
|
|
+const THINK_TAG_RE = /<think\b[^>]*>([\s\S]*?)(?:<\/think>|$)/gi
|
|
|
|
|
+
|
|
|
|
|
+const parseThinkContent = (content?: string) => {
|
|
|
|
|
+ const text = `${content || ''}`
|
|
|
|
|
+ const thinkingParts: string[] = []
|
|
|
|
|
+ const displayContent = text
|
|
|
|
|
+ .replace(THINK_TAG_RE, (_match, thinkingText) => {
|
|
|
|
|
+ const trimmed = `${thinkingText || ''}`.trim()
|
|
|
|
|
+ if (trimmed) thinkingParts.push(trimmed)
|
|
|
|
|
+ return ''
|
|
|
|
|
+ })
|
|
|
|
|
+ .trim()
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ displayContent,
|
|
|
|
|
+ thinkingText: thinkingParts.join('\n\n')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getEmbeddedThinkingText = (message: BubbleMessage) => parseThinkContent(message.content).thinkingText
|
|
|
|
|
+
|
|
|
|
|
+const getDisplayContent = (message: BubbleMessage) => parseThinkContent(message.content).displayContent
|
|
|
|
|
+
|
|
|
|
|
+const getThinkingText = (message: BubbleMessage) => {
|
|
|
|
|
+ const parts = [`${message.thinking || ''}`.trim(), getEmbeddedThinkingText(message)].filter(Boolean)
|
|
|
|
|
+ return parts.join('\n\n')
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
const shouldShowThinking = (message: BubbleMessage) => !!getThinkingText(message)
|
|
const shouldShowThinking = (message: BubbleMessage) => !!getThinkingText(message)
|
|
|
|
|
|
|
|
|
|
+const isThinkingOpen = (message: BubbleMessage) => message.thinkingOpen ?? !message.streamCompleted
|
|
|
|
|
+
|
|
|
const getThinkingStatus = (message: BubbleMessage) => {
|
|
const getThinkingStatus = (message: BubbleMessage) => {
|
|
|
if (message.error) return 'error'
|
|
if (message.error) return 'error'
|
|
|
return message.streamCompleted ? 'end' : 'thinking'
|
|
return message.streamCompleted ? 'end' : 'thinking'
|
|
@@ -173,16 +271,13 @@ const hasToolResultOutput = (toolResult: ChatToolResult) => {
|
|
|
return output !== null && output !== undefined && `${output}`.trim() !== ''
|
|
return output !== null && output !== undefined && `${output}`.trim() !== ''
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const getToolResultItems = (toolResult: ChatToolResult) => Array.isArray(toolResult.contentItems) ? toolResult.contentItems : []
|
|
|
|
|
|
|
+const getToolResultItems = (toolResult: ChatToolResult) =>
|
|
|
|
|
+ Array.isArray(toolResult.contentItems) ? toolResult.contentItems : []
|
|
|
|
|
|
|
|
const hasToolResultItems = (toolResult: ChatToolResult) => getToolResultItems(toolResult).length > 0
|
|
const hasToolResultItems = (toolResult: ChatToolResult) => getToolResultItems(toolResult).length > 0
|
|
|
|
|
|
|
|
const hasStructuredBlocks = (message: BubbleMessage) =>
|
|
const hasStructuredBlocks = (message: BubbleMessage) =>
|
|
|
- !!(
|
|
|
|
|
- getThinkingText(message) ||
|
|
|
|
|
- getToolCalls(message).length ||
|
|
|
|
|
- getToolResults(message).length
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ !!(getThinkingText(message) || getToolCalls(message).length || getToolResults(message).length)
|
|
|
|
|
|
|
|
const formatStructuredValue = (value: any) => {
|
|
const formatStructuredValue = (value: any) => {
|
|
|
if (value === null || value === undefined || value === '') return '-'
|
|
if (value === null || value === undefined || value === '') return '-'
|
|
@@ -194,7 +289,6 @@ const formatStructuredValue = (value: any) => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
const scrollContainerRef = ref<HTMLDivElement>()
|
|
const scrollContainerRef = ref<HTMLDivElement>()
|
|
|
|
|
|
|
|
defineExpose({
|
|
defineExpose({
|
|
@@ -219,6 +313,9 @@ const handleCopy = (message: BubbleMessage) => {
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
<style lang="less" scoped>
|
|
|
|
|
+:deep(.el-thinking) {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
.chat-content {
|
|
.chat-content {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -232,7 +329,7 @@ const handleCopy = (message: BubbleMessage) => {
|
|
|
padding-right: 4px;
|
|
padding-right: 4px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .message-bubble+.message-bubble {
|
|
|
|
|
|
|
+ .message-bubble + .message-bubble {
|
|
|
margin-top: 16px;
|
|
margin-top: 16px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -400,7 +497,7 @@ const handleCopy = (message: BubbleMessage) => {
|
|
|
word-break: break-all;
|
|
word-break: break-all;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.tool-card__section+.tool-card__section {
|
|
|
|
|
|
|
+.tool-card__section + .tool-card__section {
|
|
|
margin-top: 8px;
|
|
margin-top: 8px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -477,7 +574,6 @@ const handleCopy = (message: BubbleMessage) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@keyframes bounce {
|
|
@keyframes bounce {
|
|
|
-
|
|
|
|
|
0%,
|
|
0%,
|
|
|
100% {
|
|
100% {
|
|
|
transform: translateY(5px);
|
|
transform: translateY(5px);
|