fix(canvas): 创作参考内容 - 修复图片预览并将模型价格移至发送按钮
This commit is contained in:
@@ -226,7 +226,7 @@ export function ModelPicker({
|
||||
theme={theme}
|
||||
creationVariant={creationVariant}
|
||||
showConfiguredModelName={showConfiguredModelName}
|
||||
showPrice={creditsEnabled}
|
||||
showPrice={showSelectedPrice && creditsEnabled}
|
||||
disabledReason={disabledReason}
|
||||
/>
|
||||
{selected ? <Check className="canvas-model-picker-option-check" style={{ color: theme.node.activeStroke }} /> : null}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { UploadedFile } from "@/services/file-storage";
|
||||
import type { UploadedImage } from "@/services/image-storage";
|
||||
import { canvasVideoAssetPreviewUrl } from "@/lib/canvas/canvas-media-preview";
|
||||
import { resolveResourceUrl } from "@/services/api/resources";
|
||||
import type { ExternalAssetPickerReference } from "@/lib/plugins/plugin-types";
|
||||
import type { Asset, AudioAsset, ImageAsset, NewAsset } from "@/stores/use-asset-store";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
@@ -151,7 +152,7 @@ export function creationAttachmentFromDocument(file: File, uploaded: UploadedFil
|
||||
}
|
||||
|
||||
export function creationAttachmentFromAsset(asset: ImageAsset): CreationAttachment {
|
||||
const url = asset.data.dataUrl || asset.coverUrl;
|
||||
const url = resolveResourceUrl(asset.data.storageKey, asset.data.dataUrl || asset.coverUrl);
|
||||
return {
|
||||
id: `asset:${asset.id}`,
|
||||
name: asset.title || "素材图片",
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type PointerEvent, type ReactNode, type RefObject } from "react";
|
||||
import { App, Button, Drawer, Modal, Popover, Spin, Tooltip } from "antd";
|
||||
import { Reorder } from "motion/react";
|
||||
import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronLeft, ChevronRight, Clapperboard, Clock3, Copy, Download, FileText, Film, FolderOpen, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Minimize2, Music2, Paperclip, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Trash2, WandSparkles, X } from "lucide-react";
|
||||
import { ArrowDown, ArrowUp, Check, ChevronDown, ChevronLeft, ChevronRight, Clapperboard, Clock3, Copy, Download, FileText, Film, History, Image as ImageIcon, LoaderCircle, Maximize2, MessageSquareText, Minimize2, Music2, Paperclip, Plus, RefreshCw, Search, SlidersHorizontal, Sparkles, Trash2, WandSparkles, X } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { AIMessageMarkdown } from "@/components/ai/ai-message-markdown";
|
||||
import { GenerationToolCard, type GenerationToolStatus } from "@/components/ai/generation-tool-card";
|
||||
import { MessageReasoning } from "@/components/ai/message-reasoning";
|
||||
import { AssetLibraryPickerModal, type AssetLibraryPickerItem } from "@/components/assets/asset-library-picker-modal";
|
||||
import { CachedResourceImage } from "@/components/cached-resource-image";
|
||||
import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea";
|
||||
import { CanvasPromptOptimizerDrawer } from "@/components/canvas/canvas-prompt-optimizer-drawer";
|
||||
import { VoiceRecordingButton } from "@/components/conversation/voice-recording-button";
|
||||
@@ -29,6 +30,7 @@ import { isGenerationTaskCancelled, runBackendGenerationTask, runBackendGenerati
|
||||
import { listAddedSkills, type Skill } from "@/services/api/skills";
|
||||
import { subscribeGenerationTasks, type GenerationTask } from "@/services/api/task-center";
|
||||
import { isLocalDreaminaWaitStopped, localDreaminaCancellationMessage } from "@/services/local-dreamina-task-projection";
|
||||
import { resolveResourceUrl } from "@/services/api/resources";
|
||||
import { uploadMediaFile } from "@/services/file-storage";
|
||||
import { uploadImage } from "@/services/image-storage";
|
||||
import { consumeGenerationTaskMessage, generationTaskMaterializedUrls, materializeGenerationTaskAssets, projectGenerationTaskResult } from "@/services/project-asset-sync";
|
||||
@@ -1086,7 +1088,9 @@ function CreationUserMessage({ item }: { item: CreationMessage }) {
|
||||
const kind = creationAttachmentKind(attachment);
|
||||
const previewable = kind === "image" || kind === "video";
|
||||
const url = attachment.previewUrl || ("dataUrl" in attachment ? attachment.dataUrl : attachment.url) || "";
|
||||
return <button key={attachment.id} type="button" className={!previewable ? "is-file" : undefined} onClick={() => { if (!previewable) return; setPreviewType(kind === "video" ? "video" : "image"); setPreviewUrl(kind === "video" ? attachment.url || "" : url); }} aria-label={previewable ? `预览 ${attachment.name || "附件"}` : attachment.name || "附件"} disabled={previewable && !url}>{kind === "video" ? <video src={attachment.url || ""} poster={url !== attachment.url ? url : undefined} muted playsInline preload="metadata" /> : kind === "image" ? <img src={url} alt={attachment.name || "附件"} width={44} height={44} loading="lazy" /> : kind === "audio" ? <Music2 /> : <FileText />}{previewable ? <span aria-hidden="true"><Maximize2 /></span> : null}</button>;
|
||||
const imageUrl = kind === "image" ? resolveResourceUrl(attachment.storageKey, url) : "";
|
||||
const previewUrl = kind === "image" ? imageUrl : url;
|
||||
return <button key={attachment.id} type="button" className={!previewable ? "is-file" : undefined} onClick={() => { if (!previewable) return; setPreviewType(kind === "video" ? "video" : "image"); setPreviewUrl(kind === "video" ? attachment.url || "" : previewUrl); }} aria-label={previewable ? `预览 ${attachment.name || "附件"}` : attachment.name || "附件"} disabled={previewable && !previewUrl}>{kind === "video" ? <video src={attachment.url || ""} poster={url !== attachment.url ? url : undefined} muted playsInline preload="metadata" /> : kind === "image" ? <CachedResourceImage storageKey={attachment.storageKey} src={imageUrl} alt={attachment.name || "附件"} width={44} height={44} loading="lazy" decoding="async" /> : kind === "audio" ? <Music2 /> : <FileText />}{previewable ? <span aria-hidden="true"><Maximize2 /></span> : null}</button>;
|
||||
})}</div> : null}
|
||||
<CreationMediaPreviewModal url={previewUrl} type={previewType} onClose={() => setPreviewUrl("")} />
|
||||
</article>;
|
||||
@@ -1117,7 +1121,8 @@ function CreationMediaPending({ mode, ratio }: { mode: CreationMode; ratio?: str
|
||||
function CreationMessageReferences({ references }: { references: CreationReference[] }) {
|
||||
return <div className="creation-user-message-references" aria-label="本次引用">{references.map((reference) => {
|
||||
const Icon = reference.kind === "skill" ? Sparkles : reference.kind === "image" ? ImageIcon : reference.kind === "video" ? Film : reference.kind === "audio" ? Music2 : FileText;
|
||||
return <span key={reference.id} className="creation-user-message-reference">{reference.previewUrl && reference.kind === "video" ? <video src={reference.previewUrl} muted playsInline preload="metadata" aria-label={reference.label} /> : reference.previewUrl && reference.kind === "image" ? <img src={reference.previewUrl} alt="" /> : <Icon />}<span>{reference.label}</span></span>;
|
||||
const imageUrl = reference.kind === "image" ? resolveResourceUrl(reference.storageKey, reference.previewUrl) : reference.previewUrl;
|
||||
return <span key={reference.id} className="creation-user-message-reference">{imageUrl && reference.kind === "video" ? <video src={imageUrl} muted playsInline preload="metadata" aria-label={reference.label} /> : imageUrl && reference.kind === "image" ? <CachedResourceImage storageKey={reference.storageKey} src={imageUrl} alt="" loading="lazy" decoding="async" /> : <Icon />}<span>{reference.label}</span></span>;
|
||||
})}</div>;
|
||||
}
|
||||
|
||||
@@ -1133,9 +1138,11 @@ function CreationAttachmentThumbnail({ item, onPreview, onRemove }: {
|
||||
const kind = creationAttachmentKind(item);
|
||||
const previewable = kind === "image" || kind === "video";
|
||||
const url = (kind === "video" ? item.url : item.previewUrl) || "";
|
||||
const content = kind === "video" ? <video src={item.url} poster={item.previewUrl !== item.url ? item.previewUrl : undefined} muted playsInline preload="metadata" aria-label={item.name} /> : kind === "image" ? <img src={item.previewUrl} alt={item.name} /> : <span className="creation-chat-file-icon">{kind === "audio" ? <Music2 /> : <FileText />}<em>{item.name}</em></span>;
|
||||
const imageUrl = kind === "image" ? resolveResourceUrl(item.storageKey, item.previewUrl) : "";
|
||||
const previewUrl = kind === "image" ? imageUrl : url;
|
||||
const content = kind === "video" ? <video src={item.url} poster={item.previewUrl !== item.url ? item.previewUrl : undefined} muted playsInline preload="metadata" aria-label={item.name} /> : kind === "image" ? <CachedResourceImage storageKey={item.storageKey} src={imageUrl} alt={item.name} loading="lazy" decoding="async" fallback={<span className="creation-chat-file-icon"><ImageIcon /></span>} /> : <span className="creation-chat-file-icon">{kind === "audio" ? <Music2 /> : <FileText />}<em>{item.name}</em></span>;
|
||||
return <div className="creation-reference-card-content">
|
||||
{previewable ? <button type="button" className="creation-reference-card-preview" onClick={() => onPreview(kind === "video" ? "video" : "image", url)} aria-label={`放大预览 ${item.name}`} disabled={!url}>{content}<span aria-hidden="true"><Maximize2 /></span></button> : <div className="creation-reference-card-preview is-file" aria-label={item.name}>{content}</div>}
|
||||
{previewable ? <button type="button" className="creation-reference-card-preview" onClick={() => onPreview(kind === "video" ? "video" : "image", previewUrl)} aria-label={`放大预览 ${item.name}`} disabled={!previewUrl}>{content}<span aria-hidden="true"><Maximize2 /></span></button> : <div className="creation-reference-card-preview is-file" aria-label={item.name}>{content}</div>}
|
||||
<button type="button" className="creation-reference-card-remove" onPointerDownCapture={(event) => event.stopPropagation()} onMouseDownCapture={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onRemove(item.id); }} aria-label={`移除 ${item.name}`}><X /></button>
|
||||
</div>;
|
||||
}
|
||||
@@ -1194,7 +1201,8 @@ function CreationComposer(props: ComposerProps) {
|
||||
const attachmentTrackRef = useRef<HTMLUListElement>(null);
|
||||
const cardDragRef = useRef<{ startX: number; startY: number; moved: boolean } | null>(null);
|
||||
const suppressAttachmentClickRef = useRef(false);
|
||||
const [trackState, setTrackState] = useState({ canScrollLeft: false, canScrollRight: false, isExpanded: false, isDragging: false });
|
||||
const [trackState, setTrackState] = useState({ canScrollLeft: false, canScrollRight: false, isExpanded: true, isDragging: false });
|
||||
const previousAttachmentCountRef = useRef(props.attachments.length);
|
||||
const interactionBusy = props.busy || props.referenceReplacementBusy;
|
||||
const canSubmit = Boolean(props.prompt.trim()) && !interactionBusy;
|
||||
const creditsEnabled = useUserStore((state) => state.features.creditsEnabled);
|
||||
@@ -1244,7 +1252,10 @@ function CreationComposer(props: ComposerProps) {
|
||||
if (!isExpanded) setReferenceFilter("all");
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const hadAttachments = previousAttachmentCountRef.current > 0;
|
||||
if (!props.attachments.length) setReferencePanelExpanded(false);
|
||||
else if (!hadAttachments) setReferencePanelExpanded(true);
|
||||
previousAttachmentCountRef.current = props.attachments.length;
|
||||
updateTrackScrollState();
|
||||
}, [props.attachments.length, setReferencePanelExpanded, updateTrackScrollState]);
|
||||
useEffect(() => {
|
||||
@@ -1405,9 +1416,8 @@ function CreationComposer(props: ComposerProps) {
|
||||
<span>优化</span>
|
||||
</button>
|
||||
</Tooltip> : null}
|
||||
<Tooltip title="从本机上传附件"><button type="button" className="creation-chat-control" onClick={() => props.fileInputRef.current?.click()} disabled={interactionBusy || !referencesSupported} aria-label="从本机上传附件"><Paperclip /><span>附件</span></button></Tooltip>
|
||||
<Tooltip title={!referencesSupported ? "当前模型不支持参考媒体" : "从素材库选择参考内容"}><button type="button" className="creation-chat-control" onClick={props.onOpenLibrary} disabled={interactionBusy || !referencesSupported} aria-label="打开素材库选择参考内容"><FolderOpen /><span>素材库</span></button></Tooltip>
|
||||
<ModelPicker config={props.config} value={props.model} onChange={props.onModelChange} capability={props.mode} requirements={props.modelRequirements} className="creation-model-picker" placeholder={`选择${modeLabels[props.mode]}模型`} showSelectedPrice variant="creation" />
|
||||
<Tooltip title="从本机上传附件"><button type="button" className="creation-chat-control" onClick={() => props.fileInputRef.current?.click()} disabled={interactionBusy || !referencesSupported} aria-label="从本机上传附件"><Paperclip /><span>附件</span></button></Tooltip>
|
||||
<ModelPicker config={props.config} value={props.model} onChange={props.onModelChange} capability={props.mode} requirements={props.modelRequirements} className="creation-model-picker" placeholder={`选择${modeLabels[props.mode]}模型`} showSelectedPrice={false} variant="creation" />
|
||||
{props.mode === "video" || (props.mode === "image" && imageSettingsSupported) ? <GenerationSettingsMenu {...props} /> : null}
|
||||
{props.mode === "video" ? <DurationMenu profile={props.videoProfile} seconds={props.seconds} onChange={props.setSeconds} /> : null}
|
||||
</div>
|
||||
@@ -1723,7 +1733,9 @@ function StoryboardBriefAttachments({ attachments }: { attachments: CreationAtta
|
||||
const kind = creationAttachmentKind(attachment);
|
||||
const previewable = kind === "image" || kind === "video";
|
||||
const url = attachment.previewUrl || ("dataUrl" in attachment ? attachment.dataUrl : attachment.url) || "";
|
||||
return <button key={attachment.id} type="button" className={!previewable ? "is-file" : undefined} onClick={() => { if (!previewable) return; setPreviewType(kind === "video" ? "video" : "image"); setPreviewUrl(kind === "video" ? attachment.url || "" : url); }} aria-label={previewable ? `预览 ${attachment.name || "附件"}` : attachment.name || "附件"} disabled={previewable && !url}>{kind === "video" ? <video src={attachment.url || ""} poster={url !== attachment.url ? url : undefined} muted playsInline preload="metadata" /> : kind === "image" ? <img src={url} alt={attachment.name || "附件"} width={44} height={44} loading="lazy" /> : kind === "audio" ? <Music2 /> : <FileText />}{previewable ? <span aria-hidden="true"><Maximize2 /></span> : null}</button>;
|
||||
const imageUrl = kind === "image" ? resolveResourceUrl(attachment.storageKey, url) : "";
|
||||
const previewUrl = kind === "image" ? imageUrl : url;
|
||||
return <button key={attachment.id} type="button" className={!previewable ? "is-file" : undefined} onClick={() => { if (!previewable) return; setPreviewType(kind === "video" ? "video" : "image"); setPreviewUrl(kind === "video" ? attachment.url || "" : previewUrl); }} aria-label={previewable ? `预览 ${attachment.name || "附件"}` : attachment.name || "附件"} disabled={previewable && !previewUrl}>{kind === "video" ? <video src={attachment.url || ""} poster={url !== attachment.url ? url : undefined} muted playsInline preload="metadata" /> : kind === "image" ? <CachedResourceImage storageKey={attachment.storageKey} src={imageUrl} alt={attachment.name || "附件"} width={44} height={44} loading="lazy" decoding="async" /> : kind === "audio" ? <Music2 /> : <FileText />}{previewable ? <span aria-hidden="true"><Maximize2 /></span> : null}</button>;
|
||||
})}</div><CreationMediaPreviewModal url={previewUrl} type={previewType} onClose={() => setPreviewUrl("")} /></>;
|
||||
}
|
||||
|
||||
|
||||
@@ -10561,13 +10561,15 @@ html:not(.dark) .app-workspace-shell.is-creation-workspace .app-workspace-stage
|
||||
.creation-home .creation-reference-panel { width: fit-content; min-width: 0; max-width: 100%; margin: -2px 0 var(--space-2); }
|
||||
.creation-home .creation-reference-panel.is-expanded { width: 100%; }
|
||||
.creation-home .creation-reference-panel-header { display: flex; min-height: var(--space-8); flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-3); margin-bottom: var(--space-2); }
|
||||
.creation-home .creation-reference-filter-tabs { display: flex; min-width: 0; flex: 1 1 auto; align-items: center; gap: var(--space-1); overflow-x: auto; scrollbar-width: none; }
|
||||
.creation-home .creation-reference-filter-tabs { display: flex; min-width: 0; flex: 1 1 auto; align-items: center; gap: var(--space-3); overflow-x: auto; scrollbar-width: none; }
|
||||
.creation-home .creation-reference-filter-tabs::-webkit-scrollbar { display: none; }
|
||||
.creation-home .creation-reference-filter-tabs button,
|
||||
.creation-home .creation-reference-panel-actions button { min-height: var(--space-7); border: 0; border-radius: var(--r-md); background: transparent; color: var(--creation-muted); padding: 0 var(--space-3); font-size: var(--fs-caption); line-height: 1; white-space: nowrap; transition: background-color var(--motion-dur-fast-calc) ease, color var(--motion-dur-fast-calc) ease; }
|
||||
.creation-home .creation-reference-panel-actions button { min-height: var(--space-7); border: 0; border-radius: 0; background: transparent; color: var(--creation-muted); padding: 0 2px; font-size: var(--fs-caption); line-height: 1; white-space: nowrap; transition: color var(--motion-dur-fast-calc) ease, opacity var(--motion-dur-fast-calc) ease; }
|
||||
.creation-home .creation-reference-filter-tabs button:hover,
|
||||
.creation-home .creation-reference-filter-tabs button.is-active,
|
||||
.creation-home .creation-reference-panel-actions button:hover:not(:disabled) { background: var(--creation-surface-hover); color: var(--creation-text); }
|
||||
.creation-home .creation-reference-panel-actions button:hover:not(:disabled) { color: var(--creation-text); }
|
||||
.creation-home .creation-reference-filter-tabs button { position: relative; }
|
||||
.creation-home .creation-reference-filter-tabs button.is-active::after { position: absolute; right: 2px; bottom: -1px; left: 2px; height: 2px; border-radius: var(--r-full); background: var(--creation-text); content: ""; }
|
||||
.creation-home .creation-reference-filter-tabs button:focus-visible,
|
||||
.creation-home .creation-reference-panel-actions button:focus-visible,
|
||||
.creation-home .creation-reference-panel-expand:focus-visible { outline: var(--stroke-2) solid var(--control-focus-ring); outline-offset: var(--space-half); }
|
||||
@@ -10646,9 +10648,10 @@ html:not(.dark) .app-workspace-shell.is-creation-workspace .app-workspace-stage
|
||||
.creation-home .creation-reference-card-preview video { width: 100%; height: 100%; object-fit: cover; transition: transform var(--motion-dur-fast-calc) var(--motion-ease-out); }
|
||||
.creation-home .creation-reference-card-preview:hover img,
|
||||
.creation-home .creation-reference-card-preview:hover video { transform: scale(1.04); }
|
||||
.creation-home .creation-reference-card-preview > span { position: absolute; right: var(--space-1); bottom: var(--space-1); display: grid; width: var(--space-5); height: var(--space-5); place-items: center; border-radius: var(--r-xs); background: color-mix(in srgb, var(--color-neutral-950) 72%, transparent); color: var(--color-neutral-0); opacity: 0; transition: opacity var(--motion-dur-fast-calc) ease; }
|
||||
.creation-home .creation-reference-card-preview:hover > span,
|
||||
.creation-home .creation-reference-card-preview:focus-visible > span { opacity: 1; }
|
||||
.creation-home .creation-reference-card-preview > span:not(.cached-resource-image-shell) { position: absolute; right: var(--space-1); bottom: var(--space-1); display: grid; width: var(--space-5); height: var(--space-5); place-items: center; border-radius: var(--r-xs); background: color-mix(in srgb, var(--color-neutral-950) 72%, transparent); color: var(--color-neutral-0); opacity: 0; transition: opacity var(--motion-dur-fast-calc) ease; }
|
||||
.creation-home .creation-reference-card-preview:hover > span:not(.cached-resource-image-shell),
|
||||
.creation-home .creation-reference-card-preview:focus-visible > span:not(.cached-resource-image-shell) { opacity: 1; }
|
||||
.creation-home .creation-reference-card-preview > .cached-resource-image-shell { position: static; display: grid; width: 100%; height: 100%; place-items: center; }
|
||||
.creation-home .creation-reference-card-preview:focus-visible { outline: var(--stroke-2) solid var(--control-focus-ring); outline-offset: calc(-1 * var(--stroke-2)); }
|
||||
.creation-home .creation-reference-card-remove { position: absolute; top: var(--space-1); right: var(--space-1); z-index: 1; display: grid; width: var(--space-5); height: var(--space-5); place-items: center; border: 0; border-radius: var(--r-xs); background: color-mix(in srgb, var(--color-neutral-950) 76%, transparent); color: var(--color-neutral-0); padding: 0; opacity: 0; transition: opacity var(--motion-dur-fast-calc) ease, background-color var(--motion-dur-fast-calc) ease; }
|
||||
.creation-home .creation-reference-stack-card:hover .creation-reference-card-remove,
|
||||
@@ -10844,10 +10847,14 @@ html:not(.dark) .creation-home .creation-chat-composer .creation-model-picker.ca
|
||||
.creation-user-message-references { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
|
||||
.creation-user-message-reference { display: inline-flex; min-width: 0; max-width: 180px; height: 25px; align-items: center; gap: 5px; border-radius: 6px; background: rgba(255, 255, 255, .075); padding: 0 7px; color: var(--creation-muted); font-size: 10px; line-height: 1; }
|
||||
.creation-user-message-reference img, .creation-user-message-reference video { width: 17px; height: 17px; flex: 0 0 auto; border-radius: 4px; object-fit: cover; }
|
||||
.creation-user-message-reference .cached-resource-image-shell { display: block; width: 17px; height: 17px; flex: 0 0 17px; overflow: hidden; border-radius: 4px; }
|
||||
.creation-user-message-reference .cached-resource-image-shell > img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
.creation-user-message-reference svg { width: 12px; height: 12px; flex: 0 0 auto; }
|
||||
.creation-user-message-reference span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
html:not(.dark) .creation-user-message-reference { background: rgba(28, 32, 37, .07); }
|
||||
.creation-user-message-attachments button { position: relative; width: 44px; height: 44px; flex: 0 0 auto; overflow: hidden; border: 0; border-radius: 7px; background: transparent; padding: 0; }
|
||||
.creation-user-message-attachments button > .cached-resource-image-shell { display: block; width: 100%; height: 100%; }
|
||||
.creation-user-message-attachments button > .cached-resource-image-shell > img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
.creation-user-message-attachments button:focus-visible { outline: 2px solid var(--creation-text); outline-offset: 2px; }
|
||||
.creation-user-message-attachments button > span { position: absolute; right: 3px; bottom: 3px; display: grid; width: 17px; height: 17px; place-items: center; border-radius: 5px; background: rgba(0, 0, 0, .68); color: #fff; opacity: 0; transition: opacity 140ms ease; }
|
||||
.creation-user-message-attachments button:hover > span,
|
||||
|
||||
@@ -9,7 +9,7 @@ function compactSource(source: string) {
|
||||
}
|
||||
|
||||
describe("creation library button", () => {
|
||||
test("places a library control beside the generation mode picker", () => {
|
||||
test("keeps library selection in the reference area instead of the bottom dock", () => {
|
||||
const source = readFileSync(resolve(import.meta.dir, "../src/pages/create/index.tsx"), "utf8");
|
||||
const dockStart = source.indexOf('<footer className="creation-chat-dock">');
|
||||
const dockEnd = source.indexOf("</footer>", dockStart);
|
||||
@@ -19,13 +19,14 @@ describe("creation library button", () => {
|
||||
const dockSource = compactSource(source.slice(dockStart, dockEnd));
|
||||
const modePickerIndex = dockSource.indexOf("<ModePicker mode={props.mode}");
|
||||
const attachmentIndex = dockSource.indexOf('aria-label="从本机上传附件"');
|
||||
const libraryIndex = dockSource.indexOf('aria-label="打开素材库选择参考内容"');
|
||||
|
||||
expect(modePickerIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(attachmentIndex).toBeGreaterThan(modePickerIndex);
|
||||
expect(libraryIndex).toBeGreaterThan(attachmentIndex);
|
||||
expect(dockSource).toContain("onClick={props.onOpenLibrary}");
|
||||
expect(dockSource).toContain("disabled={interactionBusy || !referencesSupported}");
|
||||
expect(dockSource).not.toContain('aria-label="打开素材库选择参考内容"');
|
||||
expect(source).toContain("onClick={props.onOpenLibrary}");
|
||||
expect(source).toContain("creation-reference-add-button");
|
||||
expect(source).toContain('showSelectedPrice={false} variant="creation"');
|
||||
expect(source).toContain("canvas-node-composer-submit-cost");
|
||||
});
|
||||
|
||||
test("uploads from the library without adding a reference before confirmation", () => {
|
||||
@@ -65,7 +66,9 @@ describe("creation library button", () => {
|
||||
expect(source).toContain('axis="x"');
|
||||
expect(source).toContain("values={visibleAttachments}");
|
||||
expect(source).toContain("onReorder={reorderVisibleAttachments}");
|
||||
expect(source).toContain('className="creation-reference-card-remove" onPointerDownCapture={(event) => event.stopPropagation()}');
|
||||
expect(source).toContain('className="creation-reference-card-remove"');
|
||||
expect(source).toContain("onPointerDownCapture");
|
||||
expect(source).toContain("event.stopPropagation()");
|
||||
expect(source).toContain("<Reorder.Item");
|
||||
expect(source).toContain('layout="position"');
|
||||
expect(source).toContain("isExpanded");
|
||||
@@ -99,6 +102,18 @@ describe("creation library button", () => {
|
||||
expect(styles).toContain("@media (hover: none)");
|
||||
expect(styles).toContain(".creation-reference-card-remove { opacity: 1; }");
|
||||
expect(styles).not.toContain(".creation-reference-track:not(.is-expanded) .creation-reference-stack-card:nth-child(n+5) { display: block; }");
|
||||
expect(source).toContain("isExpanded: true");
|
||||
expect(source).toContain("else if (!hadAttachments) setReferencePanelExpanded(true)");
|
||||
expect(styles).toContain("creation-reference-filter-tabs button.is-active::after");
|
||||
});
|
||||
|
||||
test("resolves remote asset images from their stable resource key", () => {
|
||||
const assets = readFileSync(resolve(import.meta.dir, "../src/pages/create/creation-assets.ts"), "utf8");
|
||||
const createSource = readFileSync(resolve(import.meta.dir, "../src/pages/create/index.tsx"), "utf8");
|
||||
|
||||
expect(assets).toContain("resolveResourceUrl(asset.data.storageKey");
|
||||
expect(createSource).toContain("<CachedResourceImage storageKey={item.storageKey}");
|
||||
expect(createSource).toContain("resolveResourceUrl(item.storageKey, item.previewUrl)");
|
||||
});
|
||||
|
||||
test("首个、中间和末尾参考内容都按稳定 id 独立删除并保留顺序", () => {
|
||||
|
||||
Reference in New Issue
Block a user