Files
Genarrative/src/components/creative-agent/CreativeAgentInputComposer.tsx
T
2026-05-08 11:44:42 +08:00

161 lines
4.7 KiB
TypeScript

import { ArrowUp, ImagePlus, Loader2, Plus, X } from 'lucide-react';
import { type ChangeEvent, useState } from 'react';
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
export type CreativeAgentComposerImage = {
imageUrl: string;
thumbnailUrl: string;
label: string;
};
type CreativeAgentInputComposerProps = {
isBusy: boolean;
variant?: 'panel' | 'floating';
placeholder?: string;
onSubmit: (payload: {
text: string;
image: CreativeAgentComposerImage | null;
}) => void;
};
export function CreativeAgentInputComposer({
isBusy,
variant = 'panel',
placeholder = '想做成什么拼图?',
onSubmit,
}: CreativeAgentInputComposerProps) {
const [text, setText] = useState('');
const [image, setImage] = useState<CreativeAgentComposerImage | null>(null);
const [imageError, setImageError] = useState<string | null>(null);
const canSubmit = !isBusy && Boolean(text.trim() || image);
const handleImageChange = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.currentTarget.value = '';
if (!file) {
return;
}
try {
const dataUrl = await readPuzzleReferenceImageAsDataUrl(file);
setImage({
imageUrl: dataUrl,
thumbnailUrl: dataUrl,
label: file.name.trim() || '参考图',
});
setImageError(null);
} catch (error) {
setImageError(
error instanceof Error ? error.message : '参考图读取失败,请重试。',
);
}
};
const submit = () => {
if (!canSubmit) {
return;
}
onSubmit({
text: text.trim(),
image,
});
setText('');
setImage(null);
setImageError(null);
};
const floating = variant === 'floating';
return (
<section
className={
floating
? 'creative-agent-composer creative-agent-composer--floating'
: 'platform-subpanel rounded-[1.35rem] p-3 sm:p-4'
}
>
<div className="flex items-end gap-2">
<label
className={`platform-icon-button h-11 w-11 shrink-0 ${floating ? 'creative-agent-composer__media-button' : ''} ${isBusy ? 'cursor-not-allowed opacity-55' : 'cursor-pointer'}`}
title={image ? '更换参考图' : '添加参考图'}
>
{floating ? (
<Plus className="h-5 w-5" />
) : (
<ImagePlus className="h-4 w-4" />
)}
<span className="sr-only">{image ? '更换参考图' : '添加参考图'}</span>
<input
type="file"
accept="image/png,image/jpeg,image/webp"
disabled={isBusy}
onChange={(event) => {
void handleImageChange(event);
}}
className="hidden"
/>
</label>
<textarea
value={text}
disabled={isBusy}
rows={2}
onChange={(event) => setText(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
submit();
}
}}
className="min-h-11 flex-1 resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-sm leading-5 text-[var(--platform-text-strong)] outline-none"
placeholder={placeholder}
aria-label="智能创作输入"
/>
<button
type="button"
disabled={!canSubmit}
onClick={submit}
className="platform-icon-button h-11 w-11 shrink-0"
aria-label="发送"
title="发送"
>
{isBusy ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<ArrowUp className="h-4 w-4" />
)}
</button>
</div>
{image ? (
<div className="mt-3 flex items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 px-3 py-2">
<img
src={image.thumbnailUrl}
alt="创作参考图"
className="h-12 w-12 rounded-[0.8rem] object-cover"
/>
<div className="min-w-0 flex-1 truncate text-sm font-semibold text-[var(--platform-text-strong)]">
{image.label}
</div>
<button
type="button"
disabled={isBusy}
onClick={() => setImage(null)}
className="platform-icon-button h-9 w-9"
aria-label="移除参考图"
title="移除参考图"
>
<X className="h-4 w-4" />
</button>
</div>
) : null}
{imageError ? (
<div className="mt-2 text-xs leading-5 text-red-600">{imageError}</div>
) : null}
</section>
);
}