ai-robot-core/ai-service-admin/src/components/rag/StreamOutput.vue

300 lines
6.0 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>
<el-card shadow="hover" class="stream-output">
<template #header>
<div class="card-header">
<div class="header-left">
<div class="icon-wrapper" :class="{ streaming: isStreaming }">
<el-icon><Promotion /></el-icon>
</div>
<span class="header-title">流式输出</span>
</div>
<div class="header-actions">
<el-tag v-if="isStreaming" type="warning" size="small" effect="dark" class="pulse-tag">
<el-icon class="is-loading"><Loading /></el-icon>
生成中...
</el-tag>
<el-tag v-else-if="hasContent" type="success" size="small" effect="dark">
已完成
</el-tag>
<el-tag v-else type="info" size="small" effect="plain">
等待中
</el-tag>
</div>
</div>
</template>
<div class="stream-content">
<div v-if="!hasContent && !isStreaming" class="placeholder-text">
<el-icon class="placeholder-icon"><ChatLineSquare /></el-icon>
<p>启用流式输出后AI 回复将实时显示</p>
</div>
<div v-else class="output-area">
<div class="stream-text" v-html="renderedContent"></div>
<div v-if="isStreaming" class="typing-indicator">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
<div v-if="error" class="error-section">
<el-alert
:title="error"
type="error"
:closable="false"
show-icon
/>
</div>
</div>
</el-card>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Promotion, Loading, ChatLineSquare } from '@element-plus/icons-vue'
const props = defineProps<{
content: string
isStreaming: boolean
error?: string | null
}>()
const hasContent = computed(() => props.content && props.content.length > 0)
const renderedContent = computed(() => {
if (!props.content) return ''
return renderMarkdown(props.content)
})
const renderMarkdown = (text: string): string => {
let html = text
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>')
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>')
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>')
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>')
html = html.replace(/^\- (.+)$/gm, '<li>$1</li>')
html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
html = html.replace(/\n\n/g, '</p><p>')
html = html.replace(/\n/g, '<br>')
return `<p>${html}</p>`
}
</script>
<style scoped>
.stream-output {
border-radius: 16px;
border: none;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.stream-output:hover {
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.icon-wrapper {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
border-radius: 10px;
color: #ffffff;
font-size: 20px;
transition: all 0.3s ease;
}
.icon-wrapper.streaming {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(17, 153, 142, 0.4);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 0 10px rgba(17, 153, 142, 0);
}
}
.header-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.pulse-tag {
animation: pulse-tag 1.5s ease-in-out infinite;
}
@keyframes pulse-tag {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.placeholder-text {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #909399;
}
.placeholder-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
.placeholder-text p {
margin: 0;
font-size: 14px;
}
.output-area {
padding: 16px;
background: #f8f9fa;
border-radius: 12px;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
}
.stream-text {
line-height: 1.8;
color: #303133;
}
.stream-text :deep(h1) {
font-size: 24px;
font-weight: 700;
margin: 16px 0 12px;
color: #303133;
}
.stream-text :deep(h2) {
font-size: 20px;
font-weight: 600;
margin: 14px 0 10px;
color: #303133;
}
.stream-text :deep(h3) {
font-size: 16px;
font-weight: 600;
margin: 12px 0 8px;
color: #303133;
}
.stream-text :deep(pre) {
background: #1e1e1e;
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin: 12px 0;
}
.stream-text :deep(code) {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
}
.stream-text :deep(pre code) {
color: #d4d4d4;
}
.stream-text :deep(.inline-code) {
background: #e8e8e8;
padding: 2px 6px;
border-radius: 4px;
font-size: 13px;
color: #e83e8c;
}
.stream-text :deep(strong) {
font-weight: 600;
color: #303133;
}
.stream-text :deep(em) {
font-style: italic;
color: #606266;
}
.stream-text :deep(li) {
margin: 4px 0;
padding-left: 8px;
}
.typing-indicator {
display: flex;
gap: 4px;
margin-top: 12px;
padding: 8px 12px;
background: rgba(17, 153, 142, 0.1);
border-radius: 8px;
width: fit-content;
}
.typing-indicator .dot {
width: 8px;
height: 8px;
background: #11998e;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out both;
}
.typing-indicator .dot:nth-child(1) {
animation-delay: -0.32s;
}
.typing-indicator .dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.error-section {
margin-top: 16px;
}
</style>