feat: add chat assistant with role-aware prompt and vision support
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
'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;
|
||||
}
|
||||
Reference in New Issue
Block a user