Files
EasyFlow/easyflow-ui-admin/app/src/views/ai/agent-chat/index.vue
陈子默 1c68e3582c fix: 支持模型异常后继续文档对话
- 将本轮文档上下文纳入可持久化的 Agent 用户消息

=- 在异常提示中提供请重试动作并自动发送继续
2026-08-26 22:55:20 +08:00

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>