Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/agents/components/AgentTryoutPanel.vue
陈子默 9f06c238d3 fix: 兼容低版本浏览器消息复制
- 为 Chrome 90 增加剪贴板兼容回退

- 统一聊天与智能体试运行的原色对勾反馈
2026-07-22 20:07:34 +08:00

345 lines
8.8 KiB
Vue

<script setup lang="ts">
import type {
ChatTimelineMessageItem,
ChatTimelineToolApprovalPayload,
} from '@easyflow/common-ui';
import type {
AgentInfo,
AgentKnowledgeBinding,
AgentToolBinding,
} from '../types';
import { computed, onMounted, ref, watch } from 'vue';
import { ChatTimeline } from '@easyflow/common-ui';
import { BrushCleaning } from '@easyflow/icons';
import { copyTextToClipboard } from '@easyflow/utils';
import { ElButton, ElMessage } from 'element-plus';
import AiChatPanel from '#/components/ai-chat/AiChatPanel.vue';
import { loadAgentChatImage } from '#/components/ai-chat/mediaApi';
import { useAgentComposerDraft } from '#/components/ai-chat/useAgentComposerDraft';
import { approveAgentRun, rejectAgentRun } from '../api';
import { useAgentTryoutStream } from '../composables/useAgentTryoutStream';
import { resolveInteractionDisplay } from '../interaction-config';
import AgentWelcomeState from './AgentWelcomeState.vue';
const props = defineProps<{
agent: AgentInfo;
imageEnabled?: boolean;
knowledgeBindings: AgentKnowledgeBinding[];
toolBindings: AgentToolBinding[];
}>();
const emit = defineEmits<{ close: [] }>();
const {
loading,
clearDraftSession,
markToolApproving,
markToolRejected,
copyMessageText,
selectVariant,
syncDraftContext,
timelineItems,
sendDraft,
stop,
} = useAgentTryoutStream();
const approvalLoading = ref(false);
const composer = useAgentComposerDraft('DRAFT');
const interactionDisplay = computed(() =>
resolveInteractionDisplay(props.agent),
);
function getDraftContext() {
return {
agent: props.agent,
toolBindings: props.toolBindings,
knowledgeBindings: props.knowledgeBindings,
};
}
function syncCurrentDraftContext(restore = false) {
syncDraftContext(getDraftContext(), restore, composer.sessionId.value);
}
async function activateComposer() {
const agentId = String(props.agent.id || '');
if (!agentId) return;
try {
await composer.activate(agentId, `agent-draft-${agentId}`);
syncCurrentDraftContext(true);
} catch (error) {
ElMessage.warning(
error instanceof Error ? error.message : '输入草稿恢复失败',
);
}
}
onMounted(() => void activateComposer());
watch(
() => [props.agent.id, props.agent.localId],
() => void activateComposer(),
);
watch(
() => [props.agent, props.knowledgeBindings, props.toolBindings],
() => {
syncCurrentDraftContext();
},
{ deep: true },
);
async function handleSend(prompt: string) {
if (loading.value || approvalLoading.value) return;
if (composer.images.uploading.value) {
ElMessage.warning('图片上传完成后再发送');
return;
}
const failedImage = composer.images.items.value.find(
(item) => item.status === 'error',
);
if (failedImage) {
ElMessage.error(failedImage.error || '请处理上传失败的图片');
return;
}
await composer.flush();
await sendDraft({
...getDraftContext(),
prompt,
imageUploadIds: composer.images.uploadIds.value,
images: composer.images.readyItems.value.map((item) => ({ ...item })),
sessionId: composer.sessionId.value,
onAccepted: () =>
composer.markAccepted().catch(() => {
ElMessage.warning('消息已发送,草稿将在过期后自动清理');
}),
});
}
function handleDraftTextInput() {
composer.scheduleSave();
}
function handleSuggestedQuestion(question: string) {
void handleSend(question);
}
function canCopyMessage(item: ChatTimelineMessageItem) {
if (item.role === 'user') {
return Boolean(copyMessageText(item).trim());
}
return Boolean(
item.roundId && item.roundCompleted && copyMessageText(item).trim(),
);
}
function canRegenerateMessage(item: ChatTimelineMessageItem) {
return (
item.role === 'assistant' &&
Boolean(item.roundId) &&
Boolean(item.roundCompleted)
);
}
async function handleCopyMessage(item: ChatTimelineMessageItem) {
const text = copyMessageText(item).trim();
if (!text) {
return false;
}
try {
await copyTextToClipboard(text);
return true;
} catch (error) {
console.error('复制消息失败:', error);
ElMessage.error('复制失败');
return false;
}
}
async function handleRegenerateMessage(item: ChatTimelineMessageItem) {
void item;
}
function handleSelectPreviousVariant(item: ChatTimelineMessageItem) {
selectVariant(item, 'previous');
}
function handleSelectNextVariant(item: ChatTimelineMessageItem) {
selectVariant(item, 'next');
}
async function handleClearSession() {
try {
await clearDraftSession();
await composer.clear();
await activateComposer();
ElMessage.success('已清理会话');
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '清理会话失败');
}
}
async function handleAddFiles(files: File[]) {
if (!props.agent.id) {
ElMessage.warning('请先保存智能体');
return;
}
await composer.ensureSession();
const rejected = await composer.images.addFiles(files, {
agentId: String(props.agent.id),
mode: 'DRAFT',
sessionId: composer.sessionId.value,
});
if (rejected > 0) {
ElMessage.warning('每次最多添加 5 张图片');
}
composer.scheduleSave();
}
async function handleRetryImage(item: any) {
await composer.images.retry(item, {
agentId: String(props.agent.id),
mode: 'DRAFT',
sessionId: composer.sessionId.value,
});
composer.scheduleSave();
}
async function handleRemoveImage(item: any) {
try {
await composer.images.remove(item);
composer.scheduleSave();
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '图片删除失败');
}
}
function handleStop() {
if (!loading.value) {
return;
}
stop();
}
async function handleApprove(payload: ChatTimelineToolApprovalPayload) {
approvalLoading.value = true;
markToolApproving(payload);
try {
const res = await approveAgentRun(payload.requestId, payload.resumeToken);
if (res.errorCode === 0) {
ElMessage.success('已批准');
}
} catch (error) {
markToolRejected({
...payload,
reason: error instanceof Error ? error.message : '批准失败',
});
throw error;
} finally {
approvalLoading.value = false;
}
}
async function handleReject(payload: ChatTimelineToolApprovalPayload) {
approvalLoading.value = true;
markToolRejected({
...payload,
reason: '用户拒绝执行',
});
try {
await rejectAgentRun(
payload.requestId,
payload.resumeToken,
'用户拒绝执行',
);
} finally {
approvalLoading.value = false;
}
}
</script>
<template>
<AiChatPanel
v-model="composer.text.value"
:title="agent.name || '草稿试运行'"
empty-text="输入问题试运行当前智能体"
closable
:messages="[]"
:loading="loading"
:images="composer.images.items.value"
:image-enabled="imageEnabled"
:image-loader="loadAgentChatImage"
:placeholder="interactionDisplay.inputPlaceholder"
:approval-loading="approvalLoading"
@send="handleSend"
@update:model-value="handleDraftTextInput"
@add-files="handleAddFiles"
@remove-image="handleRemoveImage"
@retry-image="handleRetryImage"
@stop="handleStop"
@approve="handleApprove"
@reject="handleReject"
@close="emit('close')"
>
<template #headerActions>
<ElButton
:icon="BrushCleaning"
circle
text
:disabled="approvalLoading"
aria-label="清理会话"
title="清理会话"
@click="handleClearSession"
/>
</template>
<div class="agent-tryout__conversation">
<AgentWelcomeState
v-if="timelineItems.length === 0"
:agent-name="agent.name || '智能体'"
:avatar="agent.avatar"
:disabled="loading || approvalLoading"
:suggested-questions="interactionDisplay.suggestedQuestions"
:welcome-message="interactionDisplay.welcomeMessage"
@select-question="handleSuggestedQuestion"
/>
<ChatTimeline
v-else
:items="timelineItems"
:image-loader="loadAgentChatImage"
empty-text="输入问题试运行当前智能体"
:approval-loading="approvalLoading"
:copy-action="handleCopyMessage"
:copyable="canCopyMessage"
:regenerable="canRegenerateMessage"
:regenerate-disabled="true"
@approve="handleApprove"
@regenerate-message="handleRegenerateMessage"
@reject="handleReject"
@select-next-variant="handleSelectNextVariant"
@select-previous-variant="handleSelectPreviousVariant"
/>
</div>
</AiChatPanel>
</template>
<style scoped>
.agent-tryout__conversation {
display: flex;
flex: 1;
min-height: 0;
padding: 0 var(--space-4);
overflow: auto;
overscroll-behavior: contain;
}
.agent-tryout__conversation :deep(.chat-timeline) {
width: 100%;
min-height: 0;
}
</style>