Files
app/app/chat/page.tsx
Albert 4571a6f1cc feat: Add typing indicator while AI is thinking
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>
2025-11-09 04:57:24 +00:00

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>
);
}