mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 17:04:27 +08:00
feat(agent): update agent connection behavior to start with a blank conversation and allow user-initiated session restoration
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { useRef, type ReactNode } from "react";
|
||||
import { Button, Tooltip } from "antd";
|
||||
import { ArrowUp, ImagePlus, LoaderCircle, Square, X } from "lucide-react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { isPlainEnterKey } from "@/lib/keyboard-event";
|
||||
import type { AgentChatAttachment } from "./agent-chat-message";
|
||||
|
||||
export function AgentChatComposer({
|
||||
prompt,
|
||||
attachments = [],
|
||||
disabled,
|
||||
sending,
|
||||
placeholder,
|
||||
theme,
|
||||
onPromptChange,
|
||||
onSubmit,
|
||||
onStop,
|
||||
onAddFiles,
|
||||
onRemoveAttachment,
|
||||
left,
|
||||
}: {
|
||||
prompt: string;
|
||||
attachments?: AgentChatAttachment[];
|
||||
disabled?: boolean;
|
||||
sending?: boolean;
|
||||
placeholder: string;
|
||||
theme: (typeof canvasThemes)[keyof typeof canvasThemes];
|
||||
onPromptChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
onStop?: () => void;
|
||||
onAddFiles?: (files: FileList | File[] | null) => void | Promise<void>;
|
||||
onRemoveAttachment?: (id: string) => void;
|
||||
left?: ReactNode;
|
||||
}) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const canSubmit = !disabled && !sending && Boolean(prompt.trim() || attachments.length);
|
||||
return (
|
||||
<div className="px-2 pb-2 pt-2" onWheelCapture={(event) => event.stopPropagation()}>
|
||||
<div className="rounded-[24px] border px-3 pb-3 pt-3 shadow-lg" style={{ background: theme.toolbar.panel, borderColor: theme.node.stroke }}>
|
||||
{attachments.length ? (
|
||||
<div className="thin-scrollbar mb-2 flex gap-2 overflow-x-auto pb-1">
|
||||
{attachments.map((item) => (
|
||||
<div key={item.id} className="group relative size-14 shrink-0 overflow-hidden rounded-xl border" style={{ borderColor: theme.node.stroke }} title={item.name}>
|
||||
<img src={item.url} alt={item.name} className="size-full object-cover" />
|
||||
{onRemoveAttachment ? (
|
||||
<button type="button" className="absolute right-1 top-1 grid size-5 place-items-center rounded-full border opacity-0 shadow-sm transition group-hover:opacity-100" style={{ background: theme.toolbar.panel, borderColor: theme.node.stroke, color: theme.node.text }} onClick={() => onRemoveAttachment(item.id)} aria-label="移除图片">
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(event) => onPromptChange(event.target.value)}
|
||||
onPaste={(event) => {
|
||||
if (!onAddFiles) return;
|
||||
const images = Array.from(event.clipboardData.files).filter((file) => file.type.startsWith("image/"));
|
||||
if (!images.length) return;
|
||||
event.preventDefault();
|
||||
void onAddFiles(images);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (!isPlainEnterKey(event)) return;
|
||||
event.preventDefault();
|
||||
void onSubmit();
|
||||
}}
|
||||
className="thin-scrollbar max-h-32 min-h-20 w-full resize-none border-0 bg-transparent px-1 py-1 text-sm leading-5 outline-none placeholder:opacity-45"
|
||||
style={{ color: theme.node.text }}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<div className="mt-2 flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
{onAddFiles ? (
|
||||
<>
|
||||
<input ref={fileInputRef} hidden type="file" accept="image/*" multiple onChange={(event) => {
|
||||
void onAddFiles(event.target.files);
|
||||
event.target.value = "";
|
||||
}} />
|
||||
<Tooltip title="上传图片">
|
||||
<Button type="text" shape="circle" className="!h-9 !w-9 !min-w-9" disabled={sending} style={{ color: theme.node.muted }} icon={<ImagePlus className="size-4" />} onClick={() => fileInputRef.current?.click()} />
|
||||
</Tooltip>
|
||||
</>
|
||||
) : null}
|
||||
{left}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
{sending && onStop ? (
|
||||
<Button danger shape="circle" className="!h-10 !w-10 !min-w-10" icon={<Square className="size-4" />} onClick={() => void onStop()} aria-label="停止" />
|
||||
) : (
|
||||
<Button type="primary" shape="circle" className="!h-10 !w-10 !min-w-10" disabled={!canSubmit} icon={sending ? <LoaderCircle className="size-4 animate-spin" /> : <ArrowUp className="size-4" />} onClick={() => void onSubmit()} aria-label="发送" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user