111 lines
4.6 KiB
TypeScript
111 lines
4.6 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import type { ImageAsset } from "@/lib/types";
|
||
import { getImageUrl } from "@/lib/api";
|
||
|
||
/**
|
||
* 候选评估面板(Phase 1 占位)
|
||
*
|
||
* 对应 INFOLAYER.md 3.3.3 节:同轮生成的一组候选图的并排比较、
|
||
* 快速评分、标注差异、挑选定稿。
|
||
*/
|
||
interface CandidatePanelProps {
|
||
candidates: ImageAsset[];
|
||
onPick?: (id: string) => void;
|
||
onClose?: () => void;
|
||
}
|
||
|
||
export function CandidatePanel({ candidates, onPick, onClose }: CandidatePanelProps) {
|
||
const [ratings, setRatings] = useState<Record<string, number>>({});
|
||
const [notes, setNotes] = useState<Record<string, string>>({});
|
||
|
||
if (candidates.length === 0) return null;
|
||
|
||
return (
|
||
<div className="fixed inset-y-0 right-0 w-[min(680px,100vw)] z-40
|
||
bg-[var(--bg-secondary)]/95 backdrop-blur-xl
|
||
border-l border-[var(--border)] shadow-2xl
|
||
flex flex-col animate-in slide-in-from-right">
|
||
<div className="flex items-center justify-between px-5 py-3 border-b border-[var(--border)]">
|
||
<div className="flex items-center gap-2">
|
||
<h2 className="typo-h2">候选评估</h2>
|
||
<span className="phase-chip">Phase 1</span>
|
||
<span className="text-xs text-[var(--text-secondary)]">
|
||
共 {candidates.length} 张
|
||
</span>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="w-7 h-7 rounded-lg flex items-center justify-center
|
||
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||
hover:bg-[var(--bg-tertiary)] cursor-pointer transition-all"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-y-auto p-5 space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{candidates.map((c, i) => (
|
||
<div key={c.id} className="rounded-xl surface-2 overflow-hidden">
|
||
<div className="relative aspect-square bg-[var(--bg-tertiary)]">
|
||
{c.url ? (
|
||
<img src={getImageUrl(c.url)} alt={`候选 ${i + 1}`} className="w-full h-full object-cover" />
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center text-xs text-[var(--text-secondary)]">
|
||
占位候选 {i + 1}
|
||
</div>
|
||
)}
|
||
<div className="absolute top-2 left-2 px-2 py-0.5 rounded-md
|
||
bg-[var(--bg-primary)]/70 backdrop-blur-sm
|
||
text-[10px] font-mono text-[var(--text-primary)]">
|
||
#{i + 1}
|
||
</div>
|
||
</div>
|
||
<div className="p-2.5">
|
||
<div className="flex gap-0.5 mb-1.5">
|
||
{[1, 2, 3, 4, 5].map((n) => (
|
||
<button
|
||
key={n}
|
||
onClick={() =>
|
||
setRatings((prev) => ({ ...prev, [c.id]: prev[c.id] === n ? 0 : n }))
|
||
}
|
||
className={`text-base leading-none cursor-pointer transition-colors ${
|
||
(ratings[c.id] ?? 0) >= n ? "text-[var(--accent)]" : "text-[var(--border)]"
|
||
}`}
|
||
>
|
||
★
|
||
</button>
|
||
))}
|
||
</div>
|
||
<input
|
||
value={notes[c.id] ?? ""}
|
||
onChange={(e) => setNotes((prev) => ({ ...prev, [c.id]: e.target.value }))}
|
||
placeholder="点评(占位)"
|
||
className="w-full px-2 py-1 text-[11px] rounded-md bg-[var(--bg-primary)]
|
||
border border-[var(--border)] focus:border-[var(--accent)]/40 outline-none"
|
||
/>
|
||
<button
|
||
onClick={() => onPick?.(c.id)}
|
||
className="mt-1.5 w-full text-[11px] py-1 rounded-md
|
||
bg-[var(--accent)]/10 text-[var(--accent)] border border-[var(--accent)]/30
|
||
hover:bg-[var(--accent)]/20 cursor-pointer transition-all"
|
||
>
|
||
选为定稿
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="placeholder-card rounded-lg p-3 text-xs text-[var(--text-secondary)]">
|
||
<div className="typo-strong text-xs mb-1 text-[var(--text-primary)]">自动差异对比(占位)</div>
|
||
构图 / 配色 / 细节的自动化差分尚未接入(Phase 2)。用户可在此给出评分与点评,
|
||
Agent 将根据选中项做后续迭代。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|