交互原型大版本

This commit is contained in:
Nostars Developer
2026-04-17 19:30:23 +08:00
parent 10f9c0061a
commit b12e55a776
29 changed files with 4574 additions and 57 deletions

View File

@@ -0,0 +1,131 @@
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
/**
* 高级控制面板Phase 2 占位)
*
* 对应 INFOLAYER.md 3.3 节「高级控制」:提供给高级用户的参数调节抽屉。
* 当前所有参数均为占位,不会真实下发到后端。
*/
export function AdvancedControls() {
const [open, setOpen] = useState(false);
const [cfg, setCfg] = useState(7.5);
const [steps, setSteps] = useState(28);
const [sampler, setSampler] = useState("DPM++ 2M Karras");
const [seed, setSeed] = useState("");
const [styleWeight, setStyleWeight] = useState(0.8);
const [charWeight, setCharWeight] = useState(0.85);
return (
<div className="relative">
<button
onClick={() => setOpen(!open)}
className="h-7 px-2 rounded-lg text-xs flex items-center gap-1.5
border border-[var(--border)] bg-[var(--bg-tertiary)]
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
hover:border-[var(--accent)]/40 cursor-pointer transition-all"
title="高级控制(占位)"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
<span className="phase-chip">P2</span>
</button>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
className="absolute bottom-full mb-2 right-0 w-80 max-w-[calc(100vw-24px)]
rounded-xl surface-2 border border-[var(--border)] shadow-xl
backdrop-blur-xl z-40 p-3"
>
<div className="flex items-center justify-between mb-3">
<span className="typo-strong text-xs"></span>
<button
onClick={() => setOpen(false)}
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] cursor-pointer"
>
</button>
</div>
<div className="space-y-3">
<Slider label="CFG Scale" value={cfg} min={1} max={20} step={0.5} onChange={setCfg} />
<Slider label="Steps" value={steps} min={10} max={60} step={1} onChange={setSteps} />
<Slider label="风格强度" value={styleWeight} min={0} max={1.5} step={0.05} onChange={setStyleWeight} />
<Slider label="角色一致性" value={charWeight} min={0} max={1.5} step={0.05} onChange={setCharWeight} />
<div>
<label className="block typo-micro mb-1" style={{ textTransform: "none" }}></label>
<select
value={sampler}
onChange={(e) => setSampler(e.target.value)}
className="w-full px-2 py-1.5 text-xs rounded-md bg-[var(--bg-primary)]
border border-[var(--border)] focus:border-[var(--accent)]/40 outline-none"
>
{["DPM++ 2M Karras", "Euler a", "DDIM", "UniPC"].map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</div>
<div>
<label className="block typo-micro mb-1" style={{ textTransform: "none" }}>Seed</label>
<div className="flex gap-1.5">
<input
value={seed}
onChange={(e) => setSeed(e.target.value)}
placeholder="留空为随机"
className="flex-1 px-2 py-1.5 text-xs rounded-md bg-[var(--bg-primary)]
border border-[var(--border)] focus:border-[var(--accent)]/40 outline-none font-mono"
/>
<button
onClick={() => setSeed(Math.floor(Math.random() * 9_999_999).toString())}
className="px-2 py-1.5 text-xs rounded-md border border-[var(--border)]
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
cursor-pointer"
>
🎲
</button>
</div>
</div>
</div>
<div className="mt-3 pt-3 border-t border-[var(--border)] text-[10px] text-[var(--text-secondary)]">
Phase 2
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
function Slider({
label, value, min, max, step, onChange,
}: { label: string; value: number; min: number; max: number; step: number; onChange: (v: number) => void }) {
return (
<div>
<div className="flex items-center justify-between mb-1">
<label className="typo-micro" style={{ textTransform: "none" }}>{label}</label>
<span className="text-[11px] text-[var(--accent)] font-mono">{value}</span>
</div>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(parseFloat(e.target.value))}
className="w-full accent-[var(--accent)]"
/>
</div>
);
}