(null);
const conversationId = computed(() => {
return route.params.conversationId;
@@ -41,42 +37,49 @@ export default {
antdMessage.warning('停止生成后可发送');
return;
}
+ conversationList.value.push({
+ role: QUESTION_ROLE,
+ content: message,
+ });
initSse({ message });
};
const handleCancel = () => {
- generateLoading.value = false;
// 中止当前正在输出的回答
- if (currentTaskId.value && bubbleListRef.value?.abortTypingByKey) {
- bubbleListRef.value.abortTypingByKey(currentTaskId.value);
+ if (generateLoading.value) {
+ bubbleListRef.value?.abortTypingByKey(currentTaskId.value);
+ sseController.value?.abort?.();
}
if (showRightView.value) {
rightViewRef.value?.abortTyping?.();
}
+
+ generateLoading.value = false;
antdMessage.info('取消生成');
};
- const initSse = (inputInfo: CHAT.TInputInfo) => {
+ const initSse = (inputInfo: CHAT.TInputInfo): void => {
+ if (sseController.value) {
+ sseController.value.abort?.();
+ sseController.value = null;
+ }
+
try {
const { message } = inputInfo;
generateLoading.value = true;
- conversationList.value.push({
- role: QUESTION_ROLE,
- content: message,
+
+ sseController.value = querySSE({
+ method: 'POST',
+ handleMessage,
+ handleOpen,
+ body: JSON.stringify({
+ content: message,
+ session_id: conversationId.value,
+ agent_id: chatStore.agentInfo.agent_id,
+ }),
});
-
- const taskId = genRandomId();
- currentTaskId.value = taskId;
-
- const url = `http://localhost:3000/agent/input?content=${message}&session_id=${conversationId.value}&agent_id=${chatStore.agentInfo?.agent_id}`;
- querySSE(
- {
- handleMessage,
- },
- url,
- );
} catch (error) {
console.error('Failed to initialize SSE:', error);
antdMessage.error('初始化连接失败');
@@ -84,10 +87,30 @@ export default {
}
};
+ const {
+ roles,
+ showRightView,
+ rightViewInfo,
+ currentTaskId,
+ handleMessage,
+ handleOpen,
+ conversationList,
+ generateLoading,
+ senderRef,
+ } = useChatHandler({
+ initSse,
+ });
+
watch(
() => props.inputInfo,
(newVal) => {
- newVal && initSse(newVal);
+ if (newVal) {
+ conversationList.value.push({
+ role: QUESTION_ROLE,
+ content: newVal,
+ });
+ initSse(newVal);
+ }
},
{ deep: true },
);
@@ -101,14 +124,14 @@ export default {
roles={roles}
items={[
...conversationList.value,
- generateLoading.value ? { loading: true, role: ANSWER_ROLE } : null,
+ generateLoading.value ? { loading: true, role: LOADING_ROLE } : null,
].filter(Boolean)}
/>
(showRightView.value = false)}
/>
diff --git a/src/components/xt-chat/chat-view/useChatHandler.tsx b/src/components/xt-chat/chat-view/useChatHandler.tsx
index ffddd49..4d2f33b 100644
--- a/src/components/xt-chat/chat-view/useChatHandler.tsx
+++ b/src/components/xt-chat/chat-view/useChatHandler.tsx
@@ -1,36 +1,46 @@
-import { ref, } from 'vue';
+import { ref } from 'vue';
import type { BubbleListProps } from '@/components/xt-chat/xt-bubble/types';
import markdownit from 'markdown-it';
-import { message as antdMessage } from 'ant-design-vue';
-import { IconFile, IconCaretUp, IconDownload, IconRefresh } from '@arco-design/web-vue/es/icon';
+import { message as antdMessage, Timeline } from 'ant-design-vue';
+import { IconFile, IconCaretUp, IconDownload, IconRefresh, IconCopy } from '@arco-design/web-vue/es/icon';
import { Tooltip } from 'ant-design-vue';
import TextOverTips from '@/components/text-over-tips/index.vue';
import { genRandomId } from '@/utils/tools';
-import icon1 from "@/assets/img/agent/icon-end.png"
-import icon2 from "@/assets/img/agent/icon-loading.png"
+import icon1 from '@/assets/img/agent/icon-end.png';
+import icon2 from '@/assets/img/agent/icon-loading.png';
import { useClipboard } from '@vueuse/core';
-import { QUESTION_ROLE, ANSWER_ROLE, FILE_ROLE, THOUGHT_ROLE, ROLE_STYLE, ANSWER_STYLE } from './constants';
-import type { UseChatHandlerReturn } from "./constants"
+import {
+ QUESTION_ROLE,
+ ANSWER_ROLE,
+ INTELLECTUAL_THINKING_ROLE,
+ FILE_ROLE,
+ LOADING_ROLE,
+ THOUGHT_ROLE,
+ ROLE_STYLE,
+ EnumTeamRunStatus,
+} from './constants';
+import type { UseChatHandlerReturn } from './constants';
-/**
- * 聊天处理器Hook
- * @returns 包含角色配置、消息处理函数和对话列表的对象
+/**
+ * 聊天处理器Hook
+ * @returns 包含角色配置、消息处理函数和对话列表的对象
*/
-export default function useChatHandler(): UseChatHandlerReturn {
+export default function useChatHandler({ initSse }): UseChatHandlerReturn {
// 在内部定义对话列表
const { copy } = useClipboard();
+ const senderRef = ref(null);
const conversationList = ref([]);
const generateLoading = ref(false);
const currentTaskId = ref(null);
const showRightView = ref(false);
- const rightViewContent = ref('');
+ const rightViewInfo = ref({});
- // 初始化markdown
+ // 初始化markdown
const md = markdownit({
html: true,
breaks: true,
@@ -38,8 +48,19 @@ export default function useChatHandler(): UseChatHandlerReturn {
typographer: true,
});
- // 定义角色配置
+ // 定义角色配置
const roles: BubbleListProps['roles'] = {
+ [LOADING_ROLE]: {
+ placement: 'start',
+ variant: 'borderless',
+ style: { ...ROLE_STYLE, paddingLeft: '12px' },
+ },
+ [INTELLECTUAL_THINKING_ROLE]: {
+ placement: 'start',
+ variant: 'borderless',
+ typing: { step: 2, interval: 100 },
+ style: ROLE_STYLE,
+ },
[ANSWER_ROLE]: {
placement: 'start',
variant: 'borderless',
@@ -47,7 +68,7 @@ export default function useChatHandler(): UseChatHandlerReturn {
onTypingComplete: () => {
currentTaskId.value = null;
},
- style: ROLE_STYLE
+ style: ROLE_STYLE,
},
[FILE_ROLE]: {
placement: 'start',
@@ -67,24 +88,23 @@ export default function useChatHandler(): UseChatHandlerReturn {
));
},
- style: ROLE_STYLE
+ style: ROLE_STYLE,
},
[THOUGHT_ROLE]: {
placement: 'start',
variant: 'borderless',
- style: ROLE_STYLE
+ style: ROLE_STYLE,
},
[QUESTION_ROLE]: {
placement: 'end',
shape: 'round',
- style: ROLE_STYLE
+ style: ROLE_STYLE,
},
};
- // 下载处理
- const onDownload = (content: string) => {
- console.log('onDownload', content);
- // 这里可以添加实际的下载逻辑
+ // 下载处理
+ const onDownload = () => {
+ console.log('onDownload', rightViewInfo.value);
};
const onCopy = (content: string) => {
@@ -92,123 +112,161 @@ export default function useChatHandler(): UseChatHandlerReturn {
antdMessage.success('复制成功!');
};
- // 开始处理
- const handleStart = (data: any) => {
- const { run_id } = data;
- conversationList.value.push({
- run_id,
- role: ANSWER_ROLE,
- content: (
-
- 智能思考
-
-
- ),
- });
+ // 开始处理
+ const handleOpen = (data: any): void => {
+ // const { run_id } = data;
+ // currentTaskId.value = run_id;
+ // conversationList.value.push({
+ // key: run_id,
+ // run_id,
+ // role: INTELLECTUAL_THINKING_ROLE,
+ // content: (
+ //
+ // 智能思考
+ //
+ //
+ // ),
+ // });
};
- // 节点更新处理
- const handleNodeUpdate = (data: any) => {
- const { run_id, status, output } = data;
-
- switch (status) {
- case 'TeamRunResponseContent':
- conversationList.value.push({
- run_id,
- content: data,
- role: ANSWER_ROLE,
- messageRender: (item) => (
-
-

-
{item.message}
-
- )
- });
- break;
- case 'TeamRunCompleted':
- conversationList.value.push({
- run_id,
- content: output,
- role: ANSWER_ROLE,
- messageRender: (content: string) => ,
- style: ANSWER_STYLE
- });
- break;
- }
- };
-
- // 最终结果处理
- const handleFinalResult = (data: any) => {
+ // 最终结果处理
+ const handleFileReview = (data: MESSAGE.Answer) => {
const { run_id, output } = data;
showRightView.value = true;
- const _files = output?.files;
- rightViewContent.value = _files?.[0]?.content || '';
+ // const _files = output?.files;
+ // rightViewInfo.value = _files?.[0]?.content || '';
- conversationList.value.push({
- run_id,
- id: currentTaskId.value,
- role: FILE_ROLE,
- content: _files,
- style: ANSWER_STYLE,
- footer: ({ item }: { item: any }) => {
- const nonQuestionElements = conversationList.value.filter((item) => item.role !== QUESTION_ROLE);
- const isLastAnswer = nonQuestionElements[nonQuestionElements.length - 1]?.id === item.id;
-
- // return (
- //
- // onDownload(rightViewContent?.value || '')}>
- //
- //
- // {isLastAnswer && onRefresh && (
- // onRefresh(currentTaskId.value!, conversationList.value.length)}>
- //
- //
- // )}
- //
- // );
- },
- });
+ // conversationList.value.push({
+ // run_id,
+ // role: FILE_ROLE,
+ // content: _files,
+ // style: ANSWER_STYLE,
+ // footer: ({ item }: { item: any }) => {
+ // const nonQuestionElements = conversationList.value.filter((item) => item.role !== QUESTION_ROLE);
+ // const isLastAnswer = nonQuestionElements[nonQuestionElements.length - 1]?.id === item.id;
+ // },
+ // });
};
- // 重置生成状态
+ // 重置生成状态
const resetGenerateStatus = () => {
generateLoading.value = false;
};
- // 错误处理
- const handleError = () => {
- resetGenerateStatus();
- antdMessage.error('连接服务器失败');
- };
+ // // 错误处理
+ // const handleError = () => {
+ // resetGenerateStatus();
+ // antdMessage.error('连接服务器失败');
+ // };
- const onRefresh = (tempId: string, tempIndex: number) => {
- generateLoading.value = true;
- conversationList.value.splice(tempIndex, 1, {
- id: tempId,
- loading: true,
+ const handleTaskStart = (data: MESSAGE.Answer) => {
+ const { run_id } = data;
+ currentTaskId.value = run_id;
+ conversationList.value.push({
+ run_id,
+ content: data,
+ output: data.output,
+ role: ANSWER_ROLE,
+ messageRender: (data: MESSAGE.Answer) => {
+ let outputEleClass: string = `thought-chain-output border-l-#E6E6E8 border-l-1px pl-12px relative left-6px mb-4px`;
+ !isLastAnswer(data) && (outputEleClass += ' hasLine pb-12px pt-4px');
+ return (
+ <>
+
+ 智能思考
+
+
+
+
+

+
{data.node}
+
+
+
+ >
+ );
+ },
});
};
- // 消息处理主函数
- const handleMessage = (parsedData: { event: string; data: any }) => {
- const { event, data } = parsedData;
- switch (event) {
- case 'start':
- handleStart(data);
+ const onRefresh = (run_id: string) => {
+ generateLoading.value = true;
+ conversationList.value = conversationList.value.filter((item) => item.run_id !== run_id);
+ initSse({ message: senderRef.value?.searchValue });
+ };
+
+ const isLastAnswer = (data: MESSAGE.Answer) => {
+ const { run_id } = data;
+ const nonQuestionElements = conversationList.value.filter(
+ (item) => item.role === ANSWER_ROLE && item.run_id === run_id,
+ );
+ return nonQuestionElements[nonQuestionElements.length - 1]?.run_id === run_id;
+ };
+
+ const isLastTask = (data: MESSAGE.Answer) => {
+ const { run_id } = data;
+ const lastElement = conversationList.value[conversationList.value.length - 1];
+ return lastElement && lastElement.run_id === run_id;
+ };
+
+ // 节点更新处理
+ const handleTaskUpdate = (data: MESSAGE.Answer) => {
+ const { run_id, output } = data;
+
+ const existingItemIndex = conversationList.value.findIndex((item) => item.run_id === run_id);
+
+ if (existingItemIndex !== -1 && output) {
+ const existingItem = conversationList.value[existingItemIndex];
+ existingItem.content.output += output;
+ }
+ };
+
+ const handleTaskEnd = (data: MESSAGE.Answer) => {
+ const { run_id, output, team_session_state } = data;
+
+ const existingItem = conversationList.value.find((item) => item.run_id === run_id);
+ resetGenerateStatus();
+
+ if (existingItem) {
+ existingItem.content.output += output;
+ existingItem.footer = ({ item: any }) => {
+ return (
+
+ onCopy(existingItem.content.output)}>
+
+
+
+
+
+ {isLastTask(data) && (
+ onRefresh(run_id)}>
+
+
+ )}
+
+ );
+ };
+ }
+
+ if (team_session_state) {
+ handleFileReview(data);
+ }
+ };
+ // 消息处理主函数
+ const handleMessage = (parsedData: { event: string; data: MESSAGE.Answer }) => {
+ const { data } = parsedData;
+ const { status } = data;
+ switch (status) {
+ case EnumTeamRunStatus.TeamRunStarted:
+ handleTaskStart(data);
break;
- case 'node_update':
- handleNodeUpdate(data);
+ case EnumTeamRunStatus.TeamRunResponseContent:
+ handleTaskUpdate(data);
break;
- case 'final_result':
- handleFinalResult(data);
- break;
- case 'end':
- resetGenerateStatus();
- break;
- case 'error':
- handleError();
+ case EnumTeamRunStatus.TeamRunCompleted:
+ handleTaskEnd(data);
break;
default:
break;
@@ -217,11 +275,13 @@ export default function useChatHandler(): UseChatHandlerReturn {
return {
roles,
+ senderRef,
currentTaskId,
handleMessage,
+ handleOpen,
generateLoading,
conversationList,
showRightView,
- rightViewContent,
+ rightViewInfo,
};
-}
\ No newline at end of file
+}
diff --git a/src/components/xt-chat/xt-bubble/style.scss b/src/components/xt-chat/xt-bubble/style.scss
index 3821521..7b01927 100644
--- a/src/components/xt-chat/xt-bubble/style.scss
+++ b/src/components/xt-chat/xt-bubble/style.scss
@@ -28,6 +28,27 @@
flex-direction: row-reverse;
}
+ .thought-chain-item {
+ position: relative;
+ margin: 0;
+ font-size: 14px;
+ list-style: none;
+ .thought-chain-output {
+ position: relative;
+ &.hasLine {
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-left: 1px solid #E6E6E8;
+ }
+ }
+ }
+ }
+
.xt-bubble-avatar {
}
diff --git a/src/components/xt-chat/xt-bubble/xt-bubbleList.vue b/src/components/xt-chat/xt-bubble/xt-bubbleList.vue
index 89026b3..8404bcf 100644
--- a/src/components/xt-chat/xt-bubble/xt-bubbleList.vue
+++ b/src/components/xt-chat/xt-bubble/xt-bubbleList.vue
@@ -105,6 +105,7 @@ export default defineComponent({
// 暴露控制方法
const abortTypingByKey = (key: string | number) => {
bubbleRefs.value[key]?.abortTyping?.();
+ console.log('abortTypingByKey----', bubbleRefs.value[key])
};
// 对外暴露能力
expose({
diff --git a/src/types/chat.ts b/src/types/chat.ts
index 2116a91..5e25cc6 100644
--- a/src/types/chat.ts
+++ b/src/types/chat.ts
@@ -1,7 +1,9 @@
declare global {
namespace CHAT {
- export type TInputInfo = {
+ type TInputInfo = {
message: string;
};
}
}
+
+export default {};
diff --git a/src/types/message.ts b/src/types/message.ts
new file mode 100644
index 0000000..fcd7483
--- /dev/null
+++ b/src/types/message.ts
@@ -0,0 +1,19 @@
+declare global {
+ namespace MESSAGE {
+ type TASK_STATUS = 'TeamRunStarted' | 'TeamRunResponseContent' | 'TeamRunCompleted';
+
+ interface Answer {
+ message: string;
+ node: string;
+ output: string;
+ run_id: string;
+ status: TASK_STATUS;
+ extra_data: {
+ type: string,
+ data: Record
+ },
+ team_session_state: any
+ }
+ }
+}
+export default {};
diff --git a/src/utils/querySSE.ts b/src/utils/querySSE.ts
index 7850465..7f58191 100644
--- a/src/utils/querySSE.ts
+++ b/src/utils/querySSE.ts
@@ -3,8 +3,10 @@ import type { EventSourceMessage } from '@microsoft/fetch-event-source';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { glsWithCatch } from '@/utils/stroage';
-const customHost = 'http://localhost:3000';
-const DEFAULT_SSE_URL = `${customHost}/agent/input`;
+const customHost = 'http://192.168.40.41:8001';
+//
+// const customHost = 'http://localhost:3000';
+const DEFAULT_SSE_URL = `${customHost}/api/agent/runs`;
const SSE_HEADERS = {
'Content-Type': 'application/json',
@@ -20,26 +22,30 @@ interface SSEConfig {
handleMessage?: (data: any) => void;
handleError?: (err: any) => number | null | undefined | void;
handleClose?: () => void;
- handleOpen?: (response: Response) => Promise;
+ handleOpen?: (response: Response) => void;
}
/**
* 创建服务器发送事件(SSE)连接
* @param config SSE 配置
* @param url 可选的自定义 URL
+ * @returns 包含abort方法的对象,用于中断SSE连接
*/
-export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise => {
+export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise<{ abort: () => void }> => {
const {
body = undefined,
headers = {},
- method = 'get',
+ method = 'post',
handleMessage,
handleError,
handleOpen,
handleClose,
} = config;
const store = useEnterpriseStore();
-
+
+ // 创建AbortController实例用于中断请求
+ const abortController = new AbortController();
+
fetchEventSource(url, {
method,
// credentials: 'include',
@@ -50,6 +56,7 @@ export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise
...headers,
},
body,
+ signal: abortController.signal, // 传递signal给fetchEventSource
openWhenHidden: true, // 用户切换到另一个页面后仍能保持SSE连接
onmessage(event: EventSourceMessage) {
if (event.data) {
@@ -75,4 +82,9 @@ export default async (config: SSEConfig, url: string = DEFAULT_SSE_URL): Promise
handleOpen?.(response);
},
});
+
+ // 返回abort方法供外部调用
+ return {
+ abort: () => abortController.abort()
+ };
};
diff --git a/src/views/home/components/created/index.vue b/src/views/home/components/created/index.vue
index 3705d91..dd88f6a 100644
--- a/src/views/home/components/created/index.vue
+++ b/src/views/home/components/created/index.vue
@@ -21,21 +21,13 @@ export default {
};
const tagList = [
- '人工智能',
- '人工智能与应用',
- '行业分析与市场数据',
- '标签标签标签标签标签标签标签',
- '标签标签标签标签标签标签标签',
- '标签标签标签标签标签标签标签',
- '标签标签标签标签标签标签标签',
- '标签A',
- '啊啊啊',
- '宝宝贝贝',
- '微信',
- '吧啊啊',
- '哦哦哦哦哦哦哦哦',
- '人工智能',
- '人工智能与应用',
+ '汇总账号昨天的运营情况',
+ '查账号本月点赞量 Top3 的笔记详情',
+ '统计投流账户上周的消耗金额 + 点击率',
+ '把昨天漏采数据的账号重新抓取一次数据',
+ '规划账号未来 2 周的内容发布排期和选题',
+ '根据账号的已有选题生成具体内容稿件',
+ '根据热点生成账号的选题及内容',
];
const handleTagClick = (tag: string) => {