Files
patch-pilot/src/components/ChatSidebar.tsx
T

279 lines
7.9 KiB
TypeScript

'use client';
import { useState } from 'react';
import {
Box,
Typography,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Divider,
IconButton,
Button,
Drawer,
useMediaQuery,
Tooltip,
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import {
MessageSquare,
Plus,
Trash2,
Menu as MenuIcon,
Pencil,
Sparkles,
} from 'lucide-react';
import { useChatStore } from '../services/chat-store';
import { useI18n } from '../services/i18n';
const SIDEBAR_WIDTH = 280;
export default function ChatSidebar() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const { t } = useI18n();
const {
sessions,
activeSessionId,
setActiveSession,
createSession,
deleteSession,
renameSession,
} = useChatStore();
const [open, setOpen] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [editValue, setEditValue] = useState('');
const handleNew = () => {
createSession('Neue Reparatur');
setOpen(false);
};
const startEdit = (id: string, currentTitle: string) => {
setEditingId(id);
setEditValue(currentTitle);
};
const commitEdit = () => {
if (editingId && editValue.trim()) renameSession(editingId, editValue.trim());
setEditingId(null);
setEditValue('');
};
const handleDelete = (e: React.MouseEvent, id: string) => {
e.stopPropagation();
deleteSession(id);
};
const handleSelect = (id: string) => {
setActiveSession(id);
setOpen(false);
};
const content = (
<Box
sx={{
width: SIDEBAR_WIDTH,
height: '100%',
display: 'flex',
flexDirection: 'column',
bgcolor: 'background.paper',
borderRight: { md: '1px solid' },
borderColor: { md: 'divider' },
}}
>
<Box sx={{ p: 2 }}>
<Button
fullWidth
variant="contained"
onClick={handleNew}
startIcon={<Plus size={16} />}
sx={{ py: 1, fontSize: '0.85rem' }}
>
{t('newChat')}
</Button>
</Box>
<Divider />
<Box sx={{ px: 2, pt: 1.75, pb: 1, display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Sparkles size={12} />
<Typography
variant="caption"
sx={{
fontWeight: 700,
letterSpacing: '0.06em',
textTransform: 'uppercase',
color: 'text.secondary',
fontSize: '0.68rem',
}}
>
{t('chatHistory')} · {sessions.length}
</Typography>
</Box>
<List sx={{ flexGrow: 1, overflowY: 'auto', px: 1, py: 0 }}>
{sessions.length === 0 && (
<Box sx={{ px: 2, py: 4, textAlign: 'center', color: 'text.secondary' }}>
<MessageSquare size={28} style={{ opacity: 0.35, marginBottom: 8 }} />
<Typography variant="caption">{t('chatNoSessionHint')}</Typography>
</Box>
)}
{sessions.map((session) => {
const selected = activeSessionId === session.id;
const editing = editingId === session.id;
return (
<ListItem
key={session.id}
disablePadding
sx={{
mb: 0.5,
'& .pp-row-actions': { opacity: 0, transition: 'opacity 0.15s' },
'&:hover .pp-row-actions': { opacity: 1 },
}}
secondaryAction={
!editing && (
<Box
className="pp-row-actions"
sx={{
display: 'flex',
gap: 0,
pr: 0.25,
...(selected && { opacity: 1 }),
}}
>
<Tooltip title={t('chatRename')}>
<IconButton
edge="end"
size="small"
onClick={(e) => {
e.stopPropagation();
startEdit(session.id, session.title);
}}
sx={{
color: selected ? 'inherit' : 'text.secondary',
p: 0.5,
}}
>
<Pencil size={13} />
</IconButton>
</Tooltip>
<Tooltip title={t('chatDelete')}>
<IconButton
edge="end"
size="small"
onClick={(e) => handleDelete(e, session.id)}
sx={{
color: selected ? 'inherit' : 'text.secondary',
p: 0.5,
}}
>
<Trash2 size={13} />
</IconButton>
</Tooltip>
</Box>
)
}
>
<ListItemButton
selected={selected}
onClick={() => handleSelect(session.id)}
sx={{
borderRadius: 2,
pr: 7,
py: 0.85,
minHeight: 44,
'&.Mui-selected': {
bgcolor: 'primary.main',
color: 'white',
'&:hover': { bgcolor: 'primary.dark' },
'& .MuiListItemIcon-root': { color: 'white' },
},
}}
>
<ListItemIcon
sx={{
minWidth: 28,
color: selected ? 'white' : 'text.secondary',
}}
>
<MessageSquare size={14} />
</ListItemIcon>
{editing ? (
<input
autoFocus
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={commitEdit}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Enter') commitEdit();
if (e.key === 'Escape') setEditingId(null);
}}
style={{
flex: 1,
background: 'transparent',
border: 'none',
outline: 'none',
color: 'inherit',
fontFamily: 'inherit',
fontSize: '0.84rem',
fontWeight: 600,
}}
/>
) : (
<ListItemText
primary={session.title}
primaryTypographyProps={{
variant: 'body2',
noWrap: true,
fontWeight: selected ? 700 : 500,
fontSize: '0.84rem',
}}
/>
)}
</ListItemButton>
</ListItem>
);
})}
</List>
</Box>
);
if (isMobile) {
return (
<>
<Box sx={{ position: 'absolute', top: 12, left: 12, zIndex: 4 }}>
<IconButton
onClick={() => setOpen(true)}
sx={{
width: 36,
height: 36,
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
boxShadow: 'var(--pp-shadow-soft)',
}}
aria-label="Chat-Verlauf"
>
<MenuIcon size={16} />
</IconButton>
</Box>
<Drawer
anchor="left"
open={open}
onClose={() => setOpen(false)}
PaperProps={{ sx: { width: SIDEBAR_WIDTH } }}
>
{content}
</Drawer>
</>
);
}
return content;
}