feat: 增加样式渲染
This commit is contained in:
parent
96dcb5d20e
commit
7a6ab26379
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.15.0",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"marked": "^15.0.0",
|
"marked": "^15.0.0",
|
||||||
"marked-highlight": "^2.2.1",
|
"marked-highlight": "^2.2.1",
|
||||||
|
|
@ -67,6 +68,29 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.9.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.2.tgz",
|
||||||
|
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.9.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
||||||
|
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
|
|
@ -881,6 +905,15 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/highlight.js": {
|
||||||
|
"version": "11.11.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
"version": "5.5.3",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||||
|
|
@ -1190,7 +1223,6 @@
|
||||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.12.tgz",
|
"resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.12.tgz",
|
||||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
|
|
@ -1279,7 +1311,6 @@
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -1453,7 +1484,6 @@
|
||||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
|
|
@ -1531,7 +1561,6 @@
|
||||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.32.tgz",
|
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.32.tgz",
|
||||||
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
|
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.32",
|
"@vue/compiler-dom": "3.5.32",
|
||||||
"@vue/compiler-sfc": "3.5.32",
|
"@vue/compiler-sfc": "3.5.32",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.15.0",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"marked": "^15.0.0",
|
"marked": "^15.0.0",
|
||||||
"marked-highlight": "^2.2.1",
|
"marked-highlight": "^2.2.1",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="message-bubble" :class="[role]">
|
<div class="message-bubble" :class="[message.role]">
|
||||||
<div v-if="role === 'user'" class="avatar">user</div>
|
<div v-if="message.role === 'user'" class="avatar">user</div>
|
||||||
<div v-else class="avatar">Luxx</div>
|
<div v-else class="avatar">Luxx</div>
|
||||||
<div class="message-container">
|
<div class="message-container">
|
||||||
<!-- File attachments list -->
|
<!-- File attachments list -->
|
||||||
<div v-if="attachments && attachments.length > 0" class="attachments-list">
|
<div v-if="message.attachments && message.attachments.length > 0" class="attachments-list">
|
||||||
<div v-for="(file, index) in attachments" :key="index" class="attachment-item">
|
<div v-for="(file, index) in message.attachments" :key="index" class="attachment-item">
|
||||||
<span class="attachment-icon">{{ file.extension }}</span>
|
<span class="attachment-icon">{{ file.extension }}</span>
|
||||||
<span class="attachment-name">{{ file.name }}</span>
|
<span class="attachment-name">{{ file.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -13,30 +13,32 @@
|
||||||
<div ref="messageRef" class="message-body">
|
<div ref="messageRef" class="message-body">
|
||||||
<!-- Primary rendering path: processSteps contains all ordered steps -->
|
<!-- Primary rendering path: processSteps contains all ordered steps -->
|
||||||
<ProcessBlock
|
<ProcessBlock
|
||||||
v-if="processSteps && processSteps.length > 0"
|
v-if="message.process_steps && message.process_steps.length > 0"
|
||||||
:process-steps="processSteps"
|
:process-steps="message.process_steps"
|
||||||
/>
|
/>
|
||||||
<!-- Fallback path: old messages without processSteps in DB -->
|
<!-- Fallback path: old messages without processSteps in DB -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ProcessBlock
|
<ProcessBlock
|
||||||
v-if="toolCalls && toolCalls.length > 0"
|
v-if="message.tool_calls && message.tool_calls.length > 0"
|
||||||
:tool-calls="toolCalls"
|
:tool-calls="message.tool_calls"
|
||||||
/>
|
/>
|
||||||
<div class="md-content message-content" v-html="renderedContent"></div>
|
<div class="md-content message-content" v-html="renderedContent"></div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-footer">
|
<div class="message-footer">
|
||||||
<span class="token-info" v-if="usage">
|
<span class="message-time">{{ formatTime(message.created_at) }}</span>
|
||||||
<span class="token-item" v-if="usage.prompt_tokens">输入: {{ usage.prompt_tokens }}</span>
|
<template v-if="message.role === 'assistant' && message.usage">
|
||||||
<span class="token-item" v-if="usage.completion_tokens">输出: {{ usage.completion_tokens }}</span>
|
<span class="token-item" v-if="message.usage.prompt">{{ formatNumber(message.usage.prompt) }} in</span>
|
||||||
<span class="token-item total" v-if="usage.total_tokens">总计: {{ usage.total_tokens }}</span>
|
<span class="token-item" v-if="message.usage.completion">{{ formatNumber(message.usage.completion) }} out</span>
|
||||||
</span>
|
<span class="token-item" v-if="message.usage.total">{{ formatNumber(message.usage.total) }} total</span>
|
||||||
<span class="token-count" v-else-if="tokenCount">{{ tokenCount }} tokens</span>
|
</template>
|
||||||
<span class="message-time">{{ formatTime(createdAt) }}</span>
|
<button v-if="message.role === 'assistant'" class="ghost-btn success" @click="$emit('regenerate', message.id)" title="重新生成">
|
||||||
<button v-if="role === 'assistant'" class="ghost-btn accent" @click="copyContent" title="复制">
|
<span v-html="regenerateIcon"></span>
|
||||||
|
</button>
|
||||||
|
<button v-if="message.role === 'assistant'" class="ghost-btn accent" @click="copyContent" title="复制">
|
||||||
<span v-html="copyIcon"></span>
|
<span v-html="copyIcon"></span>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="deletable" class="ghost-btn danger" @click="$emit('delete')" title="删除">
|
<button v-if="deletable" class="ghost-btn danger" @click="$emit('delete', message.id)" title="删除">
|
||||||
<span v-html="trashIcon"></span>
|
<span v-html="trashIcon"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,27 +49,22 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { renderMarkdown } from '../utils/markdown.js'
|
import { renderMarkdown } from '../utils/markdown.js'
|
||||||
|
import { formatNumber } from '../utils/useFormatters.js'
|
||||||
import ProcessBlock from './ProcessBlock.vue'
|
import ProcessBlock from './ProcessBlock.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
role: { type: String, required: true },
|
message: { type: Object, required: true },
|
||||||
text: { type: String, default: '' },
|
|
||||||
toolCalls: { type: Array, default: () => [] },
|
|
||||||
processSteps: { type: Array, default: () => [] },
|
|
||||||
tokenCount: { type: Number, default: 0 },
|
|
||||||
usage: { type: Object, default: null },
|
|
||||||
createdAt: { type: String, default: '' },
|
|
||||||
deletable: { type: Boolean, default: false },
|
deletable: { type: Boolean, default: false },
|
||||||
attachments: { type: Array, default: () => [] },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['delete'])
|
defineEmits(['delete', 'regenerate'])
|
||||||
|
|
||||||
const messageRef = ref(null)
|
const messageRef = ref(null)
|
||||||
|
|
||||||
const renderedContent = computed(() => {
|
const renderedContent = computed(() => {
|
||||||
if (!props.text) return ''
|
const text = props.message.content || props.message.text || ''
|
||||||
return renderMarkdown(props.text)
|
if (!text) return ''
|
||||||
|
return renderMarkdown(text)
|
||||||
})
|
})
|
||||||
|
|
||||||
function formatTime(time) {
|
function formatTime(time) {
|
||||||
|
|
@ -78,9 +75,9 @@ function formatTime(time) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyContent() {
|
function copyContent() {
|
||||||
let text = props.text || ''
|
let text = props.message.content || props.message.text || ''
|
||||||
if (props.processSteps && props.processSteps.length > 0) {
|
if (props.message.process_steps && props.message.process_steps.length > 0) {
|
||||||
const parts = props.processSteps
|
const parts = props.message.process_steps
|
||||||
.filter(s => s && s.type === 'text')
|
.filter(s => s && s.type === 'text')
|
||||||
.map(s => s.content)
|
.map(s => s.content)
|
||||||
if (parts.length > 0) text = parts.join('\n\n')
|
if (parts.length > 0) text = parts.join('\n\n')
|
||||||
|
|
@ -92,6 +89,8 @@ function copyContent() {
|
||||||
const copyIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`
|
const copyIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`
|
||||||
|
|
||||||
const trashIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`
|
const trashIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`
|
||||||
|
|
||||||
|
const regenerateIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 4v6h6"></path><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -135,30 +134,6 @@ const trashIcon = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" s
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 6px 0 0;
|
padding: 6px 0 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.token-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-item {
|
|
||||||
padding: 2px 6px;
|
|
||||||
background: var(--bg-code);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-item.total {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-count,
|
|
||||||
.message-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
|
import { markedHighlight } from 'marked-highlight'
|
||||||
import katex from 'katex'
|
import katex from 'katex'
|
||||||
|
import hljs from 'highlight.js'
|
||||||
|
|
||||||
function renderMath(text, displayMode) {
|
function renderMath(text, displayMode) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -51,6 +53,15 @@ const blockMathExtension = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 配置 marked 支持代码高亮
|
||||||
|
marked.use(markedHighlight({
|
||||||
|
langPrefix: 'hljs language-',
|
||||||
|
highlight(code, lang) {
|
||||||
|
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
|
||||||
|
return hljs.highlight(code, { language }).value
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
marked.use({
|
marked.use({
|
||||||
gfm: true,
|
gfm: true,
|
||||||
breaks: true,
|
breaks: true,
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,47 @@ export function useConversations() {
|
||||||
await conversationsAPI.update(c.id, { title: newTitle })
|
await conversationsAPI.update(c.id, { title: newTitle })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除消息
|
||||||
|
const deleteMessage = async (msgId) => {
|
||||||
|
if (!selectedConv.value) return
|
||||||
|
try {
|
||||||
|
await messagesAPI.delete(msgId)
|
||||||
|
// 从本地列表中移除
|
||||||
|
convMessages.value = convMessages.value.filter(m => m.id !== msgId)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('删除消息失败:', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新生成消息(删除 assistant 消息并重新发送用户消息)
|
||||||
|
const regenerateMessage = async (msgId) => {
|
||||||
|
if (!selectedConv.value || sending.value) return
|
||||||
|
|
||||||
|
// 找到要重新生成的消息
|
||||||
|
const msgIndex = convMessages.value.findIndex(m => m.id === msgId)
|
||||||
|
if (msgIndex === -1) return
|
||||||
|
|
||||||
|
// 找到对应的用户消息(assistant 消息的前一条)
|
||||||
|
const userMsgIndex = msgIndex - 1
|
||||||
|
if (userMsgIndex < 0 || convMessages.value[userMsgIndex].role !== 'user') return
|
||||||
|
|
||||||
|
const userMsg = convMessages.value[userMsgIndex]
|
||||||
|
|
||||||
|
// 删除 assistant 消息
|
||||||
|
convMessages.value = convMessages.value.filter(m => m.id !== msgId)
|
||||||
|
|
||||||
|
// 调用 API 删除 assistant 消息
|
||||||
|
try {
|
||||||
|
await messagesAPI.delete(selectedConv.value.id, msgId)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('删除消息失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新发送用户消息
|
||||||
|
await sendMessage(userMsg.content)
|
||||||
|
}
|
||||||
|
|
||||||
// 设置流状态监听
|
// 设置流状态监听
|
||||||
let unwatchStream = null
|
let unwatchStream = null
|
||||||
const setupStreamWatch = () => {
|
const setupStreamWatch = () => {
|
||||||
|
|
@ -254,6 +295,8 @@ export function useConversations() {
|
||||||
createConv,
|
createConv,
|
||||||
deleteConv,
|
deleteConv,
|
||||||
updateConvTitle,
|
updateConvTitle,
|
||||||
|
deleteMessage,
|
||||||
|
regenerateMessage,
|
||||||
loadEnabledTools,
|
loadEnabledTools,
|
||||||
init,
|
init,
|
||||||
cleanup
|
cleanup
|
||||||
|
|
|
||||||
|
|
@ -69,21 +69,18 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="convMessages.length || currentStreamState">
|
<div v-else-if="convMessages.length || currentStreamState">
|
||||||
<!-- 历史消息 -->
|
<!-- 历史消息 -->
|
||||||
<div v-for="msg in convMessages" :key="msg.id" class="chat-message" :class="msg.role" :data-msg-id="msg.id">
|
<MessageBubble
|
||||||
<div class="message-avatar">{{ msg.role === 'user' ? '👤' : '🤖' }}</div>
|
v-for="msg in convMessages"
|
||||||
<div class="message-content">
|
:key="msg.id"
|
||||||
<!-- 工具调用步骤显示(包含思考和文本内容) -->
|
:message="msg"
|
||||||
<ProcessBlock
|
:deletable="msg.role === 'user'"
|
||||||
v-if="msg.process_steps && msg.process_steps.length"
|
:data-msg-id="msg.id"
|
||||||
:process-steps="msg.process_steps"
|
@delete="handleDeleteMessage"
|
||||||
|
@regenerate="handleRegenerateMessage"
|
||||||
/>
|
/>
|
||||||
<!-- 或仅显示消息内容 -->
|
|
||||||
<div v-else class="message-text" v-html="renderMsgContent(msg)"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- 流式消息 - 使用 store 中的状态 -->
|
<!-- 流式消息 - 使用 store 中的状态 -->
|
||||||
<div v-if="currentStreamState" class="chat-message assistant streaming">
|
<div v-if="currentStreamState" class="chat-message assistant streaming">
|
||||||
<div class="message-avatar">🤖</div>
|
<div class="message-avatar">Luxx</div>
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
<ProcessBlock
|
<ProcessBlock
|
||||||
:process-steps="currentStreamState.process_steps"
|
:process-steps="currentStreamState.process_steps"
|
||||||
|
|
@ -165,12 +162,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import { useConversations } from '../utils/useConversations.js'
|
import { useConversations } from '../utils/useConversations.js'
|
||||||
import { renderMarkdown } from '../utils/markdown.js'
|
|
||||||
import { formatDate } from '../utils/useFormatters.js'
|
import { formatDate } from '../utils/useFormatters.js'
|
||||||
import ProcessBlock from '../components/ProcessBlock.vue'
|
import ProcessBlock from '../components/ProcessBlock.vue'
|
||||||
import MessageNav from '../components/MessageNav.vue'
|
import MessageNav from '../components/MessageNav.vue'
|
||||||
|
import MessageBubble from '../components/MessageBubble.vue'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list,
|
list,
|
||||||
|
|
@ -192,6 +189,8 @@ const {
|
||||||
createConv,
|
createConv,
|
||||||
deleteConv,
|
deleteConv,
|
||||||
updateConvTitle,
|
updateConvTitle,
|
||||||
|
deleteMessage,
|
||||||
|
regenerateMessage,
|
||||||
init,
|
init,
|
||||||
cleanup
|
cleanup
|
||||||
} = useConversations()
|
} = useConversations()
|
||||||
|
|
@ -208,13 +207,6 @@ const observedElements = new Set()
|
||||||
|
|
||||||
const editConv = ref(null)
|
const editConv = ref(null)
|
||||||
|
|
||||||
// 渲染消息内容(Markdown)
|
|
||||||
const renderMsgContent = (msg) => {
|
|
||||||
const content = msg.content || msg.text || ''
|
|
||||||
if (!content) return '-'
|
|
||||||
return renderMarkdown(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理发送消息
|
// 处理发送消息
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!newMessage.value.trim()) return
|
if (!newMessage.value.trim()) return
|
||||||
|
|
@ -264,6 +256,24 @@ const handleSaveTitle = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理删除消息
|
||||||
|
const handleDeleteMessage = async (msgId) => {
|
||||||
|
try {
|
||||||
|
await deleteMessage(msgId)
|
||||||
|
} catch (e) {
|
||||||
|
alert('删除消息失败: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理重新生成消息
|
||||||
|
const handleRegenerateMessage = async (msgId) => {
|
||||||
|
try {
|
||||||
|
await regenerateMessage(msgId)
|
||||||
|
} catch (e) {
|
||||||
|
alert('重新生成失败: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const onProviderChange = () => {
|
const onProviderChange = () => {
|
||||||
const p = providers.value.find(p => p.id === form.value.provider_id)
|
const p = providers.value.find(p => p.id === form.value.provider_id)
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,8 @@ class ChatService:
|
||||||
|
|
||||||
llm, provider_max_tokens = get_llm_client(conversation)
|
llm, provider_max_tokens = get_llm_client(conversation)
|
||||||
model = conversation.model or llm.default_model or "gpt-4"
|
model = conversation.model or llm.default_model or "gpt-4"
|
||||||
# 使用 provider 的 max_tokens,如果 conversation 有自己的 max_tokens 则覆盖
|
# 直接使用 provider 的 max_tokens
|
||||||
max_tokens = conversation.max_tokens if hasattr(conversation, 'max_tokens') and conversation.max_tokens else provider_max_tokens
|
max_tokens = provider_max_tokens
|
||||||
|
|
||||||
# State tracking
|
# State tracking
|
||||||
all_steps = []
|
all_steps = []
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue