Added some features :D, Codex limits are burned out by GPT 6 Astra D:

This commit is contained in:
2026-10-05 19:33:00 +05:00
parent b33db48b63
commit 923abdc472
23 changed files with 538 additions and 53 deletions

View File

@@ -3,10 +3,12 @@ export function ContributionInstructions({
id,
bounds,
serverAddress,
isOwner = false,
}: {
id: string;
bounds: { minX: number; minZ: number; maxX: number; maxZ: number };
serverAddress: string | null;
isOwner?: boolean;
}) {
return (
<div className="token-box">
@@ -29,15 +31,26 @@ export function ContributionInstructions({
)}
<p>
Capture in the same Minecraft world, within X {bounds.minX} to{" "}
{bounds.maxX} and Z {bounds.minZ} to {bounds.maxZ}. Each player may add
up to 12 panoramas in total; the map holds at most 24, including pending
uploads.
{bounds.maxX} and Z {bounds.minZ} to {bounds.maxZ}.{" "}
{isOwner
? "As the owner, you can fill the map’s 24 panorama slots."
: "Each guest contributor may add up to 12 panoramas in total."}{" "}
The map holds at most 24, including pending uploads and unselected
captures.
</p>
<p>
In the Fabric mod, paste this map ID into “Add to map”. For the server
plugin, export the area and run:
</p>
<code>/minegeo contribute {id}</code>
<code className="command-line">/minegeo contribute {id}</code>
{isOwner && (
<p>
This command also adds your own panoramas. Open the resulting
contribution in your dashboard, submit it, then accept it in this
map’s submissions below. Finally, submit the map update for moderator
review.
</p>
)}
<p>
Map ID: <code>{id}</code>
</p>

View File

@@ -1,5 +1,5 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import Image from "next/image";
import { api } from "./StartGame";
import type { DatasetManifest } from "@/shared/protocol";
@@ -7,6 +7,8 @@ import type { ProcessingJob } from "@/shared/processing-progress";
import { ProcessingProgress } from "./ProcessingProgress";
import { PanoramaPreview } from "./PanoramaPreview";
import { OwnerContributions } from "./OwnerContributions";
import Link from "next/link";
import { MapRefresh } from "./MapRefresh";
type Draft = {
id: string;
name: string;
@@ -25,6 +27,7 @@ type Draft = {
update_state: string | null;
};
export function DraftEditor({ id }: { id: string }) {
const dirty = useRef(false);
const [draft, setDraft] = useState<Draft | null>(null),
[name, setName] = useState(""),
[description, setDescription] = useState(""),
@@ -43,10 +46,15 @@ export function DraftEditor({ id }: { id: string }) {
if (disposed) return;
setError("");
setDraft(d);
setName(d.name);
setDescription(d.description);
setSelected(d.manifest.viewpoints.map((p) => p.id));
if (["uploading", "processing"].includes(d.state))
if (!dirty.current) {
setName(d.name);
setDescription(d.description);
setSelected(d.manifest.viewpoints.map((p) => p.id));
}
if (
["uploading", "processing"].includes(d.state) ||
["queued", "running"].includes(d.upload_jobs?.state ?? "")
)
timer = setTimeout(load, 5000);
})
.catch((e) => {
@@ -71,6 +79,7 @@ export function DraftEditor({ id }: { id: string }) {
{ name, description, selectedIds: selected },
"PATCH",
);
dirty.current = false;
if (submit) {
await api("datasets/" + id + "/submit", {});
setVersion((v) => v + 1);
@@ -118,9 +127,17 @@ export function DraftEditor({ id }: { id: string }) {
!["submitted", "accepted", "rejected"].includes(
draft.contribution_status ?? "",
);
const refreshing =
draft.upload_jobs?.kind === "map" &&
["queued", "running"].includes(draft.upload_jobs.state);
const minimum = draft.parent_dataset_id ? 1 : 5;
return (
<>
<div className="editor-navigation">
<Link href="/dashboard">← Your maps</Link>
<a href="#locations">Choose locations</a>
<a href="#map-tools">Map tools</a>
</div>
<span className="badge">{draft.state}</span>
{draft.update_state && (
<span className="badge">Update: {draft.update_state}</span>
@@ -146,6 +163,19 @@ export function DraftEditor({ id }: { id: string }) {
</p>
)}
<ProcessingProgress job={draft.upload_jobs} />
{draft.upload_jobs?.kind === "map" &&
draft.upload_jobs.state === "failed" && (
<p role="alert" className="error-message">
Map refresh failed: {draft.upload_jobs.error}. Your previous map is
still available. Try refreshing again below.
</p>
)}
{refreshing && (
<p role="status" className="success-message">
Refreshing your guessing map. This page updates automatically; the
published version stays playable.
</p>
)}
{["uploading", "processing"].includes(draft.state) && (
<p role="status" className="success-message">
{draft.state === "uploading"
@@ -169,28 +199,38 @@ export function DraftEditor({ id }: { id: string }) {
src={draft.map}
width={1024}
height={1024}
loading="eager"
alt="Draft surface map"
unoptimized
/>
</div>
)}
<label className="field">
Map name
<input
value={name}
onChange={(e) => setName(e.target.value)}
disabled={!editable}
/>
</label>
<label className="field">
Description
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={!editable}
/>
</label>
<h2>Choose your locations.</h2>
<section className="editor-panel">
<h2>Map details</h2>
<label className="field">
Map name
<input
value={name}
onChange={(e) => {
dirty.current = true;
setName(e.target.value);
}}
disabled={!editable}
/>
</label>
<label className="field">
Description
<textarea
value={description}
onChange={(e) => {
dirty.current = true;
setDescription(e.target.value);
}}
disabled={!editable}
/>
</label>
</section>
<h2 id="locations">Choose your locations.</h2>
<p className="muted">
Keep at least {minimum} clear, useful panorama{minimum === 1 ? "" : "s"}
. Selected: {selected.length} / 24
@@ -204,13 +244,14 @@ export function DraftEditor({ id }: { id: string }) {
checked={selected.includes(p.id)}
disabled={!editable || busy}
aria-label={`Keep ${p.label}`}
onChange={(e) =>
onChange={(e) => {
dirty.current = true;
setSelected((s) =>
e.target.checked
? [...s, p.id]
: s.filter((id) => id !== p.id),
)
}
);
}}
/>
{p.image && (
<Image
@@ -242,14 +283,14 @@ export function DraftEditor({ id }: { id: string }) {
<div className="inline-actions">
<button
className="button"
disabled={busy || selected.length < minimum}
disabled={busy || refreshing || selected.length < minimum}
onClick={() => save()}
>
Save draft
</button>
<button
className="button primary"
disabled={busy || selected.length < minimum}
disabled={busy || refreshing || selected.length < minimum}
onClick={() => save(true)}
>
{draft.parent_dataset_id
@@ -258,12 +299,25 @@ export function DraftEditor({ id }: { id: string }) {
</button>
</div>
)}
<div id="map-tools">
<MapRefresh
id={id}
disabled={busy || !editable || refreshing}
onQueued={() => {
setMessage(
"Map refresh queued. Keep this page open to follow progress.",
);
setVersion((v) => v + 1);
}}
/>
</div>
{!draft.parent_dataset_id && (
<OwnerContributions
id={id}
allowed={draft.allow_contributions}
serverAddress={draft.contribution_server_address}
bounds={draft.manifest.bounds}
count={draft.previews.length}
locked={draft.state === "review" || draft.update_state === "review"}
onChange={() => setVersion((v) => v + 1)}
/>

View File

@@ -20,6 +20,7 @@ export function MapCard({ map, index = 0 }: { map: CardMap; index?: number }) {
src={map.image || "/images/example-map.webp"}
alt={`${map.name} terrain`}
fill
loading={index < 3 ? "eager" : "lazy"}
unoptimized
sizes="(max-width: 640px) 100vw, 33vw"
/>

87
components/MapRefresh.tsx Normal file
View File

@@ -0,0 +1,87 @@
"use client";
import { useEffect, useState } from "react";
import { api } from "./StartGame";
export function MapRefresh({
id,
disabled,
onQueued,
}: {
id: string;
disabled: boolean;
onQueued: () => void;
}) {
const [sources, setSources] = useState<
{ id: string; name: string; created_at: string }[]
>([]);
const [source, setSource] = useState(id);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
let active = true;
api<{ sources: typeof sources }>(`datasets/${id}/refresh-map`)
.then((r) => {
if (active) setSources(r.sources);
})
.catch((e) => {
if (active) setError(e.message);
});
return () => {
active = false;
};
}, [id]);
async function refresh() {
setBusy(true);
setError("");
try {
await api(`datasets/${id}/refresh-map`, { sourceId: source });
onQueued();
} catch (e) {
setError((e as Error).message);
} finally {
setBusy(false);
}
}
return (
<section className="editor-panel">
<span className="eyebrow">THE MAP PLAYERS GUESS ON</span>
<h2>Refresh guessing map</h2>
<p className="muted">
Rebuild the overhead map from a processed capture. To show new
buildings, export the same area again and upload it first. Select that
capture here; its boundaries must match this map exactly.
</p>
<label className="field">
Terrain capture
<select
value={source}
onChange={(e) => setSource(e.target.value)}
disabled={disabled || busy}
>
{sources.map((s) => (
<option key={s.id} value={s.id}>
{s.id === id ? "Original capture" : s.name} ·{" "}
{new Date(s.created_at).toLocaleDateString()} · {s.id.slice(0, 8)}
</option>
))}
</select>
</label>
<button
className="button"
disabled={disabled || busy || !sources.some((s) => s.id === source)}
onClick={refresh}
>
{busy ? "Queueing…" : "Refresh guessing map"}
</button>
<p className="muted">
Panoramas stay unchanged. Refreshing uses no panorama slots, even at
24/24. Submit the finished map update for review to make it playable.
</p>
{error && (
<p role="alert" className="error-message">
{error}
</p>
)}
</section>
);
}

View File

@@ -21,6 +21,7 @@ export function OwnerContributions({
bounds,
locked,
onChange,
count,
}: {
id: string;
allowed: boolean;
@@ -28,6 +29,7 @@ export function OwnerContributions({
bounds: { minX: number; minZ: number; maxX: number; maxZ: number };
locked: boolean;
onChange: () => void;
count: number;
}) {
const [items, setItems] = useState<Contribution[]>([]),
[address, setAddress] = useState(serverAddress ?? ""),
@@ -111,7 +113,12 @@ export function OwnerContributions({
}
return (
<section className="token-box">
<h2>Panorama settings</h2>
<h2>Add panoramas · {count}/24</h2>
<p className="muted">
Owners can fill all 24 slots, even with player contributions turned off.
Export a fresh capture, then use the command below to add it to this
map.
</p>
<label>
<input
type="checkbox"
@@ -166,6 +173,7 @@ export function OwnerContributions({
id={id}
bounds={bounds}
serverAddress={serverAddress}
isOwner
/>
<h3>Player submissions</h3>
{locked && (

View File

@@ -23,7 +23,7 @@ export function ProcessingProgress({ job }: { job: ProcessingJob | null }) {
<p>
{job.state === "queued"
? "Waiting for a processing worker"
: `${progress}% · ${processingStage(progress)}`}
: `${progress}% · ${job.kind === "map" ? "Refreshing guessing map" : processingStage(progress)}`}
</p>
<progress
max={100}