feat(artifact): make card generation error more clear

This commit is contained in:
2026-03-01 03:27:49 +07:00 Unverified
parent 0d7ea2acc1
commit 486c825bf2
3 changed files with 77 additions and 29 deletions
+48 -25
View File
@@ -7,26 +7,34 @@ import {
ImageIcon, ImageIcon,
OctagonAlert, OctagonAlert,
ScanSearch, ScanSearch,
SquircleDashed,
} from "lucide-react"; } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SimpleTooltip } from "@/components/tooltip"; import { SimpleTooltip } from "@/components/tooltip";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { getCardStatus } from "@/lib/api";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export function EnkaBrowser({ sub, uid }: { sub: string; uid: string }) { export function EnkaBrowser({ sub, uid }: { sub: string; uid: string }) {
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState<string | null>(null);
const [useWeb, setUseWeb] = useState(false); const [useWeb, setUseWeb] = useState(false);
const [isCached, setIsCached] = useState(true);
// biome-ignore lint/correctness/useExhaustiveDependencies: for update // biome-ignore lint/correctness/useExhaustiveDependencies: for update
useEffect(() => { useEffect(() => {
setReady(false); setReady(false);
setError(false); setError(null);
setIsCached(true);
getCardStatus(sub).then((s) => {
setIsCached(s.cached);
if (!s.cached) setError(s.error);
});
}, [sub, uid]); }, [sub, uid]);
return ( return (
<div className="h-full w-full relative"> <div className="h-full w-full relative">
{!ready && !error && ( {!ready && !error && (isCached || useWeb) && (
<ScanSearch <ScanSearch
className={cn( className={cn(
"animate-pulse absolute left-1/2 -translate-x-1/2 -translate-y-1/2 size-8 text-muted-foreground z-45", "animate-pulse absolute left-1/2 -translate-x-1/2 -translate-y-1/2 size-8 text-muted-foreground z-45",
@@ -36,29 +44,44 @@ export function EnkaBrowser({ sub, uid }: { sub: string; uid: string }) {
)} )}
{error && ( {error && (
<div className="flex flex-col items-center gap-2 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-40"> <div className="flex flex-col items-center gap-2 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-40">
<OctagonAlert className="size-8 text-red-500" /> {error.includes("ใหม่") ? (
<span>ไม่สามารถโหลดข้อมูลตัวละคร</span> <SquircleDashed className="size-8 text-orange-500" />
) : (
<OctagonAlert className="size-8 text-red-500" />
)}
<span>{error}</span>
</div> </div>
)} )}
{(ready || error) && (
<SimpleTooltip text={useWeb ? "สลับเป็นรูปภาพ" : "สลับเป็น Enka"}> {!error && !ready && !isCached && !useWeb && (
<Button <div
variant="outline" className={cn(
className={cn( "flex flex-col items-center gap-2 absolute left-1/2 -translate-x-1/2 -translate-y-1/2 z-40",
"absolute left-1 z-45 bg-[#222a]! backdrop-blur-sm opacity-50 md:opacity-20 hover:opacity-100", useWeb ? "top-[calc(50%+80px)]" : "top-1/2",
useWeb ? "-bottom-19" : "bottom-1", )}
)} >
size="icon" <ScanSearch className="size-8 animate-pulse" />
onClick={() => { <span>ยังไม่ได้เตรียมการ์ดไว้ล่วงหน้า จะใช้เวลาสักพัก</span>
setReady(false); </div>
setError(false);
setUseWeb((x) => !x);
}}
>
{useWeb ? <ImageIcon /> : <Dock />}
</Button>
</SimpleTooltip>
)} )}
<SimpleTooltip text={useWeb ? "สลับเป็นรูปภาพ" : "สลับเป็น Enka"}>
<Button
variant="outline"
className={cn(
"absolute left-1 z-45 bg-[#222a]! backdrop-blur-sm opacity-50 md:opacity-20 hover:opacity-100",
useWeb ? "-bottom-19" : "bottom-1",
)}
size="icon"
onClick={() => {
setReady(false);
setError(null);
setUseWeb((x) => !x);
}}
>
{useWeb ? <ImageIcon /> : <Dock />}
</Button>
</SimpleTooltip>
{useWeb ? ( {useWeb ? (
<iframe <iframe
src={`https://enka.network/u/${uid}`} src={`https://enka.network/u/${uid}`}
@@ -85,9 +108,9 @@ export function EnkaBrowser({ sub, uid }: { sub: string; uid: string }) {
title="Enka Network" title="Enka Network"
onLoad={() => { onLoad={() => {
setReady(true); setReady(true);
setError(false); setError(null);
}} }}
onError={() => setError(true)} onError={() => error || setError("ไม่สามารถโหลดข้อมูลตัวละคร")}
/> />
</div> </div>
)} )}
+28 -4
View File
@@ -1,7 +1,7 @@
import { env } from "node:process"; import { env } from "node:process";
import { redis } from "bun"; import { redis } from "bun";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { and, asc, eq, isNull, lt, not, sql } from "drizzle-orm"; import { and, asc, eq, gt, isNull, lt, not, sql } from "drizzle-orm";
import { removeExpiredSubmissions } from "@/app/(ui)/rubgram/api"; import { removeExpiredSubmissions } from "@/app/(ui)/rubgram/api";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
@@ -203,7 +203,7 @@ async function seedDatabase() {
} }
async function cacheCards() { async function cacheCards() {
const { log, schedule } = logger("cacheCards"); const { log, error, schedule } = logger("cacheCards");
const [notCached] = await db const [notCached] = await db
.select({ .select({
id: submissions.id, id: submissions.id,
@@ -228,19 +228,43 @@ async function cacheCards() {
return; return;
} }
log(`Generating card for ${notCached.id}`); log(`Generating card for ${notCached.id}`);
const res = await fetch(`http://app:3000/api/card/${notCached.id}`); const res = await fetch(`http://app:3000/api/card/${notCached.id}`).catch(
() => ({
ok: false,
status: 502,
text: () => "เกิดข้อผิดพลาดภายในระบบ กำลังพยายามลองใหม่",
}),
);
if (!res.ok) { if (!res.ok) {
log(`Wasn't okay, queueing for retry.`); log(`Wasn't okay, queueing for retry.`);
let text = await res.text();
error(text.includes("502") ? "502" : text);
if (text.includes("findIndex")) text = "ผู้เล่นนี้ไม่มีโชว์เคส มองไม่เห็นตัวละครใดๆ";
else if (text === "Invalid character provided")
text = "ตัวละครที่ผู้เล่นเลือก ไม่ได้อยู่ในโชว์เคส";
else if (text === "Failed to find card." || text === "Invalid UID Provided")
text = "ผู้เล่นนี้ไม่มีอยู่จริง โดนแบนไปแล้วรีเปล่า";
else if (text.length > 2000 || res.status === 502)
text = "ไม่สามารถสร้างการ์ดได้ กำลังพยายามลองใหม่";
text = text.split("\n")[0];
await db await db
.insert(cards) .insert(cards)
.values({ .values({
submission: notCached.id, submission: notCached.id,
tries: 1, tries: 1,
error: text,
}) })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: cards.submission, target: cards.submission,
set: { set: {
tries: res.status === 400 ? 20 : sql<number>`${cards.tries} + 1`, tries:
res.status === 400 || text === "ผู้เล่นนี้ไม่มีโชว์เคส มองไม่เห็นตัวละครใดๆ"
? 21
: sql<number>`${cards.tries} + 1`,
error: sql<string>`case when ${gt(cards.tries, 15)}
then ${text.replace(" กำลังพยายามลองใหม่", "")}
else ${text}
end`,
}, },
}); });
schedule(60, cacheCards); schedule(60, cacheCards);
+1
View File
@@ -104,6 +104,7 @@ export const cards = artifact.table("cards", {
.notNull() .notNull()
.unique(), .unique(),
tries: integer().notNull().default(0), tries: integer().notNull().default(0),
error: text(),
image: bytea(), image: bytea(),
}); });