- Increase logo size (48x48 desktop, 56x56 mobile) for better visibility - Add logo as favicon - Add logo to mobile header - Move user menu to navigation bars (sidebar on desktop, bottom bar on mobile) - Fix desktop chat layout - container structure prevents voice controls cutoff - Fix mobile bottom bar - use icon-only ActionIcons instead of truncated text buttons - Hide Create Node/New Conversation buttons on mobile to save header space - Make fixed header and voice controls work properly with containers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import { useChat } from '@ai-sdk/react';
|
|
import { Container, ScrollArea, Paper, Group, TextInput, Button, Stack, Text, Box } from '@mantine/core';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { MicrophoneRecorder } from './MicrophoneRecorder';
|
|
|
|
export function ChatInterface() {
|
|
const viewport = useRef<HTMLDivElement>(null);
|
|
const [input, setInput] = useState('');
|
|
|
|
const {
|
|
messages,
|
|
sendMessage,
|
|
status,
|
|
} = useChat();
|
|
|
|
// Auto-scroll to bottom when new messages arrive
|
|
useEffect(() => {
|
|
if (viewport.current) {
|
|
viewport.current.scrollTo({
|
|
top: viewport.current.scrollHeight,
|
|
behavior: 'smooth',
|
|
});
|
|
}
|
|
}, [messages]);
|
|
|
|
return (
|
|
<Container size="md" h="100vh" style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<Stack h="100%" gap="md" py="md">
|
|
{/* Chat messages area */}
|
|
<ScrollArea
|
|
flex={1}
|
|
type="auto"
|
|
viewportRef={viewport}
|
|
>
|
|
<Stack gap="md">
|
|
{messages.length === 0 && (
|
|
<Text c="dimmed" ta="center" mt="xl">
|
|
Start a conversation by typing or speaking...
|
|
</Text>
|
|
)}
|
|
{messages.map((message) => (
|
|
<Box
|
|
key={message.id}
|
|
style={{
|
|
alignSelf: message.role === 'user' ? 'flex-end' : 'flex-start',
|
|
maxWidth: '70%',
|
|
}}
|
|
>
|
|
<Paper
|
|
p="sm"
|
|
radius="md"
|
|
bg={message.role === 'user' ? 'dark.6' : 'dark.7'}
|
|
>
|
|
<Text size="sm">
|
|
{/* Extract text from parts */}
|
|
{('parts' in message && Array.isArray((message as any).parts))
|
|
? (message as any).parts.find((p: any) => p.type === 'text')?.text || ''
|
|
: (message as any).content || ''}
|
|
</Text>
|
|
</Paper>
|
|
</Box>
|
|
))}
|
|
</Stack>
|
|
</ScrollArea>
|
|
|
|
{/* Input area */}
|
|
<form onSubmit={(e) => {
|
|
e.preventDefault();
|
|
if (!input.trim() || status === 'submitted' || status === 'streaming') return;
|
|
sendMessage({ text: input });
|
|
setInput('');
|
|
}}>
|
|
<Paper withBorder p="sm" radius="xl">
|
|
<Group gap="xs">
|
|
<TextInput
|
|
value={input}
|
|
onChange={(e) => setInput(e.currentTarget.value)}
|
|
placeholder="Speak or type your thoughts..."
|
|
style={{ flex: 1 }}
|
|
variant="unstyled"
|
|
disabled={status === 'submitted' || status === 'streaming'}
|
|
/>
|
|
|
|
{/* Microphone Recorder */}
|
|
<MicrophoneRecorder
|
|
onTranscriptUpdate={(transcript) => {
|
|
// Update the input field in real-time
|
|
setInput(transcript);
|
|
}}
|
|
onTranscriptFinalized={(transcript) => {
|
|
// Set the input and submit
|
|
setInput(transcript);
|
|
// Trigger form submission
|
|
setTimeout(() => {
|
|
const form = document.querySelector('form');
|
|
if (form) {
|
|
form.requestSubmit();
|
|
}
|
|
}, 100);
|
|
}}
|
|
/>
|
|
|
|
<Button type="submit" radius="xl" loading={status === 'submitted' || status === 'streaming'}>
|
|
Send
|
|
</Button>
|
|
</Group>
|
|
</Paper>
|
|
</form>
|
|
</Stack>
|
|
</Container>
|
|
);
|
|
}
|