113 lines
3.4 KiB
TypeScript
113 lines
3.4 KiB
TypeScript
import { Button } from "@code/ui/components/button";
|
|
import { ImagePlus, LoaderCircle, Send } from "lucide-react";
|
|
import { useRef } from "react";
|
|
|
|
import { PendingChatAttachments } from "@/components/chat/chat-attachments";
|
|
import { useChatImages } from "@/hooks/chat/use-chat-images";
|
|
import type { WorkspaceState } from "@/lib/workspace/types";
|
|
|
|
export const ConversationComposer = ({
|
|
draft,
|
|
onDraftChange,
|
|
workspace,
|
|
}: {
|
|
readonly draft: string;
|
|
readonly onDraftChange: (draft: string) => void;
|
|
readonly workspace: WorkspaceState;
|
|
}) => {
|
|
const imageInput = useRef<HTMLInputElement>(null);
|
|
const attachments = useChatImages();
|
|
const busy =
|
|
workspace.agent.status === "submitted" ||
|
|
workspace.agent.status === "streaming";
|
|
|
|
const send = async () => {
|
|
const message = draft.trim();
|
|
if (!message || busy) {
|
|
return;
|
|
}
|
|
await workspace.agent.sendMessage(message, {
|
|
images: attachments.images.map((image) => image.file),
|
|
});
|
|
onDraftChange("");
|
|
attachments.clear();
|
|
};
|
|
|
|
return (
|
|
<div className="shrink-0 border-t border-[#d7d3c7] bg-[#faf9f4] p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]">
|
|
{attachments.images.length > 0 ? (
|
|
<div className="mx-auto max-w-2xl">
|
|
<PendingChatAttachments
|
|
images={attachments.images}
|
|
onRemove={attachments.handleRemove}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
{workspace.agent.error ? (
|
|
<p className="mx-auto mb-2 max-w-2xl text-xs text-red-700">
|
|
{workspace.agent.error.message}
|
|
</p>
|
|
) : null}
|
|
{attachments.error ? (
|
|
<p className="mx-auto mb-2 max-w-2xl text-xs text-red-700">
|
|
{attachments.error}
|
|
</p>
|
|
) : null}
|
|
<div className="mx-auto flex max-w-2xl items-end gap-2">
|
|
<input
|
|
accept="image/*"
|
|
aria-label="Attach images"
|
|
className="sr-only"
|
|
multiple
|
|
onChange={(event) => {
|
|
attachments.addFiles(event.target.files);
|
|
event.target.value = "";
|
|
}}
|
|
ref={imageInput}
|
|
type="file"
|
|
/>
|
|
<Button
|
|
aria-label="Attach images"
|
|
className="size-11 shrink-0"
|
|
disabled={busy}
|
|
onClick={() => imageInput.current?.click()}
|
|
size="icon"
|
|
type="button"
|
|
variant="outline"
|
|
>
|
|
<ImagePlus className="size-4" />
|
|
</Button>
|
|
<textarea
|
|
aria-label="Message Zopu"
|
|
className="max-h-32 min-h-11 flex-1 resize-none border border-[#c9c5b9] bg-white px-3 py-2.5 text-sm outline-none focus:border-[#55564e]"
|
|
disabled={busy}
|
|
onChange={(event) => onDraftChange(event.target.value)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
event.preventDefault();
|
|
void send();
|
|
}
|
|
}}
|
|
placeholder="Describe an outcome or problem…"
|
|
rows={1}
|
|
value={draft}
|
|
/>
|
|
<Button
|
|
aria-label="Send message"
|
|
className="size-11 shrink-0"
|
|
disabled={!draft.trim() || busy}
|
|
onClick={() => void send()}
|
|
size="icon"
|
|
type="button"
|
|
>
|
|
{busy ? (
|
|
<LoaderCircle className="size-4 animate-spin" />
|
|
) : (
|
|
<Send className="size-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|