1884 lines
52 KiB
Vue
1884 lines
52 KiB
Vue
<script setup lang="ts">
|
|
import type {
|
|
ChatDocumentAttachment,
|
|
ChatImageAttachment,
|
|
ChatTimelineErrorItem,
|
|
ChatTimelineItem,
|
|
ChatTimelineMessageItem,
|
|
ChatTimelineToolApprovalPayload,
|
|
} from '@easyflow/common-ui';
|
|
|
|
import type { AgentInfo } from '../agents/types';
|
|
import type { AgentChatSessionView } from './api';
|
|
|
|
import type {
|
|
ChatInputTriggerGroup,
|
|
ChatInputTriggerItem,
|
|
} from '#/components/chat-workspace/input-triggers/types';
|
|
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
|
|
import {
|
|
CHAT_DOCUMENT_SUPPORTED_FORMATS,
|
|
ChatDocumentAttachments,
|
|
ChatImageAttachments,
|
|
ChatTimeline,
|
|
ChatTimelineBuilder,
|
|
defaultAssistantAvatar,
|
|
} from '@easyflow/common-ui';
|
|
import { copyTextToClipboard } from '@easyflow/utils';
|
|
|
|
import {
|
|
Delete,
|
|
EditPen,
|
|
MoreFilled,
|
|
Paperclip,
|
|
Plus,
|
|
Promotion,
|
|
UploadFilled,
|
|
} from '@element-plus/icons-vue';
|
|
import {
|
|
ElButton,
|
|
ElDropdown,
|
|
ElDropdownItem,
|
|
ElDropdownMenu,
|
|
ElIcon,
|
|
ElInput,
|
|
ElMessage,
|
|
ElMessageBox,
|
|
ElOption,
|
|
ElSelect,
|
|
} from 'element-plus';
|
|
|
|
import {
|
|
createAgentArtifactLoader,
|
|
loadAgentChatDocument,
|
|
loadAgentChatImage,
|
|
} from '#/components/ai-chat/mediaApi';
|
|
import { useAgentComposerDraft } from '#/components/ai-chat/useAgentComposerDraft';
|
|
import { CHAT_IMAGE_SUPPORTED_FORMATS } from '#/components/ai-chat/useChatImageUploads';
|
|
import ChatCapabilityMenu from '#/components/chat-workspace/ChatCapabilityMenu.vue';
|
|
import ChatInputTriggerPanel from '#/components/chat-workspace/ChatInputTriggerPanel.vue';
|
|
import { useChatInputTrigger } from '#/components/chat-workspace/input-triggers/useChatInputTrigger';
|
|
|
|
import AgentWelcomeState from '../agents/components/AgentWelcomeState.vue';
|
|
import { resolveInteractionDisplay } from '../agents/interaction-config';
|
|
import { recordsToTimelineItems } from './adapters/agentTimelineAdapter';
|
|
import { agentChatRuntimeManager } from './agentChatRuntimeManager';
|
|
import {
|
|
approveAgentRun,
|
|
deleteAgentSession,
|
|
getAgentConversation,
|
|
getAgentSession,
|
|
getAgentSessions,
|
|
getPublishedAgents,
|
|
getPublishedKnowledges,
|
|
rejectAgentRun,
|
|
renameAgentSession,
|
|
saveAgentSessionExtraKnowledges,
|
|
} from './api';
|
|
import {
|
|
isMissingAgentSessionError,
|
|
resolveAgentSessionErrorMessage,
|
|
} from './sessionRecovery';
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
const agents = ref<AgentInfo[]>([]);
|
|
const sessions = ref<AgentChatSessionView[]>([]);
|
|
const timelineItems = ref<ChatTimelineItem[]>([]);
|
|
const selectedAgentId = ref('');
|
|
const currentSessionId = ref('');
|
|
const composer = useAgentComposerDraft('FORMAL');
|
|
const promptText = composer.text;
|
|
const loadCurrentAgentArtifact = createAgentArtifactLoader(() => ({
|
|
agentId: selectedAgentId.value,
|
|
mode: 'FORMAL',
|
|
sessionId: currentSessionId.value,
|
|
}));
|
|
const promptInputRef = ref();
|
|
const attachmentFileInputRef = ref<HTMLInputElement>();
|
|
const composerDragActive = ref(false);
|
|
const loadingAgents = ref(false);
|
|
const agentLoadError = ref('');
|
|
const loadingSessions = ref(false);
|
|
const loadingConversation = ref(false);
|
|
const loadingKnowledges = ref(false);
|
|
const savingExtraKnowledges = ref(false);
|
|
const sending = ref(false);
|
|
const runtimeRunning = ref(false);
|
|
const retryableErrorRoundId = ref('');
|
|
const approvalLoadingKey = ref('');
|
|
const knowledgeOptions = ref<{ label: string; value: string }[]>([]);
|
|
const knowledgeMap = ref(new Map<string, { id: string; title: string }>());
|
|
const extraKnowledgeIds = ref<string[]>([]);
|
|
const runtimeSendingState = new Map<string, boolean>();
|
|
const MAX_EXTRA_KNOWLEDGE_COUNT = 3;
|
|
let runtimeUnsubscribe: (() => void) | undefined;
|
|
|
|
const selectedAgent = computed(() =>
|
|
agents.value.find((agent) => String(agent.id) === selectedAgentId.value),
|
|
);
|
|
const selectedAgentImageSupport = computed(() =>
|
|
Boolean(
|
|
selectedAgent.value?.publishedSnapshotJson?.modelSummary?.supportImage,
|
|
),
|
|
);
|
|
const supportedAttachmentFormats = computed(() =>
|
|
selectedAgentImageSupport.value === false
|
|
? CHAT_DOCUMENT_SUPPORTED_FORMATS
|
|
: `${CHAT_DOCUMENT_SUPPORTED_FORMATS};图片支持 ${CHAT_IMAGE_SUPPORTED_FORMATS}`,
|
|
);
|
|
const interactionDisplay = computed(() =>
|
|
resolveInteractionDisplay(selectedAgent.value),
|
|
);
|
|
const currentSession = computed(() =>
|
|
sessions.value.find(
|
|
(session) => String(session.sessionId) === currentSessionId.value,
|
|
),
|
|
);
|
|
const canStopRuntime = computed(() => sending.value || runtimeRunning.value);
|
|
const canSend = computed(
|
|
() =>
|
|
(Boolean(promptText.value.trim()) ||
|
|
composer.images.readyItems.value.length > 0 ||
|
|
composer.documents.readyItems.value.length > 0) &&
|
|
!composer.images.uploading.value &&
|
|
!composer.documents.processing.value &&
|
|
!composer.images.items.value.some((item) => item.status === 'error') &&
|
|
!composer.documents.items.value.some((item) => item.status === 'error') &&
|
|
(selectedAgentImageSupport.value !== false ||
|
|
composer.images.readyItems.value.length === 0) &&
|
|
Boolean(selectedAgentId.value) &&
|
|
!sending.value &&
|
|
!runtimeRunning.value,
|
|
);
|
|
const composerPlaceholder = computed(() =>
|
|
selectedAgent.value
|
|
? interactionDisplay.value.inputPlaceholder
|
|
: '请选择智能体',
|
|
);
|
|
const selectedExtraKnowledges = computed(() => {
|
|
const knowledges: { id: string; title: string }[] = [];
|
|
for (const id of extraKnowledgeIds.value) {
|
|
const knowledge = knowledgeMap.value.get(String(id));
|
|
if (knowledge) {
|
|
knowledges.push(knowledge);
|
|
}
|
|
}
|
|
return knowledges;
|
|
});
|
|
const capabilityDisabled = computed(
|
|
() =>
|
|
sending.value ||
|
|
runtimeRunning.value ||
|
|
savingExtraKnowledges.value ||
|
|
!selectedAgentId.value,
|
|
);
|
|
const isWelcomeState = computed(
|
|
() =>
|
|
Boolean(selectedAgent.value) &&
|
|
!loadingAgents.value &&
|
|
!loadingConversation.value &&
|
|
!currentSessionId.value &&
|
|
timelineItems.value.length === 0,
|
|
);
|
|
const agentSelectWidth = computed(() => {
|
|
const name = selectedAgent.value?.name || '选择智能体';
|
|
const textWidth = [...name].reduce(
|
|
(total, char) => total + (/[\u4E00-\u9FFF]/.test(char) ? 15 : 8),
|
|
0,
|
|
);
|
|
return `${Math.min(Math.max(textWidth + 36, 116), 320)}px`;
|
|
});
|
|
const triggerGroups = computed<ChatInputTriggerGroup[]>(() => [
|
|
{
|
|
items: knowledgeOptions.value.map((item) => {
|
|
const selected = extraKnowledgeIds.value.includes(String(item.value));
|
|
return {
|
|
disabled:
|
|
!selected &&
|
|
extraKnowledgeIds.value.length >= MAX_EXTRA_KNOWLEDGE_COUNT,
|
|
id: item.value,
|
|
label: item.label,
|
|
};
|
|
}),
|
|
label: '知识库',
|
|
symbol: '@',
|
|
},
|
|
]);
|
|
const chatInputTrigger = useChatInputTrigger({
|
|
disabled: capabilityDisabled,
|
|
groups: triggerGroups,
|
|
inputRef: promptInputRef,
|
|
text: promptText,
|
|
});
|
|
|
|
function formatDate(value?: string) {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) {
|
|
return '';
|
|
}
|
|
return date.toLocaleString(undefined, {
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
month: '2-digit',
|
|
});
|
|
}
|
|
|
|
function sessionTitle(session: AgentChatSessionView) {
|
|
return session.title || session.lastMessagePreview || '新对话';
|
|
}
|
|
|
|
function sessionPreview(session: AgentChatSessionView) {
|
|
return session.lastMessagePreview || session.assistantName || '暂无消息';
|
|
}
|
|
|
|
function copyMessageText(item: ChatTimelineMessageItem) {
|
|
return item.parts
|
|
.filter((part) => part.type === 'text')
|
|
.map((part) => part.content)
|
|
.join('')
|
|
.trim();
|
|
}
|
|
|
|
function canCopyMessage(item: ChatTimelineMessageItem) {
|
|
if (item.role === 'user') {
|
|
return Boolean(copyMessageText(item));
|
|
}
|
|
return Boolean(item.roundId && item.roundCompleted && copyMessageText(item));
|
|
}
|
|
|
|
async function syncSessionRoute(sessionId?: string) {
|
|
const nextQuery = { ...route.query };
|
|
if (sessionId) {
|
|
nextQuery.sessionId = sessionId;
|
|
} else {
|
|
delete nextQuery.sessionId;
|
|
}
|
|
if (String(route.query.sessionId || '') === String(sessionId || '')) {
|
|
return;
|
|
}
|
|
await router.replace({ query: nextQuery });
|
|
}
|
|
|
|
async function removeSessionFromPage(sessionId: string) {
|
|
sessions.value = sessions.value.filter(
|
|
(item) => String(item.sessionId) !== sessionId,
|
|
);
|
|
const isCurrentSession =
|
|
currentSessionId.value === sessionId ||
|
|
String(route.query.sessionId || '') === sessionId;
|
|
if (isCurrentSession) {
|
|
try {
|
|
await composer.clear();
|
|
} catch (error) {
|
|
ElMessage.warning(
|
|
error instanceof Error ? error.message : '会话草稿清理失败',
|
|
);
|
|
}
|
|
await createNewSession();
|
|
}
|
|
}
|
|
|
|
async function loadAgents() {
|
|
loadingAgents.value = true;
|
|
agentLoadError.value = '';
|
|
try {
|
|
const res = await getPublishedAgents();
|
|
if (res.errorCode !== 0) {
|
|
throw new Error(res.message || '智能体加载失败');
|
|
}
|
|
agents.value = Array.isArray(res.data) ? res.data : [];
|
|
if (!selectedAgentId.value && agents.value[0]?.id) {
|
|
selectedAgentId.value = String(agents.value[0].id);
|
|
}
|
|
} catch (error) {
|
|
agentLoadError.value =
|
|
error instanceof Error ? error.message : '智能体加载失败';
|
|
ElMessage.error(agentLoadError.value);
|
|
} finally {
|
|
loadingAgents.value = false;
|
|
}
|
|
}
|
|
|
|
async function loadSessions() {
|
|
loadingSessions.value = true;
|
|
try {
|
|
const res = await getAgentSessions({ pageNumber: 1, pageSize: 80 });
|
|
if (res.errorCode !== 0) {
|
|
throw new Error(res.message || '历史会话加载失败');
|
|
}
|
|
sessions.value = Array.isArray(res.data?.records) ? res.data.records : [];
|
|
} catch (error) {
|
|
ElMessage.error(
|
|
error instanceof Error ? error.message : '历史会话加载失败',
|
|
);
|
|
} finally {
|
|
loadingSessions.value = false;
|
|
}
|
|
}
|
|
|
|
async function loadKnowledges() {
|
|
loadingKnowledges.value = true;
|
|
try {
|
|
const res = await getPublishedKnowledges();
|
|
if (res.errorCode !== 0) {
|
|
throw new Error(res.message || '知识库加载失败');
|
|
}
|
|
const records = Array.isArray(res.data) ? res.data : [];
|
|
knowledgeOptions.value = records
|
|
.filter((item) => item?.id)
|
|
.map((item) => ({
|
|
label: item.title || item.alias || String(item.id),
|
|
value: String(item.id),
|
|
}));
|
|
knowledgeMap.value = new Map(
|
|
records
|
|
.filter((item) => item?.id)
|
|
.map((item) => [
|
|
String(item.id),
|
|
{
|
|
id: String(item.id),
|
|
title: item.title || item.alias || String(item.id),
|
|
},
|
|
]),
|
|
);
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '知识库加载失败');
|
|
} finally {
|
|
loadingKnowledges.value = false;
|
|
}
|
|
}
|
|
|
|
function resolveSessionSortTime(session: AgentChatSessionView) {
|
|
const time = new Date(
|
|
session.lastMessageAt || session.accessAt || '',
|
|
).getTime();
|
|
return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
|
|
}
|
|
|
|
function sortSessionRecords(records: AgentChatSessionView[]) {
|
|
return [...records].sort((a, b) => {
|
|
const timeDiff = resolveSessionSortTime(b) - resolveSessionSortTime(a);
|
|
if (timeDiff !== 0) {
|
|
return timeDiff;
|
|
}
|
|
return String(b.sessionId || '').localeCompare(String(a.sessionId || ''));
|
|
});
|
|
}
|
|
|
|
function upsertSessionRecord(session: AgentChatSessionView) {
|
|
const sessionId = String(session.sessionId || '');
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
const next = [...sessions.value];
|
|
const currentIndex = next.findIndex(
|
|
(item) => String(item.sessionId) === sessionId,
|
|
);
|
|
if (currentIndex === -1) {
|
|
next.push({
|
|
...session,
|
|
sessionId,
|
|
});
|
|
} else {
|
|
next.splice(currentIndex, 1, {
|
|
...next[currentIndex],
|
|
...session,
|
|
sessionId,
|
|
});
|
|
}
|
|
sessions.value = sortSessionRecords(next);
|
|
}
|
|
|
|
function sleep(delay: number) {
|
|
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
}
|
|
|
|
async function refreshSessionRecord(sessionId?: string) {
|
|
const currentId = String(sessionId || '');
|
|
if (!currentId) {
|
|
return;
|
|
}
|
|
let lastError: unknown;
|
|
for (const delay of [0, 240, 480]) {
|
|
if (delay > 0) {
|
|
await sleep(delay);
|
|
}
|
|
try {
|
|
const res = await getAgentSession(currentId);
|
|
if (res.errorCode !== 0 || !res.data) {
|
|
throw new Error(res.message || '会话信息刷新失败');
|
|
}
|
|
upsertSessionRecord({
|
|
...res.data,
|
|
sessionId: currentId,
|
|
});
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
}
|
|
}
|
|
ElMessage.error(
|
|
lastError instanceof Error ? lastError.message : '会话信息刷新失败',
|
|
);
|
|
}
|
|
|
|
function maybeRefreshCompletedRuntimeSession(snapshot: {
|
|
sending: boolean;
|
|
sessionId: string;
|
|
}) {
|
|
const wasSending = runtimeSendingState.get(snapshot.sessionId) ?? false;
|
|
runtimeSendingState.set(snapshot.sessionId, snapshot.sending);
|
|
if (wasSending && !snapshot.sending) {
|
|
void refreshSessionRecord(snapshot.sessionId);
|
|
}
|
|
}
|
|
|
|
function buildOptimisticSession(
|
|
sessionId: string,
|
|
prompt: string,
|
|
): AgentChatSessionView {
|
|
return {
|
|
assistantId: selectedAgentId.value,
|
|
assistantName: selectedAgent.value?.name,
|
|
continuable: true,
|
|
lastMessageAt: new Date().toISOString(),
|
|
lastMessagePreview: prompt || '发送了图片',
|
|
messageCount: 1,
|
|
sessionId,
|
|
title: prompt.slice(0, 48) || '图片对话',
|
|
};
|
|
}
|
|
|
|
function syncRuntimeSnapshot(sessionId = currentSessionId.value) {
|
|
runtimeRunning.value = agentChatRuntimeManager.hasRunning();
|
|
const snapshot = sessionId
|
|
? agentChatRuntimeManager.getSnapshot(sessionId)
|
|
: undefined;
|
|
if (!snapshot) {
|
|
sending.value = false;
|
|
retryableErrorRoundId.value = '';
|
|
return false;
|
|
}
|
|
currentSessionId.value = snapshot.sessionId;
|
|
selectedAgentId.value = String(snapshot.agentId);
|
|
timelineItems.value = snapshot.items;
|
|
sending.value = snapshot.sending;
|
|
retryableErrorRoundId.value =
|
|
snapshot.terminalOutcome === 'failed' &&
|
|
snapshot.retryContextReady &&
|
|
!snapshot.sending
|
|
? snapshot.roundId
|
|
: '';
|
|
maybeRefreshCompletedRuntimeSession(snapshot);
|
|
if (snapshot.prompt && !currentSession.value) {
|
|
upsertSessionRecord(
|
|
buildOptimisticSession(snapshot.sessionId, snapshot.prompt),
|
|
);
|
|
}
|
|
void syncSessionRoute(snapshot.sessionId);
|
|
return true;
|
|
}
|
|
|
|
function persistCurrentRuntimeItems() {
|
|
if (currentSessionId.value) {
|
|
agentChatRuntimeManager.replaceItems(
|
|
currentSessionId.value,
|
|
timelineItems.value,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function loadConversation(sessionId: string) {
|
|
if (!sessionId) {
|
|
timelineItems.value = [];
|
|
sending.value = false;
|
|
retryableErrorRoundId.value = '';
|
|
return;
|
|
}
|
|
const runtimeSnapshot = agentChatRuntimeManager.getSnapshot(sessionId);
|
|
if (
|
|
runtimeSnapshot?.sending ||
|
|
runtimeSnapshot?.terminalOutcome === 'failed'
|
|
) {
|
|
syncRuntimeSnapshot(sessionId);
|
|
await syncSessionRoute(sessionId);
|
|
return;
|
|
}
|
|
loadingConversation.value = true;
|
|
retryableErrorRoundId.value = '';
|
|
try {
|
|
const detailRes = await getAgentSession(sessionId);
|
|
const res = await getAgentConversation(sessionId);
|
|
const latestRuntimeSnapshot =
|
|
agentChatRuntimeManager.getSnapshot(sessionId);
|
|
if (latestRuntimeSnapshot?.sending) {
|
|
syncRuntimeSnapshot(sessionId);
|
|
await syncSessionRoute(sessionId);
|
|
return;
|
|
}
|
|
if (res.errorCode !== 0) {
|
|
throw new Error(res.message || '会话加载失败');
|
|
}
|
|
timelineItems.value = recordsToTimelineItems(res.data?.records || []);
|
|
const session =
|
|
detailRes.errorCode === 0 && detailRes.data
|
|
? detailRes.data
|
|
: sessions.value.find((item) => String(item.sessionId) === sessionId);
|
|
if (session?.assistantId) {
|
|
selectedAgentId.value = String(session.assistantId);
|
|
}
|
|
if (detailRes.errorCode === 0 && detailRes.data) {
|
|
extraKnowledgeIds.value = (detailRes.data.extraKnowledges || [])
|
|
.map((item) => String(item.id || ''))
|
|
.filter(Boolean);
|
|
for (const item of detailRes.data.extraKnowledges || []) {
|
|
if (!item.id) {
|
|
continue;
|
|
}
|
|
knowledgeMap.value.set(String(item.id), {
|
|
id: String(item.id),
|
|
title: item.title || item.alias || String(item.id),
|
|
});
|
|
}
|
|
if ((detailRes.data.removedExtraKnowledgeNames || []).length > 0) {
|
|
const removedNames = detailRes.data.removedExtraKnowledgeNames || [];
|
|
ElMessage.warning(`以下知识库已失效并移除:${removedNames.join('、')}`);
|
|
}
|
|
}
|
|
currentSessionId.value = sessionId;
|
|
if (selectedAgentId.value) {
|
|
await activateComposer(selectedAgentId.value, sessionId);
|
|
}
|
|
sending.value = false;
|
|
await syncSessionRoute(sessionId);
|
|
} catch (error) {
|
|
if (isMissingAgentSessionError(error)) {
|
|
await removeSessionFromPage(sessionId);
|
|
return;
|
|
}
|
|
ElMessage.error(resolveAgentSessionErrorMessage(error) || '会话加载失败');
|
|
} finally {
|
|
loadingConversation.value = false;
|
|
}
|
|
}
|
|
|
|
async function createNewSession() {
|
|
currentSessionId.value = '';
|
|
timelineItems.value = [];
|
|
extraKnowledgeIds.value = [];
|
|
sending.value = false;
|
|
if (selectedAgentId.value) {
|
|
try {
|
|
await composer.startNew(selectedAgentId.value);
|
|
} catch (error) {
|
|
ElMessage.warning(
|
|
error instanceof Error ? error.message : '新会话创建失败',
|
|
);
|
|
}
|
|
}
|
|
await syncSessionRoute();
|
|
}
|
|
|
|
async function bindCreatedSession(sessionId: string, prompt: string) {
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
currentSessionId.value = sessionId;
|
|
const existingIndex = sessions.value.findIndex(
|
|
(session) => String(session.sessionId) === sessionId,
|
|
);
|
|
const nextSession = buildOptimisticSession(sessionId, prompt);
|
|
if (existingIndex === -1) {
|
|
sessions.value = [nextSession, ...sessions.value];
|
|
} else {
|
|
upsertSessionRecord(nextSession);
|
|
}
|
|
await syncSessionRoute(sessionId);
|
|
}
|
|
|
|
async function handleAgentChange() {
|
|
extraKnowledgeIds.value = [];
|
|
await (timelineItems.value.length > 0 || currentSessionId.value
|
|
? createNewSession()
|
|
: activateComposer(selectedAgentId.value));
|
|
if (
|
|
selectedAgentImageSupport.value === false &&
|
|
composer.images.items.value.length > 0
|
|
) {
|
|
ElMessage.warning('当前智能体不支持图片,请先移除图片');
|
|
}
|
|
}
|
|
|
|
async function handleExtraKnowledgeIdsChange(value: string[]) {
|
|
const previousIds = [...extraKnowledgeIds.value];
|
|
const nextIds = value.map(String);
|
|
extraKnowledgeIds.value = nextIds;
|
|
if (!currentSessionId.value) {
|
|
return;
|
|
}
|
|
savingExtraKnowledges.value = true;
|
|
try {
|
|
const res = await saveAgentSessionExtraKnowledges(
|
|
currentSessionId.value,
|
|
nextIds,
|
|
);
|
|
if (res.errorCode !== 0 || !res.data) {
|
|
throw new Error(res.message || '知识库保存失败');
|
|
}
|
|
extraKnowledgeIds.value = (res.data.extraKnowledges || [])
|
|
.map((item) => String(item.id || ''))
|
|
.filter(Boolean);
|
|
upsertSessionRecord({
|
|
...res.data,
|
|
sessionId: currentSessionId.value,
|
|
});
|
|
} catch (error) {
|
|
extraKnowledgeIds.value = previousIds;
|
|
ElMessage.error(error instanceof Error ? error.message : '知识库保存失败');
|
|
} finally {
|
|
savingExtraKnowledges.value = false;
|
|
}
|
|
}
|
|
|
|
async function handleTriggerSelect(item: ChatInputTriggerItem) {
|
|
if (item.disabled) {
|
|
return;
|
|
}
|
|
if (chatInputTrigger.activePanel.value?.symbol !== '@') {
|
|
await chatInputTrigger.replaceTriggerText('');
|
|
return;
|
|
}
|
|
const nextIds = extraKnowledgeIds.value.map(String);
|
|
if (!nextIds.includes(String(item.id))) {
|
|
nextIds.push(String(item.id));
|
|
await handleExtraKnowledgeIdsChange(nextIds);
|
|
}
|
|
await chatInputTrigger.replaceTriggerText('');
|
|
}
|
|
|
|
function buildCapabilities() {
|
|
return [
|
|
{
|
|
resourceIds: [...extraKnowledgeIds.value],
|
|
type: 'KNOWLEDGE' as const,
|
|
},
|
|
];
|
|
}
|
|
|
|
interface SendContentOptions {
|
|
includeComposer?: boolean;
|
|
retryContextReady?: boolean;
|
|
}
|
|
|
|
async function sendContent(
|
|
rawContent: string,
|
|
options: SendContentOptions = {},
|
|
) {
|
|
const content = rawContent.trim();
|
|
const includeComposer = options.includeComposer !== false;
|
|
const readyImageCount = includeComposer
|
|
? composer.images.readyItems.value.length
|
|
: 0;
|
|
const readyDocumentCount = includeComposer
|
|
? composer.documents.readyItems.value.length
|
|
: 0;
|
|
if (
|
|
(!content && readyImageCount === 0 && readyDocumentCount === 0) ||
|
|
!selectedAgentId.value ||
|
|
sending.value
|
|
) {
|
|
return;
|
|
}
|
|
if (runtimeRunning.value) {
|
|
ElMessage.warning('当前回复完成后再发送新消息');
|
|
return;
|
|
}
|
|
if (includeComposer && composer.images.uploading.value) {
|
|
ElMessage.warning('图片上传完成后再发送');
|
|
return;
|
|
}
|
|
if (includeComposer && composer.documents.processing.value) {
|
|
ElMessage.warning('文档读取完成后再发送');
|
|
return;
|
|
}
|
|
const failedImage = includeComposer
|
|
? composer.images.items.value.find((item) => item.status === 'error')
|
|
: undefined;
|
|
if (failedImage) {
|
|
ElMessage.error(failedImage.error || '请处理上传失败的图片');
|
|
return;
|
|
}
|
|
const failedDocument = includeComposer
|
|
? composer.documents.items.value.find((item) => item.status === 'error')
|
|
: undefined;
|
|
if (failedDocument) {
|
|
ElMessage.error(failedDocument.error || '请处理读取失败的文档');
|
|
return;
|
|
}
|
|
if (includeComposer) {
|
|
await composer.flush();
|
|
}
|
|
sending.value = true;
|
|
retryableErrorRoundId.value = '';
|
|
try {
|
|
const sessionId = await agentChatRuntimeManager.start({
|
|
agentId: selectedAgentId.value,
|
|
agentName: selectedAgent.value?.name,
|
|
baseItems: timelineItems.value,
|
|
capabilities: buildCapabilities(),
|
|
documentUploadIds: includeComposer
|
|
? composer.documents.uploadIds.value
|
|
: undefined,
|
|
documents: includeComposer
|
|
? composer.documents.readyItems.value.map((item) => ({ ...item }))
|
|
: undefined,
|
|
imageUploadIds: includeComposer
|
|
? composer.images.uploadIds.value
|
|
: undefined,
|
|
images: includeComposer
|
|
? composer.images.readyItems.value.map((item) => ({ ...item }))
|
|
: undefined,
|
|
onInputAccepted: includeComposer ? markComposerInputAccepted : undefined,
|
|
prompt: content,
|
|
retryContextReady: Boolean(options.retryContextReady),
|
|
sessionId: composer.sessionId.value,
|
|
});
|
|
await bindCreatedSession(sessionId, content);
|
|
syncRuntimeSnapshot(sessionId);
|
|
} catch (error) {
|
|
sending.value = false;
|
|
ElMessage.error(
|
|
error instanceof Error ? error.message : '发送失败,请稍后再试',
|
|
);
|
|
}
|
|
}
|
|
|
|
function modelErrorAction(item: ChatTimelineErrorItem) {
|
|
return item.roundId === retryableErrorRoundId.value ? '请重试' : undefined;
|
|
}
|
|
|
|
async function handleModelErrorRetry(item: ChatTimelineErrorItem) {
|
|
if (
|
|
item.roundId !== retryableErrorRoundId.value ||
|
|
sending.value ||
|
|
runtimeRunning.value
|
|
) {
|
|
return;
|
|
}
|
|
await sendContent('继续', {
|
|
includeComposer: false,
|
|
retryContextReady: true,
|
|
});
|
|
}
|
|
|
|
async function handleSend() {
|
|
await sendContent(promptText.value);
|
|
}
|
|
|
|
function handleSuggestedQuestion(question: string) {
|
|
void sendContent(question);
|
|
}
|
|
|
|
function handlePromptInput() {
|
|
chatInputTrigger.sync();
|
|
composer.scheduleSave();
|
|
}
|
|
|
|
async function activateComposer(agentId: string, sessionId?: string) {
|
|
if (!agentId) return;
|
|
try {
|
|
await composer.activate(agentId, sessionId);
|
|
} catch (error) {
|
|
ElMessage.warning(
|
|
error instanceof Error ? error.message : '输入草稿恢复失败',
|
|
);
|
|
}
|
|
}
|
|
|
|
function markComposerInputAccepted() {
|
|
return composer.markAccepted().catch(() => {
|
|
ElMessage.warning('消息已发送,草稿将在过期后自动清理');
|
|
});
|
|
}
|
|
|
|
function chooseAttachmentFiles() {
|
|
attachmentFileInputRef.value?.click();
|
|
}
|
|
|
|
async function addImageFiles(files: File[]) {
|
|
if (!selectedAgentId.value) {
|
|
ElMessage.warning('请先选择智能体');
|
|
return;
|
|
}
|
|
if (selectedAgentImageSupport.value === false) {
|
|
ElMessage.warning({
|
|
grouping: true,
|
|
message: `当前智能体不支持图片,支持:${CHAT_DOCUMENT_SUPPORTED_FORMATS}`,
|
|
});
|
|
return;
|
|
}
|
|
await composer.ensureSession();
|
|
const { rejectedCount, unsupportedCount } = await composer.images.addFiles(
|
|
files,
|
|
{
|
|
agentId: selectedAgentId.value,
|
|
mode: 'FORMAL',
|
|
sessionId: composer.sessionId.value,
|
|
},
|
|
);
|
|
composer.scheduleSave();
|
|
if (unsupportedCount > 0) {
|
|
ElMessage.warning({
|
|
grouping: true,
|
|
message: `不支持该图片格式,支持:${CHAT_IMAGE_SUPPORTED_FORMATS}`,
|
|
});
|
|
}
|
|
if (rejectedCount > 0) {
|
|
ElMessage.warning('每次最多添加 5 张图片');
|
|
}
|
|
}
|
|
|
|
async function addDocumentFiles(files: File[]) {
|
|
if (!selectedAgentId.value) {
|
|
ElMessage.warning('请先选择智能体');
|
|
return;
|
|
}
|
|
await composer.ensureSession();
|
|
const { rejectedCount, unsupportedCount } = await composer.documents.addFiles(
|
|
files,
|
|
{
|
|
agentId: selectedAgentId.value,
|
|
mode: 'FORMAL',
|
|
sessionId: composer.sessionId.value,
|
|
},
|
|
);
|
|
composer.scheduleSave();
|
|
if (unsupportedCount > 0) {
|
|
ElMessage.warning({
|
|
grouping: true,
|
|
message: `不支持该文件格式,支持:${supportedAttachmentFormats.value}`,
|
|
});
|
|
}
|
|
if (rejectedCount > 0) {
|
|
ElMessage.warning('每轮最多添加 3 份文档');
|
|
}
|
|
}
|
|
|
|
function isImageAttachmentFile(file: File) {
|
|
return (
|
|
file.type.startsWith('image/') ||
|
|
/\.(?:bmp|gif|jpe?g|png|webp)$/i.test(file.name)
|
|
);
|
|
}
|
|
|
|
function handleAttachmentFiles(event: Event) {
|
|
const target = event.target as HTMLInputElement;
|
|
const files = [...(target.files || [])];
|
|
const imageFiles = files.filter((file) => isImageAttachmentFile(file));
|
|
const documentFiles = files.filter((file) => !isImageAttachmentFile(file));
|
|
if (imageFiles.length > 0) void addImageFiles(imageFiles);
|
|
if (documentFiles.length > 0) void addDocumentFiles(documentFiles);
|
|
target.value = '';
|
|
}
|
|
|
|
function handleImagePaste(event: ClipboardEvent) {
|
|
const files = [...(event.clipboardData?.files || [])].filter((file) =>
|
|
file.type.startsWith('image/'),
|
|
);
|
|
if (files.length === 0) return;
|
|
event.preventDefault();
|
|
void addImageFiles(files);
|
|
}
|
|
|
|
function handleAttachmentDragEnter(event: DragEvent) {
|
|
if (
|
|
!capabilityDisabled.value &&
|
|
(composer.documents.items.value.length < 3 ||
|
|
(selectedAgentImageSupport.value !== false &&
|
|
composer.images.items.value.length < 5)) &&
|
|
[...(event.dataTransfer?.types || [])].includes('Files')
|
|
) {
|
|
composerDragActive.value = true;
|
|
}
|
|
}
|
|
|
|
function handleAttachmentDragOver(event: DragEvent) {
|
|
handleAttachmentDragEnter(event);
|
|
if (composerDragActive.value && event.dataTransfer) {
|
|
event.dataTransfer.dropEffect = 'copy';
|
|
}
|
|
}
|
|
|
|
function handleAttachmentDragLeave(event: DragEvent) {
|
|
const container = event.currentTarget as HTMLElement;
|
|
if (
|
|
!(event.relatedTarget instanceof Node) ||
|
|
!container.contains(event.relatedTarget)
|
|
) {
|
|
composerDragActive.value = false;
|
|
}
|
|
}
|
|
|
|
function handleAttachmentDrop(event: DragEvent) {
|
|
composerDragActive.value = false;
|
|
if (capabilityDisabled.value) return;
|
|
const files = [...(event.dataTransfer?.files || [])];
|
|
const imageFiles = files.filter((file) => isImageAttachmentFile(file));
|
|
const documentFiles = files.filter((file) => !isImageAttachmentFile(file));
|
|
if (imageFiles.length > 0) void addImageFiles(imageFiles);
|
|
if (documentFiles.length > 0) void addDocumentFiles(documentFiles);
|
|
}
|
|
|
|
async function retryImage(item: ChatImageAttachment) {
|
|
await composer.images.retry(item, {
|
|
agentId: selectedAgentId.value,
|
|
mode: 'FORMAL',
|
|
sessionId: composer.sessionId.value,
|
|
});
|
|
composer.scheduleSave();
|
|
}
|
|
|
|
async function removeImage(item: ChatImageAttachment) {
|
|
try {
|
|
await composer.images.remove(item);
|
|
composer.scheduleSave();
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '图片删除失败');
|
|
}
|
|
}
|
|
|
|
async function retryDocument(item: ChatDocumentAttachment) {
|
|
await composer.documents.retry(item, {
|
|
agentId: selectedAgentId.value,
|
|
mode: 'FORMAL',
|
|
sessionId: composer.sessionId.value,
|
|
});
|
|
composer.scheduleSave();
|
|
}
|
|
|
|
async function removeDocument(item: ChatDocumentAttachment) {
|
|
try {
|
|
await composer.documents.remove(item);
|
|
composer.scheduleSave();
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '文档删除失败');
|
|
}
|
|
}
|
|
|
|
function handlePromptKeyup() {
|
|
chatInputTrigger.sync();
|
|
}
|
|
|
|
function handlePromptClick() {
|
|
chatInputTrigger.sync();
|
|
}
|
|
|
|
function handlePromptKeydown(event: Event | KeyboardEvent) {
|
|
if (!(event instanceof KeyboardEvent)) {
|
|
return;
|
|
}
|
|
if (chatInputTrigger.activePanel.value) {
|
|
if (event.key === 'ArrowDown') {
|
|
event.preventDefault();
|
|
chatInputTrigger.move(1);
|
|
return;
|
|
}
|
|
if (event.key === 'ArrowUp') {
|
|
event.preventDefault();
|
|
chatInputTrigger.move(-1);
|
|
return;
|
|
}
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
chatInputTrigger.close();
|
|
return;
|
|
}
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
const item =
|
|
chatInputTrigger.visibleItems.value[chatInputTrigger.activeIndex.value];
|
|
if (item) {
|
|
event.preventDefault();
|
|
void handleTriggerSelect(item);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
event.preventDefault();
|
|
void handleSend();
|
|
}
|
|
}
|
|
|
|
async function handleStop() {
|
|
if (!canStopRuntime.value) {
|
|
return;
|
|
}
|
|
const runningSessionId =
|
|
agentChatRuntimeManager.getSnapshot(currentSessionId.value)?.sending &&
|
|
currentSessionId.value
|
|
? currentSessionId.value
|
|
: agentChatRuntimeManager.getLatestSnapshot()?.sessionId;
|
|
try {
|
|
await agentChatRuntimeManager.stop(runningSessionId);
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '停止失败');
|
|
return;
|
|
}
|
|
if (runningSessionId === currentSessionId.value) {
|
|
syncRuntimeSnapshot(currentSessionId.value);
|
|
} else {
|
|
runtimeRunning.value = agentChatRuntimeManager.hasRunning();
|
|
}
|
|
void refreshSessionRecord(runningSessionId);
|
|
}
|
|
|
|
async function handleCopyMessage(item: ChatTimelineMessageItem) {
|
|
const text = copyMessageText(item);
|
|
if (!text) {
|
|
return false;
|
|
}
|
|
try {
|
|
await copyTextToClipboard(text);
|
|
return true;
|
|
} catch (error) {
|
|
console.error('复制消息失败:', error);
|
|
ElMessage.error('复制失败');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function handleRenameSession(session: AgentChatSessionView) {
|
|
const sessionId = String(session.sessionId || '');
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
try {
|
|
const { value } = await ElMessageBox.prompt('请输入会话名称', '重命名', {
|
|
confirmButtonText: '保存',
|
|
inputValue: sessionTitle(session),
|
|
inputValidator: (value) => Boolean(String(value || '').trim()),
|
|
inputErrorMessage: '名称不能为空',
|
|
});
|
|
const title = String(value || '').trim();
|
|
const res = await renameAgentSession(sessionId, title);
|
|
if (res.errorCode !== 0) {
|
|
throw new Error(res.message || '重命名失败');
|
|
}
|
|
await loadSessions();
|
|
ElMessage.success('已重命名');
|
|
} catch (error) {
|
|
if (error !== 'cancel') {
|
|
ElMessage.error(error instanceof Error ? error.message : '重命名失败');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleDeleteSession(session: AgentChatSessionView) {
|
|
const sessionId = String(session.sessionId || '');
|
|
if (!sessionId) {
|
|
return;
|
|
}
|
|
try {
|
|
await ElMessageBox.confirm('删除后不可恢复,确认删除该会话?', '删除会话', {
|
|
confirmButtonText: '删除',
|
|
cancelButtonText: '取消',
|
|
type: 'warning',
|
|
});
|
|
const res = await deleteAgentSession(sessionId);
|
|
if (res.errorCode !== 0) {
|
|
throw new Error(res.message || '删除失败');
|
|
}
|
|
await removeSessionFromPage(sessionId);
|
|
ElMessage.success('已删除');
|
|
} catch (error) {
|
|
if (error !== 'cancel') {
|
|
if (isMissingAgentSessionError(error)) {
|
|
await removeSessionFromPage(sessionId);
|
|
return;
|
|
}
|
|
ElMessage.error(resolveAgentSessionErrorMessage(error) || '删除失败');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleApprove(payload: ChatTimelineToolApprovalPayload) {
|
|
approvalLoadingKey.value = payload.toolCallId || payload.approvalId;
|
|
ChatTimelineBuilder.markToolApproving(timelineItems.value, payload);
|
|
persistCurrentRuntimeItems();
|
|
try {
|
|
const res = await approveAgentRun(payload.approvalId);
|
|
if (res.errorCode !== 0) {
|
|
throw new Error(res.message || '批准失败');
|
|
}
|
|
ElMessage.success('已批准');
|
|
} catch (error) {
|
|
ChatTimelineBuilder.appendToolApproval(timelineItems.value, payload);
|
|
persistCurrentRuntimeItems();
|
|
ElMessage.error(error instanceof Error ? error.message : '批准失败');
|
|
} finally {
|
|
approvalLoadingKey.value = '';
|
|
}
|
|
}
|
|
|
|
async function handleReject(payload: ChatTimelineToolApprovalPayload) {
|
|
approvalLoadingKey.value = payload.toolCallId || payload.approvalId;
|
|
ChatTimelineBuilder.markToolApproving(timelineItems.value, payload);
|
|
persistCurrentRuntimeItems();
|
|
try {
|
|
const res = await rejectAgentRun(payload.approvalId, '用户拒绝执行');
|
|
if (res.errorCode !== 0) {
|
|
throw new Error(res.message || '拒绝失败');
|
|
}
|
|
ChatTimelineBuilder.markToolRejected(timelineItems.value, {
|
|
...payload,
|
|
reason: '用户拒绝执行',
|
|
});
|
|
persistCurrentRuntimeItems();
|
|
} catch (error) {
|
|
ChatTimelineBuilder.appendToolApproval(timelineItems.value, payload);
|
|
persistCurrentRuntimeItems();
|
|
ElMessage.error(error instanceof Error ? error.message : '拒绝失败');
|
|
} finally {
|
|
approvalLoadingKey.value = '';
|
|
}
|
|
}
|
|
|
|
async function bootstrap() {
|
|
await Promise.all([loadAgents(), loadSessions(), loadKnowledges()]);
|
|
const routeSessionId = String(route.query.sessionId || '');
|
|
if (routeSessionId) {
|
|
const runtimeSnapshot = agentChatRuntimeManager.getSnapshot(routeSessionId);
|
|
if (runtimeSnapshot?.sending) {
|
|
syncRuntimeSnapshot(routeSessionId);
|
|
await activateComposer(selectedAgentId.value, routeSessionId);
|
|
agentChatRuntimeManager.resume(routeSessionId, markComposerInputAccepted);
|
|
return;
|
|
}
|
|
await loadConversation(routeSessionId);
|
|
return;
|
|
}
|
|
const latestSnapshot = agentChatRuntimeManager.getLatestSnapshot();
|
|
if (latestSnapshot?.items.length) {
|
|
syncRuntimeSnapshot(latestSnapshot.sessionId);
|
|
await activateComposer(selectedAgentId.value, latestSnapshot.sessionId);
|
|
agentChatRuntimeManager.resume(
|
|
latestSnapshot.sessionId,
|
|
markComposerInputAccepted,
|
|
);
|
|
return;
|
|
}
|
|
await activateComposer(selectedAgentId.value);
|
|
}
|
|
|
|
onMounted(() => {
|
|
runtimeUnsubscribe = agentChatRuntimeManager.subscribe(() => {
|
|
syncRuntimeSnapshot();
|
|
});
|
|
void bootstrap();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
runtimeUnsubscribe?.();
|
|
runtimeUnsubscribe = undefined;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<section class="agent-chat">
|
|
<aside class="agent-chat__history">
|
|
<div class="agent-chat__history-head">
|
|
<div class="agent-chat__title">智能体聊天</div>
|
|
<ElButton
|
|
:icon="Plus"
|
|
circle
|
|
text
|
|
aria-label="新建会话"
|
|
title="新建会话"
|
|
@click="createNewSession"
|
|
/>
|
|
</div>
|
|
<div v-if="loadingSessions" class="agent-chat__state">加载中</div>
|
|
<div v-else-if="sessions.length === 0" class="agent-chat__state">
|
|
暂无历史会话
|
|
</div>
|
|
<div v-else class="agent-chat__session-list">
|
|
<button
|
|
v-for="session in sessions"
|
|
:key="String(session.sessionId)"
|
|
type="button"
|
|
class="agent-chat__session"
|
|
:class="{
|
|
'is-active': String(session.sessionId) === currentSessionId,
|
|
}"
|
|
@click="loadConversation(String(session.sessionId))"
|
|
>
|
|
<span class="agent-chat__session-main">
|
|
<span class="agent-chat__session-title">{{
|
|
sessionTitle(session)
|
|
}}</span>
|
|
<span class="agent-chat__session-preview">
|
|
{{ sessionPreview(session) }}
|
|
</span>
|
|
</span>
|
|
<span class="agent-chat__session-meta">
|
|
{{ formatDate(session.lastMessageAt || session.accessAt) }}
|
|
</span>
|
|
<ElDropdown
|
|
trigger="click"
|
|
class="agent-chat__session-more"
|
|
@click.stop
|
|
>
|
|
<ElButton
|
|
:icon="MoreFilled"
|
|
circle
|
|
text
|
|
aria-label="会话操作"
|
|
@click.stop
|
|
/>
|
|
<template #dropdown>
|
|
<ElDropdownMenu>
|
|
<ElDropdownItem @click="handleRenameSession(session)">
|
|
<ElIcon><EditPen /></ElIcon>
|
|
重命名
|
|
</ElDropdownItem>
|
|
<ElDropdownItem @click="handleDeleteSession(session)">
|
|
<ElIcon><Delete /></ElIcon>
|
|
删除
|
|
</ElDropdownItem>
|
|
</ElDropdownMenu>
|
|
</template>
|
|
</ElDropdown>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<main
|
|
class="agent-chat__main"
|
|
@dragenter.prevent="handleAttachmentDragEnter"
|
|
@dragover.prevent="handleAttachmentDragOver"
|
|
@dragleave.prevent="handleAttachmentDragLeave"
|
|
@drop.prevent="handleAttachmentDrop"
|
|
>
|
|
<header class="agent-chat__main-head">
|
|
<div>
|
|
<div class="agent-chat__main-title">
|
|
{{ selectedAgent?.name || '智能体聊天' }}
|
|
</div>
|
|
<div class="agent-chat__main-subtitle">
|
|
{{ currentSession ? sessionTitle(currentSession) : '新会话' }}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div
|
|
class="agent-chat__timeline-wrap"
|
|
:class="{ 'is-welcome': isWelcomeState }"
|
|
>
|
|
<div
|
|
v-if="loadingConversation || loadingAgents"
|
|
class="agent-chat__state is-center"
|
|
>
|
|
加载中
|
|
</div>
|
|
<div
|
|
v-else-if="
|
|
agentLoadError && !currentSessionId && timelineItems.length === 0
|
|
"
|
|
class="agent-chat__state is-center"
|
|
>
|
|
{{ agentLoadError }}
|
|
</div>
|
|
<div
|
|
v-else-if="
|
|
!selectedAgent && !currentSessionId && timelineItems.length === 0
|
|
"
|
|
class="agent-chat__state is-center"
|
|
>
|
|
暂无已发布智能体
|
|
</div>
|
|
<AgentWelcomeState
|
|
v-else-if="isWelcomeState"
|
|
:agent-name="selectedAgent?.name || '智能体'"
|
|
:avatar="selectedAgent?.avatar"
|
|
:disabled="sending || runtimeRunning"
|
|
:suggested-questions="interactionDisplay.suggestedQuestions"
|
|
:welcome-message="interactionDisplay.welcomeMessage"
|
|
@select-question="handleSuggestedQuestion"
|
|
/>
|
|
<ChatTimeline
|
|
v-else
|
|
:assistant-avatar="selectedAgent?.avatar || defaultAssistantAvatar"
|
|
:artifact-loader="loadCurrentAgentArtifact"
|
|
:items="timelineItems"
|
|
:document-loader="loadAgentChatDocument"
|
|
:error-action="modelErrorAction"
|
|
:error-action-disabled="sending || runtimeRunning"
|
|
:image-loader="loadAgentChatImage"
|
|
empty-text="选择智能体后开始对话"
|
|
:approval-loading="Boolean(approvalLoadingKey)"
|
|
:copy-action="handleCopyMessage"
|
|
:copyable="canCopyMessage"
|
|
@approve="handleApprove"
|
|
@error-action="handleModelErrorRetry"
|
|
@reject="handleReject"
|
|
@select-next-variant="() => undefined"
|
|
@select-previous-variant="() => undefined"
|
|
/>
|
|
</div>
|
|
|
|
<div class="agent-chat__composer">
|
|
<ChatCapabilityMenu
|
|
:disabled="capabilityDisabled"
|
|
:extra-knowledge-ids="extraKnowledgeIds"
|
|
:knowledge-options="knowledgeOptions"
|
|
:loading="loadingKnowledges"
|
|
:selected-knowledges="selectedExtraKnowledges"
|
|
:show-trigger="false"
|
|
@update:extra-knowledge-ids="handleExtraKnowledgeIdsChange"
|
|
/>
|
|
<ChatInputTriggerPanel
|
|
v-if="chatInputTrigger.activePanel.value"
|
|
class="agent-chat__trigger-panel"
|
|
:active-index="chatInputTrigger.activeIndex.value"
|
|
:group-label="chatInputTrigger.activePanel.value.groupLabel"
|
|
:items="chatInputTrigger.visibleItems.value"
|
|
:keyword="chatInputTrigger.activePanel.value.keyword"
|
|
@select="handleTriggerSelect"
|
|
@set-active="chatInputTrigger.setActiveIndex"
|
|
/>
|
|
<ChatImageAttachments
|
|
v-if="composer.images.items.value.length > 0"
|
|
:items="composer.images.items.value"
|
|
:image-loader="loadAgentChatImage"
|
|
removable
|
|
retryable
|
|
@remove="removeImage"
|
|
@retry="retryImage"
|
|
/>
|
|
<ChatDocumentAttachments
|
|
v-if="composer.documents.items.value.length > 0"
|
|
:items="composer.documents.items.value"
|
|
:document-loader="loadAgentChatDocument"
|
|
removable
|
|
retryable
|
|
@remove="removeDocument"
|
|
@retry="retryDocument"
|
|
/>
|
|
<ElInput
|
|
ref="promptInputRef"
|
|
v-model="promptText"
|
|
class="agent-chat__composer-input"
|
|
type="textarea"
|
|
:autosize="{ minRows: 2, maxRows: 6 }"
|
|
resize="none"
|
|
:placeholder="composerPlaceholder"
|
|
:disabled="sending || runtimeRunning || !selectedAgentId"
|
|
@click="handlePromptClick"
|
|
@input="handlePromptInput"
|
|
@keydown="handlePromptKeydown"
|
|
@keyup="handlePromptKeyup"
|
|
@paste="handleImagePaste"
|
|
/>
|
|
<div class="agent-chat__composer-footer">
|
|
<div class="agent-chat__composer-tools">
|
|
<input
|
|
ref="attachmentFileInputRef"
|
|
class="agent-chat__image-file-input"
|
|
type="file"
|
|
:accept="
|
|
selectedAgentImageSupport === false
|
|
? '.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.txt,.md'
|
|
: '.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.txt,.md,.png,.jpg,.jpeg,.webp,.gif,.bmp,image/png,image/jpeg,image/webp,image/gif,image/bmp'
|
|
"
|
|
multiple
|
|
@change="handleAttachmentFiles"
|
|
/>
|
|
<ElButton
|
|
:icon="Paperclip"
|
|
circle
|
|
text
|
|
:disabled="
|
|
capabilityDisabled ||
|
|
(composer.documents.items.value.length >= 3 &&
|
|
(selectedAgentImageSupport === false ||
|
|
composer.images.items.value.length >= 5))
|
|
"
|
|
aria-label="添加附件"
|
|
title="添加附件"
|
|
@click="chooseAttachmentFiles"
|
|
/>
|
|
<ChatCapabilityMenu
|
|
class="agent-chat__capability-entry"
|
|
:disabled="capabilityDisabled"
|
|
:extra-knowledge-ids="extraKnowledgeIds"
|
|
:knowledge-options="knowledgeOptions"
|
|
:loading="loadingKnowledges"
|
|
:selected-knowledges="[]"
|
|
@update:extra-knowledge-ids="handleExtraKnowledgeIdsChange"
|
|
/>
|
|
<ElSelect
|
|
v-model="selectedAgentId"
|
|
:loading="loadingAgents"
|
|
placeholder="选择智能体"
|
|
class="agent-chat__agent-select"
|
|
:style="{ width: agentSelectWidth }"
|
|
@change="handleAgentChange"
|
|
>
|
|
<ElOption
|
|
v-for="agent in agents"
|
|
:key="String(agent.id)"
|
|
:label="agent.name || String(agent.id)"
|
|
:value="String(agent.id)"
|
|
/>
|
|
</ElSelect>
|
|
</div>
|
|
<div class="agent-chat__composer-actions">
|
|
<ElButton
|
|
v-if="canStopRuntime"
|
|
type="primary"
|
|
circle
|
|
aria-label="中止"
|
|
title="中止"
|
|
class="agent-chat__send-button is-stop"
|
|
@click="handleStop"
|
|
>
|
|
<span class="agent-chat__stop-glyph"></span>
|
|
</ElButton>
|
|
<ElButton
|
|
v-else
|
|
type="primary"
|
|
circle
|
|
:icon="Promotion"
|
|
aria-label="发送"
|
|
title="发送"
|
|
class="agent-chat__send-button"
|
|
:disabled="!canSend"
|
|
@click="handleSend"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Transition name="agent-chat-drag-fade">
|
|
<div
|
|
v-if="composerDragActive"
|
|
class="agent-chat__drop-overlay"
|
|
role="status"
|
|
aria-live="polite"
|
|
>
|
|
<div class="agent-chat__drop-overlay-content">
|
|
<ElIcon class="agent-chat__drop-overlay-icon">
|
|
<UploadFilled />
|
|
</ElIcon>
|
|
<div class="agent-chat__drop-overlay-title">松手上传文件</div>
|
|
<div class="agent-chat__drop-overlay-formats">
|
|
支持 {{ supportedAttachmentFormats }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</main>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.agent-chat {
|
|
box-sizing: border-box;
|
|
display: grid;
|
|
grid-template-columns: 280px minmax(0, 1fr);
|
|
height: var(--easyflow-content-height, 100%);
|
|
min-height: 0;
|
|
max-height: var(--easyflow-content-height, 100%);
|
|
overflow: hidden;
|
|
background: var(--el-bg-color-page);
|
|
}
|
|
|
|
.agent-chat__history {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
background: var(--el-bg-color);
|
|
border-right: 1px solid var(--el-border-color-lighter);
|
|
}
|
|
|
|
.agent-chat__history-head,
|
|
.agent-chat__main-head {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
min-height: 56px;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
}
|
|
|
|
.agent-chat__title,
|
|
.agent-chat__main-title {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--el-text-color-primary);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.agent-chat__main-subtitle {
|
|
margin-top: 4px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-size: 12px;
|
|
color: var(--el-text-color-secondary);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.agent-chat__session-list {
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
min-height: 0;
|
|
padding: 8px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.agent-chat__session {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
gap: 4px 8px;
|
|
width: 100%;
|
|
min-height: 72px;
|
|
padding: 10px 44px 10px 10px;
|
|
color: inherit;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
border: 0;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.agent-chat__session:hover,
|
|
.agent-chat__session.is-active {
|
|
background: var(--el-fill-color-light);
|
|
}
|
|
|
|
.agent-chat__session-main {
|
|
min-width: 0;
|
|
padding-right: 8px;
|
|
}
|
|
|
|
.agent-chat__session-title,
|
|
.agent-chat__session-preview {
|
|
display: block;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.agent-chat__session-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
|
|
.agent-chat__session-preview,
|
|
.agent-chat__session-meta,
|
|
.agent-chat__state {
|
|
font-size: 12px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
|
|
.agent-chat__session-preview {
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.agent-chat__session-meta {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.agent-chat__session-more {
|
|
position: absolute;
|
|
top: 50%;
|
|
right: 10px;
|
|
transform: translateY(-50%);
|
|
}
|
|
|
|
.agent-chat__main {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.agent-chat__timeline-wrap {
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.agent-chat__timeline-wrap.is-welcome {
|
|
padding: 0 min(8vw, 96px);
|
|
overflow: hidden auto;
|
|
}
|
|
|
|
.agent-chat__timeline-wrap :deep(.chat-timeline) {
|
|
box-sizing: border-box;
|
|
height: 100%;
|
|
min-height: 0;
|
|
padding: 24px min(8vw, 96px);
|
|
overflow: auto;
|
|
}
|
|
|
|
.agent-chat__composer {
|
|
position: relative;
|
|
display: flex;
|
|
flex: none;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding: 16px;
|
|
margin: 0 min(8vw, 96px) 24px;
|
|
background: var(--el-bg-color);
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
border-radius: 24px;
|
|
box-shadow: var(--el-box-shadow-light);
|
|
}
|
|
|
|
.agent-chat__drop-overlay {
|
|
position: absolute;
|
|
inset: 16px;
|
|
z-index: 20;
|
|
display: grid;
|
|
pointer-events: none;
|
|
background: var(--el-color-primary-light-9);
|
|
border: 2px dashed var(--el-color-primary-light-5);
|
|
border-radius: 16px;
|
|
place-items: center;
|
|
}
|
|
|
|
.agent-chat__drop-overlay-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
align-items: center;
|
|
max-width: min(560px, calc(100% - 48px));
|
|
padding: 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.agent-chat__drop-overlay-icon {
|
|
font-size: 40px;
|
|
color: var(--el-color-primary);
|
|
}
|
|
|
|
.agent-chat__drop-overlay-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
|
|
.agent-chat__drop-overlay-formats {
|
|
font-size: 13px;
|
|
line-height: 20px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
|
|
.agent-chat-drag-fade-enter-active,
|
|
.agent-chat-drag-fade-leave-active {
|
|
transition: opacity 160ms ease;
|
|
}
|
|
|
|
.agent-chat-drag-fade-enter-from,
|
|
.agent-chat-drag-fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
.agent-chat__trigger-panel {
|
|
position: absolute;
|
|
bottom: calc(100% + 10px);
|
|
left: 0;
|
|
z-index: 5;
|
|
width: 100%;
|
|
}
|
|
|
|
.agent-chat__composer-input :deep(.el-textarea__inner) {
|
|
min-height: 48px !important;
|
|
padding: 0;
|
|
line-height: 22px;
|
|
background: transparent;
|
|
border: 0;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.agent-chat__composer-footer {
|
|
display: flex;
|
|
gap: 16px;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.agent-chat__composer-tools {
|
|
display: inline-flex;
|
|
gap: 4px;
|
|
align-items: center;
|
|
min-width: 0;
|
|
max-width: calc(100% - 64px);
|
|
}
|
|
|
|
.agent-chat__image-file-input {
|
|
display: none;
|
|
}
|
|
|
|
.agent-chat__capability-entry {
|
|
flex: none;
|
|
}
|
|
|
|
.agent-chat__agent-select {
|
|
max-width: min(320px, calc(100vw - 240px));
|
|
}
|
|
|
|
.agent-chat__agent-select :deep(.el-select__wrapper) {
|
|
min-height: 36px;
|
|
padding: 0 4px 0 0;
|
|
background: transparent;
|
|
border: 0;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.agent-chat__agent-select :deep(.el-select__wrapper.is-focused) {
|
|
box-shadow: none;
|
|
}
|
|
|
|
.agent-chat__agent-select :deep(.el-select__placeholder),
|
|
.agent-chat__agent-select :deep(.el-select__selected-item) {
|
|
min-width: 0;
|
|
max-width: none;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
color: var(--el-text-color-secondary);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.agent-chat__agent-select :deep(.el-select__caret) {
|
|
margin-left: 6px;
|
|
color: var(--el-color-primary);
|
|
}
|
|
|
|
.agent-chat__composer-actions {
|
|
display: inline-flex;
|
|
flex: none;
|
|
align-items: center;
|
|
}
|
|
|
|
.agent-chat__send-button {
|
|
width: 42px;
|
|
min-width: 42px;
|
|
height: 42px;
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 999px;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.agent-chat__send-button :deep(.el-icon) {
|
|
font-size: 15px;
|
|
}
|
|
|
|
.agent-chat__send-button.is-stop {
|
|
border: 0;
|
|
}
|
|
|
|
.agent-chat__stop-glyph {
|
|
display: block;
|
|
width: 12px;
|
|
height: 12px;
|
|
background: currentcolor;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.agent-chat__state {
|
|
padding: 16px;
|
|
}
|
|
|
|
.agent-chat__state.is-center {
|
|
display: flex;
|
|
flex: 1;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.agent-chat {
|
|
grid-template-rows: auto minmax(0, 1fr);
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.agent-chat__history {
|
|
max-height: 168px;
|
|
border-right: 0;
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
}
|
|
|
|
.agent-chat__session-list {
|
|
flex-direction: row;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.agent-chat__session {
|
|
flex: 0 0 240px;
|
|
}
|
|
|
|
.agent-chat__timeline-wrap {
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.agent-chat__timeline-wrap.is-welcome {
|
|
padding: 0 16px;
|
|
}
|
|
|
|
.agent-chat__timeline-wrap.is-welcome :deep(.agent-welcome) {
|
|
flex: 0 0 auto;
|
|
min-height: 100%;
|
|
padding-bottom: 16px;
|
|
}
|
|
|
|
.agent-chat__timeline-wrap :deep(.chat-timeline) {
|
|
padding: 16px;
|
|
}
|
|
|
|
.agent-chat__composer {
|
|
margin: 0 16px 16px;
|
|
}
|
|
|
|
.agent-chat__composer-footer {
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.agent-chat__agent-select {
|
|
width: min(280px, calc(100% - 58px));
|
|
max-width: calc(100vw - 128px);
|
|
}
|
|
|
|
.agent-chat__composer-actions {
|
|
justify-content: flex-end;
|
|
}
|
|
}
|
|
</style>
|