100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useMemo, useState } from 'react';
|
||
|
||
interface StreamingMessageProps {
|
||
text: string;
|
||
modelName?: string | null;
|
||
thinking?: string;
|
||
}
|
||
|
||
const WAITING_QUIPS = [
|
||
'The AI is warming up... give it a moment.',
|
||
'Brewing some thoughts...',
|
||
'Summoning intelligence from the void...',
|
||
'Consulting the silicon oracle...',
|
||
'Teaching electrons to think...',
|
||
];
|
||
|
||
const TIMEOUT_QUIPS = [
|
||
'The model wandered off. Let’s try to find it again.',
|
||
'Response is taking the scenic route.',
|
||
'That answer is clearly overthinking things.',
|
||
'Still working. Either brilliance or a detour.',
|
||
];
|
||
|
||
export function StreamingMessage({
|
||
text,
|
||
modelName,
|
||
thinking,
|
||
}: StreamingMessageProps): React.ReactElement {
|
||
const [elapsedMs, setElapsedMs] = useState(0);
|
||
|
||
useEffect(() => {
|
||
setElapsedMs(0);
|
||
const startedAt = Date.now();
|
||
const timer = window.setInterval(() => {
|
||
setElapsedMs(Date.now() - startedAt);
|
||
}, 1000);
|
||
return () => window.clearInterval(timer);
|
||
}, [text, modelName, thinking]);
|
||
|
||
const quip = useMemo(() => {
|
||
if (elapsedMs >= 18_000) {
|
||
return TIMEOUT_QUIPS[Math.floor((elapsedMs / 1000) % TIMEOUT_QUIPS.length)];
|
||
}
|
||
if (elapsedMs >= 4_000) {
|
||
return WAITING_QUIPS[Math.floor((elapsedMs / 1000) % WAITING_QUIPS.length)];
|
||
}
|
||
return null;
|
||
}, [elapsedMs]);
|
||
|
||
return (
|
||
<div className="flex justify-start">
|
||
<div
|
||
className="max-w-[min(78ch,85%)] rounded-3xl border px-4 py-3 text-sm shadow-[var(--shadow-ms-sm)]"
|
||
style={{
|
||
backgroundColor: 'var(--color-surface)',
|
||
borderColor: 'var(--color-border)',
|
||
color: 'var(--color-text)',
|
||
}}
|
||
>
|
||
<div className="mb-2 flex items-center gap-2 text-[11px]">
|
||
<span className="font-medium text-[var(--color-text-2)]">Assistant</span>
|
||
{modelName ? (
|
||
<span className="rounded-full border border-[var(--color-border)] px-2 py-0.5 text-[var(--color-text-2)]">
|
||
{modelName}
|
||
</span>
|
||
) : null}
|
||
<span className="text-[var(--color-muted)]">{text ? 'Responding…' : 'Thinking…'}</span>
|
||
</div>
|
||
{text ? (
|
||
<div className="whitespace-pre-wrap break-words">{text}</div>
|
||
) : (
|
||
<div className="flex items-center gap-2 text-[var(--color-muted)]">
|
||
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-[var(--color-ms-blue-500)]" />
|
||
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-[var(--color-ms-blue-500)] [animation-delay:0.2s]" />
|
||
<span className="inline-block h-2 w-2 animate-pulse rounded-full bg-[var(--color-ms-blue-500)] [animation-delay:0.4s]" />
|
||
</div>
|
||
)}
|
||
{thinking ? (
|
||
<div
|
||
className="mt-3 rounded-2xl border px-3 py-2 font-mono text-xs whitespace-pre-wrap"
|
||
style={{
|
||
backgroundColor: 'var(--color-bg-deep)',
|
||
borderColor: 'var(--color-border)',
|
||
color: 'var(--color-text-2)',
|
||
}}
|
||
>
|
||
{thinking}
|
||
</div>
|
||
) : null}
|
||
<div className="mt-2 flex items-center gap-2 text-xs text-[var(--color-muted)]">
|
||
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-[var(--color-ms-blue-500)]" />
|
||
<span>{quip ?? (text ? 'Responding…' : 'Thinking…')}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|