Refactor launcher and studio modules

This commit is contained in:
Andraxion 2026-06-27 04:36:26 -04:00
parent a20d298be2
commit ec3e0f5138
34 changed files with 10300 additions and 8600 deletions

View file

@ -0,0 +1,974 @@
// @ts-nocheck
import type { CSSProperties } from "react";
import { useEffect, useState } from "react";
import { openWorldshaperStudioWindow } from "../shared/windowing";
import launcherBackground from "../../background.png";
import { LauncherAdminPanel } from "./components/LauncherAdminPanel";
import { LauncherLogsModal } from "./components/LauncherLogsModal";
import { LauncherNewsPanel } from "./components/LauncherNewsPanel";
import { LauncherPublicRequestBoard } from "./components/LauncherPublicRequestBoard";
import { useLauncherRequestBoard } from "./useLauncherRequestBoard";
declare const __APP_BUILD__: string;
type WorldDefaultPayload = {
worldId?: string;
world?: {
id?: string;
};
};
type LaunchState = "ready" | "opening" | "opened" | "blocked" | "error";
type BoardTab = "news" | "requests";
type LauncherWindowMode = "public" | "admin";
type LauncherRequestStatus = "pending" | "active" | "implemented";
type AdminDetailTab = "routing" | "analysis";
type LauncherRequestAnalysisRouting = {
summary?: string;
ambiguity?: "low" | "medium" | "high";
matchedTerms?: string[];
suggestedTags?: string[];
suggestedSystems?: string[];
suggestedModules?: string[];
rationale?: string;
possibleDirections?: string[];
kbSections?: string[];
};
type LauncherRequest = {
id: string;
sourceSubmissionId?: string;
title: string;
status: LauncherRequestStatus;
category: string;
tags: string[];
sourceText: string;
summary: string;
implementationNotes: string;
analysis?: {
state?: "unprocessed" | "processing" | "processed" | "needs_review" | "error";
confidence?: number | null;
model?: string;
createdAt?: string;
updatedAt?: string;
error?: string;
submissionId?: string;
sourceTextSnapshot?: string;
routing?: LauncherRequestAnalysisRouting;
itemCount?: number;
items?: Array<{
title?: string;
primaryCategory?: string;
tags?: string[];
statusRecommendation?: string;
parsedInterpretation?: string;
implementationApproach?: string;
affectedSystems?: string[];
affectedFiles?: string[];
problemType?: string;
rawExcerpt?: string;
confidence?: number | null;
reviewRationale?: string;
reviewOptions?: string[];
notes?: string;
}>;
};
createdAt: string;
updatedAt: string;
};
type LauncherRequestAnalysisItem = NonNullable<NonNullable<LauncherRequest["analysis"]>["items"]>[number];
type LauncherRequestsPayload = {
requests?: LauncherRequest[];
};
type RecentSaveEvent = {
at?: string;
type?: string;
requestId?: string;
textPreview?: string;
status?: string;
category?: string;
itemCount?: number;
model?: string;
reason?: string;
provider?: string;
pid?: number;
code?: number | null;
signal?: string;
error?: string;
};
type RecentSaveEventsPayload = {
saves?: RecentSaveEvent[];
};
type ProcessPendingPayload = {
ok?: boolean;
launched?: boolean;
reason?: string;
autorunEnabled?: boolean;
configured?: boolean;
queuedPendingCount?: number;
pid?: number;
};
type RequeueAnalysisPayload = {
ok?: boolean;
launched?: boolean;
reason?: string;
request?: LauncherRequest;
requests?: LauncherRequest[];
requestId?: string;
queuedPendingCount?: number;
pid?: number;
};
type LauncherRequestMetaPayload = {
allowedTags?: string[];
};
type AdminAuthPayload = {
ok?: boolean;
accessGranted?: boolean;
adminConfigured?: boolean;
error?: string;
};
const DEFAULT_EDITOR_WORLD_ID_FALLBACK = "overworld";
function readLauncherWindowMode(): LauncherWindowMode {
if (typeof window === "undefined") {
return "public";
}
const searchParams = new URLSearchParams(window.location.search);
return searchParams.get("admin") === "requests" ? "admin" : "public";
}
function normalizeStringList(values: string[]): string[] {
return Array.from(new Set(
values
.map((entry) => String(entry || "").trim())
.filter(Boolean),
)).sort((left, right) => left.localeCompare(right));
}
function appendUniqueString(values: string[], value: string): string[] {
const normalizedValue = String(value || "").trim();
if (!normalizedValue) {
return normalizeStringList(values);
}
return normalizeStringList([...values, normalizedValue]);
}
function removeStringValue(values: string[], value: string): string[] {
const normalizedValue = String(value || "").trim().toLowerCase();
return normalizeStringList(values.filter((entry) => entry.trim().toLowerCase() !== normalizedValue));
}
function toggleStringSelection(current: string[], value: string): string[] {
return current.includes(value)
? current.filter((entry) => entry !== value)
: [...current, value].sort((left, right) => left.localeCompare(right));
}
function normalizeSearchText(value: string): string {
return String(value || "").replace(/\s+/g, " ").trim().toLowerCase();
}
function extractRoutingTerms(requestEntry: LauncherRequest): string[] {
const tagTerms = requestEntry.tags
.map((entry) => String(entry || "").trim())
.filter(Boolean);
if (tagTerms.length > 0) {
return tagTerms.slice(0, 6);
}
const seen = new Set<string>();
const stopWords = new Set([
"the", "and", "for", "with", "that", "this", "from", "into", "have", "need",
"want", "make", "more", "just", "like", "does", "dont", "cannot", "should",
"would", "could", "about", "because", "there", "their", "they", "them", "then",
"than", "over", "under", "your", "while", "where",
]);
const matches = `${requestEntry.title} ${requestEntry.sourceText}`.match(/[A-Za-z][A-Za-z0-9/-]{2,}/g) || [];
return matches
.map((entry) => entry.trim())
.filter((entry) => {
const normalized = entry.toLowerCase();
if (stopWords.has(normalized) || seen.has(normalized)) {
return false;
}
seen.add(normalized);
return true;
})
.slice(0, 6);
}
function buildRoutingSummaryFallback(requestEntry: LauncherRequest, firstItem: LauncherRequestAnalysisItem | null): string {
const normalizedSummary = String(requestEntry.summary || "").trim();
if (normalizedSummary && normalizedSummary !== "Awaiting parsing and categorization.") {
return normalizedSummary;
}
if (firstItem?.parsedInterpretation) {
return String(firstItem.parsedInterpretation).trim();
}
const normalizedSource = String(requestEntry.sourceText || "").replace(/\s+/g, " ").trim();
if (normalizedSource) {
return normalizedSource.length > 220
? `${normalizedSource.slice(0, 217).trim()}...`
: normalizedSource;
}
return "No routing summary has been stored yet.";
}
function buildFallbackRouting(requestEntry: LauncherRequest): LauncherRequestAnalysisRouting {
const firstItem = Array.isArray(requestEntry.analysis?.items) ? requestEntry.analysis.items[0] : null;
const routedTags = Array.isArray(firstItem?.tags) && firstItem.tags.length > 0
? firstItem.tags
: requestEntry.tags.length > 0
? requestEntry.tags
: (requestEntry.category && requestEntry.category !== "Unsorted" ? [requestEntry.category] : []);
const likelySystems = Array.isArray(firstItem?.affectedSystems) && firstItem.affectedSystems.length > 0
? firstItem.affectedSystems
: (routedTags.length > 0 ? routedTags : []);
const possibleDirections = Array.isArray(firstItem?.reviewOptions) && firstItem.reviewOptions.length > 0
? firstItem.reviewOptions
: (requestEntry.implementationNotes.trim() ? [requestEntry.implementationNotes.trim()] : []);
return {
summary: buildRoutingSummaryFallback(requestEntry, firstItem),
ambiguity: requestEntry.status === "pending" ? "medium" : "low",
matchedTerms: extractRoutingTerms(requestEntry),
suggestedTags: Array.isArray(routedTags) ? routedTags : [],
suggestedSystems: likelySystems,
suggestedModules: [],
rationale: String(
firstItem?.reviewRationale
|| requestEntry.implementationNotes
|| `Routing was reconstructed from the saved request title, tags, and submission text for "${requestEntry.title}".`
).trim(),
possibleDirections,
kbSections: [],
};
}
function mergeRoutingWithFallback(
requestEntry: LauncherRequest,
existingRouting: LauncherRequestAnalysisRouting | undefined,
): LauncherRequestAnalysisRouting {
const fallbackRouting = buildFallbackRouting(requestEntry);
const normalizedRouting = existingRouting || {};
return {
summary: String(normalizedRouting.summary || "").trim() || fallbackRouting.summary,
ambiguity: normalizedRouting.ambiguity || fallbackRouting.ambiguity,
matchedTerms: Array.isArray(normalizedRouting.matchedTerms) && normalizedRouting.matchedTerms.length > 0
? normalizedRouting.matchedTerms
: fallbackRouting.matchedTerms,
suggestedTags: Array.isArray(normalizedRouting.suggestedTags) && normalizedRouting.suggestedTags.length > 0
? normalizedRouting.suggestedTags
: fallbackRouting.suggestedTags,
suggestedSystems: Array.isArray(normalizedRouting.suggestedSystems) && normalizedRouting.suggestedSystems.length > 0
? normalizedRouting.suggestedSystems
: fallbackRouting.suggestedSystems,
suggestedModules: Array.isArray(normalizedRouting.suggestedModules) && normalizedRouting.suggestedModules.length > 0
? normalizedRouting.suggestedModules
: fallbackRouting.suggestedModules,
rationale: String(normalizedRouting.rationale || "").trim() || fallbackRouting.rationale,
possibleDirections: Array.isArray(normalizedRouting.possibleDirections) && normalizedRouting.possibleDirections.length > 0
? normalizedRouting.possibleDirections
: fallbackRouting.possibleDirections,
kbSections: Array.isArray(normalizedRouting.kbSections) && normalizedRouting.kbSections.length > 0
? normalizedRouting.kbSections
: fallbackRouting.kbSections,
};
}
function hydrateLauncherRequestForUi(requestEntry: LauncherRequest): LauncherRequest {
const nextRequest = cloneLauncherRequest(requestEntry);
nextRequest.analysis = {
...(nextRequest.analysis || {}),
createdAt: nextRequest.analysis?.createdAt || nextRequest.createdAt,
updatedAt: nextRequest.analysis?.updatedAt || nextRequest.updatedAt,
itemCount: nextRequest.analysis?.itemCount ?? (Array.isArray(nextRequest.analysis?.items) ? nextRequest.analysis?.items.length : 0),
items: Array.isArray(nextRequest.analysis?.items) ? nextRequest.analysis.items : [],
routing: mergeRoutingWithFallback(nextRequest, nextRequest.analysis?.routing),
};
return nextRequest;
}
function buildRequestSearchCorpus(requestEntry: LauncherRequest): string {
return normalizeSearchText([
requestEntry.title,
requestEntry.category,
requestEntry.tags.join(" "),
requestEntry.sourceText,
requestEntry.summary,
requestEntry.implementationNotes,
requestEntry.analysis?.routing?.summary,
requestEntry.analysis?.routing?.rationale,
...(Array.isArray(requestEntry.analysis?.routing?.matchedTerms) ? requestEntry.analysis?.routing?.matchedTerms : []),
...(Array.isArray(requestEntry.analysis?.items)
? requestEntry.analysis.items.flatMap((item) => [
item.title,
item.primaryCategory,
item.parsedInterpretation,
item.implementationApproach,
...(Array.isArray(item.tags) ? item.tags : []),
])
: []),
].filter(Boolean).join(" "));
}
function matchesRequestFilterToken(requestEntry: LauncherRequest, token: string): boolean {
if (token === "pending") {
return requestEntry.status === "pending";
}
if (token === "queued") {
return isQueuedPendingRequest(requestEntry);
}
if (token === "review") {
return isNeedsReviewRequest(requestEntry);
}
if (token === "active") {
return requestEntry.status === "active";
}
if (token === "implemented") {
return requestEntry.status === "implemented";
}
return false;
}
function requestMatchesFilters(
requestEntry: LauncherRequest,
searchText: string,
statusSelections: string[],
tagSelections: string[],
): boolean {
const normalizedSearchText = normalizeSearchText(searchText);
if (normalizedSearchText && !buildRequestSearchCorpus(requestEntry).includes(normalizedSearchText)) {
return false;
}
if (statusSelections.length > 0 && !statusSelections.some((token) => matchesRequestFilterToken(requestEntry, token))) {
return false;
}
if (tagSelections.length > 0 && !tagSelections.some((tag) => requestEntry.tags.includes(tag))) {
return false;
}
return true;
}
function FilterIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 6h16M7 12h10M10 18h4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
);
}
function LogsIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 5h14v14H5z" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M8 9h8M8 12h8M8 15h5" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
function SaveIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 4h11l3 3v13H5z" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" />
<path d="M8 4h7v5H8zM8 14h8v5H8z" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" />
</svg>
);
}
function CheckIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 12.5l4.2 4.2L19 7" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function PlayIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M8 6l10 6-10 6z" fill="currentColor" />
</svg>
);
}
type LauncherChipSelectorProps = {
label: string;
values: string[];
options: string[];
placeholder: string;
emptyLabel?: string;
onAdd: (value: string) => void;
onRemove: (value: string) => void;
};
function LauncherChipSelector({
label,
values,
options,
placeholder,
emptyLabel = "No tags selected yet.",
onAdd,
onRemove,
}: LauncherChipSelectorProps) {
const availableOptions = options.filter((option) => !values.includes(option));
return (
<div className="launcher-chip-field">
<div className="launcher-chip-field-head">
<span className="launcher-request-filter-label">{label}</span>
<select
className="launcher-request-filter-select"
value=""
onChange={(event) => {
const nextValue = event.target.value;
if (!nextValue) {
return;
}
onAdd(nextValue);
}}
disabled={availableOptions.length === 0}
>
<option value="">{availableOptions.length > 0 ? placeholder : "Everything added"}</option>
{availableOptions.map((option) => (
<option key={`${label}-${option}`} value={option}>{option}</option>
))}
</select>
</div>
<div className="launcher-chip-list">
{values.length > 0 ? values.map((value) => (
<span key={`${label}-${value}`} className="launcher-chip">
<span>{value}</span>
<button
type="button"
className="launcher-chip-remove"
onClick={() => onRemove(value)}
aria-label={`Remove ${value}`}
title={`Remove ${value}`}
>
x
</button>
</span>
)) : (
<span className="launcher-chip-empty">{emptyLabel}</span>
)}
</div>
</div>
);
}
async function resolveDefaultWorldId(): Promise<string> {
const response = await fetch("/api/world-default");
if (!response.ok) {
throw new Error(`Failed to load default world (${response.status}).`);
}
const payload = await response.json() as WorldDefaultPayload;
const resolvedWorldId = String(payload.worldId || payload.world?.id || "").trim();
return resolvedWorldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
}
async function fetchJsonOrThrow<T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> {
const response = await fetch(input, init);
if (!response.ok) {
let detail = `Request failed (${response.status}).`;
try {
const payload = await response.json() as { error?: string };
detail = String(payload?.error || detail);
} catch {
// Ignore JSON parse failures and fall back to status text.
}
throw new Error(detail);
}
return response.json() as Promise<T>;
}
function buildAdminHeaders(password: string, headers?: HeadersInit): HeadersInit {
const normalizedPassword = String(password || "").trim();
if (!normalizedPassword) {
return {
...(headers || {}),
};
}
return {
...(headers || {}),
"x-worldshaper-admin-password": normalizedPassword,
};
}
function isAdminAccessError(error: unknown): boolean {
const text = String(error || "").toLowerCase();
return text.includes("admin access denied")
|| text.includes("admin access is not configured");
}
function cloneLauncherRequest(requestEntry: LauncherRequest): LauncherRequest {
return JSON.parse(JSON.stringify(requestEntry)) as LauncherRequest;
}
function formatRequestTimestamp(value: string): string {
const parsed = Date.parse(String(value || ""));
if (!Number.isFinite(parsed)) {
return "Saved recently";
}
return new Intl.DateTimeFormat(undefined, {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
}).format(parsed);
}
function formatRequestSubmittedDate(value: string): string {
const parsed = Date.parse(String(value || ""));
if (!Number.isFinite(parsed)) {
return "Recently";
}
return new Intl.DateTimeFormat(undefined, {
month: "short",
day: "numeric",
year: "numeric",
}).format(parsed);
}
function normalizeAnalysisState(value: string | undefined): string {
return String(value || "").trim().toLowerCase();
}
function formatAnalysisStateLabel(value: string | undefined): string {
const normalized = normalizeAnalysisState(value);
if (normalized === "processing") {
return "Processing";
}
if (normalized === "processed") {
return "Processed";
}
if (normalized === "needs_review") {
return "Needs Review";
}
if (normalized === "error") {
return "Error";
}
return "Unprocessed";
}
function getRequestDisplayStateLabel(requestEntry: LauncherRequest): string {
if (requestEntry.status === "implemented") {
return "Implemented";
}
if (requestEntry.status === "active") {
return "Active";
}
const analysisState = normalizeAnalysisState(requestEntry.analysis?.state);
if (!analysisState || analysisState === "unprocessed") {
return "Queued";
}
if (analysisState === "needs_review") {
return "Needs Review";
}
if (analysisState === "error") {
return "Analysis Error";
}
if (analysisState === "processed") {
return "Reviewed";
}
return formatAnalysisStateLabel(analysisState);
}
function getRequestDisplayStateClassName(requestEntry: LauncherRequest): string {
if (requestEntry.status === "implemented") {
return "implemented";
}
if (requestEntry.status === "active") {
return "active";
}
const analysisState = normalizeAnalysisState(requestEntry.analysis?.state);
if (!analysisState || analysisState === "unprocessed") {
return "queued";
}
if (analysisState === "needs_review") {
return "needs-review";
}
if (analysisState === "error") {
return "error";
}
if (analysisState === "processed") {
return "processed";
}
if (analysisState === "processing") {
return "processing";
}
return "pending";
}
function isQueuedPendingRequest(requestEntry: LauncherRequest): boolean {
const analysisState = normalizeAnalysisState(requestEntry.analysis?.state);
return requestEntry.status === "pending" && (!analysisState || analysisState === "unprocessed" || analysisState === "processing");
}
function isNeedsReviewRequest(requestEntry: LauncherRequest): boolean {
const analysisState = normalizeAnalysisState(requestEntry.analysis?.state);
return requestEntry.status === "pending" && (analysisState === "needs_review" || analysisState === "error");
}
function formatEventLabel(event: RecentSaveEvent): string {
switch (String(event.type || "").trim()) {
case "launcher-request-add":
return "Request submitted";
case "launcher-request-delete":
return "Request deleted";
case "launcher-request-update":
return "Request updated";
case "launcher-request-review":
return "Analysis saved for review";
case "launcher-request-promote":
return "Pending request promoted";
case "launcher-request-analysis-error":
return "Analysis failed";
case "launcher-request-analysis-launch":
return "Queue worker launched";
case "launcher-request-analysis-finish":
return "Queue worker finished";
case "launcher-request-analysis-launch-error":
return "Queue worker launch error";
case "launcher-request-analysis-requeue":
return "Request requeued for review";
default:
return String(event.type || "Event");
}
}
function formatEventDetail(event: RecentSaveEvent): string {
const parts = [
event.requestId ? `Request ${event.requestId}` : "",
event.category ? `Category ${event.category}` : "",
event.status ? `Status ${event.status}` : "",
event.itemCount ? `${event.itemCount} item${event.itemCount === 1 ? "" : "s"}` : "",
event.provider ? `Provider ${event.provider}` : "",
event.model ? `Model ${event.model}` : "",
event.reason ? `Reason ${event.reason}` : "",
event.pid ? `PID ${event.pid}` : "",
Number.isFinite(Number(event.code)) ? `Exit ${event.code}` : "",
event.signal ? `Signal ${event.signal}` : "",
event.error ? String(event.error) : "",
event.textPreview ? `Preview: ${event.textPreview}` : "",
].filter(Boolean);
return parts.join(" • ");
}
function openStudioPopup(worldId: string): boolean {
const popup = openWorldshaperStudioWindow(worldId, window, { worldId });
return Boolean(popup);
}
function openRepo(): void {
window.location.assign("https://repo.andraxion.net/");
}
function openAdminPanelWindow(): boolean {
const nextUrl = new URL(window.location.href);
nextUrl.searchParams.set("admin", "requests");
nextUrl.searchParams.set("tab", "requests");
const popup = window.open(nextUrl.toString(), "worldshaper-admin-panel", "popup=yes,width=1620,height=980,resizable=yes,scrollbars=yes");
if (popup) {
popup.focus();
}
return Boolean(popup);
}
function WorldshaperLauncher() {
const launcherWindowMode = readLauncherWindowMode();
const adminWindowMode = launcherWindowMode === "admin";
const [launchState, setLaunchState] = useState<LaunchState>("ready");
const [error, setError] = useState("");
const [worldId, setWorldId] = useState(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
const {
adminPanelOpen,
activeBoardTab,
setActiveBoardTab,
requests,
requestsLoading,
requestsError,
requestDraftOpen,
setRequestDraftOpen,
requestDraft,
setRequestDraft,
requestSubmitting,
requestMutatingId,
requestSearchText,
setRequestSearchText,
requestFilterMenuOpen,
setRequestFilterMenuOpen,
requestStatusFilters,
setRequestStatusFilters,
requestTagFilters,
setRequestTagFilters,
expandedRequestIds,
adminAccessGranted,
adminPasswordDraft,
setAdminPasswordDraft,
adminAuthSubmitting,
adminPasswordError,
selectedAdminRequestId,
selectedAdminAnalysisIndex,
setSelectedAdminAnalysisIndex,
adminSearchText,
setAdminSearchText,
adminFilterMenuOpen,
setAdminFilterMenuOpen,
adminStatusFilters,
setAdminStatusFilters,
adminTagFilters,
setAdminTagFilters,
adminEditorDraft,
adminDetailTab,
setAdminDetailTab,
adminSaving,
recentSaveEvents,
logsLoading,
logsModalOpen,
setLogsModalOpen,
logsError,
queueTriggering,
requeueingMode,
adminNotice,
refreshAdminData,
loadRecentSaveEvents,
handleAddRequest,
handleAdminPanelToggle,
handleAdminUnlock,
handleSelectAdminRequest,
updateAdminDraft,
updateAdminDraftItem,
handleSaveAdminRequest,
handleApproveAdminRequest,
handleRequeueAnalysis,
handleToggleExpandedRequest,
handleDeleteRequest,
handleProcessPendingQueue,
requestCount,
pendingRequestCount,
activeRequestCount,
implementedRequestCount,
queuedPendingRequestCount,
needsReviewRequestCount,
requestTagFilterOptions,
requestStatusFilterOptions,
filteredRequests,
adminFilteredRequests,
selectedAnalysisItem,
standardizedTagOptions,
categoryOptions,
boardTitle,
boardHint,
} = useLauncherRequestBoard({ adminWindowMode });
useEffect(() => {
let cancelled = false;
void resolveDefaultWorldId()
.then((resolvedWorldId) => {
if (cancelled) {
return;
}
setWorldId(resolvedWorldId);
})
.catch(() => {
if (cancelled) {
return;
}
setWorldId(DEFAULT_EDITOR_WORLD_ID_FALLBACK);
});
return () => {
cancelled = true;
};
}, []);
async function handleLaunch(): Promise<void> {
setError("");
const nextWorldId = worldId || DEFAULT_EDITOR_WORLD_ID_FALLBACK;
setLaunchState("opening");
try {
const resolvedWorldId = nextWorldId || await resolveDefaultWorldId().catch(() => DEFAULT_EDITOR_WORLD_ID_FALLBACK);
setWorldId(resolvedWorldId);
if (openStudioPopup(resolvedWorldId)) {
setLaunchState("opened");
return;
}
setLaunchState("blocked");
} catch (nextError: unknown) {
const nextErrorText = String(nextError || "Failed to prepare Worldshaper Studio.");
setLaunchState("error");
setError(nextErrorText);
}
}
const isBusy = launchState === "opening";
return (
<main
className={`launcher-shell ${adminWindowMode ? "launcher-shell-admin" : ""}`}
style={{ "--launcher-background-image": `url(${launcherBackground})` } as CSSProperties}
>
<div className={`launcher-stack ${adminWindowMode ? "launcher-stack-admin" : ""}`}>
{!adminWindowMode ? (
<section className="launcher-hero-window" aria-labelledby="launcher-studio-title">
<div className="launcher-hero-body">
<div className="launcher-hero-stack">
<div className="launcher-title-bubble">
<h1 className="launcher-title" id="launcher-studio-title">Worldshaper Studio</h1>
</div>
<div className="launcher-actions launcher-actions-floating">
<button type="button" className="launcher-primary-btn" onClick={() => void handleLaunch()} disabled={isBusy}>
Launch
</button>
<button type="button" className="launcher-secondary-btn" onClick={openRepo} disabled={isBusy}>
Open Repo
</button>
</div>
{launchState === "blocked" ? <p className="launcher-error">Popup blocked. Allow popups, then press Launch again.</p> : null}
{error ? <p className="launcher-error">{error}</p> : null}
</div>
</div>
</section>
) : null}
<section className="launcher-changelog-window" aria-labelledby="launcher-board-title">
<div className="launcher-changelog-titlebar">
<div className="launcher-changelog-title" id="launcher-board-title">{boardTitle}</div>
<div className="launcher-changelog-hint">{boardHint}</div>
</div>
<div className="launcher-changelog-body">
<div className="launcher-board-content">
{!adminWindowMode ? (
<div className="launcher-board-tabs">
<button
type="button"
className={`launcher-secondary-btn launcher-board-tab ${activeBoardTab === "news" ? "is-active" : ""}`}
onClick={() => setActiveBoardTab("news")}
>
News
</button>
<button
type="button"
className={`launcher-secondary-btn launcher-board-tab ${activeBoardTab === "requests" ? "is-active" : ""}`}
onClick={() => setActiveBoardTab("requests")}
>
Requests
</button>
</div>
) : null}
{activeBoardTab === "news" ? (
<LauncherNewsPanel />
) : (
adminPanelOpen ? (
<LauncherAdminPanel
adminAccessGranted={adminAccessGranted}
adminPasswordDraft={adminPasswordDraft}
setAdminPasswordDraft={setAdminPasswordDraft}
handleAdminUnlock={handleAdminUnlock}
adminAuthSubmitting={adminAuthSubmitting}
adminPasswordError={adminPasswordError}
handleProcessPendingQueue={handleProcessPendingQueue}
queueTriggering={queueTriggering}
refreshAdminData={refreshAdminData}
logsLoading={logsLoading}
setLogsModalOpen={setLogsModalOpen}
loadRecentSaveEvents={loadRecentSaveEvents}
queuedPendingRequestCount={queuedPendingRequestCount}
needsReviewRequestCount={needsReviewRequestCount}
pendingRequestCount={pendingRequestCount}
activeRequestCount={activeRequestCount}
implementedRequestCount={implementedRequestCount}
adminNotice={adminNotice}
logsError={logsError}
adminSearchText={adminSearchText}
setAdminSearchText={setAdminSearchText}
adminFilterMenuOpen={adminFilterMenuOpen}
setAdminFilterMenuOpen={setAdminFilterMenuOpen}
requestStatusFilterOptions={requestStatusFilterOptions}
adminStatusFilters={adminStatusFilters}
setAdminStatusFilters={setAdminStatusFilters}
requestTagFilterOptions={requestTagFilterOptions}
adminTagFilters={adminTagFilters}
setAdminTagFilters={setAdminTagFilters}
toggleStringSelection={toggleStringSelection}
requestsLoading={requestsLoading}
adminFilteredRequests={adminFilteredRequests}
requestMutatingId={requestMutatingId}
selectedAdminRequestId={selectedAdminRequestId}
handleSelectAdminRequest={handleSelectAdminRequest}
handleDeleteRequest={handleDeleteRequest}
adminEditorDraft={adminEditorDraft}
adminDetailTab={adminDetailTab}
setAdminDetailTab={setAdminDetailTab}
selectedAnalysisItem={selectedAnalysisItem}
selectedAdminAnalysisIndex={selectedAdminAnalysisIndex}
setSelectedAdminAnalysisIndex={setSelectedAdminAnalysisIndex}
updateAdminDraft={updateAdminDraft}
updateAdminDraftItem={updateAdminDraftItem}
standardizedTagOptions={standardizedTagOptions}
categoryOptions={categoryOptions}
handleSaveAdminRequest={handleSaveAdminRequest}
adminSaving={adminSaving}
requeueingMode={requeueingMode}
handleApproveAdminRequest={handleApproveAdminRequest}
handleRequeueAnalysis={handleRequeueAnalysis}
/>
) : (
<LauncherPublicRequestBoard
requestCount={requestCount}
queuedPendingRequestCount={queuedPendingRequestCount}
needsReviewRequestCount={needsReviewRequestCount}
activeRequestCount={activeRequestCount}
implementedRequestCount={implementedRequestCount}
requestDraftOpen={requestDraftOpen}
requestSubmitting={requestSubmitting}
requestSearchText={requestSearchText}
requestFilterMenuOpen={requestFilterMenuOpen}
requestStatusFilterOptions={requestStatusFilterOptions}
requestTagFilterOptions={requestTagFilterOptions}
requestStatusFilters={requestStatusFilters}
requestTagFilters={requestTagFilters}
requestsLoading={requestsLoading}
requests={requests}
filteredRequests={filteredRequests}
expandedRequestIds={expandedRequestIds}
setRequestDraftOpen={setRequestDraftOpen}
handleAdminPanelToggle={handleAdminPanelToggle}
setRequestSearchText={setRequestSearchText}
setRequestFilterMenuOpen={setRequestFilterMenuOpen}
setRequestStatusFilters={setRequestStatusFilters}
setRequestTagFilters={setRequestTagFilters}
toggleStringSelection={toggleStringSelection}
handleToggleExpandedRequest={handleToggleExpandedRequest}
requestDraft={requestDraft}
setRequestDraft={setRequestDraft}
handleAddRequest={handleAddRequest}
/>
)
)}
{requestsError ? (
<p className="launcher-request-error">{requestsError}</p>
) : null}
</div>
</div>
</section>
<div className="launcher-build-stamp">Build {__APP_BUILD__}</div>
</div>
{adminPanelOpen && logsModalOpen ? (
<LauncherLogsModal
logsLoading={logsLoading}
recentSaveEvents={recentSaveEvents}
setLogsModalOpen={setLogsModalOpen}
/>
) : null}
</main>
);
}
export default WorldshaperLauncher;