345 lines
8.8 KiB
Vue
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>
|