Files
miniai/src/components/MessageBubble.vue
T

151 lines
5.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="bubble" :class="msg.role">
<!-- 工具操作卡片 -->
<div v-if="msg.tools?.length" class="tools">
<ToolCard
v-for="(t, i) in msg.tools"
:key="i"
:tool="t"
@approve="$emit('approve', t)"
@reject="$emit('reject', t)"
/>
</div>
<div v-else-if="msg.approval" class="tools">
<ToolCard
:tool="{ tool: msg.approval.tool, args: msg.approval.args, command: msg.approval.command, status: 'approval' }"
@approve="$emit('approve', msg)"
@reject="$emit('reject', msg)"
/>
</div>
<!-- 普通消息(Markdown) -->
<div v-if="msg.content" class="md" v-html="rendered" @click="onClick"></div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { marked } from "marked";
import hljs from "../lib/hljs";
import ToolCard from "./ToolCard.vue";
export interface ToolEvent {
tool: string;
args?: any;
command?: string;
status: "approval" | "running" | "done" | "failed" | "denied";
result?: string;
}
interface Msg {
id: number;
role: "user" | "assistant" | "system";
content: string;
loading?: boolean;
tools?: ToolEvent[];
approval?: { tool: string; command: string; args: any; tool_call_id: string };
}
const props = defineProps<{ msg: Msg }>();
defineEmits<{ approve: [payload: any]; reject: [payload: any] }>();
const renderer = new marked.Renderer();
renderer.code = (code, infostring) => {
const lang = (infostring || "").trim().split(/\s+/)[0];
const highlighted =
lang && hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang }).value
: hljs.highlightAuto(code).value;
return (
`<div class="code-block"><div class="code-head"><span class="code-lang">${lang}</span>` +
`<button class="copy-btn" data-code="${encodeURIComponent(code)}">复制</button></div>` +
`<pre><code class="hljs">${highlighted}</code></pre></div>`
);
};
marked.setOptions({
renderer,
breaks: true,
gfm: true,
});
const rendered = computed(() => {
if (!props.msg.content) return "";
try {
return marked.parse(props.msg.content) as string;
} catch {
return props.msg.content;
}
});
async function onClick(e: MouseEvent) {
const btn = (e.target as HTMLElement).closest(".copy-btn") as HTMLElement | null;
if (!btn) return;
const code = decodeURIComponent(btn.dataset.code || "");
try {
await navigator.clipboard.writeText(code);
btn.textContent = "已复制";
setTimeout(() => (btn.textContent = "复制"), 1500);
} catch {
/* 剪贴板不可用时忽略 */
}
}
</script>
<style scoped>
.bubble { font-size: 14.5px; line-height: 1.7; }
.bubble.user :deep(.md) {
background: var(--user-bubble); color: var(--user-text);
padding: 10px 14px; border-radius: 14px 14px 4px 14px;
word-break: break-word;
}
.bubble.assistant :deep(.md) {
background: var(--assistant-bubble); color: var(--text);
padding: 12px 16px; border-radius: 14px 14px 14px 4px;
border: 1px solid var(--border); box-shadow: var(--shadow);
word-break: break-word;
}
.tools { display: flex; flex-direction: column; gap: 8px; }
.bubble.user .tools { align-items: flex-end; }
.bubble.user .tools :deep(.tool-card) { width: 100%; max-width: 460px; }
/* Markdown 元素 */
.bubble :deep(p) { margin: 0.5em 0; }
.bubble :deep(p:first-child) { margin-top: 0; }
.bubble :deep(p:last-child) { margin-bottom: 0; }
.bubble :deep(h1), .bubble :deep(h2), .bubble :deep(h3) { margin: 0.6em 0 0.4em; line-height: 1.3; }
.bubble :deep(ul), .bubble :deep(ol) { margin: 0.4em 0; padding-left: 1.4em; }
.bubble :deep(li) { margin: 0.2em 0; }
.bubble :deep(a) { color: var(--primary); text-decoration: none; }
.bubble :deep(a:hover) { text-decoration: underline; }
.bubble :deep(code):not(pre code) {
background: var(--surface-2); padding: 1px 6px; border-radius: 5px;
font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.88em;
}
.bubble :deep(.code-block) { margin: 0.6em 0; border-radius: 10px; overflow: hidden; background: var(--code-bg); }
.bubble :deep(.code-head) {
display: flex; justify-content: space-between; align-items: center;
padding: 6px 12px; background: rgba(255, 255, 255, 0.06);
}
.bubble :deep(.code-lang) { font-size: 11px; color: rgba(255, 255, 255, 0.6); text-transform: lowercase; }
.bubble :deep(.copy-btn) {
background: rgba(255, 255, 255, 0.1); border: none; color: rgba(255, 255, 255, 0.8);
font-size: 11px; padding: 3px 10px; border-radius: 6px; cursor: pointer;
}
.bubble :deep(.copy-btn:hover) { background: rgba(255, 255, 255, 0.2); color: #fff; }
.bubble :deep(pre) {
background: var(--code-bg); color: var(--code-text);
padding: 14px 16px; overflow-x: auto; margin: 0;
}
.bubble :deep(pre code) { font-family: "SFMono-Regular", Consolas, monospace; font-size: 13px; line-height: 1.6; padding: 0; background: transparent; }
.bubble :deep(table) {
border-collapse: collapse; margin: 0.6em 0; font-size: 13px; width: 100%;
}
.bubble :deep(th), .bubble :deep(td) { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
.bubble :deep(th) { background: var(--surface-2); font-weight: 600; }
.bubble :deep(blockquote) {
border-left: 3px solid var(--primary); margin: 0.5em 0; padding: 2px 12px; color: var(--text-dim);
}
</style>