第一次提交
This commit is contained in:
537
frontend/src/components/AgentChatPreview.vue
Normal file
537
frontend/src/components/AgentChatPreview.vue
Normal 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>
|
||||
Reference in New Issue
Block a user