Show compressed previews in vision lab

这个提交包含在:
cryptocommuniums-afk
2026-03-15 02:37:12 +08:00
父节点 ae93269c62
当前提交 afb013193d

查看文件

@@ -47,6 +47,67 @@ type VisionRun = {
updatedAt: Date; updatedAt: Date;
}; };
const COMMONS_SPECIAL_FILE_PATH = "/wiki/Special:FilePath/";
const COMMONS_FILE_PAGE_PATH = "/wiki/File:";
function getCompressedVisionImageUrl(imageUrl: string, width = 960) {
try {
const url = new URL(imageUrl);
if (url.hostname !== "commons.wikimedia.org") {
return imageUrl;
}
let fileName: string | null = null;
if (url.pathname.startsWith(COMMONS_SPECIAL_FILE_PATH)) {
fileName = url.pathname.slice(COMMONS_SPECIAL_FILE_PATH.length);
} else if (url.pathname.startsWith(COMMONS_FILE_PAGE_PATH)) {
fileName = url.pathname.slice(COMMONS_FILE_PAGE_PATH.length);
}
if (!fileName) {
return imageUrl;
}
const decodedFileName = decodeURIComponent(fileName);
return `https://commons.wikimedia.org/wiki/Special:Redirect/file/${encodeURIComponent(decodedFileName)}?width=${width}`;
} catch {
return imageUrl;
}
}
function VisionPreviewImage({
src,
alt,
className,
width = 960,
}: {
src: string;
alt: string;
className: string;
width?: number;
}) {
const [displaySrc, setDisplaySrc] = useState(() => getCompressedVisionImageUrl(src, width));
useEffect(() => {
setDisplaySrc(getCompressedVisionImageUrl(src, width));
}, [src, width]);
return (
<img
src={displaySrc}
alt={alt}
className={className}
loading="lazy"
referrerPolicy="no-referrer"
onError={() => {
if (displaySrc !== src) {
setDisplaySrc(src);
}
}}
/>
);
}
function statusBadge(run: VisionRun) { function statusBadge(run: VisionRun) {
if (run.status === "failed" || run.visionStatus === "failed") { if (run.status === "failed" || run.visionStatus === "failed") {
return <Badge variant="destructive"></Badge>; return <Badge variant="destructive"></Badge>;
@@ -212,12 +273,11 @@ export default function VisionLab() {
{references.map((reference) => ( {references.map((reference) => (
<Card key={reference.id} className="overflow-hidden border-0 shadow-sm"> <Card key={reference.id} className="overflow-hidden border-0 shadow-sm">
<div className="aspect-[4/3] overflow-hidden bg-muted"> <div className="aspect-[4/3] overflow-hidden bg-muted">
<img <VisionPreviewImage
src={reference.imageUrl} src={reference.imageUrl}
alt={reference.title} alt={reference.title}
className="h-full w-full object-cover" className="h-full w-full object-cover"
loading="lazy" width={960}
referrerPolicy="no-referrer"
/> />
</div> </div>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
@@ -272,59 +332,79 @@ export default function VisionLab() {
{runs.map((run) => ( {runs.map((run) => (
<Card key={run.id} className="border-0 shadow-sm"> <Card key={run.id} className="border-0 shadow-sm">
<CardContent className="pt-5 space-y-3"> <CardContent className="pt-5 space-y-3">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between"> <div className="flex flex-col gap-4 lg:flex-row">
<div className="space-y-1"> <a
<div className="flex flex-wrap items-center gap-2"> href={run.imageUrl}
<h3 className="font-semibold">{run.title}</h3> target="_blank"
{statusBadge(run)} rel="noreferrer"
<Badge variant="outline">{run.exerciseType}</Badge> className="block overflow-hidden rounded-xl bg-muted lg:w-72 lg:flex-none"
>
<div className="aspect-[4/3]">
<VisionPreviewImage
src={run.imageUrl}
alt={run.title}
className="h-full w-full object-cover"
width={720}
/>
</div> </div>
<p className="text-xs text-muted-foreground"> </a>
{new Date(run.createdAt).toLocaleString("zh-CN")}
{user?.role === "admin" && run.userName ? ` · 提交人:${run.userName}` : ""} <div className="min-w-0 flex-1 space-y-3">
</p> <div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="font-semibold">{run.title}</h3>
{statusBadge(run)}
<Badge variant="outline">{run.exerciseType}</Badge>
</div>
<p className="text-xs text-muted-foreground">
{new Date(run.createdAt).toLocaleString("zh-CN")}
{user?.role === "admin" && run.userName ? ` · 提交人:${run.userName}` : ""}
</p>
</div>
{run.configuredModel ? (
<Badge variant="secondary">{run.configuredModel}</Badge>
) : null}
</div>
{run.summary ? <p className="text-sm">{run.summary}</p> : null}
{run.warning ? (
<p className="text-sm text-amber-700">{run.warning}</p>
) : null}
{run.error ? (
<p className="text-sm text-destructive">{run.error}</p>
) : null}
{(run.visionStatus === "fallback" || run.status === "failed") ? (
<div className="flex justify-end">
<Button
size="sm"
variant="outline"
className="gap-2"
onClick={() => retryRunMutation.mutate({ runId: run.id })}
disabled={retryRunMutation.isPending}
>
{retryRunMutation.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Microscope className="h-4 w-4" />}
</Button>
</div>
) : null}
{run.expectedFocus?.length ? (
<div className="flex flex-wrap gap-2">
{run.expectedFocus.map((item) => (
<Badge key={item} variant="outline">{item}</Badge>
))}
</div>
) : null}
{run.corrections ? (
<div className="rounded-xl bg-muted/50 p-3 text-sm leading-6 whitespace-pre-wrap">
{run.corrections}
</div>
) : null}
</div> </div>
{run.configuredModel ? (
<Badge variant="secondary">{run.configuredModel}</Badge>
) : null}
</div> </div>
{run.summary ? <p className="text-sm">{run.summary}</p> : null}
{run.warning ? (
<p className="text-sm text-amber-700">{run.warning}</p>
) : null}
{run.error ? (
<p className="text-sm text-destructive">{run.error}</p>
) : null}
{(run.visionStatus === "fallback" || run.status === "failed") ? (
<div className="flex justify-end">
<Button
size="sm"
variant="outline"
className="gap-2"
onClick={() => retryRunMutation.mutate({ runId: run.id })}
disabled={retryRunMutation.isPending}
>
{retryRunMutation.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Microscope className="h-4 w-4" />}
</Button>
</div>
) : null}
{run.expectedFocus?.length ? (
<div className="flex flex-wrap gap-2">
{run.expectedFocus.map((item) => (
<Badge key={item} variant="outline">{item}</Badge>
))}
</div>
) : null}
{run.corrections ? (
<div className="rounded-xl bg-muted/50 p-3 text-sm leading-6 whitespace-pre-wrap">
{run.corrections}
</div>
) : null}
</CardContent> </CardContent>
</Card> </Card>
))} ))}