Show compressed previews in vision lab
这个提交包含在:
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户