feat: 先进智能体功能上线
- 基于 agent-runtime 打造,默认 ReAct agent - 支持 agent 能力对接,已对接工作流、插件、知识库等 tool 能力 - 全新 agent 编排界面,支持可视化便捷配置 agent - 全新 agent 聊天界面,支持快捷操作、额外知识库选择等
This commit is contained in:
@@ -111,6 +111,135 @@ describe('agentTimelineAdapter', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('hides knowledge retrieval cards when restoring agent chat history', () => {
|
||||
const items = recordsToTimelineItems([
|
||||
{
|
||||
id: '417197643647811584',
|
||||
senderRole: 'assistant',
|
||||
contentText:
|
||||
'根据知识库中的信息,2026年暑假的时间安排是:7月1日到8月15日。',
|
||||
roundId: '417197622424633344',
|
||||
contentPayload: {
|
||||
chains: [
|
||||
{
|
||||
id: 'call_0fc660e9d203416983ccca7e',
|
||||
name: 'retrieve_knowledge',
|
||||
result: 'Retrieved 2 relevant document(s)',
|
||||
status: 'TOOL_RESULT',
|
||||
arguments: {
|
||||
query: '暑假时间',
|
||||
},
|
||||
},
|
||||
],
|
||||
agentResult: {
|
||||
text: '根据知识库中的信息,2026年暑假的时间安排是:7月1日到8月15日。',
|
||||
knowledgeReferences: [
|
||||
{
|
||||
chunkContent:
|
||||
'问题:2026 年暑假安排\n答案:2026 年7 月 1 日到 8 月 15 日',
|
||||
documentId: '411358369563336704',
|
||||
knowledgeName: 'faq',
|
||||
knowledgeType: 'FAQ',
|
||||
},
|
||||
],
|
||||
},
|
||||
messageChain: [
|
||||
{
|
||||
role: 'assistant',
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'call_0fc660e9d203416983ccca7e',
|
||||
name: 'retrieve_knowledge',
|
||||
arguments: '{query=暑假时间}',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
content: 'Retrieved 2 relevant document(s)',
|
||||
toolCallId: 'call_0fc660e9d203416983ccca7e',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content:
|
||||
'根据知识库中的信息,2026年暑假的时间安排是:7月1日到8月15日。',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(items.some((item) => item.type === 'tool')).toBe(false);
|
||||
expect(
|
||||
items.some(
|
||||
(item) => item.type === 'status' && item.label === '已检索知识库',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
items.some((item) => item.type === 'message' && item.role === 'assistant'),
|
||||
).toBe(true);
|
||||
const assistant = items.find(
|
||||
(item): item is ChatTimelineMessageItem =>
|
||||
item.type === 'message' && item.role === 'assistant',
|
||||
);
|
||||
expect(assistant?.knowledgeItems?.[0]?.knowledgeName).toBe('faq');
|
||||
});
|
||||
|
||||
it('hides internal fragment and context reload tools from history', () => {
|
||||
const items = recordsToTimelineItems([
|
||||
{
|
||||
id: 'internal-tools',
|
||||
senderRole: 'assistant',
|
||||
contentText: '已处理',
|
||||
roundId: 'round-internal',
|
||||
contentPayload: {
|
||||
chains: [
|
||||
{
|
||||
id: 'fragment-1',
|
||||
name: '__fragment__',
|
||||
status: 'TOOL_RESULT',
|
||||
result: 'fragment',
|
||||
},
|
||||
{
|
||||
id: 'context-1',
|
||||
name: 'context_reload',
|
||||
status: 'TOOL_RESULT',
|
||||
result: 'reload',
|
||||
},
|
||||
],
|
||||
agentResult: {
|
||||
text: '已处理',
|
||||
},
|
||||
messageChain: [
|
||||
{
|
||||
role: 'assistant',
|
||||
toolCalls: [
|
||||
{ id: 'fragment-1', name: '__fragment__' },
|
||||
{ id: 'context-1', name: 'context_reload' },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
toolCallId: 'fragment-1',
|
||||
content: 'fragment',
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
toolCallId: 'context-1',
|
||||
content: 'reload',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(items.some((item) => item.type === 'tool')).toBe(false);
|
||||
expect(items.some((item) => item.type === 'status')).toBe(false);
|
||||
expect(
|
||||
items.some((item) => item.type === 'message' && item.role === 'assistant'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('parses raw SSE text as message delta', () => {
|
||||
const envelope = parseAgentSseMessage({
|
||||
data: 'hello',
|
||||
@@ -227,4 +356,123 @@ describe('agentTimelineAdapter', () => {
|
||||
expect(assistant?.roundId).toBe('runtime-round-1');
|
||||
expect(assistant?.parts[0]?.content).toBe('准备调用工具');
|
||||
});
|
||||
|
||||
it('updates memory compression status within the current round', () => {
|
||||
const items: any[] = [];
|
||||
|
||||
applyAgentSseEnvelope(
|
||||
items,
|
||||
{
|
||||
domain: 'BUSINESS',
|
||||
type: 'STATUS',
|
||||
payload: {
|
||||
label: '正在整理上下文',
|
||||
phase: 'started',
|
||||
status: 'running',
|
||||
statusKey: 'memory-compression',
|
||||
},
|
||||
},
|
||||
{ roundId: 'round-a' },
|
||||
);
|
||||
applyAgentSseEnvelope(
|
||||
items,
|
||||
{
|
||||
domain: 'BUSINESS',
|
||||
type: 'STATUS',
|
||||
payload: {
|
||||
compressed: true,
|
||||
label: '已整理上下文',
|
||||
phase: 'completed',
|
||||
status: 'done',
|
||||
statusKey: 'memory-compression',
|
||||
},
|
||||
},
|
||||
{ roundId: 'round-a' },
|
||||
);
|
||||
|
||||
const statuses = items.filter((item) => item.type === 'status');
|
||||
expect(statuses).toHaveLength(1);
|
||||
expect(statuses[0]?.label).toBe('已整理上下文');
|
||||
expect(statuses[0]?.status).toBe('done');
|
||||
expect(statuses[0]?.statusKey).toBe('memory-compression:round-a');
|
||||
});
|
||||
|
||||
it('keeps memory compression statuses isolated by round', () => {
|
||||
const items: any[] = [];
|
||||
|
||||
applyAgentSseEnvelope(
|
||||
items,
|
||||
{
|
||||
domain: 'BUSINESS',
|
||||
type: 'STATUS',
|
||||
payload: {
|
||||
compressed: true,
|
||||
label: '已整理上下文',
|
||||
phase: 'completed',
|
||||
status: 'done',
|
||||
statusKey: 'memory-compression',
|
||||
},
|
||||
},
|
||||
{ roundId: 'round-a' },
|
||||
);
|
||||
applyAgentSseEnvelope(
|
||||
items,
|
||||
{
|
||||
domain: 'BUSINESS',
|
||||
type: 'STATUS',
|
||||
payload: {
|
||||
compressed: false,
|
||||
label: '无需压缩上下文',
|
||||
phase: 'completed',
|
||||
status: 'done',
|
||||
statusKey: 'memory-compression',
|
||||
},
|
||||
},
|
||||
{ roundId: 'round-b' },
|
||||
);
|
||||
|
||||
const statuses = items.filter((item) => item.type === 'status');
|
||||
expect(statuses).toHaveLength(1);
|
||||
expect(statuses[0]?.statusKey).toBe('memory-compression:round-a');
|
||||
expect(statuses[0]?.label).toBe('已整理上下文');
|
||||
});
|
||||
|
||||
it('does not show no-compression status before a later compression run', () => {
|
||||
const items: any[] = [];
|
||||
|
||||
applyAgentSseEnvelope(
|
||||
items,
|
||||
{
|
||||
domain: 'BUSINESS',
|
||||
type: 'STATUS',
|
||||
payload: {
|
||||
compressed: false,
|
||||
label: '无需压缩上下文',
|
||||
phase: 'completed',
|
||||
status: 'done',
|
||||
statusKey: 'memory-compression',
|
||||
},
|
||||
},
|
||||
{ roundId: 'round-a' },
|
||||
);
|
||||
applyAgentSseEnvelope(
|
||||
items,
|
||||
{
|
||||
domain: 'BUSINESS',
|
||||
type: 'STATUS',
|
||||
payload: {
|
||||
label: '正在整理上下文',
|
||||
phase: 'started',
|
||||
status: 'running',
|
||||
statusKey: 'memory-compression',
|
||||
},
|
||||
},
|
||||
{ roundId: 'round-a' },
|
||||
);
|
||||
|
||||
const statuses = items.filter((item) => item.type === 'status');
|
||||
expect(statuses).toHaveLength(1);
|
||||
expect(statuses[0]?.label).toBe('正在整理上下文');
|
||||
expect(statuses[0]?.status).toBe('running');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,6 +54,38 @@ function normalizeToolCallId(payload: Record<string, any>) {
|
||||
return asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id);
|
||||
}
|
||||
|
||||
function isBlankToolName(value: unknown) {
|
||||
return !normalizeToolName(value);
|
||||
}
|
||||
|
||||
function shouldSkipToolProjection(value: unknown) {
|
||||
const normalizedName = normalizeToolName(value).toLowerCase();
|
||||
return (
|
||||
normalizedName === 'context_reload' ||
|
||||
normalizedName === '__fragment__'
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeToolCallName(payload: Record<string, any>) {
|
||||
const fn = asRecord(payload.function);
|
||||
return normalizeToolName(payload.name ?? payload.toolName ?? fn.name);
|
||||
}
|
||||
|
||||
function normalizeToolCallInput(payload: Record<string, any>) {
|
||||
const fn = asRecord(payload.function);
|
||||
return payload.arguments ?? payload.input ?? fn.arguments;
|
||||
}
|
||||
|
||||
function statusKeyForProjection(
|
||||
payload: Record<string, any>,
|
||||
metadata?: Partial<ChatTimelineMessageItem>,
|
||||
fallback = 'status',
|
||||
) {
|
||||
const statusKey = asText(payload.statusKey) || fallback;
|
||||
const roundId = asText(metadata?.roundId);
|
||||
return roundId ? `${statusKey}:${roundId}` : statusKey;
|
||||
}
|
||||
|
||||
function normalizeMetadata(record: AgentChatMessageRecord) {
|
||||
return {
|
||||
createdAt: asTimestamp(record.created),
|
||||
@@ -139,14 +171,10 @@ function appendAssistantText(
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
ChatTimelineBuilder.appendMessageDelta(
|
||||
items,
|
||||
text,
|
||||
{
|
||||
...assistantMetadata(record, suffix),
|
||||
...metadata,
|
||||
},
|
||||
);
|
||||
ChatTimelineBuilder.appendMessageDelta(items, text, {
|
||||
...assistantMetadata(record, suffix),
|
||||
...metadata,
|
||||
});
|
||||
}
|
||||
|
||||
function appendAssistantThinking(
|
||||
@@ -160,14 +188,10 @@ function appendAssistantThinking(
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
ChatTimelineBuilder.appendThinkingDelta(
|
||||
items,
|
||||
text,
|
||||
{
|
||||
...assistantMetadata(record, suffix),
|
||||
...metadata,
|
||||
},
|
||||
);
|
||||
ChatTimelineBuilder.appendThinkingDelta(items, text, {
|
||||
...assistantMetadata(record, suffix),
|
||||
...metadata,
|
||||
});
|
||||
}
|
||||
|
||||
function projectHistoryChain(
|
||||
@@ -177,6 +201,7 @@ function projectHistoryChain(
|
||||
const payload = asRecord(record.contentPayload);
|
||||
let hasAssistantText = false;
|
||||
let hasAssistantThinking = false;
|
||||
const toolNameByCallId = new Map<string, string>();
|
||||
const displayChains = asArray(payload.displayChains ?? payload.chains);
|
||||
for (const chain of displayChains) {
|
||||
const item = asRecord(chain);
|
||||
@@ -187,12 +212,21 @@ function projectHistoryChain(
|
||||
continue;
|
||||
}
|
||||
const toolName = normalizeToolName(item.name ?? item.toolName);
|
||||
if (toolName) {
|
||||
const toolCallId = normalizeToolCallId(item);
|
||||
if (toolCallId && toolName) {
|
||||
toolNameByCallId.set(toolCallId, toolName);
|
||||
}
|
||||
if (toolName && !shouldSkipToolProjection(toolName)) {
|
||||
ChatTimelineBuilder.upsertToolCall(items, {
|
||||
input: item.arguments ?? item.input,
|
||||
output: item.result ?? item.output,
|
||||
status: asText(item.status) === 'TOOL_RESULT' ? 'success' : 'running',
|
||||
toolCallId: asText(item.id ?? item.toolCallId),
|
||||
statusKey: statusKeyForProjection(
|
||||
item,
|
||||
normalizeMetadata(record),
|
||||
'knowledge-retrieval',
|
||||
),
|
||||
toolCallId,
|
||||
toolName,
|
||||
});
|
||||
}
|
||||
@@ -213,21 +247,45 @@ function projectHistoryChain(
|
||||
}
|
||||
for (const toolCall of asArray(item.toolCalls)) {
|
||||
const tool = asRecord(toolCall);
|
||||
const toolCallId = normalizeToolCallId(tool);
|
||||
const toolName = normalizeToolCallName(tool);
|
||||
if (toolCallId && toolName) {
|
||||
toolNameByCallId.set(toolCallId, toolName);
|
||||
}
|
||||
if (isBlankToolName(toolName) || shouldSkipToolProjection(toolName)) {
|
||||
continue;
|
||||
}
|
||||
ChatTimelineBuilder.upsertToolCall(items, {
|
||||
input: tool.arguments ?? tool.input,
|
||||
input: normalizeToolCallInput(tool),
|
||||
status: 'running',
|
||||
toolCallId: asText(tool.id ?? tool.toolCallId),
|
||||
toolName: normalizeToolName(tool.name ?? tool.toolName),
|
||||
statusKey: statusKeyForProjection(
|
||||
tool,
|
||||
normalizeMetadata(record),
|
||||
'knowledge-retrieval',
|
||||
),
|
||||
toolCallId,
|
||||
toolName,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (role === 'tool') {
|
||||
const toolCallId = normalizeToolCallId(item);
|
||||
const toolName =
|
||||
normalizeToolCallName(item) || toolNameByCallId.get(toolCallId) || '';
|
||||
if (isBlankToolName(toolName) || shouldSkipToolProjection(toolName)) {
|
||||
continue;
|
||||
}
|
||||
ChatTimelineBuilder.upsertToolCall(items, {
|
||||
output: item.content ?? item.result,
|
||||
status: 'success',
|
||||
toolCallId: asText(item.toolCallId ?? item.id),
|
||||
toolName: normalizeToolName(item.name ?? item.toolName) || '工具调用',
|
||||
statusKey: statusKeyForProjection(
|
||||
item,
|
||||
normalizeMetadata(record),
|
||||
'knowledge-retrieval',
|
||||
),
|
||||
toolCallId,
|
||||
toolName,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -369,7 +427,11 @@ export function applyAgentSseEnvelope(
|
||||
input: payload.input ?? payload.toolInput,
|
||||
output: payload.output ?? payload.result ?? payload.text,
|
||||
status: type === 'TOOL_RESULT' ? 'success' : 'running',
|
||||
statusKey: asText(payload.statusKey) || undefined,
|
||||
statusKey: statusKeyForProjection(
|
||||
payload,
|
||||
metadata,
|
||||
'knowledge-retrieval',
|
||||
),
|
||||
toolCallId: normalizeToolCallId(payload),
|
||||
toolName: normalizeToolName(
|
||||
payload.toolDisplayName ?? payload.toolName ?? payload.name,
|
||||
@@ -394,7 +456,11 @@ export function applyAgentSseEnvelope(
|
||||
label: asText(payload.label),
|
||||
phase: asText(payload.phase),
|
||||
status: asText(payload.status),
|
||||
statusKey: asText(payload.statusKey),
|
||||
statusKey: statusKeyForProjection(
|
||||
payload,
|
||||
metadata,
|
||||
'memory-compression',
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -402,7 +468,7 @@ export function applyAgentSseEnvelope(
|
||||
ChatTimelineBuilder.upsertKnowledgeRetrievalStatus(
|
||||
items,
|
||||
asText(payload.status) === 'running' ? 'running' : 'done',
|
||||
asText(payload.statusKey),
|
||||
statusKeyForProjection(payload, metadata, 'knowledge-retrieval'),
|
||||
);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type {ChatTimelineItem} from '@easyflow/common-ui';
|
||||
import {ChatTimelineBuilder} from '@easyflow/common-ui';
|
||||
|
||||
import type {AgentChatCapabilityPayload} from './api';
|
||||
import {generateAgentSessionId, sendAgentChat, stopAgentChatStream,} from './api';
|
||||
|
||||
import {applyAgentSseEnvelope, parseAgentSseMessage,} from './adapters/agentTimelineAdapter';
|
||||
|
||||
interface RuntimeSessionState {
|
||||
@@ -34,6 +36,7 @@ interface StartOptions {
|
||||
agentId: string;
|
||||
agentName?: string;
|
||||
baseItems?: ChatTimelineItem[];
|
||||
capabilities?: AgentChatCapabilityPayload[];
|
||||
prompt: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
@@ -47,7 +50,8 @@ const listeners = new Set<() => void>();
|
||||
let latestSessionId = '';
|
||||
|
||||
function clone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
const serialized = JSON.stringify(value);
|
||||
return JSON.parse(serialized) as T;
|
||||
}
|
||||
|
||||
function createRoundId() {
|
||||
@@ -225,6 +229,7 @@ export const agentChatRuntimeManager = {
|
||||
void sendAgentChat(
|
||||
{
|
||||
agentId: options.agentId,
|
||||
capabilities: options.capabilities,
|
||||
prompt: options.prompt,
|
||||
sessionId,
|
||||
},
|
||||
|
||||
@@ -26,6 +26,20 @@ export interface AgentChatSessionView {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface AgentChatKnowledgeView {
|
||||
alias?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
id?: number | string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface AgentChatSessionDetailView extends AgentChatSessionView {
|
||||
boundKnowledges?: AgentChatKnowledgeView[];
|
||||
extraKnowledges?: AgentChatKnowledgeView[];
|
||||
removedExtraKnowledgeNames?: string[];
|
||||
}
|
||||
|
||||
export interface AgentChatSessionPage {
|
||||
pageNumber?: number;
|
||||
pageSize?: number;
|
||||
@@ -58,6 +72,11 @@ export interface AgentChatConversationView {
|
||||
variantsByRound?: Record<string, AgentChatMessageRecord[]>;
|
||||
}
|
||||
|
||||
export interface AgentChatCapabilityPayload {
|
||||
resourceIds: Array<number | string>;
|
||||
type: 'KNOWLEDGE';
|
||||
}
|
||||
|
||||
export function getPublishedAgents() {
|
||||
return api.get<RequestResult<AgentInfo[]>>('/api/v1/agent/list', {
|
||||
params: { publishedOnly: true },
|
||||
@@ -69,11 +88,20 @@ export function generateAgentSessionId() {
|
||||
}
|
||||
|
||||
export function getAgentSession(sessionId: number | string) {
|
||||
return api.get<RequestResult<AgentChatSessionView>>(
|
||||
return api.get<RequestResult<AgentChatSessionDetailView>>(
|
||||
`/api/v1/agent/session/${sessionId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getPublishedKnowledges() {
|
||||
return api.get<RequestResult<AgentChatKnowledgeView[]>>(
|
||||
'/api/v1/documentCollection/list',
|
||||
{
|
||||
params: { publishedOnly: true },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function getAgentSessions(params?: {
|
||||
agentId?: number | string;
|
||||
pageNumber?: number;
|
||||
@@ -103,6 +131,18 @@ export function renameAgentSession(sessionId: number | string, title: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function saveAgentSessionExtraKnowledges(
|
||||
sessionId: number | string,
|
||||
knowledgeIds: Array<number | string>,
|
||||
) {
|
||||
return api.post<RequestResult<AgentChatSessionDetailView>>(
|
||||
`/api/v1/agent/session/${sessionId}/extraKnowledges`,
|
||||
{
|
||||
knowledgeIds,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteAgentSession(sessionId: number | string) {
|
||||
return api.post<RequestResult>(`/api/v1/agent/session/${sessionId}/delete`);
|
||||
}
|
||||
@@ -129,6 +169,7 @@ export function rejectAgentRun(
|
||||
export function sendAgentChat(
|
||||
data: {
|
||||
agentId: number | string;
|
||||
capabilities?: AgentChatCapabilityPayload[];
|
||||
prompt: string;
|
||||
sessionId?: number | string;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="agent-chat-welcome-state" aria-live="polite">
|
||||
<h2 class="agent-chat-welcome-state__title">{{ title }}</h2>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agent-chat-welcome-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.agent-chat-welcome-state__title {
|
||||
max-width: min(720px, 100%);
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: clamp(24px, 2.6vw, 36px);
|
||||
font-weight: 600;
|
||||
line-height: 1.16;
|
||||
color: var(--el-text-color-primary);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -15,14 +15,21 @@ import {
|
||||
getAgentSession,
|
||||
getAgentSessions,
|
||||
getPublishedAgents,
|
||||
getPublishedKnowledges,
|
||||
rejectAgentRun,
|
||||
renameAgentSession,
|
||||
saveAgentSessionExtraKnowledges,
|
||||
} 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 {Delete, EditPen, MoreFilled, Plus, Promotion,} from '@element-plus/icons-vue';
|
||||
|
||||
import {Delete, EditPen, MoreFilled, Plus, Promotion,} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDropdown,
|
||||
@@ -35,25 +42,49 @@ import {
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
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 {recordsToTimelineItems} from './adapters/agentTimelineAdapter';
|
||||
import {agentChatRuntimeManager} from './agentChatRuntimeManager';
|
||||
import AgentChatWelcomeState from './components/AgentChatWelcomeState.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const WELCOME_TITLES = [
|
||||
'我们应该做些什么',
|
||||
'让协作发生',
|
||||
'今天想推进什么',
|
||||
'把想法变成行动',
|
||||
'让智能体开始工作',
|
||||
'从一个问题开始',
|
||||
'一起把事情理清楚',
|
||||
'把下一步交给协作',
|
||||
];
|
||||
|
||||
const agents = ref<AgentInfo[]>([]);
|
||||
const sessions = ref<AgentChatSessionView[]>([]);
|
||||
const timelineItems = ref<ChatTimelineItem[]>([]);
|
||||
const selectedAgentId = ref('');
|
||||
const currentSessionId = ref('');
|
||||
const promptText = ref('');
|
||||
const promptInputRef = ref();
|
||||
const loadingAgents = ref(false);
|
||||
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 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(() =>
|
||||
@@ -75,13 +106,66 @@ const canSend = computed(
|
||||
const composerPlaceholder = computed(() =>
|
||||
selectedAgent.value ? '输入消息' : '请选择智能体',
|
||||
);
|
||||
const agentSelectWidth = computed(() => {
|
||||
const name = selectedAgent.value?.name || '选择智能体';
|
||||
const textWidth = Array.from(name).reduce(
|
||||
(total, char) => total + (/[\u4E00-\u9FFF]/.test(char) ? 14 : 8),
|
||||
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(
|
||||
() =>
|
||||
!loadingConversation.value &&
|
||||
!currentSessionId.value &&
|
||||
timelineItems.value.length === 0,
|
||||
);
|
||||
const welcomeTitle = computed(() => {
|
||||
const agentKey = selectedAgentId.value || selectedAgent.value?.name || '';
|
||||
const index = [...agentKey].reduce(
|
||||
(total, char) => total + char.charCodeAt(0),
|
||||
0,
|
||||
);
|
||||
return `${Math.min(Math.max(textWidth + 36, 92), 240)}px`;
|
||||
return WELCOME_TITLES[index % WELCOME_TITLES.length] || '我们应该做些什么';
|
||||
});
|
||||
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) {
|
||||
@@ -171,8 +255,42 @@ async function loadSessions() {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
const time = new Date(
|
||||
session.lastMessageAt || session.accessAt || '',
|
||||
).getTime();
|
||||
return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
@@ -195,14 +313,14 @@ function upsertSessionRecord(session: AgentChatSessionView) {
|
||||
const currentIndex = next.findIndex(
|
||||
(item) => String(item.sessionId) === sessionId,
|
||||
);
|
||||
if (currentIndex >= 0) {
|
||||
next.splice(currentIndex, 1, {
|
||||
...next[currentIndex],
|
||||
if (currentIndex === -1) {
|
||||
next.push({
|
||||
...session,
|
||||
sessionId,
|
||||
});
|
||||
} else {
|
||||
next.push({
|
||||
next.splice(currentIndex, 1, {
|
||||
...next[currentIndex],
|
||||
...session,
|
||||
sessionId,
|
||||
});
|
||||
@@ -318,7 +436,8 @@ async function loadConversation(sessionId: string) {
|
||||
try {
|
||||
const detailRes = await getAgentSession(sessionId);
|
||||
const res = await getAgentConversation(sessionId);
|
||||
const latestRuntimeSnapshot = agentChatRuntimeManager.getSnapshot(sessionId);
|
||||
const latestRuntimeSnapshot =
|
||||
agentChatRuntimeManager.getSnapshot(sessionId);
|
||||
if (latestRuntimeSnapshot?.sending) {
|
||||
syncRuntimeSnapshot(sessionId);
|
||||
await syncSessionRoute(sessionId);
|
||||
@@ -335,6 +454,24 @@ async function loadConversation(sessionId: string) {
|
||||
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;
|
||||
sending.value = false;
|
||||
await syncSessionRoute(sessionId);
|
||||
@@ -349,6 +486,7 @@ async function createNewSession() {
|
||||
currentSessionId.value = '';
|
||||
timelineItems.value = [];
|
||||
promptText.value = '';
|
||||
extraKnowledgeIds.value = [];
|
||||
sending.value = false;
|
||||
await syncSessionRoute();
|
||||
}
|
||||
@@ -362,20 +500,77 @@ async function bindCreatedSession(sessionId: string, prompt: string) {
|
||||
(session) => String(session.sessionId) === sessionId,
|
||||
);
|
||||
const nextSession = buildOptimisticSession(sessionId, prompt);
|
||||
if (existingIndex >= 0) {
|
||||
upsertSessionRecord(nextSession);
|
||||
} else {
|
||||
if (existingIndex === -1) {
|
||||
sessions.value = [nextSession, ...sessions.value];
|
||||
} else {
|
||||
upsertSessionRecord(nextSession);
|
||||
}
|
||||
await syncSessionRoute(sessionId);
|
||||
}
|
||||
|
||||
function handleAgentChange() {
|
||||
extraKnowledgeIds.value = [];
|
||||
if (timelineItems.value.length > 0 || currentSessionId.value) {
|
||||
void createNewSession();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function handleSend() {
|
||||
const content = promptText.value.trim();
|
||||
if (!content || !selectedAgentId.value || sending.value) {
|
||||
@@ -392,6 +587,7 @@ async function handleSend() {
|
||||
agentId: selectedAgentId.value,
|
||||
agentName: selectedAgent.value?.name,
|
||||
baseItems: timelineItems.value,
|
||||
capabilities: buildCapabilities(),
|
||||
prompt: content,
|
||||
sessionId: currentSessionId.value,
|
||||
});
|
||||
@@ -406,6 +602,55 @@ async function handleSend() {
|
||||
}
|
||||
}
|
||||
|
||||
function handlePromptInput() {
|
||||
chatInputTrigger.sync();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
function handleStop() {
|
||||
if (!canStopRuntime.value) {
|
||||
return;
|
||||
@@ -539,7 +784,7 @@ async function handleReject(payload: ChatTimelineToolApprovalPayload) {
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
await Promise.all([loadAgents(), loadSessions()]);
|
||||
await Promise.all([loadAgents(), loadSessions(), loadKnowledges()]);
|
||||
const routeSessionId = String(route.query.sessionId || '');
|
||||
if (routeSessionId) {
|
||||
await loadConversation(routeSessionId);
|
||||
@@ -645,10 +890,17 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="agent-chat__timeline-wrap">
|
||||
<div
|
||||
class="agent-chat__timeline-wrap"
|
||||
:class="{ 'is-welcome': isWelcomeState }"
|
||||
>
|
||||
<div v-if="loadingConversation" class="agent-chat__state is-center">
|
||||
加载中
|
||||
</div>
|
||||
<AgentChatWelcomeState
|
||||
v-else-if="isWelcomeState"
|
||||
:title="welcomeTitle"
|
||||
/>
|
||||
<ChatTimeline
|
||||
v-else
|
||||
:items="timelineItems"
|
||||
@@ -663,8 +915,31 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="agent-chat__composer">
|
||||
<div
|
||||
class="agent-chat__composer"
|
||||
:class="{ 'is-welcome': isWelcomeState }"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<ElInput
|
||||
ref="promptInputRef"
|
||||
v-model="promptText"
|
||||
class="agent-chat__composer-input"
|
||||
type="textarea"
|
||||
@@ -672,27 +947,41 @@ onBeforeUnmount(() => {
|
||||
resize="none"
|
||||
:placeholder="composerPlaceholder"
|
||||
:disabled="sending || runtimeRunning || !selectedAgentId"
|
||||
@keydown.enter.exact.prevent="handleSend"
|
||||
@click="handlePromptClick"
|
||||
@input="handlePromptInput"
|
||||
@keydown="handlePromptKeydown"
|
||||
@keyup="handlePromptKeyup"
|
||||
/>
|
||||
<div class="agent-chat__composer-footer">
|
||||
<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)"
|
||||
<div class="agent-chat__composer-tools">
|
||||
<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>
|
||||
<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"
|
||||
v-if="canStopRuntime"
|
||||
type="primary"
|
||||
circle
|
||||
aria-label="中止"
|
||||
@@ -700,7 +989,7 @@ onBeforeUnmount(() => {
|
||||
class="agent-chat__send-button is-stop"
|
||||
@click="handleStop"
|
||||
>
|
||||
<span class="agent-chat__stop-glyph" />
|
||||
<span class="agent-chat__stop-glyph"></span>
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-else
|
||||
@@ -863,6 +1152,11 @@ onBeforeUnmount(() => {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.agent-chat__timeline-wrap.is-welcome {
|
||||
justify-content: center;
|
||||
padding: 0 min(8vw, 96px) 252px;
|
||||
}
|
||||
|
||||
.agent-chat__timeline-wrap :deep(.chat-timeline) {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
@@ -886,6 +1180,20 @@ onBeforeUnmount(() => {
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
|
||||
.agent-chat__composer.is-welcome {
|
||||
top: calc(50% + 40px);
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -902,13 +1210,25 @@ onBeforeUnmount(() => {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.agent-chat__composer-tools {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: 4px;
|
||||
max-width: calc(100% - 64px);
|
||||
}
|
||||
|
||||
.agent-chat__capability-entry {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.agent-chat__agent-select {
|
||||
max-width: min(240px, 58%);
|
||||
max-width: min(320px, calc(100vw - 240px));
|
||||
}
|
||||
|
||||
.agent-chat__agent-select :deep(.el-select__wrapper) {
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
padding: 0 4px 0 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
@@ -921,7 +1241,7 @@ onBeforeUnmount(() => {
|
||||
.agent-chat__agent-select :deep(.el-select__placeholder),
|
||||
.agent-chat__agent-select :deep(.el-select__selected-item) {
|
||||
min-width: 0;
|
||||
max-width: 184px;
|
||||
max-width: none;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
@@ -932,6 +1252,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.agent-chat__agent-select :deep(.el-select__caret) {
|
||||
color: var(--el-color-primary);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.agent-chat__composer-actions {
|
||||
@@ -1002,6 +1323,10 @@ onBeforeUnmount(() => {
|
||||
padding-bottom: 184px;
|
||||
}
|
||||
|
||||
.agent-chat__timeline-wrap.is-welcome {
|
||||
padding: 0 16px 244px;
|
||||
}
|
||||
|
||||
.agent-chat__timeline-wrap :deep(.chat-timeline) {
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -1012,12 +1337,18 @@ onBeforeUnmount(() => {
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.agent-chat__composer.is-welcome {
|
||||
top: calc(50% + 52px);
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.agent-chat__composer-footer {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.agent-chat__agent-select {
|
||||
width: min(220px, calc(100% - 58px));
|
||||
width: min(280px, calc(100% - 58px));
|
||||
max-width: calc(100vw - 128px);
|
||||
}
|
||||
|
||||
.agent-chat__composer-actions {
|
||||
|
||||
Reference in New Issue
Block a user