279 lines
7.9 KiB
TypeScript
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;
|
|
}
|