232 lines
7.9 KiB
TypeScript
232 lines
7.9 KiB
TypeScript
"use client";
|
||
|
||
import { Box, Globe2, Loader2, RefreshCw, Zap } from "lucide-react";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog";
|
||
import { Label } from "@/components/ui/label";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
|
||
interface ThreeDModelOption {
|
||
id: string;
|
||
displayName: string;
|
||
description?: string | null;
|
||
}
|
||
|
||
type ThreeDGenerationDialogMode = "model" | "world" | null;
|
||
|
||
interface PromptRegenerateDialogProps {
|
||
isProcessing: boolean;
|
||
nodeId: string;
|
||
onConfirm: () => void;
|
||
onOpenChange: (open: boolean) => void;
|
||
onPromptChange: (value: string) => void;
|
||
open: boolean;
|
||
prompt: string;
|
||
}
|
||
|
||
interface ThreeDGenerationDialogProps {
|
||
costLabel?: string;
|
||
costShortLabel?: string | null;
|
||
isProcessing: boolean;
|
||
mode: ThreeDGenerationDialogMode;
|
||
onConfirm: () => void;
|
||
onModeChange: (mode: ThreeDGenerationDialogMode) => void;
|
||
onTripoModelChange: (value: string) => void;
|
||
onWorldModelChange: (value: string) => void;
|
||
selectedTripoModelEntry?: ThreeDModelOption | null;
|
||
selectedTripoModelVersion: string;
|
||
selectedWorldModelEntry?: ThreeDModelOption | null;
|
||
selectedWorldModelVersion: string;
|
||
tripoModelOptions: ThreeDModelOption[];
|
||
worldModelOptions: ThreeDModelOption[];
|
||
}
|
||
|
||
export function PromptRegenerateDialog({
|
||
isProcessing,
|
||
nodeId,
|
||
onConfirm,
|
||
onOpenChange,
|
||
onPromptChange,
|
||
open,
|
||
prompt,
|
||
}: PromptRegenerateDialogProps) {
|
||
return (
|
||
<Dialog onOpenChange={onOpenChange} open={open}>
|
||
<DialogContent
|
||
className="border-slate-200 bg-white opacity-100 shadow-2xl backdrop-blur-none sm:max-w-[560px] dark:border-slate-700 dark:bg-slate-950"
|
||
overlayClassName="bg-black"
|
||
>
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2">
|
||
<RefreshCw className="h-4 w-4" />
|
||
提示词重绘
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="space-y-3 py-1">
|
||
<p className="text-[13px] leading-5 text-muted-foreground">
|
||
在保留当前图片主体的基础上,用新提示词生成一张新图。原图不会被覆盖。
|
||
</p>
|
||
<div className="space-y-2">
|
||
<Label htmlFor={`prompt-regenerate-${nodeId}`}>新提示词</Label>
|
||
<Textarea
|
||
className="resize-y border-slate-300 bg-white opacity-100 dark:border-slate-700 dark:bg-slate-950"
|
||
id={`prompt-regenerate-${nodeId}`}
|
||
onChange={(event) => onPromptChange(event.target.value)}
|
||
placeholder="例如:保持主体不变,把背景改成黄昏海边,氛围光影更柔和,细节更丰富"
|
||
rows={6}
|
||
value={prompt}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button onClick={() => onOpenChange(false)} variant="outline">
|
||
取消
|
||
</Button>
|
||
<Button disabled={isProcessing || !prompt.trim()} onClick={onConfirm}>
|
||
{isProcessing ? (
|
||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
) : (
|
||
<RefreshCw className="mr-2 h-4 w-4" />
|
||
)}
|
||
开始重绘
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|
||
|
||
export function ThreeDGenerationDialog({
|
||
costLabel,
|
||
costShortLabel,
|
||
isProcessing,
|
||
mode,
|
||
onConfirm,
|
||
onModeChange,
|
||
onTripoModelChange,
|
||
onWorldModelChange,
|
||
selectedTripoModelEntry,
|
||
selectedTripoModelVersion,
|
||
selectedWorldModelEntry,
|
||
selectedWorldModelVersion,
|
||
tripoModelOptions,
|
||
worldModelOptions,
|
||
}: ThreeDGenerationDialogProps) {
|
||
const isWorldMode = mode === "world";
|
||
const selectedEntry = isWorldMode
|
||
? selectedWorldModelEntry
|
||
: selectedTripoModelEntry;
|
||
const options = isWorldMode ? worldModelOptions : tripoModelOptions;
|
||
|
||
return (
|
||
<Dialog
|
||
onOpenChange={(open) => {
|
||
if (!open) {
|
||
onModeChange(null);
|
||
}
|
||
}}
|
||
open={mode !== null}
|
||
>
|
||
<DialogContent
|
||
className="border-slate-200 bg-white opacity-100 shadow-2xl backdrop-blur-none sm:max-w-[560px] dark:border-slate-700 dark:bg-slate-950"
|
||
overlayClassName="bg-black/35 backdrop-blur-[1px]"
|
||
>
|
||
<DialogHeader>
|
||
<DialogTitle className="flex items-center gap-2">
|
||
{isWorldMode ? (
|
||
<Globe2 className="h-4 w-4" />
|
||
) : (
|
||
<Box className="h-4 w-4" />
|
||
)}
|
||
{isWorldMode ? "选择 3D 世界模型" : "选择 3D 模型"}
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
<div className="space-y-3 py-1">
|
||
<p className="text-[13px] leading-5 text-muted-foreground">
|
||
{isWorldMode
|
||
? "先选择用于图生世界的生成质量,再开始创建 3D 世界任务。"
|
||
: "先选择用于图生 3D 的模型版本,再开始创建 3D 模型任务。"}
|
||
</p>
|
||
<div className="space-y-2">
|
||
<Label>{isWorldMode ? "世界模型" : "模型版本"}</Label>
|
||
<Select
|
||
onValueChange={(value) => {
|
||
if (isWorldMode) {
|
||
onWorldModelChange(value);
|
||
return;
|
||
}
|
||
onTripoModelChange(value);
|
||
}}
|
||
value={
|
||
isWorldMode ? selectedWorldModelVersion : selectedTripoModelVersion
|
||
}
|
||
>
|
||
<SelectTrigger className="border-slate-300 bg-white dark:border-slate-700 dark:bg-slate-950">
|
||
<SelectValue placeholder="选择模型" />
|
||
</SelectTrigger>
|
||
<SelectContent className="border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-950">
|
||
{options.map((option) => (
|
||
<SelectItem key={option.id} value={option.id}>
|
||
{option.displayName}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="rounded-[var(--ui-radius-control)] border border-slate-200 bg-slate-50 p-2.5 text-[13px] dark:border-slate-800 dark:bg-slate-900/80">
|
||
<div className="font-medium text-slate-900 dark:text-slate-100">
|
||
{selectedEntry?.displayName || "未选择模型"}
|
||
</div>
|
||
<div className="mt-0.5 text-[11px] leading-4 text-slate-600 dark:text-slate-400">
|
||
{selectedEntry?.description || "当前模型暂无说明。"}
|
||
</div>
|
||
{costLabel ? (
|
||
<div className="mt-3 inline-flex items-center gap-1.5 rounded-md bg-slate-900 px-2.5 py-1 text-[12px] font-medium text-white shadow-sm dark:bg-white dark:text-slate-950">
|
||
<Zap className="h-3.5 w-3.5 fill-current" />
|
||
{costLabel}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button onClick={() => onModeChange(null)} variant="outline">
|
||
取消
|
||
</Button>
|
||
<Button
|
||
disabled={isProcessing || !selectedEntry}
|
||
onClick={() => void onConfirm()}
|
||
>
|
||
{isProcessing ? (
|
||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
) : isWorldMode ? (
|
||
<Globe2 className="mr-2 h-4 w-4" />
|
||
) : (
|
||
<Box className="mr-2 h-4 w-4" />
|
||
)}
|
||
<span>开始生成</span>
|
||
{costShortLabel ? (
|
||
<span className="ml-2 inline-flex items-center gap-1 rounded-md bg-black/18 px-1.5 py-0.5 text-[12px] font-semibold text-current dark:bg-white/18">
|
||
<Zap className="h-3 w-3 fill-current" />
|
||
{costShortLabel}
|
||
</span>
|
||
) : null}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|