import { ImageSizePicker } from "@/components/image-size-picker"; import { imageResolutionUsesQuality } from "@/lib/image-size-presets"; import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent, type ReactNode, type RefObject } from "react"; import { App, Button, Dropdown, Popover } from "antd"; import { AppDrawer } from "@/components/ui/product/app-drawer"; import { AppModal } from "@/components/ui/product/app-modal"; import { Tooltip } from "@/components/ui/base/tooltip"; import { Reorder, motion, useReducedMotion } from "motion/react"; import { ArrowDown, ArrowUp, Brain, ChevronDown, ChevronLeft, ChevronRight, Clapperboard, Clock3, Copy, Download, FileText, Film, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Minimize2, MoreHorizontal, Music2, Pencil, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Trash2, UserRound, WandSparkles, Waves, X } from "lucide-react"; import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown"; import { GenerationToolCard, type GenerationToolStatus } from "@/components/ai/generation-tool-card"; import { WorkingDots, WorkingGlow } from "@/components/ai/working-indicator"; import { MessageReasoning } from "@/components/ai/message-reasoning"; import { creationResultAssetIds } from "@/lib/canvas/canvas-asset-handoff"; import { generationErrorMessage } from "@/lib/generation-error"; import { formatVideoResolutionLabel as videoResolutionLabel } from "@/lib/video-generation-options"; import { useAssetStore } from "@/stores/use-asset-store"; import { CachedResourceImage } from "@/components/cached-resource-image"; import { CanvasImagePreview } from "@/components/canvas/canvas-image-preview"; import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea"; import { VoiceRecordingButton } from "@/components/conversation/voice-recording-button"; import { ModelPicker } from "@/components/model-picker"; import { CreditSymbol, requestCreditCost } from "@/constant/credits"; import { ASSET_CATEGORY_LABELS } from "@/lib/asset-category"; import { formatShotOrdinal } from "@/lib/shot-label"; import { useCopyText } from "@/hooks/use-copy-text"; import { buildImageResolutionOptions, formatImageResolutionSize, supportsImageResolutionPresets } from "@/lib/image-resolution-tiers"; import { modelCapabilityConfigFor, normalizeVideoValue, videoDurationOptions, type ImageCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities"; import { mergedImageCapabilityConfig, type ModelRequirements } from "@/lib/model-selection"; import type { Skill } from "@/services/api/skills"; import { resolveResourceUrl } from "@/services/api/resources"; import { modelOptionName, resolveModelChannel, type AiConfig } from "@/stores/use-config-store"; import { useAppearanceStore } from "@/stores/use-appearance-store"; import { useUserStore } from "@/stores/use-user-store"; import type { PromptOptimizerProvider } from "@/lib/plugins/plugin-types"; import { displayCreationPrompt, type CreationReference } from "./creation-references"; import { creationAttachmentKind, creationMediaAspectRatio, removeCreationAttachment, type CreationAttachment, type CreationMode } from "./creation-assets"; import { conversationTimestamp, isImageAttachment, isVideoAttachment } from "./creation-conversations"; import { conversationTimeFormatter, countOptions, historyDayFormatter, messageTimeFormatter, modeLabels, qualityOptions, ratioOptions, resolutionOptions, shotScriptLabels, type CreationConversation, type CreationMessage, type CreationShotRailEntry, type CreationStatus } from "./creation-types"; import "./creation-product.css"; import { creationFeaturedWorks, inspirationSource } from "./creation-inspirations"; const CanvasPromptOptimizerDrawer = lazy(() => import("@/components/canvas/canvas-prompt-optimizer-drawer").then((module) => ({ default: module.CanvasPromptOptimizerDrawer }))); export const creationAssetCategoryLabels: Record = { all: "全部素材", ...ASSET_CATEGORY_LABELS }; function creationConversationBucket(updatedAt: string): "today" | "yesterday" | "week" | "earlier" { const at = new Date(updatedAt).getTime(); if (!Number.isFinite(at)) return "earlier"; const nowStart = new Date(); nowStart.setHours(0, 0, 0, 0); const targetStart = new Date(at); targetStart.setHours(0, 0, 0, 0); const days = Math.round((nowStart.getTime() - targetStart.getTime()) / 86400000); if (days <= 0) return "today"; if (days === 1) return "yesterday"; if (days < 7) return "week"; return "earlier"; } const creationBucketLabels: Record<"today" | "yesterday" | "week" | "earlier", string> = { today: "今天", yesterday: "昨天", week: "近 7 天", earlier: "更早" }; export function CreationHistoryDrawer({ open, conversations, activeId, onNew, onClose, onSelect, onDelete, onRename }: { open: boolean; conversations: CreationConversation[]; activeId: string; onNew: () => void; onClose: () => void; onSelect: (conversation: CreationConversation) => void; onDelete: (conversation: CreationConversation) => void; onRename: (conversation: CreationConversation, title: string) => void }) { const [keyword, setKeyword] = useState(""); const [renamingId, setRenamingId] = useState(null); const [menuOpenId, setMenuOpenId] = useState(null); const renameInputRef = useRef(null); const skipRenameCommitRef = useRef(false); const assistantName = useAppearanceStore((state) => state.appearance.brandName); const exportUser = useUserStore((state) => state.user); const { message: drawerToast } = App.useApp(); useEffect(() => { if (!open) return; setKeyword(""); setRenamingId(null); setMenuOpenId(null); }, [open]); const commitRename = (conversation: CreationConversation) => { const shouldSkip = skipRenameCommitRef.current; skipRenameCommitRef.current = false; const value = renameInputRef.current?.value.trim() || ""; setRenamingId(null); if (shouldSkip || !value) return; const original = conversation.title.trim() || "新创作"; if (value === original) return; onRename(conversation, value); }; const cancelRename = () => { skipRenameCommitRef.current = true; setRenamingId(null); }; const beginRename = (conversation: CreationConversation) => { skipRenameCommitRef.current = false; setMenuOpenId(null); setRenamingId(conversation.id); }; const visibleConversations = useMemo(() => { const query = keyword.trim().toLowerCase(); if (!query) return conversations; return conversations.filter((conversation) => { const latest = conversationPreviewMessage(conversation); const searchable = [ conversation.title, ...conversation.messages.flatMap((message) => [message.content, displayCreationPrompt(message.content, message.references || [])]), latest?.mode ? modeLabels[latest.mode] : "创作", formatConversationTime(conversation.updatedAt), ].filter(Boolean).join(" ").toLowerCase(); return searchable.includes(query); }); }, [conversations, keyword]); return } className="creation-history-drawer" rootClassName="creation-history-drawer-root" title={
历史对话{conversations.length} 个对话
}>
{visibleConversations.length ?
    {visibleConversations.flatMap((conversation, index) => { const showGroupHead = !keyword.trim() && (index === 0 || creationConversationBucket(conversation.updatedAt) !== creationConversationBucket(visibleConversations[index - 1].updatedAt)); const latest = conversationPreviewMessage(conversation); const active = conversation.id === activeId; const HistoryTypeIcon = latest?.mode === "video" ? Clapperboard : latest?.mode === "image" ? ImageIcon : latest?.mode === "text" ? MessageSquareText : Sparkles; return [ showGroupHead ?
  • {creationBucketLabels[creationConversationBucket(conversation.updatedAt)]}

  • : null,
  • {renamingId === conversation.id ? (
    event.currentTarget.select()} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); event.currentTarget.blur(); } else if (event.key === "Escape") { cancelRename(); } }} onBlur={() => commitRename(conversation)} />
    ) : (
    setMenuOpenId(open ? conversation.id : null)} overlayClassName="creation-history-menu-overlay" menu={{ items: [{ key: "rename", label: "重命名", icon: }, { key: "export", label: "导出对话", icon: }, { key: "delete", label: "删除对话", danger: true, icon: }], onClick: ({ key }) => { setMenuOpenId(null); if (key === "rename") { beginRename(conversation); } else if (key === "export") { downloadCreationConversation(conversation, assistantName, exportUser?.displayName || "你"); drawerToast.success("对话已导出为 Markdown"); } else { onDelete(conversation); } } }}>
    )}
  • , ]; })}
