完成大量美化,zustand迁移,动态表格修复
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import React from 'react';
|
||||
import './SideBarLeft.css';
|
||||
import { useSideBarLeftStore } from '../../Store/indexStore';
|
||||
import useSideBarRightStore from '../../Store/SideBarLeft/SideBarLeftSlice';
|
||||
import usePresetStore from '../../Store/SideBarLeft/PresetSlice';
|
||||
import Gallery from './tabs/Gallery';
|
||||
import CharacterCard from './tabs/CharacterCard';
|
||||
import ApiConfig from './tabs/ApiConfig';
|
||||
@@ -11,6 +11,17 @@ import WorldBook from './tabs/WorldBook';
|
||||
|
||||
const SideBarLeft = () => {
|
||||
const { activeTab, tabs, setActiveTab } = useSideBarLeftStore();
|
||||
const { fetchPresets } = usePresetStore();
|
||||
|
||||
// 处理标签切换
|
||||
const handleTabClick = (tabId) => {
|
||||
setActiveTab(tabId);
|
||||
|
||||
// 如果切换到预设标签,刷新预设列表
|
||||
if (tabId === 'presets') {
|
||||
fetchPresets();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sidebar-left">
|
||||
@@ -19,7 +30,7 @@ const SideBarLeft = () => {
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`tab-button ${activeTab === tab.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
title={tab.title}
|
||||
>
|
||||
{tab.label}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* ApiConfig - Compact Modern Design */
|
||||
.api-config-container {
|
||||
padding: var(--spacing-md);
|
||||
padding: 0; /* sidebar-content已有padding,这里不需要 */
|
||||
color: var(--color-text-primary);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 6px;
|
||||
padding: 0; /* sidebar-content已有padding,这里不需要 */
|
||||
gap: 6px;
|
||||
background: var(--color-bg-primary);
|
||||
overflow-y: auto;
|
||||
@@ -101,8 +101,8 @@
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 6px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); /* 减小最小宽度,让窄屏也能显示2列 */
|
||||
gap: 8px; /* 增加间距,让卡片更有呼吸感 */
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
@@ -110,8 +110,8 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 140px; /* 固定宽度 */
|
||||
height: 200px; /* 固定高度 */
|
||||
width: 100%; /* 改为自适应宽度 */
|
||||
height: 170px; /* 减小高度,从200px降到170px */
|
||||
background: var(--color-bg-secondary);
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
@@ -140,7 +140,7 @@
|
||||
/* 角色头像 */
|
||||
.character-avatar-wrapper {
|
||||
width: 100%;
|
||||
aspect-ratio: 3/4;
|
||||
aspect-ratio: 3/4; /* 保持3:4比例 */
|
||||
position: relative;
|
||||
background: var(--color-bg-tertiary);
|
||||
overflow: hidden;
|
||||
@@ -170,24 +170,24 @@
|
||||
|
||||
/* 角色信息 */
|
||||
.character-info {
|
||||
padding: 6px;
|
||||
padding: 5px; /* 减小padding,从6px降到5px */
|
||||
flex: 1;
|
||||
min-height: 50px;
|
||||
min-height: 40px; /* 减小最小高度,从50px降到40px */
|
||||
/* 移除 user-select,因为已经在父元素设置了 */
|
||||
}
|
||||
|
||||
.character-name {
|
||||
font-size: 12px;
|
||||
font-size: 11px; /* 稍微减小字体,从12px降到11px */
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 3px;
|
||||
margin-bottom: 2px; /* 减小间距 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.character-desc {
|
||||
font-size: 10px;
|
||||
font-size: 9px; /* 减小字体,从10px降到9px */
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
@@ -200,7 +200,7 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
margin-top: 3px;
|
||||
margin-top: 2px; /* 减小间距,从3px降到2px */
|
||||
}
|
||||
|
||||
.tag {
|
||||
@@ -280,18 +280,26 @@
|
||||
background: var(--color-bg-tertiary);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border-light);
|
||||
flex-wrap: wrap; /* 允许换行 */
|
||||
gap: 6px; /* 添加间距 */
|
||||
}
|
||||
|
||||
.edit-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
flex: 1; /* 占据剩余空间 */
|
||||
min-width: 120px; /* 最小宽度,防止过度压缩 */
|
||||
white-space: nowrap; /* 不换行 */
|
||||
overflow: hidden; /* 超出隐藏 */
|
||||
text-overflow: ellipsis; /* 显示省略号 */
|
||||
}
|
||||
|
||||
.edit-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap; /* 按钮也可以换行 */
|
||||
}
|
||||
|
||||
.export-format-selector {
|
||||
@@ -409,6 +417,12 @@
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 10px;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 聊天选择器弹窗 */
|
||||
.chat-selector-overlay {
|
||||
position: fixed;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
1// frontend-react/src/components/SideBarLeft/tabs/CharacterCard/CharacterCard.jsx
|
||||
import React, { useState, useEffect, useRef, useCallback, memo } from 'react';
|
||||
import { useCharacterStore } from '../../../../Store/SideBarLeft';
|
||||
import React, { useState, useEffect, useRef, useCallback, memo } from 'react'; // ✅ 保留 useState 用于 exportFormat
|
||||
import { useCharacterStore, useCharacterCardUIStore } from '../../../../Store/SideBarLeft'; // ✅ 新增
|
||||
import useChatBoxStore from '../../../../Store/Mid/ChatBoxSlice';
|
||||
import './CharacterCard.css';
|
||||
|
||||
@@ -76,23 +76,32 @@ const CharacterCard = () => {
|
||||
selectedCharacter,
|
||||
isLoading,
|
||||
error,
|
||||
currentPage,
|
||||
pageSize,
|
||||
characterChats,
|
||||
fetchCharacters,
|
||||
selectCharacter,
|
||||
deleteCharacter,
|
||||
exportCharacterAsPng,
|
||||
setCurrentPage,
|
||||
setPageSize,
|
||||
getCurrentPageCharacters,
|
||||
getTotalPages,
|
||||
fetchCharacterChats
|
||||
} = useCharacterStore();
|
||||
|
||||
const [filterTag, setFilterTag] = useState('');
|
||||
const [isEditing, setIsEditing] = useState(false); // 是否处于编辑模式
|
||||
const [editForm, setEditForm] = useState(null); // 编辑表单数据
|
||||
// ✅ 从 CharacterCardUIStore 获取 UI 状态
|
||||
const {
|
||||
filterTag,
|
||||
isEditing,
|
||||
editForm,
|
||||
currentPage,
|
||||
pageSize,
|
||||
setFilterTag,
|
||||
clearFilter,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
updateEditForm,
|
||||
setCurrentPage,
|
||||
setPageSize,
|
||||
nextPage,
|
||||
prevPage
|
||||
} = useCharacterCardUIStore();
|
||||
|
||||
const [exportFormat, setExportFormat] = useState('png'); // 导出格式: 'png' 或 'json'
|
||||
const fileInputRef = useRef(null);
|
||||
const clickTimeoutRef = useRef(null);
|
||||
@@ -117,11 +126,11 @@ const CharacterCard = () => {
|
||||
? characters.filter(char => (char.tags || []).includes(filterTag))
|
||||
: characters;
|
||||
|
||||
// 获取当前页的角色数据
|
||||
const currentPageCharacters = getCurrentPageCharacters();
|
||||
|
||||
// 获取总页数
|
||||
const totalPages = getTotalPages();
|
||||
// ✅ 计算当前页和总页数
|
||||
const totalPages = Math.ceil(filteredCharacters.length / pageSize) || 1;
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const currentPageCharacters = filteredCharacters.slice(startIndex, endIndex);
|
||||
|
||||
// 处理导入文件
|
||||
const handleImport = async (event) => {
|
||||
@@ -161,7 +170,7 @@ const CharacterCard = () => {
|
||||
first_mes: '',
|
||||
mes_example: '',
|
||||
categories: [],
|
||||
tags: []
|
||||
tags: [] // ✅ 使用标签数组
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('创建失败:', err);
|
||||
@@ -247,17 +256,7 @@ const CharacterCard = () => {
|
||||
selectCharacter(character);
|
||||
|
||||
// 进入编辑模式
|
||||
setIsEditing(true);
|
||||
setEditForm({
|
||||
name: character.name,
|
||||
description: character.description || '',
|
||||
personality: character.personality || '',
|
||||
scenario: character.scenario || '',
|
||||
first_mes: character.first_mes || '',
|
||||
mes_example: character.mes_example || '',
|
||||
categories: character.categories || [],
|
||||
tags: character.tags || []
|
||||
});
|
||||
startEditing(character); // ✅ 使用 store 方法
|
||||
|
||||
// 获取该角色的聊天列表,并自动切换到最后一个聊天
|
||||
try {
|
||||
@@ -267,38 +266,19 @@ const CharacterCard = () => {
|
||||
if (chats.length > 0) {
|
||||
// 有聊天记录,切换到最后一个
|
||||
const lastChat = chats[chats.length - 1].chat_name;
|
||||
|
||||
// 先设置角色和聊天,这会触发 ChatBoxStore 的监听器自动加载聊天历史
|
||||
useChatBoxStore.getState().setChatBoxRoleAndChat(character.name, lastChat);
|
||||
|
||||
console.log(`[CharacterCard] 已切换到角色 ${character.name} 的聊天: ${lastChat}`);
|
||||
} else {
|
||||
// 没有聊天记录,自动创建一个默认聊天
|
||||
console.log(`[CharacterCard] 角色 ${character.name} 没有聊天,创建默认聊天...`);
|
||||
// 没有聊天记录,只设置角色,不创建聊天文件
|
||||
// 聊天文件将在用户发送第一条消息时创建
|
||||
console.log(`[CharacterCard] 角色 ${character.name} 没有聊天,等待用户发送第一条消息...`);
|
||||
|
||||
const defaultChatName = '默认聊天';
|
||||
|
||||
try {
|
||||
const createResponse = await fetch(`/api/chat/${encodeURIComponent(character.name)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chat_name: defaultChatName,
|
||||
metadata: {
|
||||
user_name: 'User',
|
||||
character_name: character.name
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (createResponse.ok || createResponse.status === 400) {
|
||||
// 400 表示聊天已存在,也可以直接使用
|
||||
useChatBoxStore.getState().setChatBoxRoleAndChat(character.name, defaultChatName);
|
||||
console.log(`[CharacterCard] 已为角色 ${character.name} 设置聊天: ${defaultChatName}`);
|
||||
} else {
|
||||
console.error('[CharacterCard] 创建默认聊天失败:', await createResponse.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[CharacterCard] 创建聊天异常:', error);
|
||||
}
|
||||
// 只设置角色,不设置聊天(currentChat 为 null)
|
||||
// ChatBox 会显示开场白(临时消息),但不会保存到文件
|
||||
useChatBoxStore.getState().setChatBoxRoleAndChat(character.name, null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -312,8 +292,7 @@ const CharacterCard = () => {
|
||||
|
||||
// 退出编辑模式
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
setEditForm(null);
|
||||
cancelEditing(); // ✅ 使用 store 方法
|
||||
};
|
||||
|
||||
// 保存编辑
|
||||
@@ -323,8 +302,7 @@ const CharacterCard = () => {
|
||||
try {
|
||||
const { updateCharacter } = useCharacterStore.getState();
|
||||
await updateCharacter(selectedCharacter.name, editForm);
|
||||
setIsEditing(false);
|
||||
setEditForm(null);
|
||||
cancelEditing(); // ✅ 使用 store 方法
|
||||
} catch (err) {
|
||||
console.error('保存失败:', err);
|
||||
alert('保存失败: ' + err.message);
|
||||
@@ -333,10 +311,7 @@ const CharacterCard = () => {
|
||||
|
||||
// 处理表单字段变化
|
||||
const handleFormChange = (field, value) => {
|
||||
setEditForm(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
updateEditForm(field, value); // ✅ 使用 store 方法
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -485,6 +460,24 @@ const CharacterCard = () => {
|
||||
placeholder="标签1, 标签2, 标签3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>动态表格标签 (SillyTavern 关键字机制)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.tags?.join(', ') || ''}
|
||||
onChange={(e) => {
|
||||
// 支持多种分隔符:逗号、分号、空格
|
||||
const tagsList = e.target.value
|
||||
.split(/[,;\s]+/)
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag !== '');
|
||||
handleFormChange('tags', tagsList);
|
||||
}}
|
||||
placeholder="力量:80, 敏捷:65, 智力:90, HP:100"
|
||||
/>
|
||||
<small className="form-hint">每个标签是一个键值对,用逗号/分号/空格分隔。双击右侧表格中的标签可编辑。</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -514,7 +507,7 @@ const CharacterCard = () => {
|
||||
<div className="pagination-controls">
|
||||
<button
|
||||
className="pagination-btn"
|
||||
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
||||
onClick={prevPage} // ✅ 使用 store 方法
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
上一页
|
||||
@@ -526,7 +519,7 @@ const CharacterCard = () => {
|
||||
|
||||
<button
|
||||
className="pagination-btn"
|
||||
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
|
||||
onClick={nextPage} // ✅ 使用 store 方法
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
下一页
|
||||
@@ -535,7 +528,7 @@ const CharacterCard = () => {
|
||||
<select
|
||||
className="page-size-selector"
|
||||
value={pageSize}
|
||||
onChange={(e) => setPageSize(Number(e.target.value))}
|
||||
onChange={(e) => setPageSize(Number(e.target.value))} // ✅ 使用 store 方法
|
||||
>
|
||||
<option value={8}>8条/页</option>
|
||||
<option value={12}>12条/页</option>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* 主容器 */
|
||||
/* 主容器 - sidebar-content已有padding,这里不需要 */
|
||||
.preset-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
gap: 8px;
|
||||
background: var(--color-bg-subtle);
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -25,34 +25,27 @@
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
/* 预设头部 */
|
||||
/* 预设头部 - 紧凑布局 */
|
||||
.preset-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.preset-select-container {
|
||||
.preset-select-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.preset-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.preset-select {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
padding: 5px 8px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
@@ -79,15 +72,14 @@
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.preset-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
/* 操作下拉菜单 */
|
||||
.actions-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preset-action-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
.dropdown-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -97,17 +89,54 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.preset-action-btn:hover {
|
||||
.dropdown-toggle:hover {
|
||||
background: var(--color-bg-tertiary);
|
||||
border-color: var(--color-border-focus);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.preset-action-btn:active {
|
||||
transform: translateY(0);
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 100;
|
||||
min-width: 140px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dropdown-item:hover:not(:disabled) {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.dropdown-item:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 对话框 */
|
||||
@@ -293,10 +322,10 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 参数设置区域 */
|
||||
/* 参数设置区域 - 紧凑布局 */
|
||||
.preset-parameters-container {
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--shadow-xs);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -305,10 +334,11 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
padding: 8px 10px;
|
||||
background: var(--color-bg-tertiary);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
gap: 8px; /* 标题和参数之间的间距 */
|
||||
}
|
||||
|
||||
.parameters-header:hover {
|
||||
@@ -316,9 +346,44 @@
|
||||
}
|
||||
|
||||
.parameters-header span:first-child {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
flex-shrink: 0; /* 不压缩标题 */
|
||||
}
|
||||
|
||||
/* 标题栏中的参数 */
|
||||
.header-parameter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex: 1; /* 占据中间空间 */
|
||||
justify-content: center; /* 居中显示 */
|
||||
}
|
||||
|
||||
.header-param-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
cursor: help;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-param-input {
|
||||
width: 45px;
|
||||
padding: 3px 4px;
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.header-param-input:focus {
|
||||
outline: none;
|
||||
border-color: #4a6cf7;
|
||||
box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1);
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
@@ -332,162 +397,62 @@
|
||||
}
|
||||
|
||||
.preset-parameters {
|
||||
padding: 12px;
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); /* 减小最小宽度,让窄屏也能显示多列 */
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.parameter-row {
|
||||
.parameter-row-compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
flex-direction: row; /* 改为横向布局 */
|
||||
align-items: center; /* 垂直居中 */
|
||||
justify-content: space-between; /* 两端对齐 */
|
||||
gap: 4px; /* 参数名和输入框之间的间距 */
|
||||
min-width: 0; /* 允许收缩 */
|
||||
padding: 2px 4px; /* 内边距 */
|
||||
background: var(--color-bg-tertiary); /* 添加背景色,形成容器感 */
|
||||
border: 1px solid var(--color-border); /* 添加边框 */
|
||||
border-radius: 4px; /* 圆角 */
|
||||
}
|
||||
|
||||
.parameter-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.parameter-label {
|
||||
width: 100px;
|
||||
font-size: 12px;
|
||||
.parameter-label-compact {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
cursor: help;
|
||||
white-space: nowrap; /* 防止参数名换行 */
|
||||
overflow: hidden; /* 超出隐藏 */
|
||||
text-overflow: ellipsis; /* 省略号 */
|
||||
flex: 1; /* 占据剩余空间 */
|
||||
min-width: 0; /* 允许收缩 */
|
||||
}
|
||||
|
||||
.parameter-slider {
|
||||
flex: 1;
|
||||
-webkit-appearance: none;
|
||||
height: 4px;
|
||||
background: #e9ecef;
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.parameter-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #4a6cf7;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.parameter-slider::-webkit-slider-thumb:hover {
|
||||
background: #3a5ce5;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.parameter-slider::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #4a6cf7;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.parameter-slider::-moz-range-thumb:hover {
|
||||
background: #3a5ce5;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.parameter-number {
|
||||
width: 60px;
|
||||
padding: 4px 6px;
|
||||
background: var(--color-bg-tertiary);
|
||||
.parameter-input-compact {
|
||||
width: 45px; /* 固定小宽度 */
|
||||
padding: 3px 4px; /* 减小padding */
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0; /* 不压缩输入框 */
|
||||
}
|
||||
|
||||
.parameter-number:focus {
|
||||
.parameter-input-compact:focus {
|
||||
outline: none;
|
||||
border-color: #4a6cf7;
|
||||
box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1);
|
||||
}
|
||||
|
||||
.parameter-input {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.parameter-input:focus {
|
||||
outline: none;
|
||||
border-color: #4a6cf7;
|
||||
box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.1);
|
||||
}
|
||||
|
||||
.parameter-toggles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.toggle-checkbox {
|
||||
width: 38px;
|
||||
height: 20px;
|
||||
-webkit-appearance: none;
|
||||
background: #e9ecef;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-checkbox::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: 50%;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.toggle-checkbox:checked {
|
||||
background: #4a6cf7;
|
||||
}
|
||||
|
||||
.toggle-checkbox:checked::after {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
/* 预设组件区域 */
|
||||
/* 预设组件区域 - 减少padding */
|
||||
.preset-components-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: 6px;
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--shadow-xs);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -496,25 +461,25 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
padding: 8px 10px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.components-header h3 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.add-component-btn {
|
||||
padding: 4px 10px;
|
||||
padding: 3px 8px;
|
||||
background: #4a6cf7;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
@@ -529,7 +494,7 @@
|
||||
.components-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.drag-indicator {
|
||||
@@ -548,11 +513,11 @@
|
||||
.prompt-component-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 6px;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 4px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: grab;
|
||||
}
|
||||
@@ -587,14 +552,14 @@
|
||||
.component-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
color: #adb5bd;
|
||||
cursor: grab;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
user-select: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
@@ -604,8 +569,8 @@
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -613,7 +578,7 @@
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
font-size: 9px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -633,7 +598,7 @@
|
||||
}
|
||||
|
||||
.component-name {
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
flex: 1;
|
||||
@@ -688,6 +653,69 @@
|
||||
border-color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* 分页控件 */
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 6px;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
padding: 5px 12px;
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.15s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
background: var(--color-bg-elevated);
|
||||
border-color: var(--color-border-focus);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-size-selector {
|
||||
padding: 4px 8px;
|
||||
background: var(--color-bg-primary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.page-size-selector:hover {
|
||||
border-color: var(--color-border-focus);
|
||||
}
|
||||
|
||||
.page-size-selector:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 2px var(--color-accent-light);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import usePresetStore from '../../../../Store/SideBarLeft/PresetSlice';
|
||||
import useSideBarLeftStore from '../../../../Store/SideBarLeft/SideBarLeftSlice';
|
||||
import './Presets.css';
|
||||
|
||||
const PresetPanel = () => {
|
||||
@@ -10,6 +9,8 @@ const PresetPanel = () => {
|
||||
presets,
|
||||
isLoadingPresets,
|
||||
promptComponents,
|
||||
currentPage,
|
||||
pageSize,
|
||||
setSelectedPreset,
|
||||
updateParameter,
|
||||
saveCurrentAsPreset,
|
||||
@@ -22,13 +23,18 @@ const PresetPanel = () => {
|
||||
updateComponent,
|
||||
addComponent,
|
||||
removeComponent,
|
||||
moveComponent
|
||||
moveComponent,
|
||||
setCurrentPage,
|
||||
setPageSize,
|
||||
getCurrentPagePresets,
|
||||
getTotalPages
|
||||
} = usePresetStore();
|
||||
|
||||
const [showSaveDialog, setShowSaveDialog] = useState(false);
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
const [showImportDialog, setShowImportDialog] = useState(false);
|
||||
const [showComponentEditDialog, setShowComponentEditDialog] = useState(false);
|
||||
const [showActionsMenu, setShowActionsMenu] = useState(false);
|
||||
const [newPresetName, setNewPresetName] = useState('');
|
||||
const [editPresetId, setEditPresetId] = useState('');
|
||||
const [editPresetName, setEditPresetName] = useState('');
|
||||
@@ -44,27 +50,50 @@ const PresetPanel = () => {
|
||||
const [draggedItem, setDraggedItem] = useState(null);
|
||||
const [dragOverItem, setDragOverItem] = useState(null);
|
||||
|
||||
// 获取当前激活的分页
|
||||
const { activeTab } = useSideBarLeftStore();
|
||||
|
||||
// 记录上一次的分页状态
|
||||
const prevActiveTabRef = React.useRef(activeTab);
|
||||
// 点击外部关闭下拉菜单
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (showActionsMenu && !event.target.closest('.actions-dropdown')) {
|
||||
setShowActionsMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [showActionsMenu]);
|
||||
|
||||
// 参数描述映射
|
||||
// 计算分页数据
|
||||
const currentPagePresets = getCurrentPagePresets();
|
||||
const totalPages = getTotalPages();
|
||||
|
||||
// 参数描述映射(全名用于悬浮提示)
|
||||
const parameterDescriptions = {
|
||||
temperature: "生成温度,控制随机性(0-2)",
|
||||
frequency_penalty: "频率惩罚,降低重复token概率",
|
||||
presence_penalty: "存在惩罚,鼓励谈论新话题",
|
||||
top_p: "核采样,控制词汇选择范围",
|
||||
freq_penalty: "频率惩罚,降低重复token概率(-2到2)",
|
||||
pres_penalty: "存在惩罚,鼓励谈论新话题(-2到2)",
|
||||
top_p: "核采样,控制词汇选择范围(0-1)",
|
||||
top_k: "随机采样范围,从概率最高的K个词中选择",
|
||||
max_context: "上下文窗口大小(Token上限)",
|
||||
max_ctx: "上下文窗口大小(Token上限)",
|
||||
max_tokens: "单次回复的最大长度",
|
||||
max_context_unlocked: "是否允许超出限制的上下文",
|
||||
stream_openai: "是否使用流式输出",
|
||||
seed: "随机种子(-1为随机)",
|
||||
n: "生成回复的数量"
|
||||
};
|
||||
|
||||
// 参数简写映射
|
||||
const parameterLabels = {
|
||||
temperature: "温度",
|
||||
freq_penalty: "频惩",
|
||||
pres_penalty: "存惩",
|
||||
top_p: "Top-P",
|
||||
top_k: "Top-K",
|
||||
max_ctx: "上下文",
|
||||
max_tokens: "MaxTok",
|
||||
seed: "种子",
|
||||
n: "数量"
|
||||
};
|
||||
|
||||
// 显示工具提示
|
||||
const showTooltip = (event, content) => {
|
||||
setTooltip({
|
||||
@@ -83,26 +112,25 @@ const PresetPanel = () => {
|
||||
// 处理参数更新
|
||||
const handleParameterChange = (name, value) => {
|
||||
let convertedValue = value;
|
||||
if (name === 'temperature' || name === 'frequency_penalty' || name === 'presence_penalty' || name === 'top_p') {
|
||||
if (name === 'temperature' || name === 'freq_penalty' || name === 'pres_penalty' || name === 'top_p') {
|
||||
convertedValue = parseFloat(value);
|
||||
} else if (name === 'top_k' || name === 'max_context' || name === 'max_tokens' || name === 'seed' || name === 'n') {
|
||||
} else if (name === 'top_k' || name === 'max_ctx' || name === 'max_tokens' || name === 'seed' || name === 'n') {
|
||||
convertedValue = parseInt(value, 10);
|
||||
} else if (name === 'max_context_unlocked' || name === 'stream_openai') {
|
||||
convertedValue = value;
|
||||
}
|
||||
|
||||
updateParameter({ name, value: convertedValue });
|
||||
// 将简写参数名转换回原始参数名
|
||||
const paramNameMap = {
|
||||
'freq_penalty': 'frequency_penalty',
|
||||
'pres_penalty': 'presence_penalty',
|
||||
'max_ctx': 'max_context'
|
||||
};
|
||||
|
||||
const actualParamName = paramNameMap[name] || name;
|
||||
updateParameter({ name: actualParamName, value: convertedValue });
|
||||
};
|
||||
|
||||
// 加载预设列表 - 只在切换到预设分页时刷新
|
||||
useEffect(() => {
|
||||
// 检测是否从其他分页切换到预设分页
|
||||
if (activeTab === 'presets' && prevActiveTabRef.current !== 'presets') {
|
||||
fetchPresets();
|
||||
}
|
||||
// 更新上一次的分页状态
|
||||
prevActiveTabRef.current = activeTab;
|
||||
}, [activeTab, fetchPresets]);
|
||||
// 加载预设列表 - 由 SideBarLeft 标签切换时触发
|
||||
// 注意:fetchPresets 已在 SideBarLeft.jsx 的 handleTabClick 中调用
|
||||
|
||||
// 保存当前设置为预设
|
||||
const handleSavePreset = async () => {
|
||||
@@ -111,7 +139,8 @@ const PresetPanel = () => {
|
||||
await saveCurrentAsPreset({ name: newPresetName });
|
||||
setNewPresetName('');
|
||||
setShowSaveDialog(false);
|
||||
// 重新加载预设列表
|
||||
// 重新加载预设列表并重置到第一页
|
||||
setCurrentPage(1);
|
||||
fetchPresets();
|
||||
} catch (error) {
|
||||
console.error('保存预设失败:', error);
|
||||
@@ -141,21 +170,73 @@ const PresetPanel = () => {
|
||||
const handleImportPreset = async () => {
|
||||
try {
|
||||
const importedPreset = JSON.parse(importPresetData);
|
||||
if (importedPreset.name && importedPreset.parameters) {
|
||||
await saveCurrentAsPreset({ name: importedPreset.name });
|
||||
|
||||
// 支持内部结构和SillyTavern结构
|
||||
let presetName = '';
|
||||
let importedParameters = {};
|
||||
let importedComponents = [];
|
||||
|
||||
// 检测是否为内部结构(有 entries 字段)
|
||||
if (importedPreset.entries && Array.isArray(importedPreset.entries)) {
|
||||
// 内部结构
|
||||
presetName = importedPreset.name;
|
||||
importedParameters = {
|
||||
temperature: importedPreset.temperature,
|
||||
top_p: importedPreset.topP,
|
||||
top_k: importedPreset.topK,
|
||||
frequency_penalty: importedPreset.frequencyPenalty,
|
||||
presence_penalty: importedPreset.presencePenalty,
|
||||
max_tokens: importedPreset.maxLength
|
||||
};
|
||||
|
||||
// 转换 entries 为 promptComponents 格式
|
||||
importedComponents = importedPreset.entries.map(entry => ({
|
||||
identifier: entry.identifier,
|
||||
name: entry.name,
|
||||
content: entry.content || '',
|
||||
enabled: entry.enabled !== false,
|
||||
role: entry.role === 'system' ? 0 : entry.role === 'user' ? 1 : 2,
|
||||
system_prompt: entry.role === 'system',
|
||||
marker: entry.isSystemNode || false
|
||||
})).sort((a, b) => {
|
||||
const orderA = importedPreset.entries.find(e => e.identifier === a.identifier)?.order || 0;
|
||||
const orderB = importedPreset.entries.find(e => e.identifier === b.identifier)?.order || 0;
|
||||
return orderA - orderB;
|
||||
});
|
||||
}
|
||||
// 兼容SillyTavern结构(有 parameters 和 promptComponents 字段)
|
||||
else if (importedPreset.name && (importedPreset.parameters || importedPreset.temperature)) {
|
||||
presetName = importedPreset.name;
|
||||
importedParameters = importedPreset.parameters || {
|
||||
temperature: importedPreset.temperature,
|
||||
frequency_penalty: importedPreset.frequency_penalty,
|
||||
presence_penalty: importedPreset.presence_penalty,
|
||||
top_p: importedPreset.top_p,
|
||||
top_k: importedPreset.top_k,
|
||||
max_tokens: importedPreset.max_tokens || importedPreset.openai_max_tokens
|
||||
};
|
||||
importedComponents = importedPreset.promptComponents || [];
|
||||
}
|
||||
|
||||
if (presetName) {
|
||||
await saveCurrentAsPreset({ name: presetName });
|
||||
|
||||
// 更新参数
|
||||
Object.keys(importedPreset.parameters).forEach(key => {
|
||||
updateParameter({ name: key, value: importedPreset.parameters[key] });
|
||||
Object.keys(importedParameters).forEach(key => {
|
||||
if (importedParameters[key] !== undefined) {
|
||||
updateParameter({ name: key, value: importedParameters[key] });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新组件列表
|
||||
if (importedPreset.promptComponents) {
|
||||
setPromptComponents(importedPreset.promptComponents);
|
||||
if (importedComponents.length > 0) {
|
||||
setPromptComponents(importedComponents);
|
||||
}
|
||||
|
||||
setImportPresetData('');
|
||||
setShowImportDialog(false);
|
||||
// 重新加载预设列表
|
||||
// 重新加载预设列表并重置到第一页
|
||||
setCurrentPage(1);
|
||||
fetchPresets();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -169,10 +250,31 @@ const PresetPanel = () => {
|
||||
if (selectedPreset) {
|
||||
const preset = presets.find(p => p.id === selectedPreset);
|
||||
if (preset) {
|
||||
// 使用内部专有结构导出
|
||||
const exportData = {
|
||||
...preset,
|
||||
parameters,
|
||||
promptComponents
|
||||
// GenerationPreset 部分
|
||||
id: selectedPreset,
|
||||
name: preset.name,
|
||||
temperature: parameters.temperature,
|
||||
topP: parameters.top_p,
|
||||
topK: parameters.top_k,
|
||||
frequencyPenalty: parameters.frequency_penalty,
|
||||
presencePenalty: parameters.presence_penalty,
|
||||
maxLength: parameters.max_tokens,
|
||||
isDefault: false,
|
||||
|
||||
// PromptPresetView 部分
|
||||
characterId: 'global',
|
||||
entries: promptComponents.map((component, index) => ({
|
||||
identifier: component.identifier,
|
||||
name: component.name,
|
||||
enabled: component.enabled !== false,
|
||||
content: component.content || '',
|
||||
order: index,
|
||||
role: component.role === 0 ? 'system' : component.role === 1 ? 'user' : 'ai',
|
||||
tokenCount: component.content ? component.content.length : 0,
|
||||
isSystemNode: component.marker || false
|
||||
}))
|
||||
};
|
||||
const dataStr = JSON.stringify(exportData, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
@@ -208,11 +310,24 @@ const PresetPanel = () => {
|
||||
setShowComponentEditDialog(true);
|
||||
};
|
||||
|
||||
// 查看组件内容
|
||||
// 查看/编辑组件内容 - 实时获取引用内容
|
||||
const handleViewComponent = (index) => {
|
||||
const component = promptComponents[index];
|
||||
|
||||
// 如果是固定组件,实时从其他前端部件获取内容
|
||||
if (component.marker) {
|
||||
// TODO: 根据component.identifier从对应的Store中实时获取内容
|
||||
// 例如:chatHistory -> 从ChatBoxSlice获取当前聊天记录
|
||||
// charDescription -> 从CharacterSlice获取角色描述
|
||||
// 这里先显示提示信息
|
||||
setEditComponentContent(`[实时内容将从 ${component.name} 动态加载]\n\n当前暂不支持预览,该组件会在发送消息时自动从系统获取最新内容。`);
|
||||
} else {
|
||||
// 可编辑组件,显示保存的内容
|
||||
setEditComponentContent(component.content || '');
|
||||
}
|
||||
|
||||
setEditingComponentIndex(index);
|
||||
setEditComponentContent(promptComponents[index].content);
|
||||
setIsEditing(false);
|
||||
setIsEditing(!component.marker); // 固定组件不可编辑
|
||||
setShowComponentEditDialog(true);
|
||||
};
|
||||
|
||||
@@ -303,14 +418,7 @@ const PresetPanel = () => {
|
||||
{/* 顶部:预设选择与操作 */}
|
||||
<div className="preset-header">
|
||||
{/* 预设选择下拉框 */}
|
||||
<div className="preset-select-container">
|
||||
<label
|
||||
className="preset-label"
|
||||
onMouseEnter={(e) => showTooltip(e, "选择预设配置")}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
预设:
|
||||
</label>
|
||||
<div className="preset-select-wrapper">
|
||||
<select
|
||||
className="preset-select"
|
||||
value={selectedPreset}
|
||||
@@ -318,55 +426,76 @@ const PresetPanel = () => {
|
||||
disabled={isLoadingPresets}
|
||||
>
|
||||
<option value="">{isLoadingPresets ? "加载中..." : "选择预设..."}</option>
|
||||
{presets.map(preset => (
|
||||
{currentPagePresets.map(preset => (
|
||||
<option key={preset.id} value={preset.id}>{preset.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="preset-actions">
|
||||
<button
|
||||
className="preset-action-btn"
|
||||
onClick={() => setShowSaveDialog(true)}
|
||||
onMouseEnter={(e) => showTooltip(e, "保存当前设置为新预设")}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
💾
|
||||
</button>
|
||||
<button
|
||||
className="preset-action-btn"
|
||||
onClick={() => {
|
||||
if (selectedPreset) {
|
||||
const preset = presets.find(p => p.id === selectedPreset);
|
||||
if (preset) {
|
||||
setEditPresetId(selectedPreset);
|
||||
setEditPresetName(preset.name);
|
||||
setShowEditDialog(true);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onMouseEnter={(e) => showTooltip(e, "编辑当前预设")}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
className="preset-action-btn"
|
||||
onClick={() => setShowImportDialog(true)}
|
||||
onMouseEnter={(e) => showTooltip(e, "导入预设")}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
📥
|
||||
</button>
|
||||
<button
|
||||
className="preset-action-btn"
|
||||
onClick={handleExportPreset}
|
||||
onMouseEnter={(e) => showTooltip(e, "导出当前预设")}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
📤
|
||||
</button>
|
||||
|
||||
{/* 操作下拉菜单 */}
|
||||
<div className="actions-dropdown">
|
||||
<button
|
||||
className="dropdown-toggle"
|
||||
onClick={() => setShowActionsMenu(!showActionsMenu)}
|
||||
onMouseEnter={(e) => showTooltip(e, "更多操作")}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
|
||||
{showActionsMenu && (
|
||||
<div className="dropdown-menu">
|
||||
<button
|
||||
className="dropdown-item"
|
||||
title="将当前的参数设置和组件配置保存为新的预设"
|
||||
onClick={() => {
|
||||
setShowSaveDialog(true);
|
||||
setShowActionsMenu(false);
|
||||
}}
|
||||
>
|
||||
💾 保存为新预设
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
disabled={!selectedPreset}
|
||||
title="修改当前选中预设的名称"
|
||||
onClick={() => {
|
||||
if (selectedPreset) {
|
||||
const preset = presets.find(p => p.id === selectedPreset);
|
||||
if (preset) {
|
||||
setEditPresetId(selectedPreset);
|
||||
setEditPresetName(preset.name);
|
||||
setShowEditDialog(true);
|
||||
}
|
||||
}
|
||||
setShowActionsMenu(false);
|
||||
}}
|
||||
>
|
||||
✏️ 编辑名称
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
title="从 JSON 文件导入预设配置"
|
||||
onClick={() => {
|
||||
setShowImportDialog(true);
|
||||
setShowActionsMenu(false);
|
||||
}}
|
||||
>
|
||||
📥 导入预设
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item"
|
||||
disabled={!selectedPreset}
|
||||
title="将当前预设导出为 JSON 文件"
|
||||
onClick={() => {
|
||||
handleExportPreset();
|
||||
setShowActionsMenu(false);
|
||||
}}
|
||||
>
|
||||
📤 导出预设
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -456,29 +585,38 @@ const PresetPanel = () => {
|
||||
onClick={toggleParametersExpanded}
|
||||
>
|
||||
<span>参数设置</span>
|
||||
{/* 生成数量 - 放在标题栏旁边 */}
|
||||
<div className="header-parameter">
|
||||
<label
|
||||
className="header-param-label"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.n)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
{parameterLabels.n}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={parameters.n}
|
||||
onChange={(e) => handleParameterChange('n', e.target.value)}
|
||||
className="header-param-input"
|
||||
onClick={(e) => e.stopPropagation()} // 防止点击输入框时折叠
|
||||
/>
|
||||
</div>
|
||||
<span className={`expand-icon ${isParametersExpanded ? 'expanded' : ''}`}>▼</span>
|
||||
</div>
|
||||
|
||||
{isParametersExpanded && (
|
||||
<div className="preset-parameters">
|
||||
{/* 温度滑块 */}
|
||||
<div className="parameter-row">
|
||||
{/* 温度输入框 */}
|
||||
<div className="parameter-row-compact">
|
||||
<label
|
||||
className="parameter-label"
|
||||
className="parameter-label-compact"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.temperature)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
Temperature
|
||||
{parameterLabels.temperature}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={parameters.temperature}
|
||||
onChange={(e) => handleParameterChange('temperature', e.target.value)}
|
||||
className="parameter-slider"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -486,86 +624,59 @@ const PresetPanel = () => {
|
||||
step="0.1"
|
||||
value={parameters.temperature}
|
||||
onChange={(e) => handleParameterChange('temperature', e.target.value)}
|
||||
className="parameter-number"
|
||||
className="parameter-input-compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 频率惩罚滑块 */}
|
||||
<div className="parameter-row">
|
||||
{/* 频率惩罚输入框 */}
|
||||
<div className="parameter-row-compact">
|
||||
<label
|
||||
className="parameter-label"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.frequency_penalty)}
|
||||
className="parameter-label-compact"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.freq_penalty)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
Frequency Penalty
|
||||
{parameterLabels.freq_penalty}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={parameters.frequency_penalty}
|
||||
onChange={(e) => handleParameterChange('frequency_penalty', e.target.value)}
|
||||
className="parameter-slider"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={parameters.frequency_penalty}
|
||||
onChange={(e) => handleParameterChange('frequency_penalty', e.target.value)}
|
||||
className="parameter-number"
|
||||
onChange={(e) => handleParameterChange('freq_penalty', e.target.value)}
|
||||
className="parameter-input-compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 存在惩罚滑块 */}
|
||||
<div className="parameter-row">
|
||||
{/* 存在惩罚输入框 */}
|
||||
<div className="parameter-row-compact">
|
||||
<label
|
||||
className="parameter-label"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.presence_penalty)}
|
||||
className="parameter-label-compact"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.pres_penalty)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
Presence Penalty
|
||||
{parameterLabels.pres_penalty}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={parameters.presence_penalty}
|
||||
onChange={(e) => handleParameterChange('presence_penalty', e.target.value)}
|
||||
className="parameter-slider"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={parameters.presence_penalty}
|
||||
onChange={(e) => handleParameterChange('presence_penalty', e.target.value)}
|
||||
className="parameter-number"
|
||||
onChange={(e) => handleParameterChange('pres_penalty', e.target.value)}
|
||||
className="parameter-input-compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Top P 滑块 */}
|
||||
<div className="parameter-row">
|
||||
{/* Top P 输入框 */}
|
||||
<div className="parameter-row-compact">
|
||||
<label
|
||||
className="parameter-label"
|
||||
className="parameter-label-compact"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.top_p)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
Top P
|
||||
{parameterLabels.top_p}
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={parameters.top_p}
|
||||
onChange={(e) => handleParameterChange('top_p', e.target.value)}
|
||||
className="parameter-slider"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -573,55 +684,55 @@ const PresetPanel = () => {
|
||||
step="0.05"
|
||||
value={parameters.top_p}
|
||||
onChange={(e) => handleParameterChange('top_p', e.target.value)}
|
||||
className="parameter-number"
|
||||
className="parameter-input-compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Top K 输入框 */}
|
||||
<div className="parameter-row">
|
||||
<div className="parameter-row-compact">
|
||||
<label
|
||||
className="parameter-label"
|
||||
className="parameter-label-compact"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.top_k)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
Top K
|
||||
{parameterLabels.top_k}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={parameters.top_k}
|
||||
onChange={(e) => handleParameterChange('top_k', e.target.value)}
|
||||
className="parameter-input"
|
||||
className="parameter-input-compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 最大上下文输入框 */}
|
||||
<div className="parameter-row">
|
||||
<div className="parameter-row-compact">
|
||||
<label
|
||||
className="parameter-label"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.max_context)}
|
||||
className="parameter-label-compact"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.max_ctx)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
Max Context
|
||||
{parameterLabels.max_ctx}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000000"
|
||||
value={parameters.max_context}
|
||||
onChange={(e) => handleParameterChange('max_context', e.target.value)}
|
||||
className="parameter-input"
|
||||
onChange={(e) => handleParameterChange('max_ctx', e.target.value)}
|
||||
className="parameter-input-compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 最大Token输入框 */}
|
||||
<div className="parameter-row">
|
||||
<div className="parameter-row-compact">
|
||||
<label
|
||||
className="parameter-label"
|
||||
className="parameter-label-compact"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.max_tokens)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
Max Tokens
|
||||
{parameterLabels.max_tokens}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -629,79 +740,26 @@ const PresetPanel = () => {
|
||||
max="100000"
|
||||
value={parameters.max_tokens}
|
||||
onChange={(e) => handleParameterChange('max_tokens', e.target.value)}
|
||||
className="parameter-input"
|
||||
className="parameter-input-compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 随机种子输入框 */}
|
||||
<div className="parameter-row">
|
||||
<div className="parameter-row-compact">
|
||||
<label
|
||||
className="parameter-label"
|
||||
className="parameter-label-compact"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.seed)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
Seed
|
||||
{parameterLabels.seed}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={parameters.seed}
|
||||
onChange={(e) => handleParameterChange('seed', e.target.value)}
|
||||
className="parameter-input"
|
||||
className="parameter-input-compact"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 生成数量输入框 */}
|
||||
<div className="parameter-row">
|
||||
<label
|
||||
className="parameter-label"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.n)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
N (生成数量)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={parameters.n}
|
||||
onChange={(e) => handleParameterChange('n', e.target.value)}
|
||||
className="parameter-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 开关选项 */}
|
||||
<div className="parameter-toggles">
|
||||
<div className="toggle-row">
|
||||
<label
|
||||
className="toggle-label"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.max_context_unlocked)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
Max Context Unlocked
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={parameters.max_context_unlocked}
|
||||
onChange={(e) => handleParameterChange('max_context_unlocked', e.target.checked)}
|
||||
className="toggle-checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="toggle-row">
|
||||
<label
|
||||
className="toggle-label"
|
||||
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.stream_openai)}
|
||||
onMouseLeave={hideTooltip}
|
||||
>
|
||||
Stream Output
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={parameters.stream_openai}
|
||||
onChange={(e) => handleParameterChange('stream_openai', e.target.checked)}
|
||||
className="toggle-checkbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -793,6 +851,51 @@ const PresetPanel = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分页控件 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="pagination-controls">
|
||||
<button
|
||||
className="pagination-btn"
|
||||
onClick={() => {
|
||||
setCurrentPage(Math.max(1, currentPage - 1));
|
||||
fetchPresets();
|
||||
}}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
|
||||
<span className="pagination-info">
|
||||
第 {currentPage} / {totalPages} 页
|
||||
</span>
|
||||
|
||||
<button
|
||||
className="pagination-btn"
|
||||
onClick={() => {
|
||||
setCurrentPage(Math.min(totalPages, currentPage + 1));
|
||||
fetchPresets();
|
||||
}}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
|
||||
<select
|
||||
className="page-size-selector"
|
||||
value={pageSize}
|
||||
onChange={(e) => {
|
||||
setPageSize(Number(e.target.value));
|
||||
fetchPresets();
|
||||
}}
|
||||
>
|
||||
<option value={8}>8条/页</option>
|
||||
<option value={12}>12条/页</option>
|
||||
<option value={20}>20条/页</option>
|
||||
<option value={50}>50条/页</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
padding: 0; /* sidebar-content已有padding,这里不需要 */
|
||||
gap: 8px;
|
||||
background: var(--color-bg-primary);
|
||||
overflow-y: auto;
|
||||
|
||||
Reference in New Issue
Block a user