第一次提交

This commit is contained in:
rjb
2026-01-19 00:09:36 +08:00
parent de4b5059e9
commit 6674060f2f
191 changed files with 40940 additions and 0 deletions

View File

@@ -0,0 +1,537 @@
<template>
<div class="agent-chat-preview">
<div class="chat-header">
<div class="agent-info">
<el-avatar :size="32" :src="agentAvatar" :icon="UserFilled" />
<span class="agent-name">{{ agentName || 'Agent' }}</span>
</div>
<div class="header-actions">
<el-button text size="small" @click="handleClearChat">
<el-icon><Delete /></el-icon>
清空对话
</el-button>
</div>
</div>
<div class="chat-messages" ref="messagesContainer">
<!-- 开场白 -->
<div v-if="openingMessage" class="message agent-message">
<el-avatar :size="32" :src="agentAvatar" :icon="UserFilled" />
<div class="message-content">
<div class="message-bubble" v-html="formatMessage(openingMessage)"></div>
</div>
</div>
<!-- 预设问题 -->
<div v-if="presetQuestions.length > 0 && messages.length === 0" class="preset-questions">
<div
v-for="(question, index) in presetQuestions"
:key="index"
class="preset-question"
@click="handlePresetQuestion(question)"
>
{{ question }}
</div>
</div>
<!-- 对话消息 -->
<div
v-for="(message, index) in messages"
:key="index"
:class="['message', message.role === 'user' ? 'user-message' : 'agent-message']"
>
<el-avatar
v-if="message.role === 'agent'"
:size="32"
:src="agentAvatar"
:icon="UserFilled"
/>
<div class="message-content">
<div class="message-bubble" v-html="formatMessage(message.content)"></div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
<el-avatar
v-if="message.role === 'user'"
:size="32"
:icon="UserFilled"
style="background-color: #409eff;"
/>
</div>
<!-- 加载中 -->
<div v-if="loading" class="message agent-message">
<el-avatar :size="32" :src="agentAvatar" :icon="UserFilled" />
<div class="message-content">
<div class="message-bubble loading">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在思考...</span>
</div>
</div>
</div>
<!-- 节点测试结果 -->
<div v-if="props.nodeTestResult" class="node-test-result">
<el-alert
:type="props.nodeTestResult.result.status === 'success' ? 'success' : 'error'"
:closable="true"
show-icon
@close="handleCloseNodeTest"
>
<template #title>
<div class="node-test-header">
<strong>节点测试: {{ props.nodeTestResult.node.data?.label || props.nodeTestResult.node.type }}</strong>
<span class="node-test-time">{{ props.nodeTestResult.result.execution_time }}ms</span>
</div>
</template>
<div class="node-test-content">
<div v-if="props.nodeTestResult.result.status === 'success'">
<div class="test-section">
<strong>输入:</strong>
<pre>{{ JSON.stringify(props.nodeTestResult.input, null, 2) }}</pre>
</div>
<div class="test-section">
<strong>输出:</strong>
<pre>{{ JSON.stringify(props.nodeTestResult.result.output, null, 2) }}</pre>
</div>
</div>
<div v-else>
<div class="test-section">
<strong>错误:</strong>
<pre>{{ props.nodeTestResult.result.error_message || '未知错误' }}</pre>
</div>
</div>
</div>
</el-alert>
</div>
</div>
<div class="chat-input-area">
<div class="input-toolbar">
<el-button text size="small" @click="handleAttachFile">
<el-icon><Paperclip /></el-icon>
</el-button>
</div>
<el-input
v-model="inputMessage"
type="textarea"
:rows="2"
placeholder="发送消息..."
@keydown.enter.exact.prevent="handleSendMessage"
@keydown.enter.shift.exact="handleNewLine"
:disabled="loading || !agentId"
/>
<div class="input-footer">
<div class="disclaimer">
内容由AI生成无法确保真实准确仅供参考
</div>
<div class="input-actions">
<el-button text size="small" @click="handleVoiceInput">
<el-icon><Microphone /></el-icon>
</el-button>
<el-button
type="primary"
@click="handleSendMessage"
:disabled="!inputMessage.trim() || loading || !agentId"
:loading="loading"
>
<el-icon><Promotion /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import {
UserFilled,
Delete,
Loading,
Paperclip,
Microphone,
Promotion
} from '@element-plus/icons-vue'
import api from '@/api'
interface Message {
role: 'user' | 'agent'
content: string
timestamp: number
}
const props = defineProps<{
agentId?: string
agentName?: string
agentAvatar?: string
openingMessage?: string
presetQuestions?: string[]
nodeTestResult?: any
}>()
const messages = ref<Message[]>([])
const inputMessage = ref('')
const loading = ref(false)
const messagesContainer = ref<HTMLElement>()
// 发送消息
const handleSendMessage = async () => {
if (!inputMessage.value.trim() || loading.value || !props.agentId) return
const userMessage = inputMessage.value.trim()
inputMessage.value = ''
// 添加用户消息
messages.value.push({
role: 'user',
content: userMessage,
timestamp: Date.now()
})
// 滚动到底部
scrollToBottom()
// 发送到Agent
loading.value = true
try {
const response = await api.post('/api/v1/executions', {
agent_id: props.agentId,
input_data: {
USER_INPUT: userMessage,
query: userMessage
}
})
const execution = response.data
// 轮询执行状态
const checkStatus = async () => {
try {
const statusResponse = await api.get(`/api/v1/executions/${execution.id}`)
const exec = statusResponse.data
if (exec.status === 'completed') {
// 提取Agent回复
let agentReply = ''
if (exec.output_data) {
// 优先从 result 字段获取(工作流执行结果)
if (exec.output_data.result) {
// result 字段应该是纯文本字符串End节点的输出
if (typeof exec.output_data.result === 'string') {
agentReply = exec.output_data.result
} else {
agentReply = String(exec.output_data.result)
}
} else if (typeof exec.output_data === 'string') {
agentReply = exec.output_data
} else if (exec.output_data.output) {
agentReply = exec.output_data.output
} else if (exec.output_data.response) {
agentReply = exec.output_data.response
} else if (exec.output_data.text) {
agentReply = exec.output_data.text
} else {
agentReply = JSON.stringify(exec.output_data, null, 2)
}
}
messages.value.push({
role: 'agent',
content: agentReply || '执行完成',
timestamp: Date.now()
})
loading.value = false
scrollToBottom()
} else if (exec.status === 'failed') {
messages.value.push({
role: 'agent',
content: `执行失败: ${exec.error_message || '未知错误'}`,
timestamp: Date.now()
})
loading.value = false
scrollToBottom()
} else {
// 继续轮询
setTimeout(checkStatus, 1000)
}
} catch (error: any) {
messages.value.push({
role: 'agent',
content: `获取执行结果失败: ${error.response?.data?.detail || error.message}`,
timestamp: Date.now()
})
loading.value = false
scrollToBottom()
}
}
// 开始轮询
setTimeout(checkStatus, 1000)
} catch (error: any) {
console.error('发送消息失败:', error)
messages.value.push({
role: 'agent',
content: `发送失败: ${error.response?.data?.detail || error.message}`,
timestamp: Date.now()
})
loading.value = false
scrollToBottom()
ElMessage.error(error.response?.data?.detail || '发送失败')
}
}
// 预设问题点击
const handlePresetQuestion = (question: string) => {
inputMessage.value = question
handleSendMessage()
}
// 清空对话
const handleClearChat = () => {
messages.value = []
}
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
})
}
// 格式化消息支持简单的Markdown
const formatMessage = (content: string) => {
if (!content) return ''
// 简单的换行处理
return content.replace(/\n/g, '<br>')
}
// 格式化时间
const formatTime = (timestamp: number) => {
const date = new Date(timestamp)
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
// 附件
const handleAttachFile = () => {
ElMessage.info('文件上传功能开发中')
}
// 语音输入
const handleVoiceInput = () => {
ElMessage.info('语音输入功能开发中')
}
// 换行
const handleNewLine = () => {
// Shift+Enter 换行,不需要特殊处理
}
// 关闭节点测试结果
const handleCloseNodeTest = () => {
// 通过事件通知父组件清除测试结果
// 这里暂时不做处理,由父组件自动清除
}
// 监听消息变化,自动滚动
watch(messages, () => {
scrollToBottom()
}, { deep: true })
</script>
<style scoped>
.agent-chat-preview {
display: flex;
flex-direction: column;
height: 100%;
background: #f5f5f5;
border-left: 1px solid #e4e7ed;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: white;
border-bottom: 1px solid #e4e7ed;
}
.agent-info {
display: flex;
align-items: center;
gap: 8px;
}
.agent-name {
font-weight: 500;
font-size: 14px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
gap: 12px;
align-items: flex-start;
}
.user-message {
flex-direction: row-reverse;
}
.message-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.user-message .message-content {
align-items: flex-end;
}
.message-bubble {
padding: 10px 14px;
border-radius: 12px;
max-width: 70%;
word-wrap: break-word;
line-height: 1.5;
}
.agent-message .message-bubble {
background: white;
color: #303133;
border: 1px solid #e4e7ed;
}
.user-message .message-bubble {
background: #409eff;
color: white;
}
.message-bubble.loading {
display: flex;
align-items: center;
gap: 8px;
color: #909399;
}
.message-time {
font-size: 12px;
color: #909399;
padding: 0 4px;
}
.preset-questions {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
}
.preset-question {
padding: 10px 14px;
background: white;
border: 1px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
color: #409eff;
}
.preset-question:hover {
background: #ecf5ff;
border-color: #409eff;
}
.node-test-result {
margin-top: 16px;
margin-bottom: 16px;
}
.node-test-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.node-test-time {
font-size: 12px;
color: #909399;
font-weight: normal;
}
.node-test-content {
margin-top: 8px;
}
.test-section {
margin-bottom: 12px;
}
.test-section strong {
display: block;
margin-bottom: 4px;
font-size: 13px;
color: #606266;
}
.test-section pre {
background: #f5f7fa;
padding: 8px;
border-radius: 4px;
font-size: 12px;
max-height: 200px;
overflow: auto;
margin: 0;
border: 1px solid #e4e7ed;
}
.chat-input-area {
background: white;
border-top: 1px solid #e4e7ed;
padding: 12px;
}
.input-toolbar {
margin-bottom: 8px;
}
.input-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.disclaimer {
font-size: 12px;
color: #909399;
}
.input-actions {
display: flex;
gap: 8px;
align-items: center;
}
:deep(.el-textarea__inner) {
border: 1px solid #dcdfe6;
border-radius: 8px;
resize: none;
}
</style>