Files
SillyTavern_replica/frontend/src/components/SideBarLeft/tabs/Presets/Presets.jsx
T

802 lines
26 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import usePresetStore from '../../../../Store/SideBarLeft/PresetSlice';
import useSideBarLeftStore from '../../../../Store/SideBarLeft/SideBarLeftSlice';
import './Presets.css';
const PresetPanel = () => {
const {
selectedPreset,
parameters,
presets,
isLoadingPresets,
promptComponents,
setSelectedPreset,
updateParameter,
saveCurrentAsPreset,
editPresetName: updatePresetName,
isParametersExpanded,
toggleParametersExpanded,
fetchPresets,
setPromptComponents,
toggleComponentEnabled,
updateComponent,
addComponent,
removeComponent,
moveComponent
} = usePresetStore();
const [showSaveDialog, setShowSaveDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false);
const [showImportDialog, setShowImportDialog] = useState(false);
const [showComponentEditDialog, setShowComponentEditDialog] = useState(false);
const [newPresetName, setNewPresetName] = useState('');
const [editPresetId, setEditPresetId] = useState('');
const [editPresetName, setEditPresetName] = useState('');
const [importPresetData, setImportPresetData] = useState('');
const [tooltip, setTooltip] = useState({ visible: false, content: '', x: 0, y: 0 });
// 组件编辑状态
const [editingComponentIndex, setEditingComponentIndex] = useState(-1);
const [editComponentContent, setEditComponentContent] = useState('');
const [isEditing, setIsEditing] = useState(false);
// 拖拽状态
const [draggedItem, setDraggedItem] = useState(null);
const [dragOverItem, setDragOverItem] = useState(null);
// 获取当前激活的分页
const { activeTab } = useSideBarLeftStore();
// 记录上一次的分页状态
const prevActiveTabRef = React.useRef(activeTab);
// 参数描述映射
const parameterDescriptions = {
temperature: "生成温度,控制随机性(0-2)",
frequency_penalty: "频率惩罚,降低重复token概率",
presence_penalty: "存在惩罚,鼓励谈论新话题",
top_p: "核采样,控制词汇选择范围",
top_k: "随机采样范围,从概率最高的K个词中选择",
max_context: "上下文窗口大小(Token上限)",
max_tokens: "单次回复的最大长度",
max_context_unlocked: "是否允许超出限制的上下文",
stream_openai: "是否使用流式输出",
seed: "随机种子(-1为随机)",
n: "生成回复的数量"
};
// 显示工具提示
const showTooltip = (event, content) => {
setTooltip({
visible: true,
content,
x: event.clientX,
y: event.clientY
});
};
// 隐藏工具提示
const hideTooltip = () => {
setTooltip({ ...tooltip, visible: false });
};
// 处理参数更新
const handleParameterChange = (name, value) => {
let convertedValue = value;
if (name === 'temperature' || name === 'frequency_penalty' || name === 'presence_penalty' || name === 'top_p') {
convertedValue = parseFloat(value);
} else if (name === 'top_k' || name === 'max_context' || 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 });
};
// 加载预设列表 - 只在切换到预设分页时刷新
useEffect(() => {
// 检测是否从其他分页切换到预设分页
if (activeTab === 'presets' && prevActiveTabRef.current !== 'presets') {
fetchPresets();
}
// 更新上一次的分页状态
prevActiveTabRef.current = activeTab;
}, [activeTab, fetchPresets]);
// 保存当前设置为预设
const handleSavePreset = async () => {
if (newPresetName.trim()) {
try {
await saveCurrentAsPreset({ name: newPresetName });
setNewPresetName('');
setShowSaveDialog(false);
// 重新加载预设列表
fetchPresets();
} catch (error) {
console.error('保存预设失败:', error);
alert('保存预设失败: ' + error.message);
}
}
};
// 编辑预设名称
const handleEditPreset = async () => {
if (editPresetId && editPresetName.trim()) {
try {
await updatePresetName(editPresetId, editPresetName);
setEditPresetId('');
setEditPresetName('');
setShowEditDialog(false);
// 重新加载预设列表
fetchPresets();
} catch (error) {
console.error('编辑预设名称失败:', error);
alert('编辑预设名称失败: ' + error.message);
}
}
};
// 导入预设
const handleImportPreset = async () => {
try {
const importedPreset = JSON.parse(importPresetData);
if (importedPreset.name && importedPreset.parameters) {
await saveCurrentAsPreset({ name: importedPreset.name });
// 更新参数
Object.keys(importedPreset.parameters).forEach(key => {
updateParameter({ name: key, value: importedPreset.parameters[key] });
});
// 更新组件列表
if (importedPreset.promptComponents) {
setPromptComponents(importedPreset.promptComponents);
}
setImportPresetData('');
setShowImportDialog(false);
// 重新加载预设列表
fetchPresets();
}
} catch (error) {
console.error('导入预设失败:', error);
alert('导入预设失败: ' + error.message);
}
};
// 导出预设
const handleExportPreset = () => {
if (selectedPreset) {
const preset = presets.find(p => p.id === selectedPreset);
if (preset) {
const exportData = {
...preset,
parameters,
promptComponents
};
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `${preset.name}.json`;
link.click();
URL.revokeObjectURL(url);
}
}
};
// 添加新组件
const handleAddNewComponent = () => {
const newComponent = {
identifier: `component_${Date.now()}`,
name: '新组件',
content: '',
role: 0,
system_prompt: false,
marker: false,
enabled: true
};
addComponent(newComponent);
};
// 开始编辑组件
const handleStartEditComponent = (index) => {
setEditingComponentIndex(index);
setEditComponentContent(promptComponents[index].content);
setIsEditing(true);
setShowComponentEditDialog(true);
};
// 查看组件内容
const handleViewComponent = (index) => {
setEditingComponentIndex(index);
setEditComponentContent(promptComponents[index].content);
setIsEditing(false);
setShowComponentEditDialog(true);
};
// 保存组件编辑
const handleSaveComponentEdit = () => {
if (editingComponentIndex >= 0 && isEditing) {
updateComponent(editingComponentIndex, { content: editComponentContent });
}
setEditingComponentIndex(-1);
setEditComponentContent('');
setShowComponentEditDialog(false);
};
// 取消组件编辑
const handleCancelComponentEdit = () => {
setEditingComponentIndex(-1);
setEditComponentContent('');
setShowComponentEditDialog(false);
};
// 关闭组件查看对话框
const handleCloseComponentView = () => {
setEditingComponentIndex(-1);
setEditComponentContent('');
setShowComponentEditDialog(false);
};
// 切换组件启用状态
const handleToggleComponentEnabled = (index) => {
toggleComponentEnabled(index);
};
// 删除组件
const handleDeleteComponent = (index) => {
if (window.confirm('确定要删除这个组件吗?')) {
removeComponent(index);
}
};
// 拖拽开始
const handleDragStart = (e, index) => {
setDraggedItem(index);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', index.toString());
setTimeout(() => {
e.target.classList.add('dragging');
}, 0);
};
// 拖拽结束
const handleDragEnd = (e) => {
setDraggedItem(null);
setDragOverItem(null);
e.target.classList.remove('dragging');
};
// 拖拽经过
const handleDragOver = (e, index) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (draggedItem === null || draggedItem === index) return;
setDragOverItem(index);
};
// 放置
const handleDrop = (e, index) => {
e.preventDefault();
if (draggedItem === null || draggedItem === index) return;
moveComponent(draggedItem, index);
setDraggedItem(null);
setDragOverItem(null);
};
return (
<div className="preset-panel">
{/* 工具提示 */}
{tooltip.visible && (
<div
className="tooltip"
style={{ left: `${tooltip.x}px`, top: `${tooltip.y}px` }}
>
{tooltip.content}
</div>
)}
{/* 顶部:预设选择与操作 */}
<div className="preset-header">
{/* 预设选择下拉框 */}
<div className="preset-select-container">
<label
className="preset-label"
onMouseEnter={(e) => showTooltip(e, "选择预设配置")}
onMouseLeave={hideTooltip}
>
预设:
</label>
<select
className="preset-select"
value={selectedPreset}
onChange={(e) => setSelectedPreset(e.target.value)}
disabled={isLoadingPresets}
>
<option value="">{isLoadingPresets ? "加载中..." : "选择预设..."}</option>
{presets.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>
</div>
{/* 保存预设对话框 */}
{showSaveDialog && (
<div className="preset-save-dialog">
<input
type="text"
value={newPresetName}
onChange={(e) => setNewPresetName(e.target.value)}
placeholder="预设名称"
/>
<div className="dialog-buttons">
<button onClick={handleSavePreset}>保存</button>
<button onClick={() => setShowSaveDialog(false)}>取消</button>
</div>
</div>
)}
{/* 编辑预设对话框 */}
{showEditDialog && (
<div className="preset-edit-dialog">
<input
type="text"
value={editPresetName}
onChange={(e) => setEditPresetName(e.target.value)}
placeholder="预设名称"
/>
<div className="dialog-buttons">
<button onClick={handleEditPreset}>保存</button>
<button onClick={() => setShowEditDialog(false)}>取消</button>
</div>
</div>
)}
{/* 导入预设对话框 */}
{showImportDialog && (
<div className="preset-import-dialog">
<textarea
className="import-textarea"
value={importPresetData}
onChange={(e) => setImportPresetData(e.target.value)}
placeholder="粘贴预设JSON数据"
rows="5"
/>
<div className="dialog-buttons">
<button onClick={handleImportPreset}>导入</button>
<button onClick={() => setShowImportDialog(false)}>取消</button>
</div>
</div>
)}
{/* 编辑/查看组件内容对话框 */}
{showComponentEditDialog && (
<div className="component-edit-dialog">
<div className="dialog-header">
<h3>{isEditing ? '编辑' : '查看'}组件: {editingComponentIndex >= 0 && promptComponents[editingComponentIndex].name}</h3>
<button className="close-btn" onClick={handleCloseComponentView}>×</button>
</div>
<div className="dialog-content">
<textarea
value={editComponentContent}
onChange={(e) => setEditComponentContent(e.target.value)}
className="component-textarea"
readOnly={!isEditing}
rows={20}
/>
</div>
<div className="dialog-footer">
<span className="token-count">
{editComponentContent ? editComponentContent.length : 0}
</span>
{isEditing && (
<div className="dialog-buttons">
<button onClick={handleSaveComponentEdit}>保存</button>
<button onClick={handleCancelComponentEdit}>取消</button>
</div>
)}
</div>
</div>
)}
{/* 参数设置区域 */}
<div className="preset-parameters-container">
<div
className="parameters-header"
onClick={toggleParametersExpanded}
>
<span>参数设置</span>
<span className={`expand-icon ${isParametersExpanded ? 'expanded' : ''}`}>▼</span>
</div>
{isParametersExpanded && (
<div className="preset-parameters">
{/* 温度滑块 */}
<div className="parameter-row">
<label
className="parameter-label"
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.temperature)}
onMouseLeave={hideTooltip}
>
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"
max="2"
step="0.1"
value={parameters.temperature}
onChange={(e) => handleParameterChange('temperature', e.target.value)}
className="parameter-number"
/>
</div>
{/* 频率惩罚滑块 */}
<div className="parameter-row">
<label
className="parameter-label"
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.frequency_penalty)}
onMouseLeave={hideTooltip}
>
Frequency 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"
/>
</div>
{/* 存在惩罚滑块 */}
<div className="parameter-row">
<label
className="parameter-label"
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.presence_penalty)}
onMouseLeave={hideTooltip}
>
Presence 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"
/>
</div>
{/* Top P 滑块 */}
<div className="parameter-row">
<label
className="parameter-label"
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.top_p)}
onMouseLeave={hideTooltip}
>
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"
max="1"
step="0.05"
value={parameters.top_p}
onChange={(e) => handleParameterChange('top_p', e.target.value)}
className="parameter-number"
/>
</div>
{/* Top K 输入框 */}
<div className="parameter-row">
<label
className="parameter-label"
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.top_k)}
onMouseLeave={hideTooltip}
>
Top K
</label>
<input
type="number"
min="0"
value={parameters.top_k}
onChange={(e) => handleParameterChange('top_k', e.target.value)}
className="parameter-input"
/>
</div>
{/* 最大上下文输入框 */}
<div className="parameter-row">
<label
className="parameter-label"
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.max_context)}
onMouseLeave={hideTooltip}
>
Max Context
</label>
<input
type="number"
min="1"
max="10000000"
value={parameters.max_context}
onChange={(e) => handleParameterChange('max_context', e.target.value)}
className="parameter-input"
/>
</div>
{/* 最大Token输入框 */}
<div className="parameter-row">
<label
className="parameter-label"
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.max_tokens)}
onMouseLeave={hideTooltip}
>
Max Tokens
</label>
<input
type="number"
min="1"
max="100000"
value={parameters.max_tokens}
onChange={(e) => handleParameterChange('max_tokens', e.target.value)}
className="parameter-input"
/>
</div>
{/* 随机种子输入框 */}
<div className="parameter-row">
<label
className="parameter-label"
onMouseEnter={(e) => showTooltip(e, parameterDescriptions.seed)}
onMouseLeave={hideTooltip}
>
Seed
</label>
<input
type="number"
value={parameters.seed}
onChange={(e) => handleParameterChange('seed', e.target.value)}
className="parameter-input"
/>
</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>
{/* 预设组件列表 */}
<div className="preset-components-section">
<div className="components-header">
<h3>预设组件</h3>
<button
onClick={handleAddNewComponent}
className="add-component-btn"
onMouseEnter={(e) => showTooltip(e, "添加新组件")}
onMouseLeave={hideTooltip}
>
+ 添加组件
</button>
</div>
<div className="components-list draggable-container">
{promptComponents.map((component, index) => (
<React.Fragment key={component.identifier}>
{/* 拖拽指示器 - 在组件上方 */}
<div
className={`drag-indicator ${dragOverItem === index ? 'visible' : ''}`}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={(e) => handleDrop(e, index)}
/>
{/* 组件项 */}
<div
className={`prompt-component-item ${!component.enabled ? 'disabled' : ''} ${component.marker ? 'marker' : ''} ${draggedItem === index ? 'dragging' : ''}`}
draggable={!component.marker}
onDragStart={(e) => handleDragStart(e, index)}
onDragEnd={handleDragEnd}
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<div className="component-header">
<div className="component-controls">
<div className="drag-handle">⋮⋮</div>
<button
className={`toggle-btn ${component.enabled ? 'enabled' : 'disabled'}`}
onClick={() => handleToggleComponentEnabled(index)}
onMouseEnter={(e) => showTooltip(e, component.enabled ? "禁用组件" : "启用组件")}
onMouseLeave={hideTooltip}
>
{component.enabled ? '✓' : '○'}
</button>
<span className="component-name">{component.name}</span>
{component.marker && (
<span className="component-marker-badge">🔒</span>
)}
</div>
<div className="component-actions">
<button
className="edit-btn"
onClick={() => handleStartEditComponent(index)}
onMouseEnter={(e) => showTooltip(e, "编辑/查看组件")}
onMouseLeave={hideTooltip}
>
✏️
</button>
<span className="token-count">
{component.content ? component.content.length : 0}
</span>
{!component.marker && (
<button
className="delete-btn"
onClick={() => handleDeleteComponent(index)}
onMouseEnter={(e) => showTooltip(e, "删除组件")}
onMouseLeave={hideTooltip}
>
🗑️
</button>
)}
</div>
</div>
</div>
</React.Fragment>
))}
{/* 最后一个拖拽指示器 - 在列表末尾 */}
<div
className={`drag-indicator ${dragOverItem === promptComponents.length ? 'visible' : ''}`}
onDragOver={(e) => handleDragOver(e, promptComponents.length)}
onDrop={(e) => handleDrop(e, promptComponents.length)}
/>
</div>
</div>
</div>
);
};
export default PresetPanel;