UI 调整
This commit is contained in:
1 parent
19ed404da6
commit
f8f118adcb
1 file changed
+505
-155
+505
-155
@@ -1,9 +1,10 @@
|
||||
|
||||
<script setup>
|
||||
import { MarkdownHooks } from "vuemarkik";
|
||||
import CodeBlock from "./CodeBlock.vue";
|
||||
import rehypeShiki from "@shikijs/rehype";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { defineProps } from "vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
userMessage: {
|
||||
@@ -31,13 +32,24 @@ const rehypePlugins = [
|
||||
defaultLanguage: "markdown",
|
||||
langs: [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"html",
|
||||
"css",
|
||||
"json",
|
||||
"python",
|
||||
"kotlin",
|
||||
"java",
|
||||
"sql",
|
||||
"bash",
|
||||
"markdown",
|
||||
"haskell",
|
||||
"json",
|
||||
"text",
|
||||
],
|
||||
langAlias: { card: "text" },
|
||||
langAlias: {
|
||||
card: "text",
|
||||
sh: "bash",
|
||||
shell: "bash",
|
||||
},
|
||||
lazy: true,
|
||||
themes: {
|
||||
light: "github-light",
|
||||
@@ -46,128 +58,243 @@ const rehypePlugins = [
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
const userContents = computed(() =>
|
||||
(props.userMessage?.content ?? []).filter(
|
||||
item => item.type === "text" && item.text
|
||||
)
|
||||
);
|
||||
|
||||
const hasAssistantMessages = computed(
|
||||
() => props.assistantMessages.length > 0
|
||||
);
|
||||
|
||||
const hasActivity = computed(() =>
|
||||
props.assistantMessages.some(message =>
|
||||
(message.content ?? []).some(
|
||||
item => item.type !== "text"
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const isThinking = computed(
|
||||
() => !hasAssistantMessages.value
|
||||
);
|
||||
|
||||
function activityLabel(content) {
|
||||
switch (content.type) {
|
||||
case "think":
|
||||
return "正在思考";
|
||||
|
||||
case "tool_call":
|
||||
return "正在调用工具";
|
||||
|
||||
case "tool_result":
|
||||
return content.error
|
||||
? "工具执行失败"
|
||||
: "工具执行完成";
|
||||
|
||||
case "error":
|
||||
return "发生错误";
|
||||
|
||||
default:
|
||||
return content.type;
|
||||
}
|
||||
}
|
||||
|
||||
function activityIcon(content) {
|
||||
switch (content.type) {
|
||||
case "think":
|
||||
return "🧠";
|
||||
|
||||
case "tool_call":
|
||||
return "⚙️";
|
||||
|
||||
case "tool_result":
|
||||
return content.error ? "⚠️" : "✓";
|
||||
|
||||
case "error":
|
||||
return "⚠️";
|
||||
|
||||
default:
|
||||
return "ℹ️";
|
||||
}
|
||||
}
|
||||
|
||||
function activityClass(content) {
|
||||
if (content.type === "error" || content.error) {
|
||||
return "activity-error";
|
||||
}
|
||||
|
||||
if (content.type === "tool_call" || content.type === "think") {
|
||||
return "activity-running";
|
||||
}
|
||||
|
||||
return "activity-completed";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 用户消息:左侧头像,右侧气泡 -->
|
||||
<div class="message-row user-row">
|
||||
<div class="avatar user-avatar">我</div>
|
||||
<div class="conversation">
|
||||
|
||||
<div class="message-content">
|
||||
<div class="bubble">
|
||||
<MarkdownHooks
|
||||
:text="userMessage.content"
|
||||
:components="components"
|
||||
:rehype-plugins="rehypePlugins"
|
||||
:remark-plugins="remarkPlugins"
|
||||
security-mode="trusted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 消息 -->
|
||||
<div v-if="assistantMessages.length === 0" class="message-row assistant-row">
|
||||
<div class="message-content">
|
||||
<div class="activity-item">
|
||||
<span class="activity-icon">🧠</span>
|
||||
<span>正在思考...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="avatar assistant-avatar">AI</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="message-row assistant-row">
|
||||
<div class="message-content">
|
||||
<div class="bubble">
|
||||
<template v-for="message in assistantMessages" :key="message.id">
|
||||
|
||||
<!-- 过程消息 -->
|
||||
<div v-if="message.type !== 'text'" class="activity-item">
|
||||
<template v-if="message.type === 'tool_call'">
|
||||
<span class="activity-icon">🔧</span>
|
||||
<span>正在调用工具</span>
|
||||
<span class="activity-name">{{ message.tool_name }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="message.type === 'think'">
|
||||
<span class="activity-icon">🧠</span>
|
||||
<span>正在思考...</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="message.type === 'tool_result'">
|
||||
<span class="activity-icon">
|
||||
{{ message.status === 'failed' ? '⚠️' : '✓' }}
|
||||
</span>
|
||||
<span>
|
||||
{{ message.status === 'failed' ? '工具执行失败' : '工具执行完成' }}
|
||||
</span>
|
||||
<span class="activity-name">{{ message.tool_name }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<span class="activity-icon">ℹ️</span>
|
||||
<span>{{ message.type }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 正式回答 -->
|
||||
<template v-else>
|
||||
<hr></hr>
|
||||
<MarkdownHooks
|
||||
:text="message.content"
|
||||
:components="components"
|
||||
:rehype-plugins="rehypePlugins"
|
||||
:remark-plugins="remarkPlugins"
|
||||
security-mode="trusted"
|
||||
/>
|
||||
<!-- 用户消息 -->
|
||||
<div class="message-row user-row">
|
||||
<div class="message-content">
|
||||
<div class="user-bubble">
|
||||
<template
|
||||
v-for="(content, index) in userContents"
|
||||
:key="index"
|
||||
>
|
||||
<MarkdownHooks
|
||||
:text="content.text"
|
||||
:components="components"
|
||||
:rehype-plugins="rehypePlugins"
|
||||
:remark-plugins="remarkPlugins"
|
||||
security-mode="trusted"
|
||||
/>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="avatar user-avatar">我</div>
|
||||
</div>
|
||||
|
||||
<div class="avatar assistant-avatar">AI</div>
|
||||
<!-- AI 消息 -->
|
||||
<div class="message-row assistant-row">
|
||||
|
||||
<div class="avatar assistant-avatar">
|
||||
<span>AI</span>
|
||||
</div>
|
||||
|
||||
<div class="message-content">
|
||||
<div class="assistant-bubble">
|
||||
|
||||
<!-- 执行过程 -->
|
||||
<div
|
||||
v-if="hasActivity"
|
||||
class="activity-list"
|
||||
>
|
||||
<template
|
||||
v-for="(message, messageIndex) in assistantMessages"
|
||||
:key="message.id ?? messageIndex"
|
||||
>
|
||||
<template
|
||||
v-for="(content, contentIndex) in (message.content ?? [])"
|
||||
:key="content.id ?? `${messageIndex}-${contentIndex}`"
|
||||
>
|
||||
<div
|
||||
v-if="content.type !== 'text'"
|
||||
class="activity-item"
|
||||
:class="activityClass(content)"
|
||||
>
|
||||
<span class="activity-icon">
|
||||
{{ activityIcon(content) }}
|
||||
</span>
|
||||
|
||||
<span class="activity-label">
|
||||
{{ activityLabel(content) }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="content.name"
|
||||
class="activity-name"
|
||||
>
|
||||
{{ content.name }}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="content.type === 'error' && content.text"
|
||||
class="activity-detail"
|
||||
>
|
||||
{{ content.text }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 正式回答 -->
|
||||
<div
|
||||
v-if="hasAssistantMessages"
|
||||
class="assistant-answer"
|
||||
>
|
||||
<template
|
||||
v-for="(message, messageIndex) in assistantMessages"
|
||||
:key="message.id ?? messageIndex"
|
||||
>
|
||||
<template
|
||||
v-for="(content, contentIndex) in (message.content ?? [])"
|
||||
:key="content.id ?? `${messageIndex}-${contentIndex}`"
|
||||
>
|
||||
<MarkdownHooks
|
||||
v-if="content.type === 'text' && content.text"
|
||||
:text="content.text"
|
||||
:components="components"
|
||||
:rehype-plugins="rehypePlugins"
|
||||
:remark-plugins="remarkPlugins"
|
||||
security-mode="trusted"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 思考状态 -->
|
||||
<div
|
||||
v-if="isThinking"
|
||||
class="thinking-indicator"
|
||||
>
|
||||
<span class="thinking-dot"></span>
|
||||
<span class="thinking-dot"></span>
|
||||
<span class="thinking-dot"></span>
|
||||
<span class="thinking-text">
|
||||
{{ hasAssistantMessages ? "正在生成回答" : "正在思考" }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.conversation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
gap: 28px;
|
||||
width: 100%;
|
||||
padding: 12px 0 32px;
|
||||
}
|
||||
|
||||
/* 消息行 */
|
||||
/* 消息布局 */
|
||||
.message-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 用户消息靠左 */
|
||||
.user-row {
|
||||
justify-content: flex-start;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* AI 消息靠右 */
|
||||
.assistant-row {
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* 头像 */
|
||||
.avatar {
|
||||
flex: 0 0 38px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex: 0 0 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
@@ -177,100 +304,323 @@ const rehypePlugins = [
|
||||
|
||||
.assistant-avatar {
|
||||
color: #fff;
|
||||
background: #2563eb;
|
||||
background: linear-gradient(145deg, #3b82f6, #2563eb);
|
||||
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
/* 消息内容 */
|
||||
.message-content {
|
||||
max-width: 78%;
|
||||
min-width: 0;
|
||||
max-width: min(82%, 860px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 消息气泡 */
|
||||
.bubble {
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
line-height: 1.6;
|
||||
/* 用户气泡 */
|
||||
.user-bubble {
|
||||
padding: 10px 15px;
|
||||
border-radius: 14px 3px 14px 14px;
|
||||
background: #eaf2ff;
|
||||
color: #26364b;
|
||||
line-height: 1.7;
|
||||
overflow-wrap: anywhere;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.user-row .bubble {
|
||||
background: #e8f1ff;
|
||||
border-top-left-radius: 3px;
|
||||
}
|
||||
|
||||
.assistant-row .bubble {
|
||||
background: #f4f5f7;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
/* Markdown 内容 */
|
||||
.bubble :deep(p:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.bubble :deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bubble :deep(pre) {
|
||||
overflow-x: auto;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.bubble :deep(table) {
|
||||
/* AI 回复气泡 */
|
||||
.assistant-bubble {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12px 0;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 14px 18px;
|
||||
border: 1px solid var(--chat-assistant-border, #edf0f4);
|
||||
border-radius: 3px 14px 14px 14px;
|
||||
background: var(--chat-assistant-bubble, #f4f6f9);
|
||||
color: var(--chat-text, #303133);
|
||||
line-height: 1.85;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.assistant-answer {
|
||||
color: var(--chat-text, #303133);
|
||||
font-size: 14px;
|
||||
line-height: 1.85;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.bubble :deep(th),
|
||||
.bubble :deep(td) {
|
||||
border: 1px solid #dcdfe6;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bubble :deep(th) {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.bubble :deep(tr:nth-child(even)) {
|
||||
background: #fafafa;
|
||||
/* 思考和工具执行状态 */
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
padding: 10px 12px;
|
||||
border-left: 2px solid #e2e8f0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 6px;
|
||||
margin: 1px 0;
|
||||
border-radius: 4px;
|
||||
color: #84909f;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
min-height: 20px;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
line-height: 1.5;
|
||||
color: #84909f;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
flex: 0 0 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.activity-label {
|
||||
color: #7b8795;
|
||||
}
|
||||
|
||||
.activity-name {
|
||||
padding: 1px 7px;
|
||||
border-radius: 4px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-family: monospace;
|
||||
background: #eef1f5;
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 过程消息与正式回答之间的间距 */
|
||||
.activity-item + p,
|
||||
.activity-item + pre {
|
||||
margin-top: 12px;
|
||||
.activity-detail {
|
||||
width: 100%;
|
||||
padding-left: 22px;
|
||||
color: #dc5555;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.activity-running .activity-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.activity-completed .activity-icon {
|
||||
color: #16a36a;
|
||||
}
|
||||
|
||||
.activity-error .activity-icon,
|
||||
.activity-error .activity-label {
|
||||
color: #dc5555;
|
||||
}
|
||||
|
||||
/* 思考动画 */
|
||||
.thinking-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 8px 0;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.thinking-dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #94a3b8;
|
||||
animation: thinking 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.thinking-dot:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
.thinking-dot:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.thinking-text {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@keyframes thinking {
|
||||
0%, 60%, 100% {
|
||||
opacity: 0.35;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Markdown */
|
||||
.assistant-answer :deep(p),
|
||||
.user-bubble :deep(p) {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(p:last-child),
|
||||
.user-bubble :deep(p:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(h1),
|
||||
.assistant-answer :deep(h2),
|
||||
.assistant-answer :deep(h3) {
|
||||
margin: 22px 0 12px;
|
||||
line-height: 1.45;
|
||||
font-weight: 600;
|
||||
color: var(--chat-heading, #1f2937);
|
||||
}
|
||||
|
||||
.assistant-answer :deep(h1) {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(h2) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(h3) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(ul),
|
||||
.assistant-answer :deep(ol),
|
||||
.user-bubble :deep(ul),
|
||||
.user-bubble :deep(ol) {
|
||||
padding-left: 24px;
|
||||
margin: 8px 0 14px;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(li),
|
||||
.user-bubble :deep(li) {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(blockquote) {
|
||||
margin: 12px 0;
|
||||
padding: 8px 14px;
|
||||
border-left: 3px solid #93b4e8;
|
||||
background: #f6f8fb;
|
||||
color: #64748b;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* 代码块 */
|
||||
.assistant-answer :deep(pre),
|
||||
.user-bubble :deep(pre) {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
padding: 14px;
|
||||
margin: 12px 0;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 9px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(:not(pre) > code),
|
||||
.user-bubble :deep(:not(pre) > code) {
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
background: #f1f5f9;
|
||||
color: #db5a5a;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* 表格 */
|
||||
.assistant-answer :deep(table),
|
||||
.user-bubble :deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 14px 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(th),
|
||||
.assistant-answer :deep(td),
|
||||
.user-bubble :deep(th),
|
||||
.user-bubble :deep(td) {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(th),
|
||||
.user-bubble :deep(th) {
|
||||
background: #f5f7fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(tr:nth-child(even)),
|
||||
.user-bubble :deep(tr:nth-child(even)) {
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
/* 链接 */
|
||||
.assistant-answer :deep(a),
|
||||
.user-bubble :deep(a) {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(a:hover),
|
||||
.user-bubble :deep(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 图片 */
|
||||
.assistant-answer :deep(img),
|
||||
.user-bubble :deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 移动端 */
|
||||
@media (max-width: 640px) {
|
||||
.conversation {
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
flex-basis: 30px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.message-row {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: calc(100% - 38px);
|
||||
}
|
||||
|
||||
.user-bubble {
|
||||
padding: 9px 12px;
|
||||
}
|
||||
|
||||
.assistant-bubble {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.assistant-answer {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.assistant-answer :deep(table),
|
||||
.user-bubble :deep(table) {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user