完成初版聊天界面,开始进行读取本地聊天文件开发

This commit is contained in:
2026-03-18 23:46:39 +08:00
parent 1aa90f5acf
commit bd1fa14f20
4 changed files with 1001 additions and 162 deletions

View File

@@ -1,36 +1,58 @@
import React, { useState } from 'react';
// 引入各区域组件(后续创建)
// 引入各区域组件
import PresetPanel from './components/PresetPanel';
import ChatBox from './components/ChatBox';
import ImageDisplay from './components/ImageDisplay';
import DicePanel from './components/DicePanel';
import RoleSelector from './components/RoleSelector';
function App() {
const [isToolbarExpanded, setIsToolbarExpanded] = useState(false);
const [selectedRole, setSelectedRole] = useState(null);
const [selectedChat, setSelectedChat] = useState(null);
const toggleToolbar = () => {
setIsToolbarExpanded(!isToolbarExpanded);
};
// 处理角色选择变化
const handleRoleChange = (role) => {
setSelectedRole(role);
console.log('选择的角色:', role);
// 这里可以添加其他逻辑,比如加载角色的聊天记录等
};
// 处理聊天选择变化
const handleChatChange = (role, chat) => {
setSelectedChat(chat);
console.log('选择的聊天:', role, chat);
// 这里可以添加其他逻辑,比如加载聊天内容等
};
return (
<div className="app-container">
{/* 顶部工具栏 */}
<header className={`toolbar ${isToolbarExpanded ? 'expanded' : ''}`}>
<div className="toolbar-title">AI Chat Studio</div>
{/* 工具栏内容区域 */}
{isToolbarExpanded && (
<div className="toolbar-content">
{/* 角色选择器 */}
<RoleSelector
onRoleChange={handleRoleChange}
onChatChange={handleChatChange}
/>
</div>
)}
{/* 工具栏切换按钮 */}
<button
className="toolbar-toggle-btn"
className={`toolbar-toggle-btn ${isToolbarExpanded ? 'expanded' : ''}`}
onClick={toggleToolbar}
aria-label="Toggle Toolbar"
>
{isToolbarExpanded ? '▼' : '▲'}
</button>
{/* 展开后的内容区域(预留) */}
{isToolbarExpanded && (
<div className="toolbar-content">
{/* 工具栏内容待定 */}
</div>
)}
</header>
{/* 主体内容区域 */}

View File

@@ -5,16 +5,26 @@ const ChatBox = () => {
const [isImageGen, setIsImageGen] = useState(false);
const [isDynamicTable, setIsDynamicTable] = useState(false);
const [isSettingsExpanded, setIsSettingsExpanded] = useState(false);
const [editingId, setEditingId] = useState(null);
const [editContent, setEditContent] = useState('');
const textareaRef = useRef(null);
// 自动调整 Textarea 高度
const adjustHeight = () => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto'; // 重置高度以获取正确的 scrollHeight
// 重新设置高度,限制最大高度由 CSS max-height 控制
textarea.style.height = textarea.scrollHeight + 'px';
const wrapper = textarea?.closest('.chat-input-wrapper');
if (textarea && wrapper) {
textarea.style.height = '42px';
wrapper.style.height = 'auto';
const newHeight = textarea.scrollHeight;
if (newHeight > 42) {
textarea.style.height = newHeight + 'px';
} else {
textarea.style.height = '42px';
}
}
};
@@ -22,6 +32,34 @@ const ChatBox = () => {
adjustHeight();
};
// 开始编辑消息
const startEdit = (id, content) => {
setEditingId(id);
setEditContent(content);
};
// 保存编辑的消息
const saveEdit = (id) => {
// 这里可以添加向后端发送请求的代码
console.log(`保存消息 ${id} 的编辑内容: ${editContent}`);
// 更新前端显示
const messageIndex = messages.findIndex(msg => msg.id === id);
if (messageIndex !== -1) {
messages[messageIndex].content = editContent;
}
// 退出编辑模式
setEditingId(null);
setEditContent('');
};
// 取消编辑
const cancelEdit = () => {
setEditingId(null);
setEditContent('');
};
// 生成示例数据
const generateMessages = () => {
const messages = [];
@@ -30,6 +68,7 @@ const ChatBox = () => {
messages.push({
id: i,
role: isUser ? 'user' : 'ai',
name: isUser ? '我' : 'AI助手',
content: isUser
? `这是第 ${i} 条用户消息。这是一段比较长的文本,用来测试气泡的换行效果以及滚动条的表现。`
: `这是第 ${i} 条 AI 回复。<b>包含 HTML 标签</b>的内容。如果渲染开关开启,这里应该显示粗体字。如果不开启,应该显示原始标签。`
@@ -87,12 +126,62 @@ const ChatBox = () => {
{/* 消息列表 */}
{messages.map((msg) => (
<div key={msg.id} className={`message ${msg.role}`}>
<div className="bubble">
{isHtmlRender ? (
<div dangerouslySetInnerHTML={{ __html: msg.content }} />
) : (
<div>{msg.content}</div>
)}
<div className="message-container">
{/* 消息名称和工具栏在同一行 */}
<div className="message-header">
<div className="message-name">{msg.name}</div>
{/* 消息工具栏 */}
<div className="message-toolbar">
<span className="message-id">ID: {msg.id}</span>
<div className="toolbar-buttons">
<button
className="toolbar-button edit-button"
onClick={() => startEdit(msg.id, msg.content)}
>
编辑
</button>
<button className="toolbar-button expand-button">
</button>
</div>
</div>
</div>
{/* 消息内容 */}
<div className="message-content">
<div className="bubble">
{editingId === msg.id ? (
<div className="edit-container">
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
className="edit-textarea"
/>
<div className="edit-buttons">
<button
className="save-button"
onClick={() => saveEdit(msg.id)}
>
保存
</button>
<button
className="cancel-button"
onClick={cancelEdit}
>
取消
</button>
</div>
</div>
) : (
isHtmlRender ? (
<div dangerouslySetInnerHTML={{ __html: msg.content }} />
) : (
<div>{msg.content}</div>
)
)}
</div>
</div>
</div>
</div>
))}
@@ -106,7 +195,6 @@ const ChatBox = () => {
placeholder="输入消息..."
onInput={handleInput}
rows="1"
// 关键修改显式设置初始样式高度确保可见
style={{ height: '42px' }}
/>
</div>

View File

@@ -0,0 +1,95 @@
import React, { useState, useEffect } from 'react';
const RoleSelector = ({ onRoleChange, onChatChange }) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [roleData, setRoleData] = useState({});
const [selectedRole, setSelectedRole] = useState(null);
const [selectedChat, setSelectedChat] = useState(null);
// 获取角色和聊天数据
useEffect(() => {
const fetchRoleData = async () => {
try {
console.log('开始获取角色数据...');
const response = await fetch('/api/get_all_role_and_chat');
console.log('响应状态:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('获取到的数据:', data);
setRoleData(data);
} catch (error) {
console.error('获取角色数据失败:', error);
}
};
fetchRoleData();
}, []);
// 处理角色选择
const handleRoleSelect = (role) => {
setSelectedRole(role);
setSelectedChat(null);
if (onRoleChange) {
onRoleChange(role);
}
};
// 处理聊天选择
const handleChatSelect = (chat) => {
setSelectedChat(chat);
setIsDropdownOpen(false);
if (onChatChange) {
onChatChange(selectedRole, chat);
}
};
return (
<div className="role-selector">
<div className="dropdown-container">
<div
className="dropdown-trigger"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
{selectedRole || '选择角色'}
<span className="dropdown-arrow"></span>
</div>
{isDropdownOpen && (
<div className="dropdown-menu">
{Object.keys(roleData).map(role => (
<div
key={role}
className={`dropdown-item ${selectedRole === role ? 'selected' : ''}`}
onClick={() => handleRoleSelect(role)}
>
{role}
{selectedRole === role && (
<div className="nested-dropdown">
{roleData[role].map(chat => (
<div
key={chat}
className={`dropdown-item ${selectedChat === chat ? 'selected' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleChatSelect(chat);
}}
>
{chat}
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
);
};
export default RoleSelector;

File diff suppressed because it is too large Load Diff