feat: rearrange game benchmark wizard and improve overall

This commit is contained in:
2026-05-01 01:18:13 +08:00
parent 293aadbff0
commit 824fbbd13f
22 changed files with 722 additions and 143 deletions
+160 -47
View File
@@ -1,8 +1,15 @@
"use client"
import { Shield, ShieldCheck, ShieldX, ShieldQuestion } from "lucide-react"
import { useState, useEffect } from "react"
import { Shield, ShieldCheck, ShieldX, ShieldQuestion, Pencil } from "lucide-react"
import { cn } from "@/lib/utils"
export type AntiCheatData = {
antiCheatRelevant: boolean
antiCheatName: string
antiCheatStatus: "none" | "supported" | "unsupported" | "unknown"
}
interface PlatformSupportEntry {
hardwareSlug: string
antiCheatRelevant: boolean
@@ -13,6 +20,8 @@ interface PlatformSupportEntry {
interface AntiCheatStepProps {
hardwareSlug: string
platformSupport: PlatformSupportEntry[]
value: AntiCheatData
onChange: (data: AntiCheatData) => void
}
const statusConfig = {
@@ -21,94 +30,198 @@ const statusConfig = {
label: "Supported",
color: "border-green-500/30 bg-green-500/10",
textColor: "text-green-400",
message:
"This game's anti-cheat supports Linux/SteamOS. Multiplayer should work.",
message: "Anti-cheat works on Linux/SteamOS. Multiplayer should work.",
},
unsupported: {
icon: ShieldX,
label: "Unsupported",
color: "border-red-500/30 bg-red-500/10",
textColor: "text-red-400",
message:
"This game's anti-cheat does not support Linux/SteamOS. Multiplayer may not work.",
message: "Anti-cheat does not support Linux/SteamOS. Multiplayer may not work.",
},
unknown: {
icon: ShieldQuestion,
label: "Unknown",
color: "border-yellow-500/30 bg-yellow-500/10",
textColor: "text-yellow-400",
message:
"Anti-cheat compatibility is unknown. Multiplayer may or may not work.",
message: "Compatibility is unknown. Multiplayer may or may not work.",
},
none: {
icon: Shield,
label: "None",
color: "border-zinc-500/30 bg-zinc-500/10",
textColor: "text-zinc-400",
message: "No anti-cheat detected for this game.",
message: "No anti-cheat detected.",
},
} as const
export function AntiCheatStep({
hardwareSlug,
platformSupport,
value,
onChange,
}: AntiCheatStepProps) {
// Anti-cheat is a game-level property, not device-specific.
// Find the first entry with anti-cheat info (any device).
const antiCheatEntry = platformSupport.find((p) => p.antiCheatRelevant)
const [isEditing, setIsEditing] = useState(false)
if (!antiCheatEntry) {
return (
<div className="space-y-6">
<div className="flex items-start gap-3">
<Shield className="h-4 w-4 text-text mt-0.5 flex-shrink-0" />
<div>
<h3 className="text-sm font-semibold text-text">
Anti-Cheat Status
</h3>
<p className="text-xs text-text/60 mt-1">
Check the anti-cheat compatibility before submitting.
</p>
</div>
</div>
// Find the best existing entry to prefill:
// Prefer the entry for the currently selected hardware,
// otherwise fall back to any entry with anti-cheat data.
const hardwareEntry = platformSupport.find(
(p) => p.hardwareSlug === hardwareSlug && p.antiCheatRelevant
)
const anyEntry = platformSupport.find((p) => p.antiCheatRelevant)
const existingEntry = hardwareEntry ?? anyEntry
<div className="rounded-lg border border-zinc-500/30 bg-zinc-500/10 p-4">
<div className="flex items-center gap-2">
<Shield className="h-5 w-5 text-zinc-400" />
<h4 className="font-medium">No Anti-Cheat</h4>
</div>
<p className="mt-2 text-sm text-zinc-400">
This game does not use anti-cheat software. Multiplayer (if
available) should work without issues.
</p>
</div>
</div>
)
}
// Prefill once when the component mounts if the current value is the default
useEffect(() => {
if (existingEntry && !isEditing) {
onChange({
antiCheatRelevant: existingEntry.antiCheatRelevant,
antiCheatName: existingEntry.antiCheatName ?? "",
antiCheatStatus: existingEntry.antiCheatStatus,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hardwareSlug]) // re-prefill when hardware changes
const config = statusConfig[antiCheatEntry.antiCheatStatus]
const Icon = config.icon
const relevant = value.antiCheatRelevant
const currentConfig = statusConfig[value.antiCheatStatus]
const CurrentIcon = currentConfig.icon
return (
<div className="space-y-6">
<div className="flex items-start gap-3">
<Shield className="h-4 w-4 text-primary mt-0.5 flex-shrink-0" />
<div>
<h3 className="text-sm font-semibold text-text">Anti-Cheat Status</h3>
<h3 className="text-sm font-semibold text-text">Anti-Cheat</h3>
<p className="text-xs text-text/60 mt-1">
Check the anti-cheat compatibility before submitting.
Set the anti-cheat status for this game. This helps others know if multiplayer will work.
</p>
</div>
</div>
<div className={cn("rounded-lg border p-4", config.color)}>
{/* Toggle: Has anti-cheat? */}
<div className="flex items-center gap-3">
<button
type="button"
onClick={() =>
onChange({
...value,
antiCheatRelevant: !relevant,
antiCheatStatus: !relevant ? "unknown" : "none",
antiCheatName: !relevant ? value.antiCheatName : "",
})
}
className={cn(
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
relevant ? "bg-primary" : "bg-text/20"
)}
>
<span
className={cn(
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform",
relevant ? "translate-x-6" : "translate-x-1"
)}
/>
</button>
<div className="flex flex-col">
<span className="text-sm text-text">Game uses anti-cheat</span>
<span className="text-xs text-text/50">
{relevant
? "Yes — select the anti-cheat name and compatibility below"
: "No anti-cheat software detected in this game"}
</span>
</div>
</div>
{relevant && (
<div className="space-y-4">
{/* Anti-cheat name */}
<div>
<label className="block text-xs font-medium text-text/70 mb-1.5">
Anti-Cheat Name
</label>
<input
type="text"
value={value.antiCheatName}
onChange={(e) =>
onChange({ ...value, antiCheatName: e.target.value })
}
placeholder="e.g. Easy Anti-Cheat, BattlEye, Ricochet"
className="w-full px-3 py-2 rounded-lg bg-background border border-border text-sm text-text placeholder:text-text/30 focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
</div>
{/* Status radios */}
<div>
<label className="block text-xs font-medium text-text/70 mb-2">
Compatibility on Linux / SteamOS
</label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
{(
[
"supported",
"unsupported",
"unknown",
] as const
).map((status) => {
const cfg = statusConfig[status]
const Icon = cfg.icon
const active = value.antiCheatStatus === status
return (
<button
key={status}
type="button"
onClick={() =>
onChange({ ...value, antiCheatStatus: status })
}
className={cn(
"flex items-center gap-2 px-3 py-2.5 rounded-lg border text-left transition-colors",
active
? cfg.color
: "border-border bg-text/[0.02] hover:bg-text/5"
)}
>
<Icon
className={cn(
"h-4 w-4 shrink-0",
active ? cfg.textColor : "text-text/30"
)}
/>
<span
className={cn(
"text-xs font-medium",
active ? cfg.textColor : "text-text/60"
)}
>
{cfg.label}
</span>
</button>
)
})}
</div>
</div>
</div>
)}
{/* Preview card */}
<div
className={cn(
"rounded-lg border p-4",
currentConfig.color
)}
>
<div className="flex items-center gap-2">
<Icon className={cn("h-5 w-5", config.textColor)} />
<h4 className="font-medium">
Anti-Cheat: {antiCheatEntry.antiCheatName || config.label}
<CurrentIcon className={cn("h-5 w-5", currentConfig.textColor)} />
<h4 className="font-medium text-sm">
{relevant && value.antiCheatName
? value.antiCheatName
: currentConfig.label}
</h4>
</div>
<p className={cn("mt-2 text-sm", config.textColor)}>{config.message}</p>
<p className={cn("mt-2 text-sm", currentConfig.textColor)}>
{currentConfig.message}
</p>
</div>
</div>
)
+24 -4
View File
@@ -1,7 +1,7 @@
"use client"
import { motion } from "motion/react"
import { Send, Loader2, AlertCircle, Monitor, Gauge, SlidersHorizontal, Terminal, FileText } from "lucide-react"
import { Send, Loader2, AlertCircle, Monitor, Gauge, SlidersHorizontal, Terminal, FileText, GitBranch, Shield } from "lucide-react"
import { TiptapEditor } from "@/components/tiptap-editor"
import type { SettingCategory } from "@/components/wizard/settings-editor"
import type { PerformanceData } from "./performance-step"
@@ -11,6 +11,12 @@ import { UPSCALER_TYPE_OPTIONS, FRAME_GEN_OPTIONS } from "./environment-step"
export interface ReviewData {
hardwareSlug: string
hardwareName: string
gameVersionLabel: string
antiCheat: {
antiCheatRelevant: boolean
antiCheatName: string
antiCheatStatus: "none" | "supported" | "unsupported" | "unknown"
}
performance: PerformanceData
settings: SettingCategory[]
environment: EnvironmentData
@@ -56,7 +62,7 @@ export function ReviewStep({
isSubmitting,
error,
}: ReviewStepProps) {
const { hardwareName, performance, environment, settings } = data
const { hardwareName, gameVersionLabel, antiCheat, performance, environment, settings } = data
const upscalerLabel = (() => {
if (!data.environment.upscalerType || data.environment.upscalerType === "none") return "None"
@@ -85,10 +91,24 @@ export function ReviewStep({
{/* Summary Cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Hardware */}
{/* Setup: Hardware + Version + Anti-Cheat */}
<div className="rounded-lg border border-border bg-text/5 p-4">
<SectionHeader icon={Monitor} label="Hardware" />
<SectionHeader icon={Monitor} label="Setup" />
<SummaryRow label="Device" value={hardwareName || data.hardwareSlug || "Not selected"} />
<SummaryRow label="Game Version" value={gameVersionLabel} />
{antiCheat.antiCheatRelevant && (
<>
<SummaryRow label="Anti-Cheat" value={antiCheat.antiCheatName || "Unknown"} />
<SummaryRow label="Anti-Cheat Status" value={
antiCheat.antiCheatStatus === "supported" ? "Supported" :
antiCheat.antiCheatStatus === "unsupported" ? "Unsupported" :
antiCheat.antiCheatStatus === "unknown" ? "Unknown" : "None"
} />
</>
)}
{!antiCheat.antiCheatRelevant && (
<SummaryRow label="Anti-Cheat" value="None" />
)}
</div>
{/* Performance */}
+131
View File
@@ -0,0 +1,131 @@
"use client"
import { GitBranch } from "lucide-react"
import { HardwareStep } from "./hardware-step"
import { AntiCheatStep, type AntiCheatData } from "./anti-cheat-step"
export interface GameVersionInfo {
id: string
versionString: string | null
buildId: string | null
isLatest: boolean
}
interface SetupStepProps {
gameId: string
gameVersions: GameVersionInfo[]
hardwareSlug: string
onHardwareChange: (slug: string) => void
hardwareName: string
selectedVersionId: string
onVersionChange: (versionId: string) => void
newVersionString: string
onNewVersionStringChange: (value: string) => void
isCreatingVersion: boolean
antiCheat: AntiCheatData
onAntiCheatChange: (data: AntiCheatData) => void
platformSupport: {
hardwareSlug: string
antiCheatRelevant: boolean
antiCheatName: string | null
antiCheatStatus: "none" | "supported" | "unsupported" | "unknown"
}[]
}
export function SetupStep({
gameId,
gameVersions,
hardwareSlug,
onHardwareChange,
hardwareName,
selectedVersionId,
onVersionChange,
newVersionString,
onNewVersionStringChange,
isCreatingVersion,
antiCheat,
onAntiCheatChange,
platformSupport,
}: SetupStepProps) {
const isNewVersion = selectedVersionId === "__new__"
return (
<div className="space-y-8">
{/* Hardware Section */}
<section>
<HardwareStep value={hardwareSlug} onChange={onHardwareChange} />
</section>
<div className="border-t border-border" />
{/* Game Version Section */}
<section>
<div className="flex items-center gap-2 mb-4">
<GitBranch className="h-4 w-4 text-primary" />
<div>
<h3 className="text-sm font-semibold text-text">Game Version</h3>
<p className="text-xs text-text/60">
Which version of the game did you test? This helps others know if benchmarks match their version.
</p>
</div>
</div>
<div className="space-y-3">
<div className="space-y-1.5">
<label className="text-xs font-medium text-text/60">Version</label>
<select
value={selectedVersionId}
onChange={(e) => onVersionChange(e.target.value)}
className="w-full appearance-none px-4 py-3 rounded-lg border border-border bg-text/5 text-text text-sm outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-colors cursor-pointer"
>
{gameVersions.map((v) => (
<option key={v.id} value={v.id}>
{v.versionString
? v.versionString
: v.buildId
? `Build ${v.buildId}`
: "Unknown version"}
{v.isLatest ? " (latest)" : ""}
</option>
))}
<option value="__new__">
New version...
</option>
</select>
</div>
{isNewVersion && (
<div className="space-y-1.5">
<label className="text-xs font-medium text-text/60">
Version String <span className="text-red-400">*</span>
</label>
<input
type="text"
value={newVersionString}
onChange={(e) => onNewVersionStringChange(e.target.value)}
placeholder="e.g. 1.2.3, Patch 4.0, Hotfix Jan 2025"
className="w-full px-4 py-3 rounded-lg border border-border bg-text/5 text-text text-sm placeholder:text-text/40 outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-colors"
disabled={isCreatingVersion}
/>
<p className="text-xs text-text/40">
Enter the game version you tested. This will create a new version entry.
</p>
</div>
)}
</div>
</section>
<div className="border-t border-border" />
{/* Anti-Cheat Section */}
<section>
<AntiCheatStep
hardwareSlug={hardwareSlug}
platformSupport={platformSupport}
value={antiCheat}
onChange={onAntiCheatChange}
/>
</section>
</div>
)
}