This commit is contained in:
Zacharias-Brohn
2026-01-14 22:08:19 +01:00
parent 95d07eb00b
commit 7663a917da
+119 -123
View File
@@ -22,6 +22,7 @@ import {
Container, Container,
Group, Group,
Menu, Menu,
NavLink,
Paper, Paper,
ScrollArea, ScrollArea,
Select, Select,
@@ -31,7 +32,6 @@ import {
TextInputProps, TextInputProps,
Title, Title,
Tooltip, Tooltip,
UnstyledButton,
useMantineTheme, useMantineTheme,
} from '@mantine/core'; } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
@@ -619,106 +619,102 @@ export default function ChatLayout() {
</Group> </Group>
<ScrollArea style={{ flex: 1, margin: '0 -10px' }} p="xs"> <ScrollArea style={{ flex: 1, margin: '0 -10px' }} p="xs">
<Stack gap="xs"> <Stack gap={2}>
{chats.length > 0 ? ( {chats.length > 0 ? (
chats.map((chat) => ( chats.map((chat) =>
<Group editingChatId === chat.id ? (
key={chat.id} // Inline editing mode
gap={0} <Group
wrap="nowrap" key={chat.id}
style={{ wrap="nowrap"
borderRadius: 'var(--mantine-radius-md)', gap="xs"
backgroundColor: px="sm"
activeChatId === chat.id py={6}
? 'var(--mantine-color-default-hover)' style={{ minWidth: 0 }}
: 'transparent', >
transition: 'background-color 0.2s', {chat.pinned ? (
}} <IconPin size={16} stroke={1.5} style={{ minWidth: 16 }} />
> ) : (
{editingChatId === chat.id ? ( <IconMessage size={16} stroke={1.5} style={{ minWidth: 16 }} />
// Inline editing mode )}
<Group wrap="nowrap" gap="xs" p="sm" style={{ flex: 1, minWidth: 0 }}> <TextInput
{chat.pinned ? ( ref={editInputRef}
<IconPin size={18} color="gray" style={{ minWidth: 18 }} /> value={editingTitle}
) : ( onChange={(e) => setEditingTitle(e.currentTarget.value)}
<IconMessage size={18} color="gray" style={{ minWidth: 18 }} /> onBlur={saveRenamedChat}
)} onKeyDown={handleRenameKeyDown}
<TextInput size="xs"
ref={editInputRef} style={{ flex: 1 }}
value={editingTitle} />
onChange={(e) => setEditingTitle(e.currentTarget.value)} </Group>
onBlur={saveRenamedChat} ) : (
onKeyDown={handleRenameKeyDown} // Normal display mode with NavLink
size="xs" <Group key={chat.id} gap={0} wrap="nowrap">
variant="unstyled" <NavLink
styles={{ component="button"
input: { active={activeChatId === chat.id}
padding: 0, color={primaryColor}
height: 'auto', variant="subtle"
minHeight: 'unset', label={chat.title}
fontSize: 'var(--mantine-font-size-sm)', leftSection={
}, chat.pinned ? (
}} <IconPin size={16} stroke={1.5} />
style={{ flex: 1 }}
/>
</Group>
) : (
// Normal display mode
<UnstyledButton
onClick={() => handleSelectChat(chat)}
p="sm"
style={{ flex: 1, minWidth: 0 }}
>
<Group wrap="nowrap" gap="xs">
{chat.pinned ? (
<IconPin size={18} color="gray" style={{ minWidth: 18 }} />
) : ( ) : (
<IconMessage size={18} color="gray" style={{ minWidth: 18 }} /> <IconMessage size={16} stroke={1.5} />
)} )
<Text size="sm" truncate style={{ flex: 1 }}> }
{chat.title} onClick={() => handleSelectChat(chat)}
</Text> noWrap
</Group> styles={{
</UnstyledButton> root: {
)} flex: 1,
minWidth: 0,
<Menu position="bottom-end" withArrow> borderRadius: 'var(--mantine-radius-sm)',
<Menu.Target> padding: '6px 10px',
<ActionIcon },
variant="subtle" label: {
color="gray" overflow: 'hidden',
size="sm" textOverflow: 'ellipsis',
mr="xs" },
onClick={(e) => e.stopPropagation()} }}
> />
<IconDotsVertical size={16} /> <Menu position="bottom-end" withArrow>
</ActionIcon> <Menu.Target>
</Menu.Target> <ActionIcon
<Menu.Dropdown> variant="subtle"
<Menu.Item color="gray"
leftSection={<IconPencil size={14} />} size="sm"
onClick={() => handleRenameChat(chat.id)} onClick={(e) => e.stopPropagation()}
> >
Rename <IconDotsVertical size={14} />
</Menu.Item> </ActionIcon>
<Menu.Item </Menu.Target>
leftSection={<IconPin size={14} />} <Menu.Dropdown>
onClick={() => handlePinChat(chat.id)} <Menu.Item
> leftSection={<IconPencil size={14} />}
{chat.pinned ? 'Unpin' : 'Pin'} onClick={() => handleRenameChat(chat.id)}
</Menu.Item> >
<Menu.Divider /> Rename
<Menu.Item </Menu.Item>
color="red" <Menu.Item
leftSection={<IconTrash size={14} />} leftSection={<IconPin size={14} />}
onClick={() => handleRemoveChat(chat.id)} onClick={() => handlePinChat(chat.id)}
> >
Remove {chat.pinned ? 'Unpin' : 'Pin'}
</Menu.Item> </Menu.Item>
</Menu.Dropdown> <Menu.Divider />
</Menu> <Menu.Item
</Group> color="red"
)) leftSection={<IconTrash size={14} />}
onClick={() => handleRemoveChat(chat.id)}
>
Remove
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
)
)
) : ( ) : (
<Text size="sm" c="dimmed" ta="center" mt="xl"> <Text size="sm" c="dimmed" ta="center" mt="xl">
{isLoadingChats ? 'Loading...' : 'No saved chats'} {isLoadingChats ? 'Loading...' : 'No saved chats'}
@@ -744,49 +740,49 @@ export default function ChatLayout() {
onScrollPositionChange={handleScroll} onScrollPositionChange={handleScroll}
classNames={{ viewport: classes.chatScrollViewport }} classNames={{ viewport: classes.chatScrollViewport }}
> >
<Stack gap="xl" px="md" py="lg"> <Stack gap="md" px="md" py="lg">
{messages.map((message) => ( {messages.map((message) => (
<Group <Group
key={message.id} key={message.id}
justify={message.role === 'user' ? 'flex-end' : 'flex-start'} justify={message.role === 'user' ? 'flex-end' : 'flex-start'}
align="flex-start" align="flex-start"
wrap="nowrap" wrap="nowrap"
gap="sm"
> >
{message.role === 'assistant' && ( {message.role === 'assistant' && (
<Avatar radius="xl" color={primaryColor} variant="light"> <Avatar radius="xl" color={primaryColor} variant="light" size="sm">
<IconRobot size={20} /> <IconRobot size={16} />
</Avatar> </Avatar>
)} )}
<Paper {message.role === 'assistant' ? (
p="md" // Assistant message - no bubble, just text aligned with avatar
radius="lg" <div style={{ maxWidth: '85%', paddingTop: 2 }}>
bg={
message.role === 'user'
? 'var(--mantine-color-default-hover)'
: 'transparent'
}
style={{
maxWidth: '80%',
borderTopLeftRadius: message.role === 'assistant' ? 0 : undefined,
borderTopRightRadius: message.role === 'user' ? 0 : undefined,
}}
>
{message.role === 'assistant' ? (
<MarkdownMessage <MarkdownMessage
content={message.content} content={message.content}
isStreaming={message.id === streamingMessageId} isStreaming={message.id === streamingMessageId}
/> />
) : ( </div>
<Text size="sm" style={{ lineHeight: 1.6 }}> ) : (
// User message - colored bubble
<Paper
py="xs"
px="md"
radius="lg"
bg={`var(--mantine-color-${primaryColor}-light)`}
style={{
maxWidth: '75%',
}}
>
<Text size="sm" style={{ lineHeight: 1.5 }}>
{message.content} {message.content}
</Text> </Text>
)} </Paper>
</Paper> )}
{message.role === 'user' && ( {message.role === 'user' && (
<Avatar radius="xl" color="gray" variant="light"> <Avatar radius="xl" color={primaryColor} variant="light" size="sm">
<IconUser size={20} /> <IconUser size={16} />
</Avatar> </Avatar>
)} )}
</Group> </Group>