:
{keyword.trim() ? "没有找到匹配的对话" : "暂无历史对话"}
}
; } export function CreationWorkspaceToolbar({ shots, onJumpToShot, onNewConversation, onOpenHistory, onContinueCanvas, openingCanvas }: { shots: CreationShotRailEntry[]; onJumpToShot: (shot: CreationShotRailEntry) => void; onNewConversation: () => void; onOpenHistory: () => void; onContinueCanvas: () => void; openingCanvas: boolean }) { const [railOpen, setRailOpen] = useState(false); const railRef = useRef(null); useEffect(() => { if (!railOpen) return; const onPointerDown = (event: MouseEvent) => { if (railRef.current && !railRef.current.contains(event.target as Node)) setRailOpen(false); }; window.addEventListener("mousedown", onPointerDown); return () => window.removeEventListener("mousedown", onPointerDown); }, [railOpen]); return
{railOpen ?
镜头时间线{shots.length ? `共 ${shots.length} 镜` : "空轨道"}
{shots.length ?
    {shots.map((shot) => { const resultStatus = shot.result?.status; const statusLabel = resultStatus === "done" ? "完成" : resultStatus === "error" ? "生成失败" : resultStatus === "pending" ? "生成中" : resultStatus === "cancelled" ? "已停止" : "待生成"; return
  1. ; })}
:

在下方发送一条视频消息,就会自动成为第 1 镜。

}
: null}
; } export function CreationMessageView({ item, shotNumber, onRetryFailure, onCreateVariant, onEditUserMessage, onContinueCanvas, openingCanvas }: { item: CreationMessage; shotNumber: number; onRetryFailure: () => void; onCreateVariant: () => void; onEditUserMessage: (text: string) => void; onContinueCanvas: (ids?: string[]) => void; openingCanvas: boolean }) { const brandName = useAppearanceStore((state) => state.appearance.brandName); if (item.role === "user") return ; const mode = item.mode || "text"; const stateLabel = item.status === "pending" ? "生成中" : item.status === "cancelled" ? "已停止" : item.status === "error" ? "生成失败" : ""; const heading = mode !== "text" ? ( <>{shotNumber > 0 ? 镜 {shotNumber} : null}{mode === "image" ? "图像生成" : "视频生成"}{item.status === "pending" ? {brandName}正在生成{mode === "video" ? "视频" : "图像"}…… : item.status === "done" ? 你的{mode === "video" ? "视频" : "图像"}已创建 : null}{item.status === "done" ? : null}{item.createdAt ? : null}{stateLabel ? {stateLabel} : null} ) : ( <>{shotNumber > 0 ? 镜 {shotNumber} : null}{brandName}{item.createdAt ? : null}{stateLabel ? {stateLabel} : null} ); const toolStatus: GenerationToolStatus = item.status === "pending" ? "running" : item.status === "error" ? "error" : item.status === "cancelled" ? "cancelled" : "completed"; return
{mode === "text" ? <>
{heading}
{item.reasoning ?
: null}
{item.content ? {item.content} : 正在生成…}
: } {item.error && mode === "text" ?
{generationErrorMessage(item.error)}
: null}
; } function CreationUserMessage({ item, shotNumber, onEditUserMessage }: { item: CreationMessage; shotNumber: number; onEditUserMessage: (text: string) => void }) { const [previewUrl, setPreviewUrl] = useState(""); const [previewType, setPreviewType] = useState<"image" | "video">("image"); const copyText = useCopyText(); const visiblePrompt = displayCreationPrompt(item.content, item.references || []); const user = useUserStore((state) => state.user); const userAvatarUrl = user?.avatarUrl?.trim(); return
{shotNumber > 0 ? 镜 {shotNumber} : null}{item.createdAt ? : null}{user?.displayName || "你"}{userAvatarUrl ? : }

{visiblePrompt}

{item.references?.length ? : null} {item.attachments?.length ?
{item.attachments.map((attachment) => { const kind = creationAttachmentKind(attachment); const previewable = kind === "image" || kind === "video"; const url = attachment.previewUrl || ("dataUrl" in attachment ? attachment.dataUrl : attachment.url) || ""; const imageUrl = kind === "image" ? resolveResourceUrl(attachment.storageKey, url) : ""; const previewUrl = kind === "image" ? imageUrl : url; return ; })}
: null}
setPreviewUrl("")} />
; } function MediaResult({ item, onRetryFailure, onCreateVariant, onContinueCanvas, openingCanvas }: { item: CreationMessage; onRetryFailure: () => void; onCreateVariant: () => void; onContinueCanvas: (ids?: string[]) => void; openingCanvas: boolean }) { const [previewUrl, setPreviewUrl] = useState(""); const [previewType, setPreviewType] = useState<"image" | "video">("image"); const assets = useAssetStore((state) => state.assets); const resultUrls = item.resultUrls || []; const resultAssetIds = resultUrls.length ? creationResultAssetIds(assets, { messageId: item.id, taskIds: item.taskIds || [], resultUrls }) : []; const canContinueWithResults = resultUrls.length > 0 && resultAssetIds.length === resultUrls.length; if (item.status === "pending") return ; if ((item.status === "error" || item.status === "cancelled") && !resultUrls.length) return
{item.status === "cancelled" ? item.content || "已停止" : generationErrorMessage(item.error || "生成失败")}
; if (!resultUrls.length) return
没有返回可预览结果
; const isVideo = item.mode === "video"; return
{isVideo ? :
{resultUrls.map((url) => )}
}
{isVideo ? "视频结果" : `${resultUrls.length} 张图片`}{resultUrls.map((url, index) => {resultUrls.length > 1 ? `下载 ${index + 1}` : <>下载})}
setPreviewUrl("")} />
; } function CreationMediaPending({ mode, ratio }: { mode: CreationMode; ratio?: string }) { const brandName = useAppearanceStore((state) => state.appearance.brandName); return
{brandName}正在生成{mode === "video" ? "视频" : "图像"}
; } function CreationMessageReferences({ references }: { references: CreationReference[] }) { return
{references.map((reference) => { const Icon = reference.kind === "skill" ? Sparkles : reference.kind === "image" ? ImageIcon : reference.kind === "video" ? Film : reference.kind === "audio" ? Music2 : FileText; const imageUrl = reference.kind === "image" ? resolveResourceUrl(reference.storageKey, reference.previewUrl) : reference.previewUrl; return {imageUrl && reference.kind === "video" ? ; })}
; } function CreationMediaPreviewModal({ url, type, onClose }: { url: string; type: "image" | "video"; onClose: () => void }) { if (type === "image") return ; return {url ? ; } function CreationAttachmentThumbnail({ item, onPreview, onRemove }: { item: CreationAttachment; onPreview: (type: "image" | "video", url: string) => void; onRemove: (id: string) => void; }) { const kind = creationAttachmentKind(item); const previewable = kind === "image" || kind === "video"; const url = (kind === "video" ? item.url : item.previewUrl) || ""; const imageUrl = kind === "image" ? resolveResourceUrl(item.storageKey, item.previewUrl) : ""; const previewUrl = kind === "image" ? imageUrl : url; const content = kind === "video" ?