agent-Specialization/static/src/stores/chat.ts

337 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { defineStore } from 'pinia';
interface ScrollStatePayload {
autoScrollEnabled?: boolean;
userScrolling?: boolean;
}
interface ChatState {
messages: Array<any>;
currentMessageIndex: number;
streamingMessage: boolean;
expandedBlocks: Set<string>;
autoScrollEnabled: boolean;
userScrolling: boolean;
thinkingScrollLocks: Map<string, boolean>;
}
const GENERATING_LABELS = [
'正在构思…',
'稍候AI 正在准备',
'准备工具中',
'容我三思…',
'答案马上就来',
'灵感加载中',
'思路拼装中',
'琢磨最佳方案',
'脑内开会中',
'整理资料中',
'润色回复中',
'调配上下文',
'搜刮记忆中',
'快敲完了,别急'
];
function randomGeneratingLabel() {
if (!GENERATING_LABELS.length) {
return '';
}
const index = Math.floor(Math.random() * GENERATING_LABELS.length);
return GENERATING_LABELS[index];
}
function createAssistantMessage() {
return {
role: 'assistant',
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null,
activeThinkingId: null,
awaitingFirstContent: false,
generatingLabel: randomGeneratingLabel()
};
}
function randomId(prefix: string) {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
function cloneSet<T>(source: Set<T>) {
return new Set<T>(Array.from(source));
}
function cloneMap<K, V>(source: Map<K, V>) {
return new Map<K, V>(Array.from(source.entries()));
}
function clearAwaitingFirstContent(message: any) {
if (message && message.awaitingFirstContent) {
message.awaitingFirstContent = false;
}
}
export const useChatStore = defineStore('chat', {
state: (): ChatState => ({
messages: [],
currentMessageIndex: -1,
streamingMessage: false,
expandedBlocks: new Set<string>(),
autoScrollEnabled: true,
userScrolling: false,
thinkingScrollLocks: new Map<string, boolean>()
}),
getters: {
isScrollLocked: state => state.autoScrollEnabled && !state.userScrolling
},
actions: {
setStreamingMessage(active: boolean) {
this.streamingMessage = !!active;
},
setCurrentMessageIndex(index: number) {
this.currentMessageIndex = index;
},
setMessages(messages: Array<any>) {
this.messages = messages;
},
clearMessages() {
this.messages = [];
this.currentMessageIndex = -1;
},
toggleBlock(blockId: string) {
const next = cloneSet(this.expandedBlocks);
if (next.has(blockId)) {
next.delete(blockId);
} else {
next.add(blockId);
}
this.expandedBlocks = next;
},
expandBlock(blockId: string) {
const next = cloneSet(this.expandedBlocks);
next.add(blockId);
this.expandedBlocks = next;
},
collapseBlock(blockId: string) {
const next = cloneSet(this.expandedBlocks);
next.delete(blockId);
this.expandedBlocks = next;
},
clearExpandedBlocks() {
this.expandedBlocks = new Set<string>();
},
setThinkingLock(blockId: string, locked: boolean) {
const next = cloneMap(this.thinkingScrollLocks);
if (locked) {
next.set(blockId, true);
} else {
next.delete(blockId);
}
this.thinkingScrollLocks = next;
},
clearThinkingLocks() {
this.thinkingScrollLocks = new Map<string, boolean>();
},
setScrollState(payload: ScrollStatePayload) {
if (typeof payload.autoScrollEnabled !== 'undefined') {
this.autoScrollEnabled = !!payload.autoScrollEnabled;
}
if (typeof payload.userScrolling !== 'undefined') {
this.userScrolling = !!payload.userScrolling;
}
},
enableAutoScroll() {
this.setScrollState({ autoScrollEnabled: true, userScrolling: false });
},
disableAutoScroll() {
this.setScrollState({ autoScrollEnabled: false, userScrolling: false });
},
toggleScrollLockState() {
const locked = this.isScrollLocked;
if (locked) {
this.disableAutoScroll();
return false;
}
this.enableAutoScroll();
return true;
},
ensureAssistantMessage() {
if (this.currentMessageIndex >= 0) {
return this.messages[this.currentMessageIndex];
}
const message = createAssistantMessage();
this.messages.push(message);
this.currentMessageIndex = this.messages.length - 1;
return message;
},
addUserMessage(content: string, images: string[] = []) {
this.messages.push({
role: 'user',
content,
images
});
this.currentMessageIndex = -1;
},
startAssistantMessage() {
const message = createAssistantMessage();
this.messages.push(message);
this.currentMessageIndex = this.messages.length - 1;
this.streamingMessage = true;
message.awaitingFirstContent = true;
return message;
},
startThinkingAction() {
const msg = this.ensureAssistantMessage();
clearAwaitingFirstContent(msg);
msg.streamingThinking = '';
msg.currentStreamingType = 'thinking';
const actionId = randomId('thinking');
const blockId = actionId;
const action = {
id: actionId,
type: 'thinking',
content: '',
streaming: true,
timestamp: Date.now(),
blockId
};
msg.actions.push(action);
msg.activeThinkingId = actionId;
return { action, blockId };
},
appendThinkingChunk(content: string) {
if (this.currentMessageIndex < 0) return null;
const msg = this.messages[this.currentMessageIndex];
msg.streamingThinking += content;
const thinkingAction = this.getActiveThinkingAction(msg);
if (thinkingAction) {
thinkingAction.content += content;
return thinkingAction;
}
return null;
},
completeThinking(fullContent: string) {
if (this.currentMessageIndex < 0) return null;
const msg = this.messages[this.currentMessageIndex];
const thinkingAction = this.getActiveThinkingAction(msg);
if (thinkingAction) {
thinkingAction.streaming = false;
thinkingAction.content = fullContent;
msg.streamingThinking = '';
msg.currentStreamingType = null;
msg.activeThinkingId = null;
return thinkingAction.blockId || thinkingAction.id;
}
return null;
},
startTextAction() {
const msg = this.ensureAssistantMessage();
if (!msg) {
return null;
}
clearAwaitingFirstContent(msg);
msg.streamingText = '';
msg.currentStreamingType = 'text';
const action = {
id: randomId('text'),
type: 'text',
content: '',
streaming: true,
timestamp: Date.now()
};
msg.actions.push(action);
return action;
},
appendTextChunk(content: string) {
if (this.currentMessageIndex < 0) return null;
const msg = this.messages[this.currentMessageIndex];
if (!msg) {
return null;
}
if (typeof msg.streamingText !== 'string') {
msg.streamingText = '';
}
msg.streamingText += content;
const lastAction = msg.actions[msg.actions.length - 1];
if (lastAction && lastAction.type === 'text') {
lastAction.content += content;
return lastAction;
}
return null;
},
completeText(fullContent: string) {
if (this.currentMessageIndex < 0) return;
const msg = this.messages[this.currentMessageIndex];
for (let i = msg.actions.length - 1; i >= 0; i--) {
const action = msg.actions[i];
if (action.type === 'text' && action.streaming) {
action.streaming = false;
if (typeof fullContent === 'string' && fullContent.length) {
action.content = fullContent;
}
break;
}
}
msg.streamingText = '';
msg.currentStreamingType = null;
},
addSystemMessage(content: string) {
const msg = this.ensureAssistantMessage();
clearAwaitingFirstContent(msg);
msg.actions.push({
id: randomId('system'),
type: 'system',
content,
timestamp: Date.now()
});
},
addAppendPayloadAction(data: any) {
const msg = this.ensureAssistantMessage();
clearAwaitingFirstContent(msg);
msg.actions.push({
id: `append-payload-${Date.now()}-${Math.random()}`,
type: 'append_payload',
append: data,
timestamp: Date.now()
});
},
addModifyPayloadAction(data: any) {
const msg = this.ensureAssistantMessage();
clearAwaitingFirstContent(msg);
msg.actions.push({
id: `modify-payload-${Date.now()}-${Math.random()}`,
type: 'modify_payload',
modify: data,
timestamp: Date.now()
});
},
getActiveThinkingAction(msg: any) {
if (!msg || !Array.isArray(msg.actions)) {
return null;
}
if (msg.activeThinkingId) {
const found = msg.actions.find(
(action: any) => action && action.id === msg.activeThinkingId && action.type === 'thinking'
);
if (found) {
return found;
}
}
for (let i = msg.actions.length - 1; i >= 0; i--) {
const action = msg.actions[i];
if (action && action.type === 'thinking' && action.streaming !== false) {
return action;
}
}
return null;
},
resetChatState() {
this.messages = [];
this.currentMessageIndex = -1;
this.streamingMessage = false;
this.clearExpandedBlocks();
this.clearThinkingLocks();
}
}
});