1151 lines
32 KiB
Vue
1151 lines
32 KiB
Vue
<script setup lang="ts">
|
||
import type {
|
||
AiChatPanelProps,
|
||
Attachment,
|
||
ChatMessage,
|
||
DesignPreviewData,
|
||
ReasoningStep,
|
||
WorkflowVariable,
|
||
} from './types';
|
||
|
||
/**
|
||
* AI Chat Panel - 统一聊天面板组件
|
||
*
|
||
* 支持三种模式:
|
||
* - workflow: 工作流编辑器调试模式(使用草稿版本)
|
||
* - agent-debug: 智能体编辑器调试模式
|
||
* - agent-chat: 智能体正式对话模式(支持历史、反馈)
|
||
*/
|
||
import { computed, defineAsyncComponent, onUnmounted, ref, watch } from 'vue';
|
||
|
||
import { Settings2, X } from '@vben/icons';
|
||
|
||
import {
|
||
ElButton,
|
||
ElDialog,
|
||
ElForm,
|
||
ElFormItem,
|
||
ElInput,
|
||
ElMessage,
|
||
} from 'element-plus';
|
||
|
||
import { AiWorkingAnimation } from '#/components/ai-loading';
|
||
import { ChatBox } from '#/components/ChatBox';
|
||
|
||
import { useChatApi } from './composables/useChatApi';
|
||
import { useEventHandler } from './composables/useEventHandler';
|
||
|
||
const AppDesignPanel = defineAsyncComponent(() =>
|
||
import('#/components/form-editor/AppDesignPanel.vue'),
|
||
);
|
||
const AppSettingsPanel = defineAsyncComponent(() =>
|
||
import('#/components/form-editor/AppSettingsPanel.vue'),
|
||
);
|
||
const DashboardBasicInfoConfirmPanel = defineAsyncComponent(() =>
|
||
import('#/components/form-editor/DashboardBasicInfoConfirmPanel.vue'),
|
||
);
|
||
const DashboardDesignConfirmPanel = defineAsyncComponent(() =>
|
||
import('#/components/form-editor/DashboardDesignConfirmPanel.vue'),
|
||
);
|
||
const DashboardPublishConfirmPanel = defineAsyncComponent(() =>
|
||
import('#/components/form-editor/DashboardPublishConfirmPanel.vue'),
|
||
);
|
||
const DesignEditorPanel = defineAsyncComponent(() =>
|
||
import('#/components/form-editor/DesignEditorPanel.vue'),
|
||
);
|
||
const SystemSummaryConfirmPanel = defineAsyncComponent(() =>
|
||
import('#/components/form-editor/SystemSummaryConfirmPanel.vue'),
|
||
);
|
||
|
||
// ==================== Props ====================
|
||
const props = withDefaults(defineProps<AiChatPanelProps>(), {
|
||
mode: 'workflow',
|
||
useDraft: true,
|
||
enableVariablesForm: undefined,
|
||
enableHistory: undefined,
|
||
enableFeedback: false,
|
||
enableWelcome: undefined,
|
||
enableNodeEvents: undefined,
|
||
showClearButton: true,
|
||
showCloseButton: true,
|
||
showHeader: true,
|
||
showAvatar: true,
|
||
enableVoice: true,
|
||
enableAttachment: false,
|
||
enableImage: false,
|
||
layout: 'sidebar',
|
||
});
|
||
|
||
// ==================== Emits ====================
|
||
const emit = defineEmits<{
|
||
(e: 'node-start', data: any): void;
|
||
(e: 'node-complete', data: any): void;
|
||
(e: 'node-streaming', data: any): void;
|
||
(e: 'loop-iteration', data: any): void;
|
||
(e: 'clear-status'): void;
|
||
(e: 'conversation-created', conversationId: string): void;
|
||
(e: 'close'): void;
|
||
(e: 'message-sent', message: string): void;
|
||
(e: 'message-complete', conversationId: null | string): void;
|
||
}>();
|
||
|
||
// ==================== 计算默认值 ====================
|
||
const enableVariablesForm = computed(
|
||
() => props.enableVariablesForm ?? props.mode === 'workflow',
|
||
);
|
||
const enableHistory = computed(
|
||
() => props.enableHistory ?? props.mode === 'agent-chat',
|
||
);
|
||
const enableFeedback = computed(
|
||
() => props.enableFeedback ?? props.mode === 'agent-chat',
|
||
);
|
||
const enableWelcome = computed(
|
||
() => props.enableWelcome ?? props.mode.startsWith('agent'),
|
||
);
|
||
const enableNodeEvents = computed(
|
||
() => props.enableNodeEvents ?? props.mode === 'workflow',
|
||
);
|
||
|
||
// ==================== 计算 UI 配置 ====================
|
||
const panelTitle = computed(() => {
|
||
if (props.title) return props.title;
|
||
switch (props.mode) {
|
||
case 'agent-chat': {
|
||
return props.agent?.name || '对话';
|
||
}
|
||
case 'agent-debug': {
|
||
return '调试预览';
|
||
}
|
||
case 'workflow': {
|
||
return '运行预览';
|
||
}
|
||
default: {
|
||
return '对话';
|
||
}
|
||
}
|
||
});
|
||
|
||
const placeholderText = computed(() => {
|
||
if (props.placeholder) return props.placeholder;
|
||
if (props.disabled) return '请先发布后再进行对话';
|
||
switch (props.mode) {
|
||
case 'agent-chat': {
|
||
return '输入消息开始对话...';
|
||
}
|
||
case 'agent-debug': {
|
||
return '输入消息进行调试...';
|
||
}
|
||
case 'workflow': {
|
||
return '输入消息运行工作流...';
|
||
}
|
||
default: {
|
||
return '输入消息...';
|
||
}
|
||
}
|
||
});
|
||
|
||
const emptyText = computed(() => {
|
||
if (props.emptyText) return props.emptyText;
|
||
switch (props.mode) {
|
||
case 'agent-chat':
|
||
case 'agent-debug': {
|
||
return '开始对话吧';
|
||
}
|
||
case 'workflow': {
|
||
return '输入消息开始运行工作流';
|
||
}
|
||
default: {
|
||
return '开始对话吧';
|
||
}
|
||
}
|
||
});
|
||
|
||
const assistantName = computed(() => {
|
||
if (props.assistantName) return props.assistantName;
|
||
switch (props.mode) {
|
||
case 'agent-chat':
|
||
case 'agent-debug': {
|
||
return props.agent?.name || '助手';
|
||
}
|
||
case 'workflow': {
|
||
return '工作流';
|
||
}
|
||
default: {
|
||
return '助手';
|
||
}
|
||
}
|
||
});
|
||
|
||
// ==================== 欢迎配置 ====================
|
||
const welcomeConfig = computed(() => {
|
||
if (!enableWelcome.value || !props.agent) return undefined;
|
||
|
||
const isPublished = props.agent.status === 'published';
|
||
return {
|
||
title: props.agent.name,
|
||
description: isPublished
|
||
? props.agent.welcome_message || '发送消息开始对话'
|
||
: '请先发布智能体后再进行对话',
|
||
suggestions: isPublished ? props.agent.suggested_questions : [],
|
||
};
|
||
});
|
||
|
||
// ==================== 核心状态 ====================
|
||
const messages = ref<ChatMessage[]>([]);
|
||
const running = ref(false);
|
||
const streamingContent = ref('');
|
||
const currentSteps = ref<ReasoningStep[]>([]);
|
||
const currentRunId = ref('');
|
||
const currentAssistantMsgId = ref<null | string>(null);
|
||
const waitingForInput = ref(false);
|
||
const waitingConfig = ref<any>(null);
|
||
let cancelStream: (() => void) | null = null;
|
||
|
||
// ==================== 多变量输入对话框 ====================
|
||
const variablesDialogVisible = ref(false);
|
||
const variablesForm = ref<Record<string, any>>({});
|
||
const pendingMessage = ref('');
|
||
|
||
// ==================== 设计预览面板状态 ====================
|
||
const showDesignPanel = ref(false);
|
||
const currentDesign = ref<DesignPreviewData | undefined>(undefined);
|
||
|
||
const showAppDesignPanel = ref(false);
|
||
const currentAppDesign = ref<DesignPreviewData | undefined>(undefined);
|
||
|
||
const showAppSettingsPanel = ref(false);
|
||
const currentAppSettings = ref<DesignPreviewData | undefined>(undefined);
|
||
|
||
const showDashboardBasicInfoPanel = ref(false);
|
||
const currentDashboardBasicInfo = ref<DesignPreviewData | undefined>(undefined);
|
||
|
||
const showDashboardDesignPanel = ref(false);
|
||
const currentDashboardDesign = ref<DesignPreviewData | undefined>(undefined);
|
||
|
||
const showDashboardPublishPanel = ref(false);
|
||
const currentDashboardPublish = ref<DesignPreviewData | undefined>(undefined);
|
||
|
||
const showSystemSummaryPanel = ref(false);
|
||
const currentSystemSummary = ref<DesignPreviewData | undefined>(undefined);
|
||
|
||
// AI 工作动画状态(用于 application 类型工作流)
|
||
const showLoadingAnimation = ref(false);
|
||
const loadingAnimationTitle = ref('AI 正在开发...');
|
||
|
||
// 是否有任何设计面板打开
|
||
const hasAnyDesignPanelOpen = computed(
|
||
() =>
|
||
showDesignPanel.value ||
|
||
showAppDesignPanel.value ||
|
||
showAppSettingsPanel.value ||
|
||
showDashboardBasicInfoPanel.value ||
|
||
showDashboardDesignPanel.value ||
|
||
showDashboardPublishPanel.value ||
|
||
showSystemSummaryPanel.value ||
|
||
showLoadingAnimation.value,
|
||
);
|
||
|
||
// ==================== 工作流变量 ====================
|
||
const startNode = computed(() => props.nodes?.find((n) => n.type === 'start'));
|
||
const variables = computed<WorkflowVariable[]>(
|
||
() => startNode.value?.data?.variables || [],
|
||
);
|
||
const hasMultipleVariables = computed(() => variables.value.length > 1);
|
||
|
||
// 初始化变量表单
|
||
watch(
|
||
variables,
|
||
(vars) => {
|
||
const newForm: Record<string, any> = {};
|
||
vars.forEach((v) => {
|
||
newForm[v.variable] =
|
||
variablesForm.value[v.variable] || v.default_value || '';
|
||
});
|
||
variablesForm.value = newForm;
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
// ==================== API 适配器 ====================
|
||
const { adapter: chatApi, conversationId } = useChatApi(props);
|
||
|
||
// 创建一个类型安全的 conversationId 引用用于事件处理器
|
||
const safeConversationId = ref<null | string>(conversationId.value ?? null);
|
||
watch(conversationId, (newId) => {
|
||
safeConversationId.value = newId ?? null;
|
||
});
|
||
|
||
// 监听 running 状态变化,执行完成时关闭加载动画
|
||
watch(running, (isRunning) => {
|
||
if (!isRunning && showLoadingAnimation.value) {
|
||
showLoadingAnimation.value = false;
|
||
}
|
||
});
|
||
|
||
// ==================== 事件处理器 ====================
|
||
const generateId = () =>
|
||
`msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||
|
||
const handleDesignPreview = (data: DesignPreviewData) => {
|
||
// 关闭加载动画
|
||
showLoadingAnimation.value = false;
|
||
|
||
switch (data.type) {
|
||
case 'app_design': {
|
||
currentAppDesign.value = data;
|
||
showAppDesignPanel.value = true;
|
||
break;
|
||
}
|
||
case 'app_settings': {
|
||
currentAppSettings.value = data;
|
||
showAppSettingsPanel.value = true;
|
||
break;
|
||
}
|
||
case 'dashboard_basic_info': {
|
||
currentDashboardBasicInfo.value = data;
|
||
showDashboardBasicInfoPanel.value = true;
|
||
break;
|
||
}
|
||
case 'dashboard_design': {
|
||
currentDashboardDesign.value = data;
|
||
showDashboardDesignPanel.value = true;
|
||
break;
|
||
}
|
||
case 'dashboard_publish': {
|
||
currentDashboardPublish.value = data;
|
||
showDashboardPublishPanel.value = true;
|
||
break;
|
||
}
|
||
case 'system_summary': {
|
||
currentSystemSummary.value = data;
|
||
showSystemSummaryPanel.value = true;
|
||
break;
|
||
}
|
||
default: {
|
||
currentDesign.value = data;
|
||
showDesignPanel.value = true;
|
||
break;
|
||
}
|
||
}
|
||
};
|
||
|
||
const { handleStreamEvent, updateAssistantMessage } = useEventHandler({
|
||
props: {
|
||
...props,
|
||
enableNodeEvents: enableNodeEvents.value,
|
||
},
|
||
emit: emit as any,
|
||
messages,
|
||
currentSteps,
|
||
currentRunId,
|
||
currentAssistantMsgId,
|
||
waitingForInput,
|
||
waitingConfig,
|
||
running,
|
||
conversationId: safeConversationId,
|
||
onDesignPreview: handleDesignPreview,
|
||
});
|
||
|
||
// ==================== 加载历史消息 ====================
|
||
watch(
|
||
() => props.conversationId,
|
||
async (newId) => {
|
||
if (enableHistory.value && newId && chatApi.loadHistory) {
|
||
try {
|
||
const history = await chatApi.loadHistory(newId);
|
||
messages.value = history;
|
||
} catch (error) {
|
||
console.error('加载历史消息失败:', error);
|
||
}
|
||
} else if (!newId) {
|
||
messages.value = [];
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
// ==================== 发送消息 ====================
|
||
// 存储待发送的附件
|
||
const pendingAttachments = ref<Attachment[]>([]);
|
||
|
||
const handleSend = (text: string, attachments?: Attachment[]) => {
|
||
if (!text || running.value) return;
|
||
|
||
emit('message-sent', text);
|
||
|
||
// 如果正在等待用户输入,恢复执行
|
||
if (waitingForInput.value && currentRunId.value) {
|
||
const lastAssistantMsg = messages.value.findLast(
|
||
(m) => m.role === 'assistant',
|
||
);
|
||
if (lastAssistantMsg?.interaction) {
|
||
const msgIndex = messages.value.findIndex(
|
||
(m) => m.id === lastAssistantMsg.id,
|
||
);
|
||
if (msgIndex !== -1) {
|
||
messages.value[msgIndex] = {
|
||
...lastAssistantMsg,
|
||
interaction: undefined,
|
||
};
|
||
}
|
||
}
|
||
|
||
const userMsg: ChatMessage = {
|
||
id: generateId(),
|
||
role: 'user',
|
||
content: text,
|
||
timestamp: new Date(),
|
||
};
|
||
messages.value.push(userMsg);
|
||
|
||
waitingForInput.value = false;
|
||
waitingConfig.value = null;
|
||
|
||
resumeExecution(text);
|
||
return;
|
||
}
|
||
|
||
// 工作流模式:检查多变量
|
||
if (
|
||
props.mode === 'workflow' &&
|
||
enableVariablesForm.value &&
|
||
hasMultipleVariables.value
|
||
) {
|
||
pendingMessage.value = text;
|
||
pendingAttachments.value = attachments || [];
|
||
const firstVar = variables.value[0];
|
||
if (firstVar) {
|
||
variablesForm.value[firstVar.variable] = text;
|
||
}
|
||
variablesDialogVisible.value = true;
|
||
return;
|
||
}
|
||
|
||
// 检查最后一条用户消息是否是语音消息且内容已更新
|
||
// 如果是,则跳过创建新消息,直接发送给 AI
|
||
const lastUserMsg = messages.value.findLast((m) => m.role === 'user');
|
||
const isVoiceMessageUpdate =
|
||
lastUserMsg?.voice && lastUserMsg.content === text;
|
||
|
||
if (isVoiceMessageUpdate) {
|
||
// 语音消息已存在,直接发送给 AI(不创建新消息)
|
||
const inputs =
|
||
props.mode === 'workflow'
|
||
? { user_input: text }
|
||
: props.initialInputs || undefined;
|
||
runMessageWithExistingUserMsg(lastUserMsg, inputs);
|
||
} else {
|
||
// 发送消息(创建新的用户消息)
|
||
const inputs =
|
||
props.mode === 'workflow'
|
||
? { user_input: text }
|
||
: props.initialInputs || undefined;
|
||
runMessage(text, inputs, attachments);
|
||
}
|
||
};
|
||
|
||
// 确认变量表单后运行
|
||
const handleVariablesConfirm = () => {
|
||
variablesDialogVisible.value = false;
|
||
runMessage(
|
||
pendingMessage.value,
|
||
variablesForm.value,
|
||
pendingAttachments.value,
|
||
);
|
||
pendingAttachments.value = [];
|
||
};
|
||
|
||
// 运行消息(使用已存在的用户消息,用于语音消息)
|
||
const runMessageWithExistingUserMsg = (
|
||
userMsg: ChatMessage,
|
||
inputs?: Record<string, any>,
|
||
) => {
|
||
// 如果是 application 类型工作流且是全屏布局,显示加载动画
|
||
if (
|
||
props.layout === 'fullscreen' &&
|
||
props.agent?.workflow_type === 'application'
|
||
) {
|
||
showLoadingAnimation.value = true;
|
||
loadingAnimationTitle.value = 'AI 正在开发...';
|
||
}
|
||
|
||
// 添加 AI 消息(pending 状态)
|
||
const assistantMsgId = generateId();
|
||
const assistantMsg: ChatMessage = {
|
||
id: assistantMsgId,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
status: 'pending',
|
||
reasoning_steps: [],
|
||
};
|
||
messages.value.push(assistantMsg);
|
||
currentAssistantMsgId.value = assistantMsgId;
|
||
currentSteps.value = [];
|
||
streamingContent.value = '';
|
||
|
||
running.value = true;
|
||
|
||
cancelStream = chatApi.sendMessage(
|
||
userMsg.content,
|
||
inputs,
|
||
(event) => handleStreamEvent(event, assistantMsgId),
|
||
(error) => {
|
||
console.error('Stream error:', error);
|
||
updateAssistantMessage(assistantMsgId, {
|
||
status: 'failed',
|
||
error_message: error.message || '执行失败',
|
||
});
|
||
running.value = false;
|
||
ElMessage.error(error.message || '执行失败');
|
||
},
|
||
() => {
|
||
cancelStream = null;
|
||
},
|
||
userMsg.attachments,
|
||
);
|
||
};
|
||
|
||
// 运行消息
|
||
const runMessage = (
|
||
userMessage: string,
|
||
inputs?: Record<string, any>,
|
||
attachments?: Attachment[],
|
||
) => {
|
||
// 添加用户消息(包含附件)
|
||
// 附件使用 file_id 关联文件管理系统
|
||
const userMsg: ChatMessage = {
|
||
id: generateId(),
|
||
role: 'user',
|
||
content: userMessage,
|
||
timestamp: new Date(),
|
||
attachments: attachments?.map((att) => ({
|
||
file_id: att.file_id || att.id,
|
||
id: att.file_id || att.id,
|
||
type: att.type as 'audio' | 'file' | 'image' | 'video',
|
||
name: att.name,
|
||
url: att.url,
|
||
size: att.size,
|
||
thumbnail: att.thumbnail,
|
||
})),
|
||
};
|
||
messages.value.push(userMsg);
|
||
|
||
// 如果是 application 类型工作流且是全屏布局,显示加载动画
|
||
if (
|
||
props.layout === 'fullscreen' &&
|
||
props.agent?.workflow_type === 'application'
|
||
) {
|
||
showLoadingAnimation.value = true;
|
||
loadingAnimationTitle.value = 'AI 正在开发...';
|
||
}
|
||
|
||
// 添加 AI 消息(pending 状态)
|
||
const assistantMsgId = generateId();
|
||
const assistantMsg: ChatMessage = {
|
||
id: assistantMsgId,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
status: 'pending',
|
||
reasoning_steps: [],
|
||
};
|
||
messages.value.push(assistantMsg);
|
||
currentAssistantMsgId.value = assistantMsgId;
|
||
currentSteps.value = [];
|
||
streamingContent.value = '';
|
||
|
||
running.value = true;
|
||
|
||
cancelStream = chatApi.sendMessage(
|
||
userMessage,
|
||
inputs,
|
||
(event) => handleStreamEvent(event, assistantMsgId),
|
||
(error) => {
|
||
console.error('Stream error:', error);
|
||
updateAssistantMessage(assistantMsgId, {
|
||
status: 'failed',
|
||
error_message: error.message || '执行失败',
|
||
});
|
||
running.value = false;
|
||
ElMessage.error(error.message || '执行失败');
|
||
},
|
||
() => {
|
||
cancelStream = null;
|
||
},
|
||
attachments,
|
||
);
|
||
};
|
||
|
||
// 恢复执行
|
||
const resumeExecution = (userInput: any) => {
|
||
// 工作流模式需要 runId,Agent 模式需要 conversationId
|
||
if (props.mode === 'workflow' && !currentRunId.value) return;
|
||
if (props.mode !== 'workflow' && !conversationId.value) return;
|
||
|
||
const assistantMsgId = generateId();
|
||
const assistantMsg: ChatMessage = {
|
||
id: assistantMsgId,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
status: 'pending',
|
||
};
|
||
messages.value.push(assistantMsg);
|
||
currentAssistantMsgId.value = assistantMsgId;
|
||
|
||
running.value = true;
|
||
|
||
cancelStream = chatApi.resumeExecution(
|
||
currentRunId.value || conversationId.value || '',
|
||
userInput,
|
||
(event) => handleStreamEvent(event, assistantMsgId),
|
||
(error) => {
|
||
console.error('Resume stream error:', error);
|
||
updateAssistantMessage(assistantMsgId, {
|
||
status: 'failed',
|
||
error_message: error.message || '恢复执行失败',
|
||
});
|
||
running.value = false;
|
||
// 关闭加载动画
|
||
showLoadingAnimation.value = false;
|
||
ElMessage.error(error.message || '恢复执行失败');
|
||
},
|
||
() => {
|
||
cancelStream = null;
|
||
},
|
||
);
|
||
};
|
||
|
||
// ==================== 交互处理 ====================
|
||
const handleInteractionSubmit = (messageId: string, value: any) => {
|
||
const msgIndex = messages.value.findIndex((m) => m.id === messageId);
|
||
if (msgIndex !== -1) {
|
||
const msg = messages.value[msgIndex];
|
||
if (msg) {
|
||
messages.value[msgIndex] = { ...msg, interaction: undefined };
|
||
}
|
||
}
|
||
|
||
// 全屏布局且是 application 类型工作流时,显示加载动画
|
||
if (
|
||
props.layout === 'fullscreen' &&
|
||
props.agent?.workflow_type === 'application'
|
||
) {
|
||
showLoadingAnimation.value = true;
|
||
loadingAnimationTitle.value = 'AI 正在继续开发...';
|
||
}
|
||
|
||
const userMsg: ChatMessage = {
|
||
id: generateId(),
|
||
role: 'user',
|
||
content: formatInteractionValue(value),
|
||
timestamp: new Date(),
|
||
};
|
||
messages.value.push(userMsg);
|
||
|
||
// 工作流模式需要 runId,Agent 模式需要 conversationId
|
||
if (
|
||
currentRunId.value ||
|
||
(props.mode !== 'workflow' && conversationId.value)
|
||
) {
|
||
resumeExecution(value);
|
||
}
|
||
|
||
waitingForInput.value = false;
|
||
waitingConfig.value = null;
|
||
};
|
||
|
||
const handleInteractionCancel = (messageId: string) => {
|
||
const msgIndex = messages.value.findIndex((m) => m.id === messageId);
|
||
if (msgIndex !== -1) {
|
||
const msg = messages.value[msgIndex];
|
||
if (msg) {
|
||
messages.value[msgIndex] = { ...msg, interaction: undefined };
|
||
}
|
||
}
|
||
|
||
// 全屏布局且是 application 类型工作流时,显示加载动画
|
||
if (
|
||
props.layout === 'fullscreen' &&
|
||
props.agent?.workflow_type === 'application'
|
||
) {
|
||
showLoadingAnimation.value = true;
|
||
loadingAnimationTitle.value = 'AI 正在继续开发...';
|
||
}
|
||
|
||
const userMsg: ChatMessage = {
|
||
id: generateId(),
|
||
role: 'user',
|
||
content: waitingConfig.value?.cancel_text || '取消',
|
||
timestamp: new Date(),
|
||
};
|
||
messages.value.push(userMsg);
|
||
|
||
// 工作流模式需要 runId,Agent 模式需要 conversationId
|
||
if (
|
||
currentRunId.value ||
|
||
(props.mode !== 'workflow' && conversationId.value)
|
||
) {
|
||
resumeExecution(false);
|
||
}
|
||
|
||
waitingForInput.value = false;
|
||
waitingConfig.value = null;
|
||
};
|
||
|
||
const formatInteractionValue = (value: any): string => {
|
||
if (value === true) {
|
||
return waitingConfig.value?.confirm_text || '确认';
|
||
}
|
||
if (Array.isArray(value)) {
|
||
const options = waitingConfig.value?.options || [];
|
||
const labels = value.map((v: string) => {
|
||
const opt = options.find((o: any) => o.value === v);
|
||
return opt?.label || v;
|
||
});
|
||
return labels.join(', ');
|
||
}
|
||
if (waitingConfig.value?.type === 'choice') {
|
||
const options = waitingConfig.value?.options || [];
|
||
const opt = options.find((o: any) => o.value === value);
|
||
return opt?.label || value;
|
||
}
|
||
return String(value);
|
||
};
|
||
|
||
// ==================== 其他操作 ====================
|
||
const handleStop = () => {
|
||
cancelStream?.();
|
||
cancelStream = null;
|
||
running.value = false;
|
||
|
||
if (currentAssistantMsgId.value) {
|
||
updateAssistantMessage(currentAssistantMsgId.value, {
|
||
status: 'failed',
|
||
error_message: '用户停止',
|
||
});
|
||
currentAssistantMsgId.value = null;
|
||
}
|
||
};
|
||
|
||
const handleClear = () => {
|
||
messages.value = [];
|
||
streamingContent.value = '';
|
||
currentSteps.value = [];
|
||
currentRunId.value = '';
|
||
waitingForInput.value = false;
|
||
waitingConfig.value = null;
|
||
emit('clear-status');
|
||
};
|
||
|
||
const handleFeedback = (messageId: string, feedback: 'dislike' | 'like') => {
|
||
if (chatApi.sendFeedback) {
|
||
chatApi.sendFeedback(messageId, feedback);
|
||
const msgIndex = messages.value.findIndex((m) => m.id === messageId);
|
||
if (msgIndex !== -1) {
|
||
messages.value[msgIndex] = {
|
||
...messages.value[msgIndex]!,
|
||
feedback,
|
||
};
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleAddMessage = (message: ChatMessage) => {
|
||
messages.value.push(message);
|
||
};
|
||
|
||
const handleUpdateMessage = (
|
||
messageId: string,
|
||
updates: Partial<ChatMessage>,
|
||
) => {
|
||
const idx = messages.value.findIndex((m) => m.id === messageId);
|
||
if (idx !== -1) {
|
||
messages.value[idx] = { ...messages.value[idx]!, ...updates };
|
||
}
|
||
};
|
||
|
||
// ==================== 设计预览处理 ====================
|
||
const createDesignConfirmHandler = (
|
||
showRef: ReturnType<typeof ref<boolean>>,
|
||
dataRef: ReturnType<typeof ref<DesignPreviewData | undefined>>,
|
||
confirmMessage: string,
|
||
) => {
|
||
return (data: Record<string, any>) => {
|
||
showRef.value = false;
|
||
dataRef.value = undefined;
|
||
|
||
// 全屏布局且是 application 类型工作流时,显示加载动画
|
||
if (
|
||
props.layout === 'fullscreen' &&
|
||
props.agent?.workflow_type === 'application'
|
||
) {
|
||
showLoadingAnimation.value = true;
|
||
loadingAnimationTitle.value = 'AI 正在继续开发...';
|
||
}
|
||
|
||
const userMsg: ChatMessage = {
|
||
id: generateId(),
|
||
role: 'user',
|
||
content: confirmMessage,
|
||
timestamp: new Date(),
|
||
};
|
||
messages.value.push(userMsg);
|
||
|
||
waitingForInput.value = false;
|
||
waitingConfig.value = null;
|
||
|
||
resumeExecution(JSON.stringify(data));
|
||
};
|
||
};
|
||
|
||
const createDesignCloseHandler = (
|
||
showRef: ReturnType<typeof ref<boolean>>,
|
||
dataRef: ReturnType<typeof ref<DesignPreviewData | undefined>>,
|
||
) => {
|
||
return () => {
|
||
showRef.value = false;
|
||
dataRef.value = undefined;
|
||
};
|
||
};
|
||
|
||
const handleDesignConfirm = createDesignConfirmHandler(
|
||
showDesignPanel,
|
||
currentDesign,
|
||
'确认设计',
|
||
);
|
||
const handleDesignClose = createDesignCloseHandler(
|
||
showDesignPanel,
|
||
currentDesign,
|
||
);
|
||
|
||
const handleAppDesignConfirm = createDesignConfirmHandler(
|
||
showAppDesignPanel,
|
||
currentAppDesign,
|
||
'确认应用设计',
|
||
);
|
||
const handleAppDesignClose = createDesignCloseHandler(
|
||
showAppDesignPanel,
|
||
currentAppDesign,
|
||
);
|
||
|
||
const handleAppSettingsConfirm = createDesignConfirmHandler(
|
||
showAppSettingsPanel,
|
||
currentAppSettings,
|
||
'确认应用设置',
|
||
);
|
||
const handleAppSettingsClose = createDesignCloseHandler(
|
||
showAppSettingsPanel,
|
||
currentAppSettings,
|
||
);
|
||
|
||
const handleDashboardBasicInfoConfirm = createDesignConfirmHandler(
|
||
showDashboardBasicInfoPanel,
|
||
currentDashboardBasicInfo,
|
||
'确认仪表盘基础信息',
|
||
);
|
||
const handleDashboardBasicInfoClose = createDesignCloseHandler(
|
||
showDashboardBasicInfoPanel,
|
||
currentDashboardBasicInfo,
|
||
);
|
||
|
||
const handleDashboardDesignConfirm = createDesignConfirmHandler(
|
||
showDashboardDesignPanel,
|
||
currentDashboardDesign,
|
||
'确认仪表盘设计',
|
||
);
|
||
const handleDashboardDesignClose = createDesignCloseHandler(
|
||
showDashboardDesignPanel,
|
||
currentDashboardDesign,
|
||
);
|
||
|
||
const handleDashboardPublishConfirm = (data: Record<string, any>) => {
|
||
showDashboardPublishPanel.value = false;
|
||
currentDashboardPublish.value = undefined;
|
||
|
||
const userMsg: ChatMessage = {
|
||
id: generateId(),
|
||
role: 'user',
|
||
content: `发布到菜单:${data.menu_name}`,
|
||
timestamp: new Date(),
|
||
};
|
||
messages.value.push(userMsg);
|
||
|
||
waitingForInput.value = false;
|
||
waitingConfig.value = null;
|
||
|
||
resumeExecution(JSON.stringify(data));
|
||
};
|
||
const handleDashboardPublishClose = createDesignCloseHandler(
|
||
showDashboardPublishPanel,
|
||
currentDashboardPublish,
|
||
);
|
||
|
||
const handleSystemSummaryConfirm = createDesignConfirmHandler(
|
||
showSystemSummaryPanel,
|
||
currentSystemSummary,
|
||
'完成',
|
||
);
|
||
const handleSystemSummaryClose = createDesignCloseHandler(
|
||
showSystemSummaryPanel,
|
||
currentSystemSummary,
|
||
);
|
||
|
||
// ==================== 暴露方法 ====================
|
||
defineExpose({
|
||
clearChat: handleClear,
|
||
sendMessage: handleSend,
|
||
});
|
||
|
||
// ==================== 生命周期 ====================
|
||
onUnmounted(() => {
|
||
cancelStream?.();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="flex h-full overflow-hidden">
|
||
<!-- 聊天面板 -->
|
||
<div
|
||
class="flex h-full flex-col overflow-hidden"
|
||
:class="[
|
||
layout === 'fullscreen'
|
||
? hasAnyDesignPanelOpen
|
||
? 'w-[400px] flex-shrink-0'
|
||
: 'flex-1'
|
||
: 'bg-card w-96 rounded-[8px] shadow-xl',
|
||
]"
|
||
>
|
||
<!-- 头部 -->
|
||
<div
|
||
v-if="showHeader"
|
||
class="flex items-center justify-between px-4 py-4"
|
||
>
|
||
<div class="text-foreground flex items-center gap-2 font-medium">
|
||
{{ panelTitle }}
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
<!-- 变量设置按钮(工作流模式且有多变量时显示) -->
|
||
<ElButton
|
||
v-if="
|
||
mode === 'workflow' && enableVariablesForm && hasMultipleVariables
|
||
"
|
||
link
|
||
:icon="Settings2"
|
||
title="配置输入变量"
|
||
@click="variablesDialogVisible = true"
|
||
/>
|
||
<ElButton
|
||
v-if="showCloseButton"
|
||
link
|
||
:icon="X"
|
||
@click="$emit('close')"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ChatBox 组件 -->
|
||
<ChatBox
|
||
:messages="messages"
|
||
:loading="running"
|
||
:streaming-content="streamingContent"
|
||
:show-avatar="showAvatar"
|
||
:assistant-name="assistantName"
|
||
:placeholder="placeholderText"
|
||
:disabled="disabled"
|
||
:enable-attachment="enableAttachment"
|
||
:enable-image="enableImage"
|
||
:enable-voice="enableVoice"
|
||
:enable-feedback="enableFeedback"
|
||
:show-clear-button="showClearButton"
|
||
:empty-text="emptyText"
|
||
:welcome-config="welcomeConfig"
|
||
class="flex-1 rounded-none border-0"
|
||
@send="handleSend"
|
||
@stop="handleStop"
|
||
@clear="handleClear"
|
||
@feedback="handleFeedback"
|
||
@interaction-submit="handleInteractionSubmit"
|
||
@interaction-cancel="handleInteractionCancel"
|
||
@add-message="handleAddMessage"
|
||
@update-message="handleUpdateMessage"
|
||
/>
|
||
|
||
<!-- 多变量输入对话框 -->
|
||
<ElDialog
|
||
v-model="variablesDialogVisible"
|
||
title="配置输入变量"
|
||
width="400px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<ElForm label-position="top">
|
||
<ElFormItem
|
||
v-for="v in variables"
|
||
:key="v.variable"
|
||
:label="v.variable"
|
||
:required="v.required"
|
||
>
|
||
<ElInput
|
||
v-model="variablesForm[v.variable]"
|
||
:placeholder="`请输入 ${v.variable}`"
|
||
:type="v.type === 'text' ? 'textarea' : 'text'"
|
||
:rows="v.type === 'text' ? 3 : undefined"
|
||
/>
|
||
</ElFormItem>
|
||
</ElForm>
|
||
<template #footer>
|
||
<ElButton @click="variablesDialogVisible = false">取消</ElButton>
|
||
<ElButton type="primary" @click="handleVariablesConfirm">
|
||
开始运行
|
||
</ElButton>
|
||
</template>
|
||
</ElDialog>
|
||
</div>
|
||
|
||
<!-- 设计面板区域(全屏布局时并排显示) -->
|
||
<div
|
||
v-if="layout === 'fullscreen' && hasAnyDesignPanelOpen"
|
||
class="flex flex-1 flex-col overflow-hidden"
|
||
>
|
||
<!-- AI 工作动画 -->
|
||
<AiWorkingAnimation
|
||
v-if="showLoadingAnimation"
|
||
:title="loadingAnimationTitle"
|
||
subtitle="正在分析需求并生成代码"
|
||
:show-background="false"
|
||
class="ml-3 h-full min-h-[400px] flex-1 rounded-lg"
|
||
/>
|
||
|
||
<!-- 设计编辑面板 -->
|
||
<DesignEditorPanel
|
||
v-if="showDesignPanel"
|
||
:visible="showDesignPanel"
|
||
:design="currentDesign as any"
|
||
@update:visible="showDesignPanel = $event"
|
||
@confirm="handleDesignConfirm"
|
||
@close="handleDesignClose"
|
||
/>
|
||
|
||
<!-- 应用设计面板 -->
|
||
<AppDesignPanel
|
||
v-if="showAppDesignPanel"
|
||
:visible="showAppDesignPanel"
|
||
:design="currentAppDesign as any"
|
||
@update:visible="showAppDesignPanel = $event"
|
||
@confirm="handleAppDesignConfirm"
|
||
@close="handleAppDesignClose"
|
||
/>
|
||
|
||
<!-- 应用设置面板 -->
|
||
<AppSettingsPanel
|
||
v-if="showAppSettingsPanel"
|
||
:visible="showAppSettingsPanel"
|
||
:settings="currentAppSettings as any"
|
||
@update:visible="showAppSettingsPanel = $event"
|
||
@confirm="handleAppSettingsConfirm"
|
||
@close="handleAppSettingsClose"
|
||
/>
|
||
|
||
<!-- 仪表盘基础信息面板 -->
|
||
<DashboardBasicInfoConfirmPanel
|
||
v-if="showDashboardBasicInfoPanel"
|
||
:visible="showDashboardBasicInfoPanel"
|
||
:basic-info="currentDashboardBasicInfo as any"
|
||
@update:visible="showDashboardBasicInfoPanel = $event"
|
||
@confirm="handleDashboardBasicInfoConfirm"
|
||
@close="handleDashboardBasicInfoClose"
|
||
/>
|
||
|
||
<!-- 仪表盘设计面板 -->
|
||
<DashboardDesignConfirmPanel
|
||
v-if="showDashboardDesignPanel"
|
||
:visible="showDashboardDesignPanel"
|
||
:design="currentDashboardDesign as any"
|
||
@update:visible="showDashboardDesignPanel = $event"
|
||
@confirm="handleDashboardDesignConfirm"
|
||
@close="handleDashboardDesignClose"
|
||
/>
|
||
|
||
<!-- 仪表盘发布面板 -->
|
||
<DashboardPublishConfirmPanel
|
||
v-if="showDashboardPublishPanel"
|
||
:visible="showDashboardPublishPanel"
|
||
:publish-data="currentDashboardPublish as any"
|
||
@update:visible="showDashboardPublishPanel = $event"
|
||
@confirm="handleDashboardPublishConfirm"
|
||
@close="handleDashboardPublishClose"
|
||
/>
|
||
|
||
<!-- 系统总结面板 -->
|
||
<SystemSummaryConfirmPanel
|
||
v-if="showSystemSummaryPanel"
|
||
:visible="showSystemSummaryPanel"
|
||
:data="currentSystemSummary as any"
|
||
@update:visible="showSystemSummaryPanel = $event"
|
||
@confirm="handleSystemSummaryConfirm"
|
||
@close="handleSystemSummaryClose"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 侧边栏布局时的设计面板(保持原有行为) -->
|
||
<template v-if="layout === 'sidebar'">
|
||
<DesignEditorPanel
|
||
:visible="showDesignPanel"
|
||
:design="currentDesign as any"
|
||
@update:visible="showDesignPanel = $event"
|
||
@confirm="handleDesignConfirm"
|
||
@close="handleDesignClose"
|
||
/>
|
||
|
||
<AppDesignPanel
|
||
:visible="showAppDesignPanel"
|
||
:design="currentAppDesign as any"
|
||
@update:visible="showAppDesignPanel = $event"
|
||
@confirm="handleAppDesignConfirm"
|
||
@close="handleAppDesignClose"
|
||
/>
|
||
|
||
<AppSettingsPanel
|
||
:visible="showAppSettingsPanel"
|
||
:settings="currentAppSettings as any"
|
||
@update:visible="showAppSettingsPanel = $event"
|
||
@confirm="handleAppSettingsConfirm"
|
||
@close="handleAppSettingsClose"
|
||
/>
|
||
|
||
<DashboardBasicInfoConfirmPanel
|
||
:visible="showDashboardBasicInfoPanel"
|
||
:basic-info="currentDashboardBasicInfo as any"
|
||
@update:visible="showDashboardBasicInfoPanel = $event"
|
||
@confirm="handleDashboardBasicInfoConfirm"
|
||
@close="handleDashboardBasicInfoClose"
|
||
/>
|
||
|
||
<DashboardDesignConfirmPanel
|
||
:visible="showDashboardDesignPanel"
|
||
:design="currentDashboardDesign as any"
|
||
@update:visible="showDashboardDesignPanel = $event"
|
||
@confirm="handleDashboardDesignConfirm"
|
||
@close="handleDashboardDesignClose"
|
||
/>
|
||
|
||
<DashboardPublishConfirmPanel
|
||
:visible="showDashboardPublishPanel"
|
||
:publish-data="currentDashboardPublish as any"
|
||
@update:visible="showDashboardPublishPanel = $event"
|
||
@confirm="handleDashboardPublishConfirm"
|
||
@close="handleDashboardPublishClose"
|
||
/>
|
||
|
||
<SystemSummaryConfirmPanel
|
||
:visible="showSystemSummaryPanel"
|
||
:data="currentSystemSummary as any"
|
||
@update:visible="showSystemSummaryPanel = $event"
|
||
@confirm="handleSystemSummaryConfirm"
|
||
@close="handleSystemSummaryClose"
|
||
/>
|
||
</template>
|
||
</div>
|
||
</template>
|