重构路由架构,修复导致的前端出错
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user