Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import axios from 'axios'; | |
| const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; | |
| interface ChatMessage { | |
| role: string; | |
| content: string; | |
| timestamp: string; | |
| } | |
| interface ChatSession { | |
| session_id: string; | |
| title: string; | |
| created_at: string; | |
| updated_at: string; | |
| domain_id: string; | |
| model_id?: string; | |
| messages: ChatMessage[]; | |
| } | |
| interface ChatHistorySidebarProps { | |
| onSessionSelect: (sessionId: string) => void; | |
| currentSessionId?: string; | |
| onNewChat: () => void; | |
| } | |
| const ChatHistorySidebar: React.FC<ChatHistorySidebarProps> = ({ | |
| onSessionSelect, | |
| currentSessionId, | |
| onNewChat | |
| }) => { | |
| const [sessions, setSessions] = useState<ChatSession[]>([]); | |
| const [loading, setLoading] = useState(true); | |
| const [editingId, setEditingId] = useState<string | null>(null); | |
| const [editTitle, setEditTitle] = useState(''); | |
| useEffect(() => { | |
| loadSessions(); | |
| }, []); | |
| const loadSessions = async () => { | |
| try { | |
| const response = await axios.get(`${API_URL}/api/chat/sessions/`); | |
| setSessions(response.data); | |
| } catch (error) { | |
| console.error('Failed to load chat sessions:', error); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const handleDeleteSession = async (sessionId: string, e: React.MouseEvent) => { | |
| e.stopPropagation(); | |
| if (!confirm('このチャットを削除しますか?')) return; | |
| try { | |
| await axios.delete(`${API_URL}/api/chat/sessions/${sessionId}`); | |
| setSessions(sessions.filter(s => s.session_id !== sessionId)); | |
| if (currentSessionId === sessionId) { | |
| onNewChat(); | |
| } | |
| } catch (error) { | |
| console.error('Failed to delete session:', error); | |
| } | |
| }; | |
| const handleEditStart = (session: ChatSession, e: React.MouseEvent) => { | |
| e.stopPropagation(); | |
| setEditingId(session.session_id); | |
| setEditTitle(session.title); | |
| }; | |
| const handleEditSave = async (sessionId: string) => { | |
| if (!editTitle.trim()) return; | |
| try { | |
| await axios.put(`${API_URL}/api/chat/sessions/${sessionId}`, { | |
| title: editTitle | |
| }); | |
| setSessions(sessions.map(s => | |
| s.session_id === sessionId ? { ...s, title: editTitle } : s | |
| )); | |
| setEditingId(null); | |
| } catch (error) { | |
| console.error('Failed to update session title:', error); | |
| } | |
| }; | |
| const handleEditCancel = () => { | |
| setEditingId(null); | |
| setEditTitle(''); | |
| }; | |
| const formatDate = (dateString: string) => { | |
| const date = new Date(dateString); | |
| const now = new Date(); | |
| const diff = now.getTime() - date.getTime(); | |
| const days = Math.floor(diff / (1000 * 60 * 60 * 24)); | |
| if (days === 0) { | |
| return date.toLocaleTimeString('ja-JP', { hour: '2-digit', minute: '2-digit' }); | |
| } else if (days === 1) { | |
| return '昨日'; | |
| } else if (days < 7) { | |
| return `${days}日前`; | |
| } else { | |
| return date.toLocaleDateString('ja-JP', { month: 'short', day: 'numeric' }); | |
| } | |
| }; | |
| return ( | |
| <div style={{ | |
| width: '280px', | |
| height: '100vh', | |
| backgroundColor: '#1a1a1a', | |
| borderRight: '1px solid #333', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| overflow: 'hidden' | |
| }}> | |
| {/* Header */} | |
| <div style={{ | |
| padding: '20px', | |
| borderBottom: '1px solid #333' | |
| }}> | |
| <h2 style={{ | |
| margin: '0 0 16px 0', | |
| fontSize: '20px', | |
| fontWeight: '600', | |
| color: '#fff' | |
| }}> | |
| NULL-AI | |
| </h2> | |
| <button | |
| onClick={onNewChat} | |
| style={{ | |
| width: '100%', | |
| padding: '12px', | |
| backgroundColor: '#2563eb', | |
| color: 'white', | |
| border: 'none', | |
| borderRadius: '8px', | |
| fontSize: '14px', | |
| fontWeight: '500', | |
| cursor: 'pointer', | |
| transition: 'background-color 0.2s' | |
| }} | |
| onMouseOver={(e) => e.currentTarget.style.backgroundColor = '#1d4ed8'} | |
| onMouseOut={(e) => e.currentTarget.style.backgroundColor = '#2563eb'} | |
| > | |
| + 新しいチャット | |
| </button> | |
| </div> | |
| {/* Chat List */} | |
| <div style={{ | |
| flex: 1, | |
| overflowY: 'auto', | |
| padding: '8px' | |
| }}> | |
| {loading ? ( | |
| <div style={{ padding: '20px', textAlign: 'center', color: '#888' }}> | |
| 読み込み中... | |
| </div> | |
| ) : sessions.length === 0 ? ( | |
| <div style={{ padding: '20px', textAlign: 'center', color: '#888' }}> | |
| チャット履歴はありません | |
| </div> | |
| ) : ( | |
| sessions.map(session => ( | |
| <div | |
| key={session.session_id} | |
| onClick={() => onSessionSelect(session.session_id)} | |
| style={{ | |
| padding: '12px', | |
| marginBottom: '4px', | |
| backgroundColor: currentSessionId === session.session_id ? '#2a2a2a' : 'transparent', | |
| borderRadius: '8px', | |
| cursor: 'pointer', | |
| transition: 'background-color 0.2s', | |
| position: 'relative', | |
| border: currentSessionId === session.session_id ? '1px solid #444' : '1px solid transparent' | |
| }} | |
| onMouseOver={(e) => { | |
| if (currentSessionId !== session.session_id) { | |
| e.currentTarget.style.backgroundColor = '#252525'; | |
| } | |
| }} | |
| onMouseOut={(e) => { | |
| if (currentSessionId !== session.session_id) { | |
| e.currentTarget.style.backgroundColor = 'transparent'; | |
| } | |
| }} | |
| > | |
| {editingId === session.session_id ? ( | |
| <div onClick={(e) => e.stopPropagation()}> | |
| <input | |
| type="text" | |
| value={editTitle} | |
| onChange={(e) => setEditTitle(e.target.value)} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter') handleEditSave(session.session_id); | |
| if (e.key === 'Escape') handleEditCancel(); | |
| }} | |
| autoFocus | |
| style={{ | |
| width: '100%', | |
| padding: '4px 8px', | |
| backgroundColor: '#333', | |
| border: '1px solid #555', | |
| borderRadius: '4px', | |
| color: '#fff', | |
| fontSize: '14px' | |
| }} | |
| /> | |
| <div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}> | |
| <button | |
| onClick={() => handleEditSave(session.session_id)} | |
| style={{ | |
| flex: 1, | |
| padding: '4px', | |
| backgroundColor: '#2563eb', | |
| color: '#fff', | |
| border: 'none', | |
| borderRadius: '4px', | |
| fontSize: '12px', | |
| cursor: 'pointer' | |
| }} | |
| > | |
| 保存 | |
| </button> | |
| <button | |
| onClick={handleEditCancel} | |
| style={{ | |
| flex: 1, | |
| padding: '4px', | |
| backgroundColor: '#444', | |
| color: '#fff', | |
| border: 'none', | |
| borderRadius: '4px', | |
| fontSize: '12px', | |
| cursor: 'pointer' | |
| }} | |
| > | |
| キャンセル | |
| </button> | |
| </div> | |
| </div> | |
| ) : ( | |
| <> | |
| <div style={{ | |
| fontSize: '14px', | |
| fontWeight: '500', | |
| color: '#fff', | |
| marginBottom: '4px', | |
| overflow: 'hidden', | |
| textOverflow: 'ellipsis', | |
| whiteSpace: 'nowrap' | |
| }}> | |
| {session.title} | |
| </div> | |
| <div style={{ | |
| fontSize: '12px', | |
| color: '#888', | |
| display: 'flex', | |
| justifyContent: 'space-between', | |
| alignItems: 'center' | |
| }}> | |
| <span>{formatDate(session.updated_at)}</span> | |
| <div style={{ display: 'flex', gap: '4px' }}> | |
| <button | |
| onClick={(e) => handleEditStart(session, e)} | |
| style={{ | |
| padding: '4px 8px', | |
| backgroundColor: 'transparent', | |
| color: '#888', | |
| border: 'none', | |
| borderRadius: '4px', | |
| fontSize: '12px', | |
| cursor: 'pointer' | |
| }} | |
| onMouseOver={(e) => e.currentTarget.style.color = '#fff'} | |
| onMouseOut={(e) => e.currentTarget.style.color = '#888'} | |
| > | |
| ✏️ | |
| </button> | |
| <button | |
| onClick={(e) => handleDeleteSession(session.session_id, e)} | |
| style={{ | |
| padding: '4px 8px', | |
| backgroundColor: 'transparent', | |
| color: '#888', | |
| border: 'none', | |
| borderRadius: '4px', | |
| fontSize: '12px', | |
| cursor: 'pointer' | |
| }} | |
| onMouseOver={(e) => e.currentTarget.style.color = '#ef4444'} | |
| onMouseOut={(e) => e.currentTarget.style.color = '#888'} | |
| > | |
| 🗑️ | |
| </button> | |
| </div> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default ChatHistorySidebar; | |