基于react的jsx写法
This commit is contained in:
18
frontend-react/src/components/ChatBox.jsx
Normal file
18
frontend-react/src/components/ChatBox.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
const ChatBox = () => {
|
||||
return (
|
||||
<div className="chat-box">
|
||||
<div className="chat-messages">
|
||||
{/* 消息列表占位 */}
|
||||
<div>历史消息区域</div>
|
||||
</div>
|
||||
<div className="chat-input-area">
|
||||
{/* 输入框占位 */}
|
||||
<input type="text" placeholder="输入消息..." disabled />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatBox;
|
||||
@@ -1,137 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useChatStore } from '@/store/useChatStore';
|
||||
import { Message } from '@/types';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
// --- 组件:单条消息气泡 ---
|
||||
const MessageBubble: React.FC<{ message: Message; renderHtml: boolean }> = ({ message, renderHtml }) => {
|
||||
// 根据角色决定样式
|
||||
const isUser = message.role === 'user';
|
||||
|
||||
return (
|
||||
<div className={`flex w-full mb-4 ${isUser ? 'justify-end' : 'justify-start'}`}>
|
||||
<div
|
||||
className={`
|
||||
max-w-[80%] rounded-lg p-3 shadow-sm
|
||||
${isUser
|
||||
? 'bg-blue-50 text-gray-800 border border-blue-100'
|
||||
: 'bg-white text-gray-800 border border-gray-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* 消息头部:角色名(可选) */}
|
||||
<div className="text-xs font-bold mb-1 text-gray-500">
|
||||
{isUser ? 'User' : 'Assistant'}
|
||||
</div>
|
||||
|
||||
{/* 消息内容 */}
|
||||
<div className="prose prose-sm max-w-none">
|
||||
{renderHtml && !isUser ? (
|
||||
// 如果开启 HTML 渲染且是 AI,使用 ReactMarkdown 解析
|
||||
<ReactMarkdown>{message.content}</ReactMarkdown>
|
||||
) : (
|
||||
// 否则纯文本显示(注意:实际项目中应防范 XSS,这里仅作演示)
|
||||
<div className="whitespace-pre-wrap">{message.content}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- 主组件:聊天窗口 ---
|
||||
const ChatWindow: React.FC = () => {
|
||||
// 1. 从 Store 获取状态
|
||||
const { messages, currentMessageId, renderHtml } = useChatStore();
|
||||
|
||||
// 2. 滚动容器引用
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 3. 自动滚动到底部
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages, currentMessageId]); // 当消息更新或切换分支时滚动
|
||||
|
||||
// 4. 构建当前显示的消息链
|
||||
// 这是一个简化逻辑:从 currentMessageId 开始回溯 parentId 直到根节点
|
||||
// 实际项目中可能需要更复杂的树遍历逻辑
|
||||
const displayMessages: Message[] = React.useMemo(() => {
|
||||
if (!currentMessageId) return [];
|
||||
|
||||
const chain: Message[] = [];
|
||||
let currId: string | null = currentMessageId;
|
||||
|
||||
// 向上回溯 (最多回溯 100 条防止死循环)
|
||||
let safetyCount = 0;
|
||||
while (currId && safetyCount < 100) {
|
||||
const msg = messages[currId];
|
||||
if (msg) {
|
||||
chain.unshift(msg); // 添加到数组开头
|
||||
currId = msg.parentId;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
safetyCount++;
|
||||
}
|
||||
|
||||
return chain;
|
||||
}, [messages, currentMessageId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-[#F0F4F8]">
|
||||
|
||||
{/* --- 顶部控制栏 (数据集/文件选择) --- */}
|
||||
{/* 注意:这部分逻辑可能需要移到 Layout 或单独的 Header 组件中 */}
|
||||
<div className="flex-none h-12 border-b border-[#D1D9E6] bg-white flex items-center px-4 justify-between shrink-0">
|
||||
<div className="flex items-center space-x-2 text-sm">
|
||||
<span className="text-gray-500">当前会话:</span>
|
||||
<span className="font-semibold text-gray-700">Active_Session_01</span>
|
||||
</div>
|
||||
|
||||
{/* HTML 渲染开关 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-gray-500">HTML 渲染</span>
|
||||
<button
|
||||
onClick={() => {/* TODO: 调用 store.toggleRenderHtml() */}}
|
||||
className={`w-10 h-5 rounded-full p-1 transition-colors duration-200 ease-in-out ${
|
||||
renderHtml ? 'bg-blue-600' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`bg-white w-3 h-3 rounded-full shadow-md transform transition-transform duration-200 ease-in-out ${
|
||||
renderHtml ? 'translate-x-5' : 'translate-x-0'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- 消息列表区域 (可滚动) --- */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 overflow-y-auto p-4 custom-scrollbar scroll-smooth"
|
||||
>
|
||||
{displayMessages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||
<p>暂无消息,请开始对话</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{displayMessages.map((msg) => (
|
||||
<MessageBubble
|
||||
key={msg.id}
|
||||
message={msg}
|
||||
renderHtml={renderHtml}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatWindow;
|
||||
11
frontend-react/src/components/DicePanel.jsx
Normal file
11
frontend-react/src/components/DicePanel.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const DicePanel = () => {
|
||||
return (
|
||||
<div className="dice-panel">
|
||||
<div>骰子区</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DicePanel;
|
||||
11
frontend-react/src/components/ImageDisplay.jsx
Normal file
11
frontend-react/src/components/ImageDisplay.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
const ImageDisplay = () => {
|
||||
return (
|
||||
<div className="image-display">
|
||||
<div>图片展示区</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageDisplay;
|
||||
@@ -1,131 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useChatStore } from '@/store/useChatStore';
|
||||
import { Send } from 'lucide-react'; // 图标库
|
||||
|
||||
// --- 主组件:输入框 ---
|
||||
const InputBox: React.FC = () => {
|
||||
// 1. 状态管理
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// 2. 从 Store 获取状态和方法
|
||||
const {
|
||||
addMessage,
|
||||
updateStreamingMessage,
|
||||
isLoading,
|
||||
messages,
|
||||
genParams,
|
||||
currentMessageId
|
||||
} = useChatStore();
|
||||
|
||||
// 3. 自动调整 Textarea 高度
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto'; // 重置高度
|
||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; // 设置为内容高度
|
||||
}
|
||||
}, [inputValue]);
|
||||
|
||||
// 4. 处理发送逻辑
|
||||
const handleSend = async () => {
|
||||
const text = inputValue.trim();
|
||||
if (!text || isLoading) return;
|
||||
|
||||
// --- A. 更新前端 UI ---
|
||||
setInputValue(''); // 清空输入框
|
||||
|
||||
// 1. 添加用户消息到 Store
|
||||
const userMsgId = addMessage('user', text, currentMessageId);
|
||||
|
||||
// 2. 创建一个空的 AI 消息占位符
|
||||
const aiMsgId = addMessage('assistant', '', userMsgId);
|
||||
|
||||
// --- B. 准备请求数据 ---
|
||||
// 预期后端接收格式:
|
||||
// {
|
||||
// "prompt": text,
|
||||
// "history": Object.values(messages).filter(...), // 发送当前上下文
|
||||
// "params": genParams
|
||||
// }
|
||||
const payload = {
|
||||
prompt: text,
|
||||
history: Object.values(messages), // 简化处理:发送所有消息
|
||||
params: genParams,
|
||||
};
|
||||
|
||||
try {
|
||||
// --- C. 发起请求 (模拟) ---
|
||||
// TODO: 替换为实际的 API 调用,例如:
|
||||
// import { generateReplyStream } from '@/api/client';
|
||||
// await generateReplyStream(payload, (chunk) => { updateStreamingMessage(aiMsgId, chunk); });
|
||||
|
||||
// 这里仅模拟流式效果
|
||||
const simulatedResponse = "这是一个模拟的 AI 回复。在实际项目中,这里将替换为真实的后端流式数据。";
|
||||
|
||||
// 模拟逐字显示
|
||||
for (let i = 0; i < simulatedResponse.length; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 50)); // 模拟延迟
|
||||
updateStreamingMessage(aiMsgId, simulatedResponse[i]);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
// 错误处理:可以更新消息内容显示错误信息
|
||||
updateStreamingMessage(aiMsgId, "\n[错误: 无法连接到服务器]", true);
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 处理键盘事件 (Ctrl+Enter 发送)
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-none bg-[#F0F4F8] p-4 border-t border-[#D1D9E6]">
|
||||
<div className="relative flex items-end w-full bg-white border border-[#0056B3] rounded-lg shadow-sm focus-within:ring-2 focus-within:ring-blue-200 transition-all">
|
||||
|
||||
{/* 文本输入区域 */}
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="输入消息... (支持 /命令,Ctrl+Enter 发送)"
|
||||
className="w-full max-h-48 min-h-[60px] p-3 pr-12 bg-transparent resize-none outline-none text-[#1A1A1A] text-sm custom-scrollbar"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
{/* 发送按钮 */}
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={isLoading || !inputValue.trim()}
|
||||
className={`
|
||||
absolute right-2 bottom-2 p-2 rounded-md transition-colors
|
||||
${isLoading || !inputValue.trim()
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-50 text-blue-600 hover:bg-blue-100'
|
||||
}
|
||||
`}
|
||||
title="发送 (Ctrl+Enter)"
|
||||
>
|
||||
{isLoading ? (
|
||||
// 加载图标 (简单的 CSS 动画)
|
||||
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<Send size={18} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 底部提示 */}
|
||||
<div className="mt-2 text-xs text-gray-400 text-right">
|
||||
{isLoading ? 'AI 正在思考...' : 'Enter 换行,Ctrl+Enter 发送'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputBox;
|
||||
@@ -1,185 +0,0 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
// --- 1. 定义 Props 接口 ---
|
||||
interface LayoutProps {
|
||||
left?: ReactNode;
|
||||
middle?: ReactNode;
|
||||
right?: ReactNode;
|
||||
}
|
||||
|
||||
// --- 2. 占位符组件 (当未传入 props 时显示) ---
|
||||
const SidebarPlaceholder = () => (
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="h-8 bg-gray-200 rounded w-3/4 animate-pulse"></div>
|
||||
<div className="h-32 bg-gray-100 rounded border border-gray-300"></div>
|
||||
<div className="h-32 bg-gray-100 rounded border border-gray-300"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ChatPlaceholder = () => (
|
||||
<div className="flex flex-col items-center justify-center h-full text-gray-400 space-y-2">
|
||||
<div className="w-12 h-12 border-4 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
|
||||
<p>聊天区域加载中...</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const InputPlaceholder = () => (
|
||||
<div className="h-full w-full border border-[#0056B3] rounded-lg bg-white p-2 opacity-50">
|
||||
<div className="h-full bg-gray-100 rounded animate-pulse"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const RightPanelPlaceholder = () => (
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="h-32 bg-gray-100 rounded border border-gray-300"></div>
|
||||
<div className="h-32 bg-gray-100 rounded border border-gray-300"></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// --- 3. 主布局组件 ---
|
||||
const Layout: React.FC<LayoutProps> = ({ left, middle, right }) => {
|
||||
return (
|
||||
// 1. 最外层容器:全屏高度,浅蓝灰背景,Flex 列布局
|
||||
<div className="flex flex-col h-screen bg-[#F0F4F8] text-[#1A1A1A] font-sans overflow-hidden">
|
||||
|
||||
{/* --- 顶部工具栏 --- */}
|
||||
<header className="h-14 bg-white border-b border-[#D1D9E6] flex items-center justify-between px-4 shrink-0 z-20 shadow-sm">
|
||||
|
||||
{/* --- 左侧:双层下拉框 (替换原来的 打开/保存按钮) --- */}
|
||||
<div className="flex items-center space-x-2">
|
||||
|
||||
{/* 1. 第一层:角色选择 */}
|
||||
<select
|
||||
value={currentRole || ''}
|
||||
onChange={(e) => selectRole(e.target.value)}
|
||||
className="text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:border-blue-500 bg-white"
|
||||
>
|
||||
<option value="" disabled>选择角色</option>
|
||||
{Object.keys(datasets).map((role) => (
|
||||
<option key={role} value={role}>{role}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* 2. 第二层:文件选择 (仅在选择角色后显示) */}
|
||||
{currentRole && datasets[currentRole] && (
|
||||
<div className="flex items-center space-x-1 relative"> {/* relative 用于定位菜单 */}
|
||||
|
||||
<select
|
||||
value={currentChatFile || ''}
|
||||
onChange={(e) => selectChatFile(e.target.value)}
|
||||
className="text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:border-blue-500 bg-white"
|
||||
>
|
||||
<option value="" disabled>选择记录</option>
|
||||
{datasets[currentRole].map((file) => {
|
||||
// 去掉 .jsonl 后缀用于显示
|
||||
const displayName = file.replace('.jsonl', '');
|
||||
return <option key={file} value={file}>{displayName}</option>;
|
||||
})}
|
||||
</select>
|
||||
|
||||
{/* 3. 操作按钮 (重命名/删除) */}
|
||||
<button
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
className="text-gray-500 hover:text-blue-600 px-1"
|
||||
title="操作"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
|
||||
{/* 悬浮菜单 */}
|
||||
{showMenu && (
|
||||
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded shadow-lg py-1 z-50 min-w-[100px]">
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
console.log('重命名功能待实现');
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
重命名
|
||||
</button>
|
||||
<button
|
||||
className="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50"
|
||||
onClick={() => {
|
||||
console.log('删除功能待实现');
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* --- 中间:标题 (替换原来的 AI WorkFlow Engine) --- */}
|
||||
{/* 这里留空,或者您可以放一个小的 Logo/图标 */}
|
||||
<div className="text-lg font-bold text-[#0056B3]">
|
||||
{/* 可以在这里放图标,或者直接留空让布局更紧凑 */}
|
||||
</div>
|
||||
|
||||
{/* --- 右侧:设置 (保留原来的代码) --- */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<button className="text-[#0056B3] font-semibold hover:bg-blue-50 px-3 py-1 rounded transition">
|
||||
⚙️ 设置
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
{/* --- 主体区域:三栏布局 --- */}
|
||||
<main className="flex-1 flex overflow-hidden">
|
||||
|
||||
{/* --- 左侧栏 (1份) --- */}
|
||||
<aside className="w-1/4 bg-white border-r border-[#D1D9E6] overflow-y-auto shrink-0 custom-scrollbar">
|
||||
{left || <SidebarPlaceholder />}
|
||||
</aside>
|
||||
|
||||
{/* --- 中间栏 (3份) --- */}
|
||||
{/* 注意:这里我们假设 middle prop 包含了 ChatWindow 和 InputBox */}
|
||||
{/* 如果 middle 为空,我们显示占位符 */}
|
||||
<section className="flex-1 flex flex-col border-r border-[#D1D9E6] bg-white relative">
|
||||
{middle ? (
|
||||
middle
|
||||
) : (
|
||||
<>
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-[#F0F4F8] custom-scrollbar scroll-smooth">
|
||||
<ChatPlaceholder />
|
||||
</div>
|
||||
<div className="flex-none h-32 border-t border-[#D1D9E6] bg-[#F0F4F8] p-4 shrink-0">
|
||||
<InputPlaceholder />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* --- 右侧栏 (1份) --- */}
|
||||
<aside className="w-1/4 bg-white overflow-y-auto shrink-0 custom-scrollbar">
|
||||
{right || <RightPanelPlaceholder />}
|
||||
</aside>
|
||||
|
||||
</main>
|
||||
|
||||
{/* --- 全局滚动条样式 --- */}
|
||||
<style jsx global>{`
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
29
frontend-react/src/components/PresetPanel.jsx
Normal file
29
frontend-react/src/components/PresetPanel.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
const PresetPanel = () => {
|
||||
return (
|
||||
<div className="preset-panel">
|
||||
{/* 顶部:预设选择与输入 */}
|
||||
<div className="preset-header">
|
||||
{/* 下拉框 */}
|
||||
<select className="preset-select">
|
||||
<option>选择预设...</option>
|
||||
</select>
|
||||
{/* 输入框组(待定) */}
|
||||
<div className="preset-inputs">
|
||||
{/* inputs here */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 下方:大槽位区域 */}
|
||||
<div className="preset-slots">
|
||||
{/* 槽位列表 */}
|
||||
<div className="slot-item">槽位 1</div>
|
||||
<div className="slot-item">槽位 2</div>
|
||||
{/* ... */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PresetPanel;
|
||||
@@ -1,175 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Dice1, Image as ImageIcon, Table } from 'lucide-react';
|
||||
import { useChatStore } from '@/store/useChatStore';
|
||||
|
||||
// --- 子组件:本地图库 ---
|
||||
const LocalGallery: React.FC = () => {
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 模拟加载图片数据
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
// TODO: 替换为真实的 API 调用
|
||||
// import { getLocalImages } from '@/api/client';
|
||||
// const data = await getLocalImages('/assets/images');
|
||||
// setImages(data);
|
||||
|
||||
// 模拟延迟
|
||||
setTimeout(() => {
|
||||
setImages([
|
||||
'placeholder1.jpg',
|
||||
'placeholder2.jpg',
|
||||
'placeholder3.jpg',
|
||||
'placeholder4.jpg'
|
||||
]);
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<ImageIcon size={16} />
|
||||
<h3 className="text-sm font-bold text-gray-700">🖼️ 本地图库</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar pr-1">
|
||||
{loading ? (
|
||||
<div className="text-center text-xs text-gray-400 py-4">加载中...</div>
|
||||
) : images.length === 0 ? (
|
||||
<div className="text-center text-xs text-gray-400 py-4">文件夹为空</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{images.map((img, idx) => (
|
||||
<div key={idx} className="bg-white p-1 border border-gray-200 rounded shadow-sm">
|
||||
{/* 图片占位符 */}
|
||||
<div className="aspect-square bg-gray-100 rounded mb-1 flex items-center justify-center text-gray-400 text-xs">
|
||||
{img}
|
||||
</div>
|
||||
<div className="text-[10px] text-center text-gray-500 truncate">{img}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- 子组件:骰子检定 ---
|
||||
const DiceTool: React.FC = () => {
|
||||
const [result, setResult] = useState<number | null>(null);
|
||||
const [rollType, setRollType] = useState<'difficulty' | 'opposed'>('difficulty');
|
||||
const [difficulty, setDifficulty] = useState<number>(50); // 默认普通难度
|
||||
|
||||
const handleRoll = () => {
|
||||
// 生成 1-100 随机数
|
||||
const res = Math.floor(Math.random() * 100) + 1;
|
||||
setResult(res);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full pt-4 border-t border-gray-100">
|
||||
<div className="flex items-center space-x-2 mb-3">
|
||||
<Dice1 size={16} />
|
||||
<h3 className="text-sm font-bold text-gray-700">🎲 检定工具</h3>
|
||||
</div>
|
||||
|
||||
{/* Tab 切换 */}
|
||||
<div className="flex border-b border-gray-200 mb-3">
|
||||
<button
|
||||
onClick={() => setRollType('difficulty')}
|
||||
className={`flex-1 py-1 text-xs font-medium transition-colors ${
|
||||
rollType === 'difficulty'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
难度检定
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setRollType('opposed')}
|
||||
className={`flex-1 py-1 text-xs font-medium transition-colors ${
|
||||
rollType === 'opposed'
|
||||
? 'text-blue-600 border-b-2 border-blue-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
对抗骰
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 难度选择 (仅在难度检定模式下显示) */}
|
||||
{rollType === 'difficulty' && (
|
||||
<div className="mb-4 space-y-2">
|
||||
<label className="text-xs text-gray-500">选择难度</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ label: '极难', val: 95 },
|
||||
{ label: '困难', val: 75 },
|
||||
{ label: '普通', val: 50 }
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.val}
|
||||
onClick={() => setDifficulty(opt.val)}
|
||||
className={`text-xs py-1 px-2 rounded border transition-colors ${
|
||||
difficulty === opt.val
|
||||
? 'bg-blue-50 border-blue-500 text-blue-700'
|
||||
: 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{opt.label} ({opt.val})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 投掷区域 */}
|
||||
<div className="flex items-center justify-between bg-white p-3 rounded border border-gray-200 shadow-sm">
|
||||
<div className="flex-1">
|
||||
<div className="text-xs text-gray-400 mb-1">
|
||||
{rollType === 'difficulty' ? `目标: ${difficulty}` : '对抗检定'}
|
||||
</div>
|
||||
{result !== null && (
|
||||
<div
|
||||
className={`text-2xl font-bold ${
|
||||
rollType === 'difficulty' && result > difficulty
|
||||
? 'text-red-500' // 失败
|
||||
: 'text-green-500' // 成功
|
||||
}`}
|
||||
>
|
||||
{result}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRoll}
|
||||
className="ml-4 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 transition shadow-sm active:transform active:scale-95"
|
||||
>
|
||||
投掷
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- 主组件:右侧面板 ---
|
||||
const RightPanel: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4 overflow-y-auto custom-scrollbar">
|
||||
{/* 上半部分:图库 */}
|
||||
<div className="flex-1 min-h-0 mb-4">
|
||||
<LocalGallery />
|
||||
</div>
|
||||
|
||||
{/* 下半部分:骰子 */}
|
||||
<div className="flex-none h-auto">
|
||||
<DiceTool />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RightPanel;
|
||||
@@ -1,221 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useChatStore } from '@/store/useChatStore';
|
||||
import { Slider } from '@/components/ui/slider'; // 假设您有这个组件,或者用原生 input[type=range]
|
||||
import { Switch } from '@/components/ui/switch'; // 假设您有这个组件,或者用原生 input[type=checkbox]
|
||||
import { Label } from '@/components/ui/label'; // 假设您有这个组件,或者用原生 label
|
||||
|
||||
// --- 类型定义 ---
|
||||
// 这里复用 Store 中的类型,或者从 types/index.ts 导入
|
||||
type SpliceBlockType = 'system' | 'world' | 'history' | 'character';
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
// 1. 从 Store 获取状态和方法
|
||||
const {
|
||||
genParams,
|
||||
spliceBlocks,
|
||||
updateGenParams,
|
||||
toggleSpliceBlock,
|
||||
} = useChatStore();
|
||||
|
||||
// 2. 处理参数变更
|
||||
const handleParamChange = (key: keyof typeof genParams, value: number | boolean) => {
|
||||
updateGenParams({ [key]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4 space-y-6 overflow-y-auto custom-scrollbar">
|
||||
|
||||
{/* --- 标题:全局预设 --- */}
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-gray-700 mb-3 flex items-center">
|
||||
📜 全局预设
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<select className="flex-1 p-2 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-500">
|
||||
<option>Default</option>
|
||||
<option>A.U.T.O. v1.47</option>
|
||||
<option>Roleplay Pro</option>
|
||||
</select>
|
||||
<button className="px-3 py-2 bg-white border border-gray-300 rounded hover:bg-gray-50 text-sm">
|
||||
📥
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- 标题:生成参数 --- */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold text-gray-700">⚙️ 生成参数</h3>
|
||||
|
||||
{/* 参数网格布局 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 温度 */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<label>温度</label>
|
||||
<span>{genParams.temperature.toFixed(1)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={genParams.temperature}
|
||||
onChange={(e) => handleParamChange('temperature', parseFloat(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Top P */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<label>Top P</label>
|
||||
<span>{genParams.top_p.toFixed(1)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={genParams.top_p}
|
||||
onChange={(e) => handleParamChange('top_p', parseFloat(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 频率惩罚 */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<label>频率惩罚</label>
|
||||
<span>{genParams.frequency_penalty.toFixed(1)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={genParams.frequency_penalty}
|
||||
onChange={(e) => handleParamChange('frequency_penalty', parseFloat(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 存在惩罚 */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<label>存在惩罚</label>
|
||||
<span>{genParams.presence_penalty.toFixed(1)}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={genParams.presence_penalty}
|
||||
onChange={(e) => handleParamChange('presence_penalty', parseFloat(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 上下文长度 */}
|
||||
<div className="col-span-1">
|
||||
<label className="text-xs text-gray-500">上下文长度</label>
|
||||
<input
|
||||
type="number"
|
||||
value={genParams.context_length}
|
||||
onChange={(e) => handleParamChange('context_length', parseInt(e.target.value))}
|
||||
className="w-full mt-1 p-1 text-sm border border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 最大回复 */}
|
||||
<div className="col-span-1">
|
||||
<label className="text-xs text-gray-500">最大回复</label>
|
||||
<input
|
||||
type="number"
|
||||
value={genParams.max_tokens}
|
||||
onChange={(e) => handleParamChange('max_tokens', parseInt(e.target.value))}
|
||||
className="w-full mt-1 p-1 text-sm border border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 流式传输开关 */}
|
||||
<div className="flex items-center space-x-2 pt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="stream-check"
|
||||
checked={genParams.stream}
|
||||
onChange={(e) => handleParamChange('stream', e.target.checked)}
|
||||
className="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="stream-check" className="text-sm text-gray-700 select-none">
|
||||
✅ 流式传输
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* --- 标题:内容拼接块 --- */}
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<h3 className="text-sm font-bold text-gray-700 mb-2">🧩 内容拼接块</h3>
|
||||
<p className="text-xs text-gray-500 mb-3">控制发送至后端的上下文组成</p>
|
||||
|
||||
{/* 拼接块列表 */}
|
||||
<div className="flex-1 overflow-y-auto space-y-2 pr-1 custom-scrollbar">
|
||||
{spliceBlocks.map((block) => (
|
||||
<div
|
||||
key={block.id}
|
||||
className={`flex items-center justify-between p-2 rounded border transition-colors ${
|
||||
block.active
|
||||
? 'bg-white border-[#D1D9E6] shadow-sm'
|
||||
: 'bg-gray-50 border-gray-200 opacity-70'
|
||||
}`}
|
||||
>
|
||||
{/* 左侧:图标和名称 */}
|
||||
<div className="flex items-center space-x-2 overflow-hidden">
|
||||
<span className="text-lg shrink-0">
|
||||
{block.type === 'world' ? '🌍' : block.type === 'history' ? '💬' : '📄'}
|
||||
</span>
|
||||
<span
|
||||
className={`text-sm truncate ${
|
||||
block.active ? 'text-gray-900 font-medium' : 'text-gray-500 line-through'
|
||||
}`}
|
||||
>
|
||||
{block.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 右侧:操作按钮 */}
|
||||
<div className="flex items-center space-x-2 shrink-0">
|
||||
{/* 编辑按钮 (如果不可编辑则置灰) */}
|
||||
<button
|
||||
className={`text-xs p-1 rounded ${
|
||||
block.editable === false ? 'text-gray-300 cursor-not-allowed' : 'text-gray-500 hover:text-blue-600'
|
||||
}`}
|
||||
disabled={block.editable === false}
|
||||
title="编辑"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
|
||||
{/* 激活开关 */}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={block.active}
|
||||
onChange={() => toggleSpliceBlock(block.id)}
|
||||
className="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 添加按钮 */}
|
||||
<button className="mt-3 w-full py-2 text-sm text-blue-600 border border-blue-200 rounded hover:bg-blue-50 transition">
|
||||
+ 添加拼接块
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
Reference in New Issue
Block a user