Files
zopu-code/apps/web/src/components/workspace/conversation-composer.tsx

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