重构路由架构,修复导致的前端出错

This commit is contained in:
2026-04-06 00:52:04 +08:00
parent 7a62139683
commit e8dedb5ec4
12 changed files with 2055 additions and 1102 deletions

View File

@@ -3,46 +3,46 @@ import { subscribeWithSelector } from 'zustand/middleware';
const useChatBoxStore = create(
subscribeWithSelector((set, get) => ({
// 聊天历史消息列表
messages: [],
// 聊天历史消息列表
messages: [],
// 用户名称
userName: '',
// 用户名称
userName: '',
// 角色名称
characterName: '',
// 角色名称
characterName: '',
// 当前选中的角色
currentRole: null,
// 当前选中的角色
currentRole: null,
// 当前选中的聊天
currentChat: null,
// 当前选中的聊天
currentChat: null,
// 是否正在加载
isLoading: false,
// 是否正在加载
isLoading: false,
// 是否正在生成
isGenerating: false,
// 是否正在生成
isGenerating: false,
// 错误信息
error: null,
// 错误信息
error: null,
// 设置消息列表
setMessages: (messages) => set({ messages }),
// 设置消息列表
setMessages: (messages) => set({ messages }),
// 设置用户名称
setUserName: (userName) => set({ userName }),
// 设置用户名称
setUserName: (userName) => set({ userName }),
// 设置角色名称
setCharacterName: (characterName) => set({ characterName }),
// 设置角色名称
setCharacterName: (characterName) => set({ characterName }),
// 设置当前角色
setCurrentRole: (role) => set({ currentRole: role }),
// 设置当前角色
setCurrentRole: (role) => set({ currentRole: role }),
// 设置当前聊天
setCurrentChat: (chat) => set({ currentChat: chat }),
// 设置当前聊天
setCurrentChat: (chat) => set({ currentChat: chat }),
// 同时设置角色和聊天
// 同时设置角色和聊天
setChatBoxRoleAndChat: (role, chat) => {
console.log('[ChatBoxStore] setChatBoxRoleAndChat called with:', { role, chat });
set({
@@ -51,99 +51,207 @@ const useChatBoxStore = create(
});
},
// 设置生成状态
setIsGenerating: (status) => set({ isGenerating: status }),
// 设置生成状态
setIsGenerating: (status) => set({ isGenerating: status }),
// 发送消息
sendMessage: async (content) => {
const { messages, userName, characterName, currentRole, currentChat } = get();
sendMessage: async (content) => {
const { messages, userName, characterName, currentRole, currentChat } = get();
set({
isGenerating: true,
messages: [...messages, {
id: Date.now(),
floor: messages.length + 1,
mes: content,
is_user: true
}]
});
set({
isGenerating: true,
messages: [...messages, {
id: Date.now(),
floor: messages.length + 1,
mes: content,
is_user: true
}]
});
try {
const response = await fetch(`/api/chats/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
floor: messages.length + 1,
mes: content,
is_user: true
})
});
try {
const response = await fetch('/api/chat_box/send_message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
role_name: currentRole,
chat_name: currentChat,
message: content
})
});
if (!response.ok) {
throw new Error('Failed to send message');
}
if (!response.ok) {
throw new Error('Failed to send message');
}
const data = await response.json();
set((state) => ({
messages: [...state.messages, {
id: Date.now(),
floor: state.messages.length + 1,
mes: data.response,
is_user: false
}],
isGenerating: false
}));
} catch (error) {
set({
error: error.message,
isGenerating: false
});
}
},
const data = await response.json();
set((state) => ({
messages: [...state.messages, {
id: Date.now(),
floor: state.messages.length + 1,
mes: data.response,
is_user: false
}],
isGenerating: false
}));
} catch (error) {
set({
error: error.message,
isGenerating: false
});
}
},
// 终止生成
stopGeneration: () => set({ isGenerating: false }),
// 终止生成
stopGeneration: () => set({ isGenerating: false }),
// 加载聊天历史
fetchChatHistory: async (roleName, chatName) => {
set({ isLoading: true, error: null });
try {
const response = await fetch(`/api/chat_box/get_chat_history?role_name=${encodeURIComponent(roleName)}&chat_name=${encodeURIComponent(chatName)}`);
if (!response.ok) {
throw new Error('Failed to fetch chat history');
}
const data = await response.json();
// 修改数据处理逻辑适配API返回的数据结构
set({
messages: data || [], // 直接使用返回的数组
userName: 'User', // 固定用户名
characterName: roleName || 'Assistant', // 使用角色名作为角色名称
isLoading: false
});
} catch (error) {
set({
error: error.message,
isLoading: false
});
}
},
// 加载聊天历史
fetchChatHistory: async (roleName, chatName) => {
set({ isLoading: true, error: null });
try {
const response = await fetch(`/api/chats/${encodeURIComponent(roleName)}/${encodeURIComponent(chatName)}`);
if (!response.ok) {
throw new Error('Failed to fetch chat history');
}
const data = await response.json();
set({
messages: data.messages || [],
userName: data.metadata?.user_name || 'User',
characterName: data.metadata?.character_name || roleName || 'Assistant',
isLoading: false
});
} catch (error) {
set({
error: error.message,
isLoading: false
});
}
},
// 清空聊天历史
clearChatHistory: () => set({
messages: [],
userName: '',
characterName: '',
error: null
}),
// 清空聊天历史
clearChatHistory: () => set({
messages: [],
userName: '',
characterName: '',
error: null
}),
// 更新特定消息的内容
updateMessage: (id, content) => set((state) => ({
messages: state.messages.map((msg) =>
msg.id === id ? { ...msg, content } : msg
)
})),
// 更新特定消息的内容
updateMessage: async (floor, content) => {
const { currentRole, currentChat } = get();
try {
const response = await fetch(`/api/chats/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/messages/${floor}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ mes: content })
});
if (!response.ok) {
throw new Error('Failed to update message');
}
set((state) => ({
messages: state.messages.map((msg) =>
msg.floor === floor ? { ...msg, mes: content } : msg
)
}));
} catch (error) {
set({ error: error.message });
}
},
// 删除特定消息
deleteMessage: async (floor) => {
const { currentRole, currentChat } = get();
try {
const response = await fetch(`/api/chats/${encodeURIComponent(currentRole)}/${encodeURIComponent(currentChat)}/messages/${floor}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete message');
}
set((state) => ({
messages: state.messages.filter((msg) => msg.floor !== floor)
}));
} catch (error) {
set({ error: error.message });
}
},
// 创建新聊天
createChat: async (roleName, chatName, metadata = {}) => {
try {
const response = await fetch(`/api/chats/${encodeURIComponent(roleName)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_name: chatName,
metadata: {
user_name: 'User',
character_name: roleName,
...metadata
}
})
});
if (!response.ok) {
throw new Error('Failed to create chat');
}
return await response.json();
} catch (error) {
set({ error: error.message });
throw error;
}
},
// 更新聊天元数据
updateChatMetadata: async (roleName, chatName, metadata) => {
try {
const response = await fetch(`/api/chats/${encodeURIComponent(roleName)}/${encodeURIComponent(chatName)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ metadata })
});
if (!response.ok) {
throw new Error('Failed to update chat metadata');
}
return await response.json();
} catch (error) {
set({ error: error.message });
throw error;
}
},
// 删除聊天
deleteChat: async (roleName, chatName) => {
try {
const response = await fetch(`/api/chats/${encodeURIComponent(roleName)}/${encodeURIComponent(chatName)}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete chat');
}
return await response.json();
} catch (error) {
set({ error: error.message });
throw error;
}
}
}))
);
@@ -151,20 +259,17 @@ const useChatBoxStore = create(
useChatBoxStore.subscribe(
(state) => ({ role: state.currentRole, chat: state.currentChat }),
({ role, chat }, prev) => {
// 只有当角色或聊天发生变化时才处理
if (role !== prev.role || chat !== prev.chat) {
// 确保角色和聊天都存在且不为null
if (role && chat) {
useChatBoxStore.getState().fetchChatHistory(role, chat);
} else {
useChatBoxStore.getState().clearChatHistory();
}
}
// 只有当角色或聊天发生变化时才处理
if (role !== prev.role || chat !== prev.chat) {
// 确保角色和聊天都存在且不为null
if (role && chat) {
useChatBoxStore.getState().fetchChatHistory(role, chat);
} else {
useChatBoxStore.getState().clearChatHistory();
}
}
},
{ equalityFn: (a, b) => a.role === b.role && a.chat === b.chat }
);
export default useChatBoxStore;

View File

@@ -100,14 +100,16 @@ const usePresetStore = create((set, get) => ({
fetchPresets: async () => {
set({ isLoadingPresets: true });
try {
const response = await fetch('/api/presets/list');
const response = await fetch('/api/presets');
const data = await response.json();
// 转换为预设对象数组
const presetList = data.presets.map(name => ({
id: name,
name,
parameters: {} // 参数将在选择预设时加载
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 });
@@ -150,14 +152,19 @@ const usePresetStore = create((set, get) => ({
// 处理预设组件
let components = [];
if (presetData.prompts && Array.isArray(presetData.prompts)) {
// 获取当前角色的prompt_order
const currentOrder = presetData.prompt_order && presetData.prompt_order.length > 0
// 获取当前角色的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.identifier === prompt.identifier);
const orderItem = currentOrder.find(item => item && item.identifier === prompt.identifier);
return {
...prompt,
enabled: orderItem ? orderItem.enabled : true,
@@ -168,13 +175,14 @@ const usePresetStore = create((set, get) => ({
// 如果有prompt_order按照它排序
if (currentOrder.length > 0) {
components.sort((a, b) => {
const indexA = currentOrder.findIndex(item => item.identifier === a.identifier);
const indexB = currentOrder.findIndex(item => item.identifier === b.identifier);
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,
@@ -187,8 +195,6 @@ const usePresetStore = create((set, get) => ({
}
},
// 更新参数
updateParameter: ({ name, value }) => set((state) => ({
parameters: { ...state.parameters, [name]: value }
@@ -200,25 +206,97 @@ const usePresetStore = create((set, get) => ({
})),
// 保存当前设置为预设
saveCurrentAsPreset: ({ name }) => set((state) => {
const newPreset = {
id: `preset_${Date.now()}`,
name,
parameters: { ...state.parameters },
promptComponents: [...state.promptComponents]
};
return {
presets: [...state.presets, newPreset],
selectedPreset: newPreset.id
};
}),
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: (presetId, newName) => set((state) => ({
presets: state.presets.map(preset =>
preset.id === presetId ? { ...preset, name: newName } : preset
)
})),
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) => ({

View File

@@ -3,7 +3,7 @@ import { create } from 'zustand';
// 异步获取角色数据
const fetchRoleData = async () => {
try {
const response = await fetch('/api/tool_bar/get_all_role_and_chat', {
const response = await fetch('/api/chats', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache',
@@ -12,7 +12,19 @@ const fetchRoleData = async () => {
}
});
const data = await response.json();
return data;
// 转换数据格式以适应前端需求
const roleData = {};
if (data.chats && Array.isArray(data.chats)) {
data.chats.forEach(chat => {
if (!roleData[chat.role_name]) {
roleData[chat.role_name] = [];
}
roleData[chat.role_name].push(chat.chat_name);
});
}
return roleData;
} catch (error) {
console.error('获取角色数据失败:', error);
throw error;
@@ -66,52 +78,92 @@ const useRoleSelectorStore = create((set, get) => ({
// 同时更新角色和聊天
setSelectedRoleAndChat: (role, chat) => set({ selectedRole: role, selectedChat: chat }),
handleRenameRole: (oldName, newName) => {
// 处理角色重命名
handleRenameRole: async (oldName, newName) => {
const { roleData, selectedRole } = get();
if (newName && newName !== oldName) {
const newRoleData = { ...roleData };
const chats = newRoleData[oldName];
delete newRoleData[oldName];
newRoleData[newName] = chats;
try {
// 获取该角色下的所有聊天
const chats = roleData[oldName] || [];
if (selectedRole === oldName) {
set({
roleData: newRoleData,
selectedRole: newName,
editingRole: null
});
} else {
set({
roleData: newRoleData,
editingRole: null
});
// 为每个聊天更新元数据中的角色名称
for (const chatName of chats) {
await fetch(`/api/chats/${encodeURIComponent(oldName)}/${encodeURIComponent(chatName)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
metadata: { character_name: newName }
})
});
}
// 更新本地状态
const newRoleData = { ...roleData };
newRoleData[newName] = chats;
delete newRoleData[oldName];
if (selectedRole === oldName) {
set({
roleData: newRoleData,
selectedRole: newName,
editingRole: null
});
} else {
set({
roleData: newRoleData,
editingRole: null
});
}
} catch (error) {
console.error('重命名角色失败:', error);
set({ editingRole: null });
}
} else {
set({ editingRole: null });
}
},
handleRenameChat: (oldName, newName) => {
// 处理聊天重命名
handleRenameChat: async (oldName, newName) => {
const { roleData, selectedRole, selectedChat } = get();
if (newName && newName !== oldName) {
const newRoleData = { ...roleData };
const chatIndex = newRoleData[selectedRole].indexOf(oldName);
if (chatIndex !== -1) {
newRoleData[selectedRole][chatIndex] = newName;
try {
// 更新后端聊天名称
await fetch(`/api/chats/${encodeURIComponent(selectedRole)}/${encodeURIComponent(oldName)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
metadata: { chat_name: newName }
})
});
if (selectedChat === oldName) {
set({
roleData: newRoleData,
selectedChat: newName,
editingChat: null
});
// 更新本地状态
const newRoleData = { ...roleData };
const chatIndex = newRoleData[selectedRole].indexOf(oldName);
if (chatIndex !== -1) {
newRoleData[selectedRole][chatIndex] = newName;
if (selectedChat === oldName) {
set({
roleData: newRoleData,
selectedChat: newName,
editingChat: null
});
} else {
set({
roleData: newRoleData,
editingChat: null
});
}
} else {
set({
roleData: newRoleData,
editingChat: null
});
set({ editingChat: null });
}
} else {
} catch (error) {
console.error('重命名聊天失败:', error);
set({ editingChat: null });
}
} else {
@@ -119,34 +171,27 @@ const useRoleSelectorStore = create((set, get) => ({
}
},
confirmDelete: () => {
// 确认删除
confirmDelete: async () => {
const { roleData, selectedRole, selectedChat, showDeleteConfirm, deleteType } = get();
const newRoleData = { ...roleData };
if (deleteType === 'role') {
delete newRoleData[showDeleteConfirm];
if (selectedRole === showDeleteConfirm) {
set({
roleData: newRoleData,
selectedRole: null,
selectedChat: null,
showDeleteConfirm: null,
deleteType: null
});
} else {
set({
roleData: newRoleData,
showDeleteConfirm: null,
deleteType: null
});
}
} else if (deleteType === 'chat') {
const chatIndex = newRoleData[selectedRole].indexOf(showDeleteConfirm);
if (chatIndex !== -1) {
newRoleData[selectedRole].splice(chatIndex, 1);
if (selectedChat === showDeleteConfirm) {
try {
if (deleteType === 'role') {
// 删除角色下的所有聊天
const chats = roleData[showDeleteConfirm] || [];
for (const chatName of chats) {
await fetch(`/api/chats/${encodeURIComponent(showDeleteConfirm)}/${encodeURIComponent(chatName)}`, {
method: 'DELETE'
});
}
const newRoleData = { ...roleData };
delete newRoleData[showDeleteConfirm];
if (selectedRole === showDeleteConfirm) {
set({
roleData: newRoleData,
selectedRole: null,
selectedChat: null,
showDeleteConfirm: null,
deleteType: null
@@ -158,41 +203,116 @@ const useRoleSelectorStore = create((set, get) => ({
deleteType: null
});
}
} else {
set({
showDeleteConfirm: null,
deleteType: null
} else if (deleteType === 'chat') {
// 删除单个聊天
await fetch(`/api/chats/${encodeURIComponent(selectedRole)}/${encodeURIComponent(showDeleteConfirm)}`, {
method: 'DELETE'
});
const newRoleData = { ...roleData };
const chatIndex = newRoleData[selectedRole].indexOf(showDeleteConfirm);
if (chatIndex !== -1) {
newRoleData[selectedRole].splice(chatIndex, 1);
if (selectedChat === showDeleteConfirm) {
set({
roleData: newRoleData,
selectedChat: null,
showDeleteConfirm: null,
deleteType: null
});
} else {
set({
roleData: newRoleData,
showDeleteConfirm: null,
deleteType: null
});
}
} else {
set({
showDeleteConfirm: null,
deleteType: null
});
}
}
} catch (error) {
console.error('删除失败:', error);
set({
showDeleteConfirm: null,
deleteType: null
});
}
},
// 取消删除
cancelDelete: () => set({ showDeleteConfirm: null, deleteType: null }),
handleAddRole: () => {
// 添加新角色
handleAddRole: async () => {
const { roleData } = get();
const newRole = '新角色';
const newRoleData = { ...roleData };
newRoleData[newRole] = [];
set({
roleData: newRoleData,
editingRole: newRole
});
try {
// 创建新角色(通过创建一个默认聊天)
await fetch(`/api/chats/${encodeURIComponent(newRole)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_name: '默认聊天',
metadata: {
user_name: 'User',
character_name: newRole
}
})
});
const newRoleData = { ...roleData };
newRoleData[newRole] = ['默认聊天'];
set({
roleData: newRoleData,
editingRole: newRole
});
} catch (error) {
console.error('添加角色失败:', error);
}
},
handleAddChat: () => {
// 添加新聊天
handleAddChat: async () => {
const { roleData, selectedRole } = get();
if (!selectedRole) return;
const newChat = '新聊天';
const newRoleData = { ...roleData };
newRoleData[selectedRole].push(newChat);
set({
roleData: newRoleData,
editingChat: newChat
});
try {
await fetch(`/api/chats/${encodeURIComponent(selectedRole)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_name: newChat,
metadata: {
user_name: 'User',
character_name: selectedRole
}
})
});
const newRoleData = { ...roleData };
newRoleData[selectedRole].push(newChat);
set({
roleData: newRoleData,
editingChat: newChat
});
} catch (error) {
console.error('添加聊天失败:', error);
}
},
// 重置面板状态
resetPanel: () => set({
hoveredRole: null,
clickedRole: null,

View File

@@ -37,7 +37,7 @@ const PresetPanel = () => {
// 组件编辑状态
const [editingComponentIndex, setEditingComponentIndex] = useState(-1);
const [editComponentContent, setEditComponentContent] = useState('');
const [isEditing, setIsEditing] = useState(false); // 添加编辑状态标志
const [isEditing, setIsEditing] = useState(false);
// 拖拽状态
const [draggedItem, setDraggedItem] = useState(null);
@@ -75,7 +75,6 @@ const PresetPanel = () => {
// 处理参数更新
const handleParameterChange = (name, value) => {
// 根据参数类型转换值
let convertedValue = value;
if (name === 'temperature' || name === 'frequency_penalty' || name === 'presence_penalty' || name === 'top_p') {
convertedValue = parseFloat(value);
@@ -93,30 +92,44 @@ const PresetPanel = () => {
}, [fetchPresets]);
// 保存当前设置为预设
const handleSavePreset = () => {
const handleSavePreset = async () => {
if (newPresetName.trim()) {
saveCurrentAsPreset({ name: newPresetName });
setNewPresetName('');
setShowSaveDialog(false);
try {
await saveCurrentAsPreset({ name: newPresetName });
setNewPresetName('');
setShowSaveDialog(false);
// 重新加载预设列表
fetchPresets();
} catch (error) {
console.error('保存预设失败:', error);
alert('保存预设失败: ' + error.message);
}
}
};
// 编辑预设名称
const handleEditPreset = () => {
const handleEditPreset = async () => {
if (editPresetId && editPresetName.trim()) {
updatePresetName(editPresetId, editPresetName);
setEditPresetId('');
setEditPresetName('');
setShowEditDialog(false);
try {
await updatePresetName(editPresetId, editPresetName);
setEditPresetId('');
setEditPresetName('');
setShowEditDialog(false);
// 重新加载预设列表
fetchPresets();
} catch (error) {
console.error('编辑预设名称失败:', error);
alert('编辑预设名称失败: ' + error.message);
}
}
};
// 导入预设
const handleImportPreset = () => {
const handleImportPreset = async () => {
try {
const importedPreset = JSON.parse(importPresetData);
if (importedPreset.name && importedPreset.parameters) {
saveCurrentAsPreset({ name: importedPreset.name });
await saveCurrentAsPreset({ name: importedPreset.name });
// 更新参数
Object.keys(importedPreset.parameters).forEach(key => {
updateParameter({ name: key, value: importedPreset.parameters[key] });
@@ -129,9 +142,12 @@ const PresetPanel = () => {
setImportPresetData('');
setShowImportDialog(false);
// 重新加载预设列表
fetchPresets();
}
} catch (error) {
console.error('导入预设失败:', error);
alert('导入预设失败: ' + error.message);
}
};
@@ -175,7 +191,7 @@ const PresetPanel = () => {
const handleStartEditComponent = (index) => {
setEditingComponentIndex(index);
setEditComponentContent(promptComponents[index].content);
setIsEditing(true); // 设置为编辑模式
setIsEditing(true);
setShowComponentEditDialog(true);
};
@@ -183,13 +199,13 @@ const PresetPanel = () => {
const handleViewComponent = (index) => {
setEditingComponentIndex(index);
setEditComponentContent(promptComponents[index].content);
setIsEditing(false); // 设置为查看模式
setIsEditing(false);
setShowComponentEditDialog(true);
};
// 保存组件编辑
const handleSaveComponentEdit = () => {
if (editingComponentIndex >= 0 && isEditing) { // 只在编辑模式下保存
if (editingComponentIndex >= 0 && isEditing) {
updateComponent(editingComponentIndex, { content: editComponentContent });
}
setEditingComponentIndex(-1);
@@ -228,7 +244,6 @@ const PresetPanel = () => {
setDraggedItem(index);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', index.toString());
// 添加拖拽时的样式
setTimeout(() => {
e.target.classList.add('dragging');
}, 0);
@@ -254,10 +269,8 @@ const PresetPanel = () => {
e.preventDefault();
if (draggedItem === null || draggedItem === index) return;
// 移动组件
moveComponent(draggedItem, index);
// 重置拖拽状态
setDraggedItem(null);
setDragOverItem(null);
};
@@ -404,13 +417,13 @@ const PresetPanel = () => {
value={editComponentContent}
onChange={(e) => setEditComponentContent(e.target.value)}
className="component-textarea"
readOnly={!isEditing} // 根据模式设置是否只读
readOnly={!isEditing}
rows={20}
/>
</div>
<div className="dialog-footer">
<span className="token-count">
字符数: {editComponentContent ? editComponentContent.length : 0}
{editComponentContent ? editComponentContent.length : 0}
</span>
{isEditing && (
<div className="dialog-buttons">
@@ -581,7 +594,6 @@ const PresetPanel = () => {
type="number"
min="1"
max="10000000"
defaultValue="1000000"
value={parameters.max_context}
onChange={(e) => handleParameterChange('max_context', e.target.value)}
className="parameter-input"
@@ -601,7 +613,6 @@ const PresetPanel = () => {
type="number"
min="1"
max="100000"
defaultValue="30000"
value={parameters.max_tokens}
onChange={(e) => handleParameterChange('max_tokens', e.target.value)}
className="parameter-input"
@@ -729,7 +740,7 @@ const PresetPanel = () => {
</button>
<span className="component-name">{component.name}</span>
{component.marker && (
<span className="component-marker-badge">固定</span>
<span className="component-marker-badge">🔒</span>
)}
</div>
<div className="component-actions">
@@ -739,10 +750,10 @@ const PresetPanel = () => {
onMouseEnter={(e) => showTooltip(e, "编辑/查看组件")}
onMouseLeave={hideTooltip}
>
编辑
</button>
<span className="token-count">
{component.content ? component.content.length : 0}
{component.content ? component.content.length : 0}
</span>
{!component.marker && (
<button
@@ -751,7 +762,7 @@ const PresetPanel = () => {
onMouseEnter={(e) => showTooltip(e, "删除组件")}
onMouseLeave={hideTooltip}
>
删除
🗑
</button>
)}
</div>

View File

@@ -12,11 +12,9 @@ const WorldBook = () => {
isSelecting,
error,
showWorldBookDropdown,
showAddWorldBookDropdown,
globalWorldBooks,
fetchWorldBooks,
toggleWorldBookDropdown,
toggleAddWorldBookDropdown,
addGlobalWorldBook,
removeGlobalWorldBook,
createWorldBook,
@@ -28,6 +26,7 @@ const WorldBook = () => {
toggleEditPanel,
} = useWorldBookStore();
const [newEntry, setNewEntry] = useState({
name: '',
content: '',
@@ -37,9 +36,11 @@ const WorldBook = () => {
order: 0,
});
useEffect(() => {
fetchWorldBooks();
}, [fetchWorldBooks]);
useEffect(() => {
fetchWorldBooks();
}, [fetchWorldBooks]);
const handleCreateWorldBook = async () => {
const name = prompt('请输入世界书名称:');
@@ -71,62 +72,75 @@ const WorldBook = () => {
await updateEntry(editingEntry.uid, updatedEntry);
};
const isGlobalBook = (bookUid) => {
return globalWorldBooks.some(gb => gb.uid === bookUid);
};
const handleToggleGlobalBook = (bookUid) => {
if (isGlobalBook(bookUid)) {
removeGlobalWorldBook(bookUid);
} else {
addGlobalWorldBook(bookUid);
}
};
const sortedWorldBooks = [...worldBooks].sort((a, b) => {
const aIsGlobal = isGlobalBook(a.uid);
const bIsGlobal = isGlobalBook(b.uid);
if (aIsGlobal && !bIsGlobal) return -1;
if (!aIsGlobal && bIsGlobal) return 1;
return 0;
});
return (
<div className="worldbook-content">
{/* 全局世界书槽位 */}
<div className="global-worldbooks-slot">
<div className="global-books-header">
<span>全局世界书</span>
<div className="dropdown">
<button className="add-global-book-btn" onClick={toggleAddWorldBookDropdown}>
+ 添加
</button>
{showAddWorldBookDropdown && (
<div className="dropdown-menu">
<div className="dropdown-item" onClick={handleCreateWorldBook}>
新建世界书
</div>
{worldBooks
.filter(book => !globalWorldBooks.find(gb => gb.uid === book.uid))
.map(book => (
<div
key={book.uid}
className="dropdown-item"
onClick={() => {
addGlobalWorldBook(book.uid);
toggleAddWorldBookDropdown(false);
}}
>
{book.name}
{/* 全局世界书区域 */}
<div className="global-worldbooks-section">
<div className="global-worldbooks-slot">
<div className="global-books-header">
<span className="title-text">全局世界书</span>
{globalWorldBooks.length > 0 && (
<div className="active-books-list">
{globalWorldBooks.map(book => (
<span
key={book.uid}
className="active-book-item"
onClick={() => selectWorldBook(book.uid)}
>
{book.name}
<span
className="remove-btn"
onClick={(e) => {
e.stopPropagation();
removeGlobalWorldBook(book.uid);
}}
>
</span>
</span>
))}
</div>
))}
)}
</div>
</div>
{/* 操作按钮组 */}
<div className="worldbook-actions">
<button className="action-btn" onClick={handleCreateWorldBook}>
+ 新建
</button>
<button className="action-btn">
📋 复制
</button>
<button className="action-btn">
📥 导入
</button>
<button className="action-btn">
📤 导出
</button>
</div>
)}
</div>
</div>
<div className="global-books-list">
{globalWorldBooks.map(book => (
<div
key={book.uid}
className={`global-book-tag ${selectedWorldBook?.uid === book.uid ? 'active' : ''}`}
onClick={() => selectWorldBook(book.uid)}
>
{book.name}
{selectedWorldBook?.uid === book.uid && (
<span
className="remove-btn"
onClick={(e) => {
e.stopPropagation();
removeGlobalWorldBook(book.uid);
}}
>
</span>
)}
</div>
))}
</div>
</div>
{/* 世界书选择区域 */}
<div className="worldbook-selector">

File diff suppressed because it is too large Load Diff

View File

@@ -5,93 +5,104 @@
padding: 12px;
gap: 12px;
overflow: hidden;
background: rgba(0, 0, 0, 0.3);
}
/* 全局世界书槽位 */
.global-worldbooks-slot {
/* 全局世界书区域 */
.global-worldbooks-section {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
background: rgba(0, 0, 0, 0.4);
}
.global-worldbooks-slot {
background: white;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.2);
border: 1px solid #e0e0e0;
overflow: hidden;
}
.global-books-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: rgba(255, 255, 255, 0.95);
margin-bottom: 4px;
flex-direction: column;
gap: 4px;
padding: 8px 12px;
}
.title-text {
font-size: 11px;
color: #777;
font-weight: 500;
}
.global-books-list {
.active-books-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
min-height: 28px;
gap: 4px;
}
.global-book-tag {
.active-book-item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: rgba(100, 149, 237, 0.25);
border: 1px solid rgba(100, 149, 237, 0.5);
border-radius: 4px;
gap: 4px;
font-size: 12px;
color: rgba(255, 255, 255, 1);
color: #4a6cf7;
font-weight: 500;
padding: 2px 6px;
background: rgba(74, 108, 247, 0.1);
border-radius: 3px;
cursor: pointer;
transition: all 0.15s ease;
font-weight: 500;
}
.global-book-tag:hover {
background: rgba(100, 149, 237, 0.4);
border-color: rgba(100, 149, 237, 0.7);
.active-book-item:hover {
background: rgba(74, 108, 247, 0.2);
}
.global-book-tag.active {
background: rgba(100, 149, 237, 0.5);
border-color: rgba(100, 149, 237, 0.8);
box-shadow: 0 0 8px rgba(100, 149, 237, 0.3);
}
.global-book-tag .remove-btn {
opacity: 0.9;
.active-book-item .remove-btn {
opacity: 0.7;
cursor: pointer;
transition: opacity 0.15s ease;
font-weight: bold;
}
.global-book-tag .remove-btn:hover {
.active-book-item .remove-btn:hover {
opacity: 1;
color: rgba(255, 107, 107, 1);
color: #e74c3c;
}
.add-global-book-btn {
padding: 4px 8px;
background: rgba(100, 149, 237, 0.3);
border: 1px solid rgba(100, 149, 237, 0.5);
/* 操作按钮组 */
.worldbook-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.action-btn {
padding: 5px 10px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
color: rgba(255, 255, 255, 1);
color: #555;
cursor: pointer;
font-size: 12px;
transition: all 0.15s ease;
font-weight: 500;
flex: 1;
min-width: 60px;
}
.add-global-book-btn:hover {
background: rgba(100, 149, 237, 0.5);
border-color: rgba(100, 149, 237, 0.8);
.action-btn:hover {
background: #f5f5f5;
border-color: #4a6cf7;
color: #4a6cf7;
}
/* 世界书选择区域 */
.worldbook-selector {
display: flex;
gap: 8px;
align-items: center;
}
/* 下拉菜单 */
.dropdown {
position: relative;
}
@@ -99,10 +110,10 @@
.dropdown-btn {
width: 100%;
padding: 8px 10px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
color: rgba(255, 255, 255, 0.95);
color: #333;
cursor: pointer;
text-align: left;
display: flex;
@@ -114,8 +125,8 @@
}
.dropdown-btn:hover {
background: rgba(0, 0, 0, 0.4);
border-color: rgba(255, 255, 255, 0.3);
background: #f5f5f5;
border-color: #4a6cf7;
}
.dropdown-menu {
@@ -123,14 +134,14 @@
top: 100%;
left: 0;
right: 0;
background: rgba(10, 10, 10, 0.98);
border: 1px solid rgba(255, 255, 255, 0.2);
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
margin-top: 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.dropdown-item {
@@ -138,24 +149,17 @@
cursor: pointer;
transition: background 0.15s ease;
font-size: 13px;
color: rgba(255, 255, 255, 0.95);
color: #333;
font-weight: 500;
}
.dropdown-item:hover {
background: rgba(100, 149, 237, 0.3);
background: #f5f5f5;
}
.dropdown-item.active {
background: rgba(100, 149, 237, 0.4);
color: rgba(255, 255, 255, 1);
}
/* 世界书选择区域 */
.worldbook-selector {
display: flex;
gap: 8px;
align-items: center;
background: rgba(74, 108, 247, 0.1);
color: #4a6cf7;
}
/* 条目列表区域 */
@@ -169,48 +173,55 @@
.entry-item {
padding: 10px;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.15);
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
}
.entry-item:hover {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.25);
background: #f9f9f9;
border-color: #4a6cf7;
}
.entry-item.active {
background: rgba(100, 149, 237, 0.2);
border-color: rgba(100, 149, 237, 0.5);
background: rgba(74, 108, 247, 0.1);
border-color: #4a6cf7;
}
.entry-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.entry-name {
font-weight: 600;
font-size: 14px;
color: rgba(255, 255, 255, 0.95);
color: #333;
}
.entry-status {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
color: #777;
padding: 2px 6px;
border-radius: 3px;
background: rgba(0, 0, 0, 0.3);
background: #f5f5f5;
font-weight: 500;
}
.entry-status.enabled {
color: rgba(100, 255, 149, 1);
background: rgba(100, 255, 149, 0.2);
color: #4a90e2;
background: rgba(74, 144, 226, 0.1);
}
.entry-meta {
display: flex;
gap: 12px;
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
color: #777;
font-weight: 500;
}
@@ -221,13 +232,14 @@
right: 0;
bottom: 0;
left: 300px;
background: rgba(20, 20, 20, 0.98);
background: white;
z-index: 1000;
padding: 16px;
overflow-y: auto;
transform: translateX(100%);
transition: transform 0.2s ease-out;
border-left: 1px solid rgba(255, 255, 255, 0.1);
border-left: 1px solid #e0e0e0;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
}
.edit-panel.open {
@@ -240,19 +252,20 @@
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: 1px solid #e0e0e0;
}
.edit-panel-header h2 {
font-size: 16px;
color: rgba(255, 255, 255, 0.9);
color: #333;
margin: 0;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
color: #999;
font-size: 20px;
cursor: pointer;
padding: 4px;
@@ -260,7 +273,7 @@
}
.close-btn:hover {
color: rgba(255, 255, 255, 0.9);
color: #333;
}
.form-group {
@@ -271,7 +284,8 @@
display: block;
margin-bottom: 6px;
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
color: #555;
font-weight: 500;
}
.form-input,
@@ -279,10 +293,10 @@
.form-select {
width: 100%;
padding: 8px 10px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
background: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
color: rgba(255, 255, 255, 0.9);
color: #333;
font-size: 13px;
transition: all 0.15s ease;
}
@@ -291,8 +305,8 @@
.form-textarea:focus,
.form-select:focus {
outline: none;
background: rgba(255, 255, 255, 0.08);
border-color: rgba(100, 149, 237, 0.5);
border-color: #4a6cf7;
box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1);
}
.form-textarea {
@@ -306,7 +320,8 @@
gap: 8px;
cursor: pointer;
font-size: 13px;
color: rgba(255, 255, 255, 0.9);
color: #555;
font-weight: 500;
}
.form-checkbox input {
@@ -321,30 +336,25 @@
cursor: pointer;
font-size: 13px;
transition: all 0.15s ease;
font-weight: 500;
}
.btn-primary {
background: rgba(100, 149, 237, 0.3);
border: 1px solid rgba(100, 149, 237, 0.5);
color: rgba(255, 255, 255, 1);
font-weight: 500;
background: #4a6cf7;
color: white;
}
.btn-primary:hover {
background: rgba(100, 149, 237, 0.5);
border-color: rgba(100, 149, 237, 0.8);
background: #3a5ce5;
}
.btn-danger {
background: rgba(255, 107, 107, 0.3);
border: 1px solid rgba(255, 107, 107, 0.5);
color: rgba(255, 255, 255, 1);
font-weight: 500;
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: rgba(255, 107, 107, 0.5);
border-color: rgba(255, 107, 107, 0.8);
background: #c0392b;
}
/* 加载和错误状态 */
@@ -356,52 +366,13 @@
}
.loading {
color: rgba(255, 255, 255, 0.8);
color: #777;
font-weight: 500;
}
.error {
color: rgba(255, 107, 107, 1);
background: rgba(255, 107, 107, 0.2);
color: #e74c3c;
background: rgba(231, 76, 60, 0.1);
border-radius: 4px;
font-weight: 500;
}
.form-label {
display: block;
margin-bottom: 6px;
font-size: 13px;
color: rgba(255, 255, 255, 0.95);
font-weight: 500;
}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 8px 10px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
color: rgba(255, 255, 255, 0.95);
font-size: 13px;
transition: all 0.15s ease;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
outline: none;
background: rgba(0, 0, 0, 0.4);
border-color: rgba(100, 149, 237, 0.6);
}
.form-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 13px;
color: rgba(255, 255, 255, 0.95);
font-weight: 500;
}