feat(scanner): complete localized artifact quality checkpoint

This commit is contained in:
AzuTear
2026-07-11 15:59:19 +02:00
parent 639b0b7f59
commit 8b9f948c6b
215 changed files with 35440 additions and 7273 deletions
+218 -49
View File
@@ -1,62 +1,231 @@
import { AlertTriangle } from "lucide-react";
import { useBuildCardModel } from "./hooks/useBuildCardModel";
import {
AlertTriangle,
Archive,
Check,
Fingerprint,
LockKeyhole,
Orbit,
RefreshCw,
ShieldCheck,
Sparkles,
Users,
} from "lucide-react";
import { useBuildsViewModel } from "./hooks/useBuildsViewModel";
import type { BuildCardProps, BuildsViewProps } from "./types";
import type { BuildsViewProps } from "./types";
import { useI18n } from "../../i18n";
import { getBuildCopy } from "./buildsCopy";
function BuildCard({ build, snapshot }: BuildCardProps) {
const artifactLookup = new Map(snapshot.artifacts.map((artifact) => [artifact.id, artifact]));
export function BuildsView({ snapshot, onOpenInventory }: BuildsViewProps) {
const { locale } = useI18n();
const copy = getBuildCopy(locale);
const {
characterName,
qualityLabel,
roundedScore,
explanation,
artifactRows,
warnings,
hasWarnings,
} = useBuildCardModel({
build,
characters: snapshot.characters,
artifactLookup,
});
artifactCount,
hasScanData,
foundationLabel,
foundationDetail,
contractLabel,
contractDetail,
profileLabel,
profileDetail,
nativeResultCount,
isLoadingBuildFit,
buildFitLoadError,
ranking,
aggregateContextFields,
setAggregateContextDraft,
refreshBuildFit,
} = useBuildsViewModel({ snapshot, locale });
const suggestions = ranking.suggestions;
const hasSuggestions = suggestions.length > 0;
return (
<section className="panel build-card">
<div className="panel-heading">
<div>
<p className="eyebrow">{qualityLabel}</p>
<h2>{characterName}</h2>
</div>
<strong className="score">{roundedScore}</strong>
</div>
<p className="build-copy">{explanation}</p>
<div className="build-pieces">
{artifactRows.map((artifact) => (
<div key={artifact.id}>
<span>{artifact.slot}</span>
<strong>{artifact.setName}</strong>
<small>{artifact.details}</small>
<section className="build-preview-page" aria-labelledby="build-preview-title" aria-busy={isLoadingBuildFit}>
<div className="build-preview-hero">
<div className="build-preview-copy">
<span className="build-preview-kicker"><Sparkles size={14} aria-hidden="true" /> {copy.heroKicker}</span>
<h2 id="build-preview-title">{hasSuggestions ? copy.heroReadyTitle : copy.heroEmptyTitle}</h2>
<p>
{hasSuggestions
? copy.heroReadyDescription
: copy.heroEmptyDescription}
</p>
<div className="build-preview-actions">
<button className="ghost-button build-inventory-action" type="button" onClick={onOpenInventory}>
<Archive size={16} aria-hidden="true" />
{artifactCount > 0 ? copy.openArtifacts(artifactCount) : copy.prepareArtifacts}
</button>
<button
className="ghost-button build-refresh-action"
type="button"
onClick={() => void refreshBuildFit()}
disabled={isLoadingBuildFit}
>
<RefreshCw size={16} aria-hidden="true" className={isLoadingBuildFit ? "spin" : ""} />
{isLoadingBuildFit ? copy.loadingEvidence : copy.refreshEvidence}
</button>
</div>
))}
</div>
{hasWarnings && (
<div className="warning-list">
{warnings.map((warning) => (
<span key={warning}><AlertTriangle size={14} />{warning}</span>
))}
</div>
<div className={`build-preview-lock ${hasSuggestions ? "ready" : ""}`} aria-label={hasSuggestions ? copy.suggestionsAvailable : copy.suggestionsWaiting}>
<div className="build-orbit" aria-hidden="true"><Orbit size={64} /></div>
<div className="build-lock-core">{hasSuggestions ? <ShieldCheck size={27} aria-hidden="true" /> : <LockKeyhole size={27} aria-hidden="true" />}</div>
<strong>{hasSuggestions ? copy.suggestionCount(suggestions.length) : copy.noSuggestionTitle}</strong>
<span>{hasSuggestions ? copy.nativeChecked(nativeResultCount) : copy.blockedDetail}</span>
</div>
</div>
<div className="build-readiness-grid" aria-label={copy.readinessLabel}>
<article className={hasScanData ? "complete" : "pending"}>
<span className="build-stage-icon">{hasScanData ? <Check size={18} /> : <Archive size={18} />}</span>
<p>{copy.stageArtifacts}</p>
<strong>{foundationLabel}</strong>
<small>{foundationDetail}</small>
</article>
<article className="complete">
<span className="build-stage-icon"><Fingerprint size={18} /></span>
<p>{copy.stageContract}</p>
<strong>{contractLabel}</strong>
<small>{contractDetail}</small>
</article>
<article className={hasSuggestions ? "complete" : "active"}>
<span className="build-stage-icon"><Users size={18} /></span>
<p>{copy.stageProfiles}</p>
<strong>{profileLabel}</strong>
<small>{profileDetail}</small>
</article>
</div>
{isLoadingBuildFit ? (
<section className="build-empty-state" role="status" aria-live="polite">
<RefreshCw size={19} className="spin" aria-hidden="true" />
<div><strong>{copy.loadingTitle}</strong><p>{copy.loadingDescription}</p></div>
</section>
) : buildFitLoadError ? (
<section className="build-empty-state warning" role="status">
<AlertTriangle size={19} aria-hidden="true" />
<div><strong>{copy.lockedTitle}</strong><p>{buildFitLoadError}</p></div>
</section>
) : hasSuggestions ? (
<section className="build-suggestion-list" aria-label={copy.suggestionList}>
{suggestions.map((suggestion) => (
<article key={suggestion.id} className="build-suggestion-card">
<header>
<div>
<span>{suggestion.characterName}</span>
<h3>{suggestion.targetLabel}</h3>
</div>
<strong title={copy.coverageTitle}>{suggestion.evidenceCoverage}% {copy.coverageLabel}</strong>
</header>
<p className="build-suggestion-description">{suggestion.targetDescription}</p>
<div className="build-suggestion-meta">
<span><ShieldCheck size={14} aria-hidden="true" />{suggestion.matchedSetPlanLabel}</span>
<span><Fingerprint size={14} aria-hidden="true" />{copy.safeSlots(suggestion.artifacts.length)}</span>
</div>
<ul className="build-suggestion-artifacts" aria-label={copy.selectedArtifacts(suggestion.characterName)}>
{suggestion.artifacts.map((artifact) => (
<li key={artifact.artifactId}>
<span>{slotLabel(artifact.slot, copy)}</span>
<strong>{artifact.name}</strong>
<small>{artifact.setName} · {artifact.mainStat} {artifact.mainValue}</small>
</li>
))}
</ul>
<ul className="build-suggestion-reasons">
{suggestion.reasons.map((reason) => <li key={reason}>{reason}</li>)}
</ul>
{suggestion.warnings.length > 0 ? (
<div className="build-suggestion-warnings">
<AlertTriangle size={14} aria-hidden="true" />
<span>{suggestion.warnings.join(" ")}</span>
</div>
) : null}
<p className="build-suggestion-source">{copy.sourceNote} {suggestion.sourceLabels.join(" · ")}. {copy.sourceDisclaimer}</p>
</article>
))}
</section>
) : (
<section className="build-empty-state" aria-label={copy.blockedProfiles}>
<LockKeyhole size={19} aria-hidden="true" />
<div>
<strong>{copy.noSuggestionTitle}</strong>
<p>{copy.noSuggestionDescription}</p>
<ul className="build-deferred-list">
{ranking.deferredProfiles.map((profile) => (
<li key={profile.profileId}><strong>{profile.characterName}</strong> · {profile.targetLabel}: {deferredLabel(profile.reason, copy)} {profile.detail}</li>
))}
</ul>
</div>
</section>
)}
{aggregateContextFields.length > 0 ? (
<section className="build-context-editor" aria-label={copy.aggregateContext}>
<div className="build-context-editor-head">
<Fingerprint size={17} aria-hidden="true" />
<div>
<strong>{copy.aggregateContext}</strong>
<p>{copy.aggregateContextDescription}</p>
</div>
</div>
{aggregateContextFields.map((field) => (
<div key={`${field.profileId}:${field.targetKey}`} className="build-context-field">
<div>
<span>{field.characterName} · {field.targetLabel}</span>
<strong>{copy.outsideArtifacts(field.stat, targetRangeLabel(field, copy))}</strong>
<small>{field.reason}</small>
</div>
<label className="build-context-number">
<span>{copy.nonArtifactValue}</span>
<input
type="number"
min="0"
step="0.1"
inputMode="decimal"
value={field.value}
onChange={(event) => setAggregateContextDraft(field.profileId, field.targetKey, { value: event.target.value })}
aria-describedby={`${field.profileId}-${field.targetKey}-help`}
/>
<em>{field.unit === "percent" ? "%" : "flat"}</em>
</label>
<label className="build-context-confirm">
<input
type="checkbox"
checked={field.confirmed}
onChange={(event) => setAggregateContextDraft(field.profileId, field.targetKey, { confirmed: event.target.checked })}
/>
<span>{copy.contextConfirm}</span>
</label>
<small id={`${field.profileId}-${field.targetKey}-help`} className={field.error ? "error" : ""}>
{field.error || copy.contextPending}
</small>
</div>
))}
</section>
) : null}
<div className="build-contract-note">
<LockKeyhole size={18} aria-hidden="true" />
<div>
<strong>{copy.contractBoundary}</strong>
<p>{copy.contractDescription}</p>
</div>
</div>
</section>
);
}
export function BuildsView({ snapshot }: BuildsViewProps) {
const { visibleBuilds } = useBuildsViewModel({ snapshot });
return (
<div className="build-grid">
{visibleBuilds.map(({ build }) => (
<BuildCard key={build.id} build={build} snapshot={snapshot} />
))}
</div>
);
function slotLabel(slot: string, copy: ReturnType<typeof getBuildCopy>) {
return copy.slots[slot] ?? slot;
}
function deferredLabel(reason: string, copy: ReturnType<typeof getBuildCopy>) {
return copy.deferredReasons[reason] ?? copy.deferredFallback;
}
function targetRangeLabel(field: { minimum?: number; maximum?: number; unit: "flat" | "percent" }, copy: ReturnType<typeof getBuildCopy>) {
const unit = field.unit === "percent" ? "%" : "";
if (field.minimum !== undefined && field.maximum !== undefined) return `(${field.minimum}${unit}${field.maximum}${unit})`;
if (field.minimum !== undefined) return copy.rangeAtLeast(field.minimum, unit);
if (field.maximum !== undefined) return copy.rangeAtMost(field.maximum, unit);
return "";
}