完成世界书、骰子、apiconfig页面处理
This commit is contained in:
203
frontend/src/Store/SideBarLeft/ApiConfigSlice.jsx
Normal file
203
frontend/src/Store/SideBarLeft/ApiConfigSlice.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
// frontend-react/src/Store/SideBarLeft/ApiConfigSlice.jsx
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
|
||||
// 定义初始状态
|
||||
const initialState = {
|
||||
profiles: [], // 所有配置文件列表
|
||||
currentProfile: null, // 当前加载的配置文件
|
||||
activeMap: {}, // 当前激活的配置映射 { category: profileId }
|
||||
loading: false,
|
||||
error: null,
|
||||
notification: {
|
||||
show: false,
|
||||
message: '',
|
||||
type: 'info' // 'success', 'error', 'info'
|
||||
}
|
||||
};
|
||||
|
||||
// 创建 Store
|
||||
const useApiConfigStore = create(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
...initialState,
|
||||
|
||||
// Action: 获取所有配置文件列表
|
||||
fetchProfiles: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch('/api/api-config/profiles');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch profiles');
|
||||
}
|
||||
const data = await response.json();
|
||||
set({ profiles: data, loading: false });
|
||||
} catch (err) {
|
||||
set({ error: err.message, loading: false });
|
||||
get().showNotification('error', `获取配置文件列表失败: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
// Action: 获取单个配置文件
|
||||
fetchProfile: async (profileId) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`/api/api-config/profiles/${profileId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch profile');
|
||||
}
|
||||
const data = await response.json();
|
||||
set({ currentProfile: data, loading: false });
|
||||
return data;
|
||||
} catch (err) {
|
||||
set({ error: err.message, loading: false });
|
||||
get().showNotification('error', `获取配置文件失败: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Action: 保存配置文件(批量、增量)
|
||||
saveProfile: async (profileId, name, apisToSave) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch('/api/api-config/profiles', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
profileId,
|
||||
name,
|
||||
apis: apisToSave // 只包含要保存的 API
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || 'Failed to save profile');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 更新本地状态
|
||||
set(state => {
|
||||
const existingIndex = state.profiles.findIndex(p => p.id === profileId);
|
||||
let newProfiles = [...state.profiles];
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// 更新现有配置文件
|
||||
newProfiles[existingIndex] = {
|
||||
id: result.id,
|
||||
name: result.name
|
||||
};
|
||||
} else {
|
||||
// 添加新配置文件
|
||||
newProfiles.push({
|
||||
id: result.id,
|
||||
name: result.name
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
profiles: newProfiles,
|
||||
currentProfile: result,
|
||||
loading: false
|
||||
};
|
||||
});
|
||||
|
||||
get().showNotification('success', '配置文件已保存');
|
||||
return result;
|
||||
} catch (err) {
|
||||
set({ error: err.message, loading: false });
|
||||
get().showNotification('error', `保存失败: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Action: 删除配置文件
|
||||
deleteProfile: async (profileId) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`/api/api-config/profiles/${profileId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete profile');
|
||||
}
|
||||
|
||||
set(state => ({
|
||||
profiles: state.profiles.filter(p => p.id !== profileId),
|
||||
currentProfile: state.currentProfile?.id === profileId ? null : state.currentProfile,
|
||||
loading: false
|
||||
}));
|
||||
|
||||
get().showNotification('success', '配置文件已删除');
|
||||
} catch (err) {
|
||||
set({ error: err.message, loading: false });
|
||||
get().showNotification('error', `删除失败: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Action: 测试连接并获取模型列表
|
||||
testConnection: async (apiUrl, apiKey, category) => {
|
||||
try {
|
||||
const response = await fetch('/api/api-config/test-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ apiUrl, apiKey, category, model: '' })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || 'Connection test failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
return data.models || [];
|
||||
} else {
|
||||
throw new Error(data.message || '获取模型列表失败');
|
||||
}
|
||||
} catch (err) {
|
||||
get().showNotification('error', `连接测试失败: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Action: 设置激活配置
|
||||
setActiveConfig: async (category, profileId) => {
|
||||
try {
|
||||
set(state => ({
|
||||
activeMap: { ...state.activeMap, [category]: profileId }
|
||||
}));
|
||||
get().showNotification('success', '已设置为默认配置');
|
||||
} catch (err) {
|
||||
get().showNotification('error', `设置失败: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
// Action: 显示通知
|
||||
showNotification: (type, message) => {
|
||||
set({ notification: { show: true, type, message } });
|
||||
setTimeout(() => {
|
||||
set({ notification: { ...get().notification, show: false } });
|
||||
}, 3000);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'ApiConfigStore',
|
||||
partialize: (state) => ({
|
||||
activeMap: state.activeMap
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 导出 Hook
|
||||
export default useApiConfigStore;
|
||||
356
frontend/src/Store/SideBarLeft/PresetSlice.jsx
Normal file
356
frontend/src/Store/SideBarLeft/PresetSlice.jsx
Normal file
@@ -0,0 +1,356 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
const usePresetStore = create((set, get) => ({
|
||||
// 预设选择
|
||||
selectedPreset: '',
|
||||
|
||||
// 核心参数
|
||||
parameters: {
|
||||
temperature: 1.0,
|
||||
frequency_penalty: 0.0,
|
||||
presence_penalty: 0.0,
|
||||
top_p: 1.0,
|
||||
top_k: 0,
|
||||
max_context: 1000000,
|
||||
max_tokens: 30000,
|
||||
max_context_unlocked: false,
|
||||
stream_openai: true,
|
||||
seed: -1,
|
||||
n: 1
|
||||
},
|
||||
|
||||
// 可用的预设列表 - 初始为空,将从后端加载
|
||||
presets: [],
|
||||
|
||||
// 是否正在加载预设列表
|
||||
isLoadingPresets: false,
|
||||
|
||||
// 参数设置折叠状态
|
||||
isParametersExpanded: true,
|
||||
|
||||
// 预设组件列表
|
||||
promptComponents: [
|
||||
{
|
||||
identifier: "dialogueExamples",
|
||||
name: "Chat Examples",
|
||||
system_prompt: true,
|
||||
marker: true,
|
||||
enabled: true,
|
||||
role: 0
|
||||
},
|
||||
{
|
||||
identifier: "chatHistory",
|
||||
name: "Chat History",
|
||||
system_prompt: true,
|
||||
marker: true,
|
||||
enabled: true,
|
||||
role: 0
|
||||
},
|
||||
{
|
||||
identifier: "worldInfoAfter",
|
||||
name: "World Info (after)",
|
||||
system_prompt: true,
|
||||
marker: true,
|
||||
enabled: true,
|
||||
role: 0
|
||||
},
|
||||
{
|
||||
identifier: "worldInfoBefore",
|
||||
name: "World Info (before)",
|
||||
system_prompt: true,
|
||||
marker: true,
|
||||
enabled: true,
|
||||
role: 0
|
||||
},
|
||||
{
|
||||
identifier: "charDescription",
|
||||
name: "Char Description",
|
||||
system_prompt: true,
|
||||
marker: true,
|
||||
enabled: true,
|
||||
role: 0
|
||||
},
|
||||
{
|
||||
identifier: "charPersonality",
|
||||
name: "Char Personality",
|
||||
system_prompt: true,
|
||||
marker: true,
|
||||
enabled: true,
|
||||
role: 0
|
||||
},
|
||||
{
|
||||
identifier: "scenario",
|
||||
name: "Scenario",
|
||||
system_prompt: true,
|
||||
marker: true,
|
||||
enabled: true,
|
||||
role: 0
|
||||
},
|
||||
{
|
||||
identifier: "personaDescription",
|
||||
name: "Persona Description",
|
||||
system_prompt: true,
|
||||
marker: true,
|
||||
enabled: true,
|
||||
role: 0
|
||||
}
|
||||
],
|
||||
|
||||
// 从后端加载预设列表
|
||||
fetchPresets: async () => {
|
||||
set({ isLoadingPresets: true });
|
||||
try {
|
||||
const response = await fetch('/api/presets');
|
||||
const data = await response.json();
|
||||
|
||||
// 转换为预设对象数组
|
||||
const presetList = data.presets.map(preset => ({
|
||||
id: preset.name,
|
||||
name: preset.name,
|
||||
description: preset.description,
|
||||
component_count: preset.component_count,
|
||||
temperature: preset.temperature
|
||||
}));
|
||||
|
||||
set({ presets: presetList, isLoadingPresets: false });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch presets:', error);
|
||||
set({ isLoadingPresets: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 设置选中的预设
|
||||
setSelectedPreset: async (presetId) => {
|
||||
try {
|
||||
// 从后端获取预设的完整内容
|
||||
const response = await fetch(`/api/presets/${presetId}`);
|
||||
const presetData = await response.json();
|
||||
|
||||
// 记录原始数据用于调试
|
||||
console.log('从后端获取的预设数据:', presetData);
|
||||
|
||||
// 提取参数并更新状态,确保所有参数都有默认值
|
||||
const parameters = {
|
||||
temperature: presetData.temperature !== undefined ? presetData.temperature : 1.0,
|
||||
frequency_penalty: presetData.frequency_penalty !== undefined ? presetData.frequency_penalty : 0.0,
|
||||
presence_penalty: presetData.presence_penalty !== undefined ? presetData.presence_penalty : 0.0,
|
||||
top_p: presetData.top_p !== undefined ? presetData.top_p : 1.0,
|
||||
top_k: presetData.top_k !== undefined ? presetData.top_k : 0,
|
||||
max_context: presetData.openai_max_context !== undefined ? presetData.openai_max_context :
|
||||
(presetData.max_context !== undefined ? presetData.max_context : 1000000),
|
||||
max_tokens: presetData.openai_max_tokens !== undefined ? presetData.openai_max_tokens :
|
||||
(presetData.max_tokens !== undefined ? presetData.max_tokens : 30000),
|
||||
max_context_unlocked: presetData.max_context_unlocked !== undefined ? presetData.max_context_unlocked : false,
|
||||
stream_openai: presetData.stream_openai !== undefined ? presetData.stream_openai : true,
|
||||
seed: presetData.seed !== undefined ? presetData.seed : -1,
|
||||
n: presetData.n !== undefined ? presetData.n : 1
|
||||
};
|
||||
|
||||
// 记录映射后的参数用于调试
|
||||
console.log('映射后的参数:', parameters);
|
||||
|
||||
// 处理预设组件
|
||||
let components = [];
|
||||
if (presetData.prompts && Array.isArray(presetData.prompts)) {
|
||||
// 获取当前角色的prompt_order,添加更严格的检查
|
||||
const currentOrder = (presetData.prompt_order &&
|
||||
Array.isArray(presetData.prompt_order) &&
|
||||
presetData.prompt_order.length > 0 &&
|
||||
presetData.prompt_order[0] &&
|
||||
presetData.prompt_order[0].order &&
|
||||
Array.isArray(presetData.prompt_order[0].order))
|
||||
? presetData.prompt_order[0].order
|
||||
: [];
|
||||
|
||||
// 根据prompt_order排序组件
|
||||
components = presetData.prompts.map(prompt => {
|
||||
const orderItem = currentOrder.find(item => item && item.identifier === prompt.identifier);
|
||||
return {
|
||||
...prompt,
|
||||
enabled: orderItem ? orderItem.enabled : true,
|
||||
role: prompt.role !== undefined ? prompt.role : (prompt.system_prompt ? 0 : 1)
|
||||
};
|
||||
});
|
||||
|
||||
// 如果有prompt_order,按照它排序
|
||||
if (currentOrder.length > 0) {
|
||||
components.sort((a, b) => {
|
||||
const indexA = currentOrder.findIndex(item => item && item.identifier === a.identifier);
|
||||
const indexB = currentOrder.findIndex(item => item && item.identifier === b.identifier);
|
||||
return indexA - indexB;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 更新状态,确保参数容器展开
|
||||
set({
|
||||
selectedPreset: presetId,
|
||||
parameters,
|
||||
promptComponents: components,
|
||||
isParametersExpanded: true // 确保参数容器展开
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load preset:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 更新参数
|
||||
updateParameter: ({ name, value }) => set((state) => ({
|
||||
parameters: { ...state.parameters, [name]: value }
|
||||
})),
|
||||
|
||||
// 添加预设
|
||||
addPreset: (preset) => set((state) => ({
|
||||
presets: [...state.presets, preset]
|
||||
})),
|
||||
|
||||
// 保存当前设置为预设
|
||||
saveCurrentAsPreset: async ({ name }) => {
|
||||
const state = get();
|
||||
try {
|
||||
// 构建预设数据
|
||||
const presetData = {
|
||||
...state.parameters,
|
||||
prompts: state.promptComponents.map(component => ({
|
||||
identifier: component.identifier,
|
||||
name: component.name,
|
||||
content: component.content || '',
|
||||
role: component.role,
|
||||
system_prompt: component.system_prompt,
|
||||
marker: component.marker
|
||||
})),
|
||||
prompt_order: [{
|
||||
order: state.promptComponents.map(component => ({
|
||||
identifier: component.identifier,
|
||||
enabled: component.enabled !== false
|
||||
}))
|
||||
}]
|
||||
};
|
||||
|
||||
// 发送到后端
|
||||
const response = await fetch('/api/presets', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
preset_name: name,
|
||||
...presetData
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save preset');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 添加到本地预设列表
|
||||
const newPreset = {
|
||||
id: name,
|
||||
name,
|
||||
description: '',
|
||||
component_count: state.promptComponents.length,
|
||||
temperature: state.parameters.temperature
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
presets: [...state.presets, newPreset],
|
||||
selectedPreset: name
|
||||
}));
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to save preset:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 编辑预设名称
|
||||
editPresetName: async (presetId, newName) => {
|
||||
try {
|
||||
const response = await fetch(`/api/presets/${presetId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: newName
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update preset name');
|
||||
}
|
||||
|
||||
// 更新本地状态
|
||||
set((state) => ({
|
||||
presets: state.presets.map(preset =>
|
||||
preset.id === presetId ? { ...preset, name: newName } : preset
|
||||
)
|
||||
}));
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Failed to update preset name:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 切换参数设置折叠状态
|
||||
toggleParametersExpanded: () => set((state) => ({
|
||||
isParametersExpanded: !state.isParametersExpanded
|
||||
})),
|
||||
|
||||
// 设置预设组件列表
|
||||
setPromptComponents: (components) => set({ promptComponents: components }),
|
||||
|
||||
// 更新组件
|
||||
updateComponent: (index, updatedComponent) => set((state) => {
|
||||
const newComponents = [...state.promptComponents];
|
||||
newComponents[index] = { ...newComponents[index], ...updatedComponent };
|
||||
return { promptComponents: newComponents };
|
||||
}),
|
||||
|
||||
// 切换组件启用状态
|
||||
toggleComponentEnabled: (index) => set((state) => {
|
||||
const newComponents = [...state.promptComponents];
|
||||
newComponents[index] = {
|
||||
...newComponents[index],
|
||||
enabled: !newComponents[index].enabled
|
||||
};
|
||||
return { promptComponents: newComponents };
|
||||
}),
|
||||
|
||||
// 添加新组件
|
||||
addComponent: (component) => set((state) => ({
|
||||
promptComponents: [...state.promptComponents, component]
|
||||
})),
|
||||
|
||||
// 删除组件
|
||||
removeComponent: (index) => set((state) => {
|
||||
const newComponents = [...state.promptComponents];
|
||||
newComponents.splice(index, 1);
|
||||
return { promptComponents: newComponents };
|
||||
}),
|
||||
|
||||
// 移动组件位置
|
||||
moveComponent: (fromIndex, toIndex) => set((state) => {
|
||||
const newComponents = [...state.promptComponents];
|
||||
const [movedComponent] = newComponents.splice(fromIndex, 1);
|
||||
newComponents.splice(toIndex, 0, movedComponent);
|
||||
return { promptComponents: newComponents };
|
||||
}),
|
||||
|
||||
// 获取当前预设的prompt_order
|
||||
getPromptOrder: () => {
|
||||
const { promptComponents } = get();
|
||||
return promptComponents.map(component => ({
|
||||
identifier: component.identifier,
|
||||
enabled: component.enabled !== false
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
export default usePresetStore;
|
||||
26
frontend/src/Store/SideBarLeft/SideBarLeftSlice.jsx
Normal file
26
frontend/src/Store/SideBarLeft/SideBarLeftSlice.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
const useSideBarLeftStore = create(
|
||||
persist(
|
||||
(set) => ({
|
||||
activeTab: 'gallery',
|
||||
|
||||
tabs: [
|
||||
{ id: 'gallery', label: '画廊', title: '查看和管理生成的图片' },
|
||||
{ id: 'character', label: '角色', title: '管理AI角色卡' },
|
||||
{ id: 'api', label: 'API', title: '配置LLM和生图、向量化API连接' },
|
||||
{ id: 'presets', label: '预设', title: '管理对话预设和系统提示词' },
|
||||
{ id: 'worldbook', label: '世界', title: '管理世界观设定和背景知识' }
|
||||
],
|
||||
|
||||
setActiveTab: (tab) => set({ activeTab: tab })
|
||||
}),
|
||||
{
|
||||
name: 'sidebar-left-storage', // localStorage key
|
||||
partialize: (state) => ({ activeTab: state.activeTab }) // 只保存 activeTab
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useSideBarLeftStore;
|
||||
688
frontend/src/Store/SideBarLeft/WorldBookSlice.jsx
Normal file
688
frontend/src/Store/SideBarLeft/WorldBookSlice.jsx
Normal file
@@ -0,0 +1,688 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
// LocalStorage 键名
|
||||
const GLOBAL_WORLDBOOKS_KEY = 'global_worldbooks';
|
||||
|
||||
// 辅助函数:从 LocalStorage 加载全局世界书
|
||||
const loadGlobalWorldBooks = () => {
|
||||
try {
|
||||
const stored = localStorage.getItem(GLOBAL_WORLDBOOKS_KEY);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch (error) {
|
||||
console.error('加载全局世界书失败:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 辅助函数:保存全局世界书到 LocalStorage
|
||||
const saveGlobalWorldBooks = (globalBooks) => {
|
||||
try {
|
||||
localStorage.setItem(GLOBAL_WORLDBOOKS_KEY, JSON.stringify(globalBooks));
|
||||
} catch (error) {
|
||||
console.error('保存全局世界书失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 辅助函数:处理 API 响应
|
||||
const handleResponse = async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
return response.json().catch(() => ({}));
|
||||
};
|
||||
|
||||
// 辅助函数:处理文件下载
|
||||
const handleFileDownload = async (response, filename) => {
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// 创建世界书 store
|
||||
const useWorldBookStore = create((set, get) => ({
|
||||
// 状态
|
||||
worldBooks: [], // 世界书列表
|
||||
globalWorldBooks: loadGlobalWorldBooks(), // 从 LocalStorage 初始化全局世界书列表
|
||||
currentWorldBook: null, // 当前选中的世界书
|
||||
currentEntries: [], // 当前世界书的条目列表
|
||||
currentEntry: null, // 当前选中的条目
|
||||
loading: false, // 加载状态
|
||||
error: null, // 错误信息
|
||||
success: false, // 操作成功状态
|
||||
message: '', // 成功或错误消息
|
||||
|
||||
// Actions
|
||||
clearError: () => set({ error: null, message: '' }),
|
||||
|
||||
clearSuccess: () => set({ success: false, message: '' }),
|
||||
|
||||
setCurrentWorldBook: (worldBook) => set({
|
||||
currentWorldBook: worldBook,
|
||||
currentEntries: [],
|
||||
currentEntry: null
|
||||
}),
|
||||
|
||||
setCurrentEntry: (entry) => set({ currentEntry: entry }),
|
||||
|
||||
resetCurrentWorldBook: () => set({
|
||||
currentWorldBook: null,
|
||||
currentEntries: [],
|
||||
currentEntry: null
|
||||
}),
|
||||
|
||||
// 异步操作:切换世界书的全局状态
|
||||
toggleGlobalWorldBook: async (name, isGlobal) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('is_global', isGlobal);
|
||||
|
||||
const response = await fetch(`/api/worldbooks/${name}`, {
|
||||
method: 'PUT',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
const updatedWorldBooks = state.worldBooks.map(wb =>
|
||||
wb.name === data.name ? data : wb
|
||||
);
|
||||
|
||||
// 更新全局世界书列表
|
||||
let updatedGlobalBooks = [...state.globalWorldBooks];
|
||||
const globalIndex = updatedGlobalBooks.findIndex(wb => wb.name === data.name);
|
||||
|
||||
if (isGlobal) {
|
||||
// 如果是世界书被标记为全局
|
||||
if (globalIndex === -1) {
|
||||
// 如果不在全局列表中,添加它
|
||||
updatedGlobalBooks = [...updatedGlobalBooks, data];
|
||||
} else {
|
||||
// 如果已经在全局列表中,更新它
|
||||
updatedGlobalBooks[globalIndex] = data;
|
||||
}
|
||||
} else {
|
||||
// 如果世界书不再全局,从全局列表中移除
|
||||
if (globalIndex !== -1) {
|
||||
updatedGlobalBooks = updatedGlobalBooks.filter(wb => wb.name !== data.name);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到 LocalStorage
|
||||
saveGlobalWorldBooks(updatedGlobalBooks);
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
worldBooks: updatedWorldBooks,
|
||||
globalWorldBooks: updatedGlobalBooks,
|
||||
currentWorldBook: state.currentWorldBook?.name === data.name
|
||||
? data
|
||||
: state.currentWorldBook,
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:获取所有世界书
|
||||
fetchWorldBooks: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/`);
|
||||
const data = await handleResponse(response);
|
||||
|
||||
// 从 LocalStorage 获取全局世界书列表
|
||||
const globalBooks = loadGlobalWorldBooks();
|
||||
|
||||
set({
|
||||
loading: false,
|
||||
worldBooks: data,
|
||||
globalWorldBooks: globalBooks,
|
||||
error: null
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:获取指定世界书
|
||||
fetchWorldBook: async (name) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}`);
|
||||
const data = await handleResponse(response);
|
||||
set({
|
||||
loading: false,
|
||||
currentWorldBook: data,
|
||||
error: null
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:创建世界书
|
||||
createWorldBook: async ({ name, is_global, file }) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
if (is_global !== undefined) {
|
||||
formData.append('is_global', is_global);
|
||||
}
|
||||
if (file) {
|
||||
formData.append('file', file);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/worldbooks/`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
const newWorldBooks = [...state.worldBooks, data];
|
||||
let newGlobalBooks = [...state.globalWorldBooks];
|
||||
|
||||
// 如果是世界书被标记为全局,添加到全局列表
|
||||
if (data.is_global) {
|
||||
newGlobalBooks = [...newGlobalBooks, data];
|
||||
saveGlobalWorldBooks(newGlobalBooks);
|
||||
}
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
worldBooks: newWorldBooks,
|
||||
globalWorldBooks: newGlobalBooks,
|
||||
currentWorldBook: data,
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:更新世界书
|
||||
updateWorldBook: async ({ name, is_global, file }) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const formData = new FormData();
|
||||
if (is_global !== undefined) {
|
||||
formData.append('is_global', is_global);
|
||||
}
|
||||
if (file) {
|
||||
formData.append('file', file);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/worldbooks/${name}`, {
|
||||
method: 'PUT',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
const updatedWorldBooks = state.worldBooks.map(wb =>
|
||||
wb.name === data.name ? data : wb
|
||||
);
|
||||
|
||||
// 更新全局世界书列表
|
||||
let updatedGlobalBooks = [...state.globalWorldBooks];
|
||||
const globalIndex = updatedGlobalBooks.findIndex(wb => wb.name === data.name);
|
||||
|
||||
if (data.is_global) {
|
||||
// 如果是世界书被标记为全局
|
||||
if (globalIndex === -1) {
|
||||
// 如果不在全局列表中,添加它
|
||||
updatedGlobalBooks = [...updatedGlobalBooks, data];
|
||||
} else {
|
||||
// 如果已经在全局列表中,更新它
|
||||
updatedGlobalBooks[globalIndex] = data;
|
||||
}
|
||||
} else {
|
||||
// 如果世界书不再全局,从全局列表中移除
|
||||
if (globalIndex !== -1) {
|
||||
updatedGlobalBooks = updatedGlobalBooks.filter(wb => wb.name !== data.name);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到 LocalStorage
|
||||
saveGlobalWorldBooks(updatedGlobalBooks);
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
worldBooks: updatedWorldBooks,
|
||||
globalWorldBooks: updatedGlobalBooks,
|
||||
currentWorldBook: state.currentWorldBook?.name === data.name
|
||||
? data
|
||||
: state.currentWorldBook,
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:删除世界书
|
||||
deleteWorldBook: async (name) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
const filteredWorldBooks = state.worldBooks.filter(wb => wb.name !== name);
|
||||
const filteredGlobalBooks = state.globalWorldBooks.filter(wb => wb.name !== name);
|
||||
|
||||
// 保存到 LocalStorage
|
||||
saveGlobalWorldBooks(filteredGlobalBooks);
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
worldBooks: filteredWorldBooks,
|
||||
globalWorldBooks: filteredGlobalBooks,
|
||||
currentWorldBook: state.currentWorldBook?.name === name
|
||||
? null
|
||||
: state.currentWorldBook,
|
||||
currentEntries: state.currentWorldBook?.name === name
|
||||
? []
|
||||
: state.currentEntries,
|
||||
currentEntry: state.currentWorldBook?.name === name
|
||||
? null
|
||||
: state.currentEntry,
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
return name;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:获取世界书的所有条目
|
||||
fetchWorldBookEntries: async (name) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}/entries`);
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
if (state.currentWorldBook?.name === name) {
|
||||
return {
|
||||
loading: false,
|
||||
currentEntries: data,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
return { loading: false, error: null };
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:获取世界书的指定条目
|
||||
fetchWorldBookEntry: async (name, uid) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}/entries/${uid}`);
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
if (state.currentWorldBook?.name === name) {
|
||||
return {
|
||||
loading: false,
|
||||
currentEntry: data,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
return { loading: false, error: null };
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:创建世界书条目
|
||||
createWorldBookEntry: async (name, entryData) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
// 处理触发配置数据
|
||||
const processedEntryData = { ...entryData };
|
||||
if (processedEntryData.trigger_config && processedEntryData.trigger_config.triggers) {
|
||||
// 创建新的触发配置对象
|
||||
const triggerConfig = {
|
||||
triggers: {}
|
||||
};
|
||||
|
||||
// 处理每个触发策略
|
||||
for (const [strategy, triggerInfo] of Object.entries(processedEntryData.trigger_config.triggers)) {
|
||||
if (Array.isArray(triggerInfo) && triggerInfo.length >= 2) {
|
||||
triggerConfig.triggers[strategy] = [
|
||||
triggerInfo[0], // 是否启用
|
||||
triggerInfo[1] // 配置对象
|
||||
];
|
||||
} else {
|
||||
// 如果格式不正确,设置为不启用
|
||||
triggerConfig.triggers[strategy] = [false, null];
|
||||
}
|
||||
}
|
||||
|
||||
processedEntryData.trigger_config = triggerConfig;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/worldbooks/${name}/entries`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(processedEntryData)
|
||||
});
|
||||
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
if (state.currentWorldBook?.name === name) {
|
||||
return {
|
||||
loading: false,
|
||||
currentEntries: [...state.currentEntries, data],
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
loading: false,
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:更新世界书条目
|
||||
updateWorldBookEntry: async (name, uid, entryData) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
// 处理触发配置数据
|
||||
const processedEntryData = { ...entryData };
|
||||
if (processedEntryData.trigger_config && processedEntryData.trigger_config.triggers) {
|
||||
// 创建新的触发配置对象
|
||||
const triggerConfig = {
|
||||
triggers: {}
|
||||
};
|
||||
|
||||
// 处理每个触发策略
|
||||
for (const [strategy, triggerInfo] of Object.entries(processedEntryData.trigger_config.triggers)) {
|
||||
if (Array.isArray(triggerInfo) && triggerInfo.length >= 2) {
|
||||
triggerConfig.triggers[strategy] = [
|
||||
triggerInfo[0], // 是否启用
|
||||
triggerInfo[1] // 配置对象
|
||||
];
|
||||
} else {
|
||||
// 如果格式不正确,设置为不启用
|
||||
triggerConfig.triggers[strategy] = [false, null];
|
||||
}
|
||||
}
|
||||
|
||||
processedEntryData.trigger_config = triggerConfig;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/worldbooks/${name}/entries/${uid}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(processedEntryData)
|
||||
});
|
||||
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
if (state.currentWorldBook?.name === name) {
|
||||
const updatedEntries = state.currentEntries.map(entry =>
|
||||
entry.uid === data.uid ? data : entry
|
||||
);
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
currentEntries: updatedEntries,
|
||||
currentEntry: state.currentEntry?.uid === data.uid
|
||||
? data
|
||||
: state.currentEntry,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
loading: false,
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:删除世界书条目
|
||||
deleteWorldBookEntry: async (name, uid) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}/entries/${uid}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
if (state.currentWorldBook?.name === name) {
|
||||
const filteredEntries = state.currentEntries.filter(entry => entry.uid !== uid);
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
currentEntries: filteredEntries,
|
||||
currentEntry: state.currentEntry?.uid === uid
|
||||
? null
|
||||
: state.currentEntry,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
loading: false,
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
return { name, uid };
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:导入世界书
|
||||
importWorldBook: async (name, file) => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch(`/api/worldbooks/${name}/import`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await handleResponse(response);
|
||||
|
||||
set(state => {
|
||||
const existingIndex = state.worldBooks.findIndex(wb => wb.name === data.name);
|
||||
let updatedWorldBooks;
|
||||
let updatedGlobalBooks = [...state.globalWorldBooks];
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
updatedWorldBooks = [...state.worldBooks];
|
||||
updatedWorldBooks[existingIndex] = data;
|
||||
|
||||
// 更新全局世界书列表
|
||||
const globalIndex = updatedGlobalBooks.findIndex(wb => wb.name === data.name);
|
||||
if (data.is_global) {
|
||||
if (globalIndex === -1) {
|
||||
updatedGlobalBooks = [...updatedGlobalBooks, data];
|
||||
} else {
|
||||
updatedGlobalBooks[globalIndex] = data;
|
||||
}
|
||||
} else {
|
||||
if (globalIndex !== -1) {
|
||||
updatedGlobalBooks = updatedGlobalBooks.filter(wb => wb.name !== data.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updatedWorldBooks = [...state.worldBooks, data];
|
||||
|
||||
// 如果是世界书被标记为全局,添加到全局列表
|
||||
if (data.is_global) {
|
||||
updatedGlobalBooks = [...updatedGlobalBooks, data];
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到 LocalStorage
|
||||
saveGlobalWorldBooks(updatedGlobalBooks);
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
worldBooks: updatedWorldBooks,
|
||||
globalWorldBooks: updatedGlobalBooks,
|
||||
currentWorldBook: state.currentWorldBook?.name === data.name
|
||||
? data
|
||||
: state.currentWorldBook,
|
||||
success: true,
|
||||
message: '世界书导入成功'
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 异步操作:导出世界书
|
||||
exportWorldBook: async (name, format = 'internal') => {
|
||||
set({ loading: true, error: null, success: false });
|
||||
try {
|
||||
const response = await fetch(`/api/worldbooks/${name}/export?format=${format}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
// 处理文件下载
|
||||
const filename = format === 'sillytavern' ? `${name}_sillytavern.json` : `${name}.json`;
|
||||
await handleFileDownload(response, filename);
|
||||
|
||||
set({
|
||||
loading: false,
|
||||
success: true,
|
||||
message: '世界书导出成功'
|
||||
});
|
||||
|
||||
return { name, format };
|
||||
} catch (error) {
|
||||
set({
|
||||
loading: false,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export default useWorldBookStore;
|
||||
5
frontend/src/Store/SideBarLeft/index.js
Normal file
5
frontend/src/Store/SideBarLeft/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// SideBarLeft 相关的 Store
|
||||
export { default as useSideBarLeftStore } from './SideBarLeftSlice';
|
||||
export { default as useApiConfigStore } from './ApiConfigSlice';
|
||||
export { default as usePresetStore } from './PresetSlice';
|
||||
export { default as useWorldBookStore } from './WorldBookSlice';
|
||||
Reference in New Issue
Block a user