|
@@ -12,9 +12,23 @@ interface Message {
|
|
|
role: string;
|
|
role: string;
|
|
|
content: string;
|
|
content: string;
|
|
|
displayContent: string;
|
|
displayContent: string;
|
|
|
|
|
+ thinkingContent: string;
|
|
|
|
|
+ thinkingDisplayContent: string;
|
|
|
|
|
+ isThinkingCollapsed: boolean;
|
|
|
|
|
+ searchingItems: string[];
|
|
|
|
|
+ phase: 'thinking' | 'searching' | 'answer' | 'done';
|
|
|
isStreaming: boolean;
|
|
isStreaming: boolean;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const blankMeta = () => ({
|
|
|
|
|
+ thinkingContent: '',
|
|
|
|
|
+ thinkingDisplayContent: '',
|
|
|
|
|
+ isThinkingCollapsed: true,
|
|
|
|
|
+ searchingItems: [] as string[],
|
|
|
|
|
+ phase: 'done' as const,
|
|
|
|
|
+ isStreaming: false,
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
export const useChatStore = defineStore('chat', () => {
|
|
export const useChatStore = defineStore('chat', () => {
|
|
|
const messages = ref<Message[]>([]);
|
|
const messages = ref<Message[]>([]);
|
|
|
const isLoading = ref(false);
|
|
const isLoading = ref(false);
|
|
@@ -34,17 +48,17 @@ export const useChatStore = defineStore('chat', () => {
|
|
|
role: msg.role,
|
|
role: msg.role,
|
|
|
content: msg.content,
|
|
content: msg.content,
|
|
|
displayContent: msg.content,
|
|
displayContent: msg.content,
|
|
|
- isStreaming: false,
|
|
|
|
|
|
|
+ ...blankMeta(),
|
|
|
}));
|
|
}));
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('Error loading chat history: ', error);
|
|
console.error('Error loading chat history: ', error);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const sendMessage = async (message: string, stream: boolean) => {
|
|
|
|
|
|
|
+ const sendMessage = async (message: string, stream: boolean) => {
|
|
|
if (!message.trim() || !userStore.userId) return;
|
|
if (!message.trim() || !userStore.userId) return;
|
|
|
|
|
|
|
|
- messages.value.push({ role: 'user', content: message, displayContent: message, isStreaming: false });
|
|
|
|
|
|
|
+ messages.value.push({ role: 'user', content: message, displayContent: message, ...blankMeta() });
|
|
|
isLoading.value = true;
|
|
isLoading.value = true;
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -53,32 +67,43 @@ export const useChatStore = defineStore('chat', () => {
|
|
|
} else {
|
|
} else {
|
|
|
const { data } = await axios.post(
|
|
const { data } = await axios.post(
|
|
|
`${import.meta.env.VITE_API_URL}/chat/chat`,
|
|
`${import.meta.env.VITE_API_URL}/chat/chat`,
|
|
|
- { messages: [{ role: 'user', content: message}], stream: false },
|
|
|
|
|
- // { messages: messages.value.map(m => ({ role: m.role, content: m.content })), model, temperature, max_tokens, stream: false },
|
|
|
|
|
|
|
+ { messages: [{ role: 'user', content: message }], stream: false },
|
|
|
{ headers: { 'Authorization': `Bearer ${userStore.userId}`, 'Content-Type': 'application/json' } }
|
|
{ headers: { 'Authorization': `Bearer ${userStore.userId}`, 'Content-Type': 'application/json' } }
|
|
|
);
|
|
);
|
|
|
messages.value.push({
|
|
messages.value.push({
|
|
|
role: data.message.role,
|
|
role: data.message.role,
|
|
|
content: data.message.content,
|
|
content: data.message.content,
|
|
|
displayContent: data.message.content,
|
|
displayContent: data.message.content,
|
|
|
- isStreaming: false,
|
|
|
|
|
|
|
+ ...blankMeta(),
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('Error sending message: ', error);
|
|
console.error('Error sending message: ', error);
|
|
|
- messages.value.push({ role: 'assistant', content: 'Error: unable to process request', displayContent: 'Error: unable to process request', isStreaming: false });
|
|
|
|
|
|
|
+ messages.value.push({
|
|
|
|
|
+ role: 'assistant', content: 'Error: unable to process request',
|
|
|
|
|
+ displayContent: 'Error: unable to process request',
|
|
|
|
|
+ ...blankMeta(),
|
|
|
|
|
+ });
|
|
|
} finally {
|
|
} finally {
|
|
|
isLoading.value = false;
|
|
isLoading.value = false;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleStreamResponse = async () => {
|
|
const handleStreamResponse = async () => {
|
|
|
- // 先快照当前对话历史(不含即将添加的占位),用于发送请求
|
|
|
|
|
- const historySnapshot = [{role:'user',content: messages.value[messages.value.length - 1].content}];
|
|
|
|
|
- // const historySnapshot = messages.value.map(m => ({ role: m.role, content: m.content }));
|
|
|
|
|
|
|
+ const historySnapshot = [{ role: 'user', content: messages.value[messages.value.length - 1].content }];
|
|
|
|
|
|
|
|
const aiMessageIndex = messages.value.length;
|
|
const aiMessageIndex = messages.value.length;
|
|
|
- messages.value.push({ role: 'assistant', content: '', displayContent: '', isStreaming: true });
|
|
|
|
|
|
|
+ messages.value.push({
|
|
|
|
|
+ role: 'assistant',
|
|
|
|
|
+ content: '',
|
|
|
|
|
+ displayContent: '',
|
|
|
|
|
+ thinkingContent: '',
|
|
|
|
|
+ thinkingDisplayContent: '',
|
|
|
|
|
+ isThinkingCollapsed: false,
|
|
|
|
|
+ searchingItems: [],
|
|
|
|
|
+ phase: 'thinking',
|
|
|
|
|
+ isStreaming: true,
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/chat/chat`, {
|
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/chat/chat`, {
|
|
@@ -88,7 +113,6 @@ export const useChatStore = defineStore('chat', () => {
|
|
|
'Authorization': `Bearer ${userStore.userId}`,
|
|
'Authorization': `Bearer ${userStore.userId}`,
|
|
|
},
|
|
},
|
|
|
body: JSON.stringify({ messages: historySnapshot, stream: true }),
|
|
body: JSON.stringify({ messages: historySnapshot, stream: true }),
|
|
|
- // body: JSON.stringify({ messages: historySnapshot, model, temperature, max_tokens, stream: true }),
|
|
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
@@ -97,19 +121,24 @@ export const useChatStore = defineStore('chat', () => {
|
|
|
const reader = response.body.getReader();
|
|
const reader = response.body.getReader();
|
|
|
const decoder = new TextDecoder();
|
|
const decoder = new TextDecoder();
|
|
|
let buffer = '';
|
|
let buffer = '';
|
|
|
- // 待显示字符队列
|
|
|
|
|
- let charQueue = '';
|
|
|
|
|
- let isTyping = false;
|
|
|
|
|
-
|
|
|
|
|
- // 逐字打印函数,每个字符间隔 20ms
|
|
|
|
|
- const typeNextChar = () => {
|
|
|
|
|
- if (charQueue.length === 0) {
|
|
|
|
|
- isTyping = false;
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- messages.value[aiMessageIndex].displayContent += charQueue[0];
|
|
|
|
|
- charQueue = charQueue.slice(1);
|
|
|
|
|
- setTimeout(typeNextChar, 20);
|
|
|
|
|
|
|
+
|
|
|
|
|
+ let answerQueue = '';
|
|
|
|
|
+ let isAnswerTyping = false;
|
|
|
|
|
+ let thinkingQueue = '';
|
|
|
|
|
+ let isThinkingTyping = false;
|
|
|
|
|
+
|
|
|
|
|
+ const typeNextAnswerChar = () => {
|
|
|
|
|
+ if (answerQueue.length === 0) { isAnswerTyping = false; return; }
|
|
|
|
|
+ messages.value[aiMessageIndex].displayContent += answerQueue[0];
|
|
|
|
|
+ answerQueue = answerQueue.slice(1);
|
|
|
|
|
+ setTimeout(typeNextAnswerChar, 20);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const typeNextThinkingChar = () => {
|
|
|
|
|
+ if (thinkingQueue.length === 0) { isThinkingTyping = false; return; }
|
|
|
|
|
+ messages.value[aiMessageIndex].thinkingDisplayContent += thinkingQueue[0];
|
|
|
|
|
+ thinkingQueue = thinkingQueue.slice(1);
|
|
|
|
|
+ setTimeout(typeNextThinkingChar, 10);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
while (true) {
|
|
while (true) {
|
|
@@ -129,14 +158,28 @@ export const useChatStore = defineStore('chat', () => {
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
const data = JSON.parse(jsonStr);
|
|
const data = JSON.parse(jsonStr);
|
|
|
- if (data.content) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (data.type === 'thinking' && data.content) {
|
|
|
|
|
+ messages.value[aiMessageIndex].thinkingContent += data.content;
|
|
|
|
|
+ thinkingQueue += data.content;
|
|
|
|
|
+ if (!isThinkingTyping) { isThinkingTyping = true; typeNextThinkingChar(); }
|
|
|
|
|
+
|
|
|
|
|
+ } else if (data.type === 'searching' && data.content) {
|
|
|
|
|
+ // 立即刷新思考打字队列,折叠思考块,切换到搜索阶段
|
|
|
|
|
+ messages.value[aiMessageIndex].thinkingDisplayContent = messages.value[aiMessageIndex].thinkingContent;
|
|
|
|
|
+ thinkingQueue = '';
|
|
|
|
|
+ isThinkingTyping = false;
|
|
|
|
|
+ messages.value[aiMessageIndex].isThinkingCollapsed = true;
|
|
|
|
|
+ messages.value[aiMessageIndex].phase = 'searching';
|
|
|
|
|
+ messages.value[aiMessageIndex].searchingItems.push(data.content);
|
|
|
|
|
+
|
|
|
|
|
+ } else if ((data.type === 'answer' || !data.type) && data.content) {
|
|
|
|
|
+ messages.value[aiMessageIndex].phase = 'answer';
|
|
|
messages.value[aiMessageIndex].content += data.content;
|
|
messages.value[aiMessageIndex].content += data.content;
|
|
|
- charQueue += data.content;
|
|
|
|
|
- if (!isTyping) {
|
|
|
|
|
- isTyping = true;
|
|
|
|
|
- typeNextChar();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ answerQueue += data.content;
|
|
|
|
|
+ if (!isAnswerTyping) { isAnswerTyping = true; typeNextAnswerChar(); }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
if (data.finished) break;
|
|
if (data.finished) break;
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.warn('Failed to parse JSON:', jsonStr, e);
|
|
console.warn('Failed to parse JSON:', jsonStr, e);
|
|
@@ -146,12 +189,12 @@ export const useChatStore = defineStore('chat', () => {
|
|
|
|
|
|
|
|
reader.releaseLock();
|
|
reader.releaseLock();
|
|
|
|
|
|
|
|
- // 等待打字队列清空后再关闭光标
|
|
|
|
|
const waitTypingDone = () => {
|
|
const waitTypingDone = () => {
|
|
|
- if (charQueue.length > 0) {
|
|
|
|
|
|
|
+ if (answerQueue.length > 0 || thinkingQueue.length > 0) {
|
|
|
setTimeout(waitTypingDone, 50);
|
|
setTimeout(waitTypingDone, 50);
|
|
|
} else {
|
|
} else {
|
|
|
messages.value[aiMessageIndex].isStreaming = false;
|
|
messages.value[aiMessageIndex].isStreaming = false;
|
|
|
|
|
+ messages.value[aiMessageIndex].phase = 'done';
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
waitTypingDone();
|
|
waitTypingDone();
|
|
@@ -164,4 +207,4 @@ export const useChatStore = defineStore('chat', () => {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
return { messages, isLoading, loadChatHistory, sendMessage };
|
|
return { messages, isLoading, loadChatHistory, sendMessage };
|
|
|
-});
|
|
|
|
|
|
|
+});
|