Submatter/apps/web/components/ai/workspace-composer.tsx

122 lines
3.7 KiB
TypeScript
Raw Normal View History

"use client";
import { useEffect, useId, useRef } from "react";
import type { FormEvent, RefObject } from "react";
import { Send, Square } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { AIMode } from "./workspace-mode";
import { submitComposer } from "./composer-events";
export function shouldSubmitOnEnter(event: {
key: string;
shiftKey: boolean;
isComposing: boolean;
}): boolean {
return event.key === "Enter" && !event.shiftKey && !event.isComposing;
}
interface WorkspaceComposerProps {
mode: AIMode;
input: string;
onInputChange: (value: string) => void;
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
busy?: boolean;
onStop?: () => void;
inputRef?: RefObject<HTMLTextAreaElement | null>;
active?: boolean;
}
export function WorkspaceComposer({
mode,
input,
onInputChange,
onSubmit,
busy = false,
onStop,
inputRef,
active = true,
}: WorkspaceComposerProps) {
const localRef = useRef<HTMLTextAreaElement>(null);
const ref = inputRef ?? localRef;
const hintId = useId();
const isChat = mode === "chat";
useEffect(() => {
if (!active || !ref.current) return;
ref.current.style.height = "auto";
ref.current.style.height = `${Math.min(ref.current.scrollHeight, 200)}px`;
}, [input, active, ref]);
return (
<div className="shrink-0 border-t bg-background px-4 py-4">
<form
aria-label={isChat ? "Chat composer" : "Agent builder composer"}
onSubmit={(event) => {
submitComposer(event, input, busy, onSubmit, () =>
ref.current?.focus(),
);
}}
className="mx-auto flex max-w-3xl items-end gap-3"
>
<textarea
ref={ref}
data-composer-mode={mode}
value={input}
onChange={(event) => onInputChange(event.target.value)}
onKeyDown={(event) => {
if (
shouldSubmitOnEnter({
key: event.key,
shiftKey: event.shiftKey,
isComposing: event.nativeEvent.isComposing,
})
) {
event.preventDefault();
event.currentTarget.form?.requestSubmit();
}
}}
placeholder={isChat ? "Ask anything…" : "Describe a reusable agent…"}
aria-label={isChat ? "Chat message" : "Agent instructions"}
aria-describedby={hintId}
rows={1}
readOnly={busy}
className="max-h-[200px] min-h-[44px] flex-1 resize-none rounded-xl border bg-muted/30 px-4 py-3 text-sm outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring read-only:opacity-60"
/>
{busy && onStop ? (
<Button
type="button"
size="icon"
variant="outline"
className="h-11 w-11 shrink-0 rounded-xl"
aria-label="Stop generating"
onClick={() => {
onStop();
ref.current?.focus();
}}
>
<Square className="size-4" aria-hidden="true" />
</Button>
) : (
<Button
type="submit"
size="icon"
className="h-11 w-11 shrink-0 rounded-xl"
disabled={busy || !input.trim()}
aria-label={isChat ? "Send message" : "Check builder availability"}
>
<Send className="size-4" aria-hidden="true" />
</Button>
)}
</form>
<p
id={hintId}
className="mx-auto mt-2 max-w-3xl text-center text-[10px] text-muted-foreground"
>
{isChat ? "AI can make mistakes. " : "Builder is not connected yet. "}
Enter to send · Shift+Enter for a new line.
</p>
</div>
);
}