"use client"; import { ArrowUp, Bot, Check, ChevronDown, ChevronUp, Globe2, Image as ImageIcon, ImagePlus, Library, Paperclip, Plus, Sparkles, Square, Video, X } from "lucide-react"; import { forwardRef, type ReactNode, useMemo, useRef, useState } from "react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Switch } from "@/components/ui/switch"; import { useI18n } from "@/i18n/i18n"; import { PromptComposer, type PromptComposerHandle, type UploadedReferenceImage, type CanvasAnnotation, type AnnotationPreviewRequest } from "@/ui/components/PromptComposer"; export type ComposerMode = "agent" | "image"; export type ComposerImageQuality = "auto" | "high" | "medium" | "low"; export type ComposerImageRatio = "1:1" | "3:2" | "2:3" | "4:3" | "3:4" | "9:16" | "1:1-2k" | "16:9-2k" | "9:16-2k" | "16:9-4k" | "9:16-4k" | "auto"; export type ComposerImageSettings = { quality: ComposerImageQuality; ratio: ComposerImageRatio; width: number; height: number; count: number; }; export type CanvasComposerModel = { id: string; label: string; description?: string; duration?: string; credits: number; }; export type CanvasAgentComposerVariant = "panel" | "floating"; export type CanvasAgentComposerProps = { text: string; mode: ComposerMode; models: CanvasComposerModel[]; selectedModel: CanvasComposerModel; settings: ComposerImageSettings; webSearchEnabled: boolean; generating: boolean; variant?: CanvasAgentComposerVariant; references?: UploadedReferenceImage[]; annotations?: CanvasAnnotation[]; previewSlot?: ReactNode; onTextChange: (text: string) => void; onModeChange: (mode: ComposerMode) => void; onSettingsChange: (settings: ComposerImageSettings) => void; onModelChange: (modelId: string) => void; onWebSearchChange: (enabled: boolean) => void; onUploadFile: (file: File | null) => void | Promise; onReferencesChange: (references: UploadedReferenceImage[]) => void; onReferenceRemoved: (reference: UploadedReferenceImage) => void; onAnnotationsChange?: (annotations: CanvasAnnotation[]) => void; onAnnotationPreviewChange?: (preview: AnnotationPreviewRequest | null) => void; onSubmit: () => void; onStop?: () => void; }; const qualityOptions: Array<{ id: ComposerImageQuality; labelKey: string }> = [ { id: "auto", labelKey: "qualityAuto" }, { id: "high", labelKey: "qualityHigh" }, { id: "medium", labelKey: "qualityMedium" }, { id: "low", labelKey: "qualityLow" } ]; export const composerRatioOptions: Array<{ id: ComposerImageRatio; label: string; width: number; height: number }> = [ { id: "1:1", label: "1:1", width: 1024, height: 1024 }, { id: "3:2", label: "3:2", width: 1536, height: 1024 }, { id: "2:3", label: "2:3", width: 1024, height: 1536 }, { id: "4:3", label: "4:3", width: 1365, height: 1024 }, { id: "3:4", label: "3:4", width: 1024, height: 1365 }, { id: "9:16", label: "9:16", width: 1024, height: 1820 }, { id: "1:1-2k", label: "1:1(2k)", width: 2048, height: 2048 }, { id: "16:9-2k", label: "16:9(2k)", width: 2048, height: 1152 }, { id: "9:16-2k", label: "9:16(2k)", width: 1152, height: 2048 }, { id: "16:9-4k", label: "16:9(4k)", width: 4096, height: 2304 }, { id: "9:16-4k", label: "9:16(4k)", width: 2304, height: 4096 }, { id: "auto", label: "auto", width: 1024, height: 1024 } ]; export const defaultComposerImageSettings: ComposerImageSettings = { quality: "auto", ratio: "1:1", width: 1024, height: 1024, count: 1 }; export function composerImageSize(settings: ComposerImageSettings) { return `${Math.max(1, Math.round(settings.width))}x${Math.max(1, Math.round(settings.height))}`; } export function composerSettingsSummary(settings: ComposerImageSettings) { const ratioLabel = composerRatioOptions.find((option) => option.id === settings.ratio)?.label ?? settings.ratio; return `${qualityLabel(settings.quality)} · ${ratioLabel} · ${settings.count} img`; } function qualityLabel(quality: ComposerImageQuality) { if (quality === "high") return "高"; if (quality === "medium") return "中"; if (quality === "low") return "低"; return "自动"; } function hasComposerPromptText(value: string) { return value.replace(/\u00a0/g, " ").trim().length > 0; } export const CanvasAgentComposer = forwardRef(function CanvasAgentComposer( { text, mode, models, selectedModel, settings, webSearchEnabled, generating, variant = "panel", references, annotations, previewSlot, onTextChange, onModeChange, onSettingsChange, onModelChange, onWebSearchChange, onUploadFile, onReferencesChange, onReferenceRemoved, onAnnotationsChange, onAnnotationPreviewChange, onSubmit, onStop }, ref ) { const { t } = useI18n(); const fileInputRef = useRef(null); const [assetMenuOpen, setAssetMenuOpen] = useState(false); const [modeMenuOpen, setModeMenuOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); const [modelOpen, setModelOpen] = useState(false); const [upgradeVisible, setUpgradeVisible] = useState(true); const sendCost = useMemo(() => Math.max(1, selectedModel.credits * Math.max(1, settings.count)), [selectedModel.credits, settings.count]); const modeLabel = mode === "image" ? t("imageMode") : t("agentShort"); const ModeIcon = mode === "image" ? ImagePlus : Bot; const canSubmit = hasComposerPromptText(text); const patchSettings = (patch: Partial) => { onSettingsChange({ ...settings, ...patch }); }; const selectRatio = (ratio: ComposerImageRatio) => { const nextRatio = composerRatioOptions.find((option) => option.id === ratio) ?? composerRatioOptions[0]; patchSettings({ ratio, width: nextRatio.width, height: nextRatio.height }); }; const composerCard = (
{mode === "image"} {previewSlot}
{ const file = event.target.files?.[0] ?? null; event.currentTarget.value = ""; void onUploadFile(file); setAssetMenuOpen(false); }} /> fileInputRef.current?.click()}> {t("uploadFile")} {t("chooseFromLibrary")} { event.preventDefault(); onWebSearchChange(!webSearchEnabled); }} > {t("webSearch")} event.stopPropagation()} onCheckedChange={onWebSearchChange} /> { onModeChange("agent"); setModeMenuOpen(false); }} > {t("agentShort")} {mode === "agent" && } { onModeChange("image"); setModeMenuOpen(false); }} > {t("imageMode")} {mode === "image" && } {mode === "image" && ( )}
{models.map((model) => ( { onModelChange(model.id); setModelOpen(false); }} > {model.label} ⚡{model.credits} {model.id === selectedModel.id && } ))}
); if (variant === "floating") return composerCard; return (
{upgradeVisible && (
{t("unlockModels")}
)} {composerCard}
); }); export function ComposerSettingsPanel({ settings, onPatch, onSelectRatio }: { settings: ComposerImageSettings; onPatch: (patch: Partial) => void; onSelectRatio: (ratio: ComposerImageRatio) => void; }) { const { t } = useI18n(); return (

{t("quality")}

{qualityOptions.map((option) => ( ))}

{t("imageSizeLabel")}

{composerRatioOptions.map((option) => ( ))}

Image

{Array.from({ length: 10 }, (_, index) => index + 1).map((count) => ( ))}
); } function RatioGlyph({ ratio }: { ratio: ComposerImageRatio }) { const portrait = ratio.startsWith("2:3") || ratio.startsWith("3:4") || ratio.startsWith("9:16"); const wide = ratio.startsWith("3:2") || ratio.startsWith("4:3") || ratio.startsWith("16:9"); return