256 lines
7.1 KiB
React
256 lines
7.1 KiB
React
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
/**
|
|
* 动态表格 Store
|
|
* 管理 SillyTavern 风格的标签数据
|
|
*/
|
|
const useTableStore = create(
|
|
persist(
|
|
(set, get) => ({
|
|
// ==================== 状态 ====================
|
|
|
|
// 标签数组
|
|
tags: [],
|
|
|
|
// 当前角色和聊天
|
|
currentRole: null,
|
|
currentChat: null,
|
|
|
|
// 加载状态
|
|
isLoading: false,
|
|
|
|
// 最后更新时间
|
|
lastUpdated: null,
|
|
|
|
// ==================== Actions ====================
|
|
|
|
/**
|
|
* 加载标签数据
|
|
* @param {string} role - 角色名
|
|
* @param {string} chat - 聊天名
|
|
*/
|
|
loadTags: async (role, chat) => {
|
|
if (!role) {
|
|
set({ tags: [], currentRole: null, currentChat: null });
|
|
return;
|
|
}
|
|
|
|
set({ isLoading: true, currentRole: role, currentChat: chat });
|
|
|
|
try {
|
|
let fetchedTags = [];
|
|
|
|
// 优先级1:从聊天文件读取
|
|
if (chat) {
|
|
console.log('[TableStore] 从聊天文件读取标签数据');
|
|
const response = await fetch(`/api/chat/${encodeURIComponent(role)}/${encodeURIComponent(chat)}`);
|
|
|
|
if (!response.ok) throw new Error('获取聊天数据失败');
|
|
|
|
const chatData = await response.json();
|
|
const header = chatData.header || {};
|
|
|
|
fetchedTags = header.tags || [];
|
|
|
|
if (fetchedTags.length > 0) {
|
|
set({
|
|
tags: fetchedTags,
|
|
lastUpdated: Date.now(),
|
|
isLoading: false
|
|
});
|
|
console.log(`[TableStore] ✅ 已加载 ${fetchedTags.length} 个标签`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 优先级2:从角色卡读取(降级)
|
|
console.log('[TableStore] 聊天中无标签,从角色卡读取');
|
|
const charResponse = await fetch(`/api/characters/${encodeURIComponent(role)}`);
|
|
|
|
if (!charResponse.ok) throw new Error('获取角色卡失败');
|
|
|
|
const charData = await charResponse.json();
|
|
fetchedTags = charData.tags || [];
|
|
|
|
set({
|
|
tags: fetchedTags,
|
|
lastUpdated: Date.now(),
|
|
isLoading: false
|
|
});
|
|
console.log(`[TableStore] ✅ 已从角色卡加载 ${fetchedTags.length} 个标签`);
|
|
|
|
} catch (error) {
|
|
console.error('[TableStore] 获取标签数据失败:', error);
|
|
set({ tags: [], isLoading: false });
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 保存标签到后端(带防抖)
|
|
* @param {Array} newTags - 新的标签数组
|
|
*/
|
|
saveTags: (newTags) => {
|
|
const { currentRole, currentChat } = get();
|
|
|
|
if (!currentRole || !currentChat) {
|
|
console.warn('[TableStore] 无法保存:缺少角色或聊天信息');
|
|
return;
|
|
}
|
|
|
|
// 清除之前的定时器(如果存在)
|
|
if (get().saveTimeoutId) {
|
|
clearTimeout(get().saveTimeoutId);
|
|
}
|
|
|
|
// 延迟 1.5 秒后保存
|
|
const timeoutId = setTimeout(async () => {
|
|
try {
|
|
const response = await fetch(
|
|
`/api/chat/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/table`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ tags: newTags })
|
|
}
|
|
);
|
|
|
|
if (!response.ok) throw new Error('保存失败');
|
|
|
|
set({ lastUpdated: Date.now() });
|
|
console.log('[TableStore] ✅ 标签已保存到后端');
|
|
} catch (error) {
|
|
console.error('[TableStore] 保存标签失败:', error);
|
|
alert('保存失败: ' + error.message);
|
|
}
|
|
}, 1500);
|
|
|
|
// 保存定时器 ID
|
|
set({ saveTimeoutId: timeoutId });
|
|
},
|
|
|
|
/**
|
|
* 更新标签(乐观更新 + 延迟保存)
|
|
* @param {Array} newTags - 新的标签数组
|
|
*/
|
|
updateTags: (newTags) => {
|
|
set({ tags: newTags });
|
|
get().saveTags(newTags);
|
|
},
|
|
|
|
/**
|
|
* 添加标签
|
|
* @param {string|string[]} tagOrTags - 单个标签或标签数组
|
|
*/
|
|
addTag: (tagOrTags) => {
|
|
const { tags } = get();
|
|
|
|
// 支持批量添加
|
|
const newTagsList = Array.isArray(tagOrTags) ? tagOrTags : [tagOrTags];
|
|
const newTags = [...tags, ...newTagsList];
|
|
|
|
get().updateTags(newTags);
|
|
},
|
|
|
|
/**
|
|
* 删除标签
|
|
* @param {number} index - 标签索引
|
|
*/
|
|
deleteTag: (index) => {
|
|
const { tags } = get();
|
|
const newTags = tags.filter((_, i) => i !== index);
|
|
get().updateTags(newTags);
|
|
},
|
|
|
|
/**
|
|
* 移动标签
|
|
* @param {number} fromIndex - 起始索引
|
|
* @param {number} toIndex - 目标索引
|
|
*/
|
|
moveTag: (fromIndex, toIndex) => {
|
|
const { tags } = get();
|
|
|
|
if (fromIndex < 0 || toIndex < 0 || fromIndex >= tags.length || toIndex >= tags.length) {
|
|
return;
|
|
}
|
|
|
|
const newTags = [...tags];
|
|
[newTags[fromIndex], newTags[toIndex]] = [newTags[toIndex], newTags[fromIndex]];
|
|
|
|
get().updateTags(newTags);
|
|
},
|
|
|
|
/**
|
|
* 左移标签
|
|
* @param {number} index - 标签索引
|
|
*/
|
|
moveLeft: (index) => {
|
|
if (index === 0) return;
|
|
get().moveTag(index, index - 1);
|
|
},
|
|
|
|
/**
|
|
* 右移标签
|
|
* @param {number} index - 标签索引
|
|
*/
|
|
moveRight: (index) => {
|
|
const { tags } = get();
|
|
if (index === tags.length - 1) return;
|
|
get().moveTag(index, index + 1);
|
|
},
|
|
|
|
/**
|
|
* 编辑标签
|
|
* @param {number} index - 标签索引
|
|
* @param {string} newValue - 新值
|
|
*/
|
|
editTag: (index, newValue) => {
|
|
const { tags } = get();
|
|
const newTags = [...tags];
|
|
newTags[index] = newValue.trim();
|
|
get().updateTags(newTags);
|
|
},
|
|
|
|
/**
|
|
* 清空状态
|
|
*/
|
|
clear: () => {
|
|
// 清除定时器
|
|
if (get().saveTimeoutId) {
|
|
clearTimeout(get().saveTimeoutId);
|
|
}
|
|
|
|
set({
|
|
tags: [],
|
|
currentRole: null,
|
|
currentChat: null,
|
|
isLoading: false,
|
|
lastUpdated: null,
|
|
saveTimeoutId: null
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 强制刷新(立即从后端重新加载)
|
|
*/
|
|
refresh: async () => {
|
|
const { currentRole, currentChat } = get();
|
|
if (!currentRole) return;
|
|
|
|
await get().loadTags(currentRole, currentChat);
|
|
}
|
|
}),
|
|
{
|
|
name: 'table-storage', // localStorage key
|
|
partialize: (state) => ({
|
|
tags: state.tags,
|
|
currentRole: state.currentRole,
|
|
currentChat: state.currentChat,
|
|
lastUpdated: state.lastUpdated
|
|
})
|
|
}
|
|
)
|
|
);
|
|
|
|
export default useTableStore;
|