Added a visual typing indicator that displays while the AI is generating a response. Shows "AI Thinking..." with a loading spinner to give users feedback that their message is being processed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
196 lines
5.7 KiB
TypeScript
196 lines
5.7 KiB
TypeScript
'use client';
|
|
|
|
import { useChat } from '@ai-sdk/react';
|
|
import {
|
|
Stack,
|
|
TextInput,
|
|
Button,
|
|
Paper,
|
|
ScrollArea,
|
|
Title,
|
|
Container,
|
|
Group,
|
|
Text,
|
|
Loader,
|
|
} from '@mantine/core';
|
|
import { useRef, useState, useEffect } from 'react';
|
|
import { MicrophoneRecorder } from '@/components/MicrophoneRecorder';
|
|
|
|
export default function ChatPage() {
|
|
const viewport = useRef<HTMLDivElement>(null);
|
|
const [input, setInput] = useState('');
|
|
|
|
const { messages, sendMessage, isLoading, setMessages } = useChat({
|
|
api: '/api/chat',
|
|
body: {
|
|
persona: 'Socratic',
|
|
},
|
|
credentials: 'include',
|
|
});
|
|
|
|
// Add initial greeting message on first load
|
|
useEffect(() => {
|
|
if (messages.length === 0) {
|
|
setMessages([
|
|
{
|
|
id: 'initial-greeting',
|
|
role: 'assistant',
|
|
parts: [
|
|
{
|
|
type: 'text',
|
|
text: 'Welcome to Ponderants! I\'m here to help you explore and structure your ideas through conversation.\n\nWhat would you like to talk about today? I can adapt my interview style to best suit your needs (Socratic questioning, collaborative brainstorming, or other approaches).\n\nJust start sharing your thoughts, and we\'ll discover meaningful insights together.',
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
}
|
|
}, []);
|
|
|
|
// Auto-scroll to bottom
|
|
useEffect(() => {
|
|
viewport.current?.scrollTo({
|
|
top: viewport.current.scrollHeight,
|
|
behavior: 'smooth',
|
|
});
|
|
}, [messages]);
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!input.trim() || isLoading) return;
|
|
|
|
sendMessage({ text: input });
|
|
setInput('');
|
|
};
|
|
|
|
return (
|
|
<Container size="md" h="100vh" style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<Title order={2} py="md">
|
|
Ponderants Interview
|
|
</Title>
|
|
|
|
<ScrollArea
|
|
h="100%"
|
|
style={{ flex: 1 }}
|
|
viewportRef={viewport}
|
|
>
|
|
<Stack gap="md" pb="xl">
|
|
{messages.map((m) => (
|
|
<Paper
|
|
key={m.id}
|
|
withBorder
|
|
shadow="md"
|
|
p="sm"
|
|
radius="lg"
|
|
style={{
|
|
alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start',
|
|
backgroundColor:
|
|
m.role === 'user' ? '#343a40' : '#212529',
|
|
}}
|
|
w="80%"
|
|
>
|
|
<Text fw={700} size="sm">{m.role === 'user' ? 'You' : 'AI'}</Text>
|
|
{m.parts.map((part, i) => {
|
|
if (part.type === 'text') {
|
|
return (
|
|
<Text key={i} style={{ whiteSpace: 'pre-wrap' }}>
|
|
{part.text}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
// Handle tool calls (e.g., suggest_node)
|
|
if (part.type === 'tool-call') {
|
|
return (
|
|
<Paper key={i} withBorder p="xs" mt="xs" bg="dark.6">
|
|
<Text size="xs" c="dimmed" mb="xs">
|
|
💡 Node Suggestion
|
|
</Text>
|
|
<Text fw={600}>{part.args.title}</Text>
|
|
<Text size="sm" mt="xs">
|
|
{part.args.content}
|
|
</Text>
|
|
{part.args.tags && part.args.tags.length > 0 && (
|
|
<Group gap="xs" mt="xs">
|
|
{part.args.tags.map((tag: string, tagIdx: number) => (
|
|
<Text key={tagIdx} size="xs" c="blue.4">
|
|
#{tag}
|
|
</Text>
|
|
))}
|
|
</Group>
|
|
)}
|
|
</Paper>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
})}
|
|
</Paper>
|
|
))}
|
|
|
|
{/* Typing indicator while AI is generating a response */}
|
|
{isLoading && (
|
|
<Paper
|
|
withBorder
|
|
shadow="md"
|
|
p="sm"
|
|
radius="lg"
|
|
style={{
|
|
alignSelf: 'flex-start',
|
|
backgroundColor: '#212529',
|
|
}}
|
|
w="80%"
|
|
>
|
|
<Text fw={700} size="sm">AI</Text>
|
|
<Group gap="xs" mt="xs">
|
|
<Loader size="xs" />
|
|
<Text size="sm" c="dimmed">Thinking...</Text>
|
|
</Group>
|
|
</Paper>
|
|
)}
|
|
</Stack>
|
|
</ScrollArea>
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<Paper withBorder p="sm" radius="xl" my="md">
|
|
<Group>
|
|
<TextInput
|
|
value={input}
|
|
onChange={(e) => setInput(e.currentTarget.value)}
|
|
placeholder="Speak or type your thoughts..."
|
|
style={{ flex: 1 }}
|
|
styles={{
|
|
input: {
|
|
paddingLeft: '1rem',
|
|
paddingRight: '0.5rem',
|
|
},
|
|
}}
|
|
variant="unstyled"
|
|
disabled={isLoading}
|
|
/>
|
|
|
|
{/* Microphone Recorder */}
|
|
<MicrophoneRecorder
|
|
onTranscriptUpdate={(transcript) => {
|
|
setInput(transcript);
|
|
}}
|
|
onTranscriptFinalized={(transcript) => {
|
|
setInput(transcript);
|
|
setTimeout(() => {
|
|
const form = document.querySelector('form');
|
|
if (form) {
|
|
form.requestSubmit();
|
|
}
|
|
}, 100);
|
|
}}
|
|
/>
|
|
|
|
<Button type="submit" radius="xl" loading={isLoading}>
|
|
Send
|
|
</Button>
|
|
</Group>
|
|
</Paper>
|
|
</form>
|
|
</Container>
|
|
);
|
|
}
|