kofdai's picture
Deploy NullAI Knowledge System to Spaces
075a2b6 verified
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;