This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 单条消息气泡:用户右侧、助手左侧;助手消息下挂工具执行轨迹。
|
||||
* 助手侧带一个小名牌,让长对话里"谁在说话"一眼可辨。
|
||||
* 文本做最小化处理:换行保留、代码块/多余 markdown 符号不渲染(保持纯文本可读)。
|
||||
*/
|
||||
import ToolTraceCard from "./ToolTraceCard.vue";
|
||||
import FormCard from "./FormCard.vue";
|
||||
import type { ChatItem } from "@/types/agent";
|
||||
|
||||
const props = defineProps<{ item: ChatItem; pending?: boolean }>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "form-submit", payload: { item: ChatItem; form: { tool: string; args: Record<string, any> } }): void;
|
||||
}>();
|
||||
|
||||
/** 表单提交:冒泡给对话页统一处理(统一走同一条提交链路) */
|
||||
function onFormSubmit(p: { tool: string; args: Record<string, any> }) {
|
||||
emit("form-submit", { item: props.item, form: p });
|
||||
}
|
||||
|
||||
/** 把回复拆成段落,保留换行 */
|
||||
function paragraphs(text: string): string[] {
|
||||
return (text || "")
|
||||
.split(/\n{2,}/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="msg" :class="item.role === 'user' ? 'msg--user' : 'msg--assistant'">
|
||||
<div class="msg__avatar">
|
||||
<svg v-if="item.role === 'assistant'" viewBox="0 0 24 24" width="17" height="17" aria-hidden="true">
|
||||
<path d="M12 2.6l2.3 6.1 6.1 2.3-6.1 2.3L12 19.4l-2.3-6.1L3.6 11l6.1-2.3L12 2.6z" fill="#fff" />
|
||||
</svg>
|
||||
<template v-else>我</template>
|
||||
</div>
|
||||
|
||||
<div class="msg__content">
|
||||
<span v-if="item.role === 'assistant'" class="msg__name">校园智能助手</span>
|
||||
|
||||
<div class="msg__stack">
|
||||
<!-- 助手等待中:阶段提示 -->
|
||||
<div v-if="pending" class="msg__stage">
|
||||
<span class="dot" />
|
||||
<span class="dot" />
|
||||
<span class="dot" />
|
||||
<span class="msg__stage-text">{{ item.stage || "思考中…" }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 正文 -->
|
||||
<div v-if="item.text" class="msg__bubble">
|
||||
<p v-for="(p, i) in paragraphs(item.text)" :key="i">{{ p }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 工具轨迹 -->
|
||||
<ToolTraceCard v-if="item.traces && item.traces.length" :traces="item.traces" />
|
||||
|
||||
<!-- 补全表单:信息不全时让用户直接填,比反复追问清晰得多 -->
|
||||
<FormCard
|
||||
v-if="item.form"
|
||||
:form="item.form"
|
||||
:submitted="item.formSubmitted"
|
||||
:disabled="pending"
|
||||
@submit="onFormSubmit"
|
||||
/>
|
||||
|
||||
<!-- 错误 -->
|
||||
<div v-if="item.error" class="msg__error">{{ item.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.msg {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 22px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.msg--user {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.msg--assistant {
|
||||
animation: rise 0.28s ease both;
|
||||
}
|
||||
|
||||
@keyframes rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------ 头像 ------------------------------ */
|
||||
.msg__avatar {
|
||||
flex: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: linear-gradient(140deg, #3b82f6, #7c3aed);
|
||||
box-shadow: 0 8px 16px -10px rgba(59, 130, 246, 0.95);
|
||||
}
|
||||
|
||||
.msg--user .msg__avatar {
|
||||
background: linear-gradient(140deg, #64748b, #475569);
|
||||
box-shadow: 0 8px 16px -10px rgba(71, 85, 105, 0.9);
|
||||
}
|
||||
|
||||
/* ------------------------------ 内容列 ------------------------------ */
|
||||
.msg__content {
|
||||
max-width: 84%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.msg--assistant .msg__content {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.msg--user .msg__content {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.msg__name {
|
||||
font-size: 11.5px;
|
||||
color: var(--agent-text-mute);
|
||||
margin: 0 0 6px 3px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.msg__stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* ------------------------------ 气泡 ------------------------------ */
|
||||
.msg__bubble {
|
||||
padding: 11px 15px;
|
||||
border-radius: 14px;
|
||||
font-size: 14px;
|
||||
line-height: 1.75;
|
||||
color: var(--agent-text);
|
||||
background: var(--agent-surface);
|
||||
border: 1px solid var(--agent-border);
|
||||
box-shadow: var(--agent-shadow-xs);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.msg__bubble p {
|
||||
margin: 0 0 7px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.msg__bubble p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.msg--user .msg__bubble {
|
||||
background: linear-gradient(135deg, #2563eb, #4f46e5);
|
||||
border-color: transparent;
|
||||
color: #fff;
|
||||
box-shadow: 0 12px 24px -14px rgba(37, 99, 235, 0.85);
|
||||
}
|
||||
|
||||
/* ------------------------------ 阶段提示 ------------------------------ */
|
||||
.msg__stage {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
align-self: flex-start;
|
||||
padding: 10px 15px;
|
||||
border-radius: 14px;
|
||||
background: var(--agent-surface);
|
||||
border: 1px solid var(--agent-border);
|
||||
box-shadow: var(--agent-shadow-xs);
|
||||
font-size: 13px;
|
||||
color: var(--agent-text-sub);
|
||||
}
|
||||
|
||||
.msg__stage-text {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--agent-primary);
|
||||
animation: bounce 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
.dot:nth-child(3) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 0.35;
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-4px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------ 错误 ------------------------------ */
|
||||
.msg__error {
|
||||
margin-top: 8px;
|
||||
padding: 9px 13px;
|
||||
border-radius: 10px;
|
||||
background: var(--agent-danger-soft);
|
||||
border: 1px solid #f8c9c5;
|
||||
color: var(--agent-danger);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user