Co-Authored-By: Claude Haiku 4.5 <[email protected]>
99 lines
2.8 KiB
TypeScript
99 lines
2.8 KiB
TypeScript
import { useState, type KeyboardEvent, type ReactElement } from 'react';
|
|
|
|
interface ComposerProps {
|
|
onSend: (input: { content: string; provider?: string; modelId?: string }) => void;
|
|
onStop: () => void;
|
|
streaming: boolean;
|
|
/** True from local send time through server turn startup/ack and
|
|
* throughout streaming — a superset of `streaming` that also covers the
|
|
* pre-ack window where a second send could otherwise slip through. */
|
|
sending: boolean;
|
|
hasConversation: boolean;
|
|
}
|
|
|
|
export function Composer({
|
|
onSend,
|
|
onStop,
|
|
streaming,
|
|
sending,
|
|
hasConversation,
|
|
}: ComposerProps): ReactElement {
|
|
const [content, setContent] = useState('');
|
|
const [provider, setProvider] = useState('');
|
|
const [modelId, setModelId] = useState('');
|
|
const busy = streaming || sending;
|
|
|
|
function submit(): void {
|
|
if (busy) return;
|
|
const trimmed = content.trim();
|
|
if (!trimmed) return;
|
|
onSend({
|
|
content: trimmed,
|
|
provider: provider.trim() || undefined,
|
|
modelId: modelId.trim() || undefined,
|
|
});
|
|
setContent('');
|
|
}
|
|
|
|
function handleKeyDown(event: KeyboardEvent<HTMLTextAreaElement>): void {
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
event.preventDefault();
|
|
submit();
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
submit();
|
|
}}
|
|
className="flex flex-col gap-2 border-t p-4"
|
|
>
|
|
<div className="flex flex-wrap gap-2">
|
|
<input
|
|
aria-label="Provider"
|
|
value={provider}
|
|
onChange={(event) => setProvider(event.target.value)}
|
|
placeholder="Provider (optional)"
|
|
className="rounded border px-2 py-1 text-xs"
|
|
/>
|
|
<input
|
|
aria-label="Model"
|
|
value={modelId}
|
|
onChange={(event) => setModelId(event.target.value)}
|
|
placeholder="Model (optional)"
|
|
className="rounded border px-2 py-1 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="flex items-end gap-2">
|
|
<textarea
|
|
aria-label="Message"
|
|
value={content}
|
|
onChange={(event) => setContent(event.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
rows={2}
|
|
placeholder="Message… (Enter to send, Shift+Enter for a new line)"
|
|
className="flex-1 resize-none rounded border px-3 py-2 text-sm"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={!content.trim() || busy}
|
|
className="rounded px-3 py-2 text-sm font-medium"
|
|
>
|
|
Send
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="Stop"
|
|
disabled={!hasConversation || !streaming}
|
|
onClick={onStop}
|
|
className="rounded px-3 py-2 text-sm font-medium"
|
|
>
|
|
Stop
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|