337 lines
9.5 KiB
TypeScript
337 lines
9.5 KiB
TypeScript
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();
|
||
}
|
||
}
|
||
});
|