| <!doctype html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="utf-8" />
|
| <meta name="viewport" content="width=device-width, initial-scale=1" />
|
| <title>Phantom Grid</title>
|
| <link rel="stylesheet" href="/static/app.css?v=19" />
|
| </head>
|
| <body>
|
| <main class="game-board" id="gameScene" aria-live="polite">
|
| <header class="top-banner">
|
| <section class="bureau-crest" aria-label="Lantern Watch Bureau">
|
| <img src="/static/assets/reference/crest_frame.png" alt="" />
|
| </section>
|
|
|
| <section class="title-panel" aria-label="Case title">
|
| <h1 id="gameTitle" contenteditable="true" spellcheck="false">Phantom Grid</h1>
|
| <p id="gameSubtitle" contenteditable="true" spellcheck="false">Catch John Doe before he vanishes again!</p>
|
| </section>
|
|
|
| <section class="turn-panel" aria-label="Turn counter">
|
| <span>Turn</span>
|
| <strong id="caseClock">-</strong>
|
| <small id="turnPhase">Evening</small>
|
| </section>
|
|
|
| <button id="helpButton" class="gear-button help-button" type="button" title="How to play" aria-label="Open how-to-play tutorial">
|
| <span aria-hidden="true">?</span>
|
| </button>
|
| <button id="settingsButton" class="gear-button" type="button" title="Settings" aria-label="Open settings">
|
| <span aria-hidden="true">⚙</span>
|
| </button>
|
| </header>
|
|
|
| <section class="table-grid">
|
| <aside class="left-rail">
|
| <section class="wanted-card" aria-label="Wanted poster">
|
| <h2>Wanted</h2>
|
| <img id="suspectImage" class="suspect-image" src="/static/assets/reference/suspect_portrait_placeholder.png" alt="Current suspect portrait" />
|
| <dl>
|
| <dt>Alias</dt>
|
| <dd id="wantedAlias">John Doe</dd>
|
| <dt>Description</dt>
|
| <dd id="wantedDescription">Male, approx. 35-45. Gray raincoat. Carries red folder.</dd>
|
| <dt>Last Seen</dt>
|
| <dd id="wantedLastSeen">Awaiting confirmed location</dd>
|
| </dl>
|
| <strong>£5,000 Reward</strong>
|
| <small>Dead or Alive</small>
|
| </section>
|
|
|
| <section class="active-units" aria-label="Active unit counts">
|
| <h2>Active Units</h2>
|
| <div class="unit-row" id="unitIcons"></div>
|
| <strong id="activeUnitsText">12 / 12 left</strong>
|
| </section>
|
|
|
| <button id="advanceButton" class="advance-button" type="button">Advance Turn</button>
|
| <button id="newCaseButton" class="new-case-button" type="button">New Case</button>
|
| <div class="case-control-row">
|
| <button id="stopGameButton" type="button">Stop Game</button>
|
| <button id="restartGameButton" type="button">Restart</button>
|
| </div>
|
| </aside>
|
|
|
| <section class="center-stage">
|
| <section class="tactic-strip" aria-label="Drag tactics onto the map">
|
| <h2>Drag Tactics Onto The Map</h2>
|
| <div id="tacticTray" class="tactic-tray"></div>
|
| </section>
|
|
|
| <section class="map-shell" aria-label="Investigation map">
|
| <nav class="layer-tabs" id="layerTabs" aria-label="Map layers"></nav>
|
| <div class="map-wrap" id="mapWrap">
|
| <div class="map-canvas" id="mapCanvas">
|
| <img id="mapImage" alt="London junction map" draggable="false" />
|
| <div id="selectionLayer" class="overlay-layer"></div>
|
| <div id="witnessLayer" class="overlay-layer"></div>
|
| <div id="tacticLayer" class="overlay-layer"></div>
|
| </div>
|
| <div class="map-controls" aria-label="Map zoom and pan controls">
|
| <button id="zoomOutButton" type="button" aria-label="Zoom out">-</button>
|
| <output id="zoomValue">145%</output>
|
| <button id="zoomInButton" type="button" aria-label="Zoom in">+</button>
|
| <button id="zoomResetButton" type="button">Reset</button>
|
| <span class="map-control-divider" aria-hidden="true"></span>
|
| <button id="toggleWitnessesButton" class="map-visibility-toggle active" type="button" aria-pressed="true">Witnesses</button>
|
| <button id="toggleTacticsButton" class="map-visibility-toggle active" type="button" aria-pressed="true">Tactics</button>
|
| <button id="toggleFocusButton" class="map-visibility-toggle active" type="button" aria-pressed="true">Focus</button>
|
| <button id="witnessModeButton" type="button">Witness Mode</button>
|
| </div>
|
| <div id="mapMessage" class="map-message">Drop tactics on junctions. Drag the map to navigate.</div>
|
| </div>
|
| <footer class="legend-strip" id="legendStrip"></footer>
|
| </section>
|
| </section>
|
|
|
| <aside class="right-rail">
|
| <section class="lookout-board">
|
| <h2>Commissioner's Notepad</h2>
|
| <article class="paper-note">
|
| <h3>Case Notes</h3>
|
| <textarea id="notesText" spellcheck="true" placeholder="Write deductions, routes, descriptions, and questions here..."></textarea>
|
| <p id="notesStatus">Saved with this case.</p>
|
| </article>
|
| </section>
|
|
|
| <section class="statements-panel">
|
| <h2>Previous Witness Statements</h2>
|
| <div id="statementList" class="statement-list"></div>
|
| </section>
|
| </aside>
|
| </section>
|
|
|
| <div class="event-ticker" id="eventTicker">Opening the board...</div>
|
| </main>
|
|
|
| <section id="setupOverlay" class="setup-overlay" aria-live="polite">
|
| <div class="setup-card">
|
| <small>Phantom Grid Local AI</small>
|
| <h2 id="setupTitle">Preparing Your Investigation Desk</h2>
|
| <p id="setupMessage">Checking the bundled llama.cpp runtime and MiniCPM-o model...</p>
|
|
|
| <section id="setupPicker" class="setup-picker" hidden>
|
| <p class="setup-picker-intro">Pick the model and hardware Phantom Grid should run on. You can change these later from Settings.</p>
|
| <p id="setupStorageHint" class="setup-storage-hint"></p>
|
| <div class="setup-picker-grid">
|
| <label>
|
| Model variant
|
| <select id="pickerQuantization"></select>
|
| <small id="pickerQuantHint">Smaller variants fit lower-VRAM cards; larger ones give cleaner answers.</small>
|
| </label>
|
| <label>
|
| Run on
|
| <select id="pickerDevice"></select>
|
| <small id="pickerDeviceHint">Detected from your system. Pick CPU to skip GPU acceleration.</small>
|
| </label>
|
| <label>
|
| GPU offload
|
| <select id="pickerGpuLayers"></select>
|
| <small>How many transformer layers to load onto the GPU. Auto is safe.</small>
|
| </label>
|
| <label>
|
| Context length
|
| <select id="pickerContext"></select>
|
| <small>Larger context handles longer cases but uses more VRAM.</small>
|
| </label>
|
| </div>
|
| </section>
|
|
|
| <progress id="setupProgress" max="100" value="0"></progress>
|
| <span id="setupProgressText">Checking setup</span>
|
| <button id="setupStartButton" class="setup-start-button" type="button" disabled>Preparing Local AI...</button>
|
| <button id="setupSettingsButton" class="setup-settings-button" type="button">Advanced Settings</button>
|
| </div>
|
| </section>
|
|
|
| <section id="detailPopup" class="detail-popup" hidden aria-live="polite"></section>
|
|
|
| <dialog id="tutorialDialog" class="tutorial-dialog">
|
| <section class="tutorial-shell" tabindex="-1">
|
| <header class="tutorial-head">
|
| <div>
|
| <small>Lantern Watch Bureau / Field Manual</small>
|
| <h2 id="tutorialTitle">How To Play</h2>
|
| </div>
|
| <button id="tutorialSkip" type="button" class="tutorial-skip">Skip tutorial</button>
|
| </header>
|
| <div class="tutorial-body">
|
| <figure class="tutorial-figure">
|
| <img id="tutorialImage" alt="" draggable="false" />
|
| </figure>
|
| <div class="tutorial-copy">
|
| <span id="tutorialTag" class="tutorial-tag"></span>
|
| <h3 id="tutorialHeading"></h3>
|
| <ul id="tutorialText" class="tutorial-text"></ul>
|
| </div>
|
| </div>
|
| <footer class="tutorial-foot">
|
| <div id="tutorialDots" class="tutorial-dots" aria-hidden="true"></div>
|
| <div class="tutorial-nav">
|
| <span id="tutorialCounter" class="tutorial-counter"></span>
|
| <button id="tutorialBack" type="button">Back</button>
|
| <button id="tutorialNext" type="button" class="tutorial-primary">Next</button>
|
| </div>
|
| </footer>
|
| </section>
|
| </dialog>
|
|
|
| <dialog id="caseIntroDialog" class="case-intro-dialog">
|
| <section class="case-intro-shell" tabindex="-1">
|
| <header class="case-intro-heading">
|
| <div>
|
| <small>Lantern Watch Bureau / Priority Dossier</small>
|
| <h2 id="caseIntroTitle">A New Case</h2>
|
| <p id="caseIntroKicker"></p>
|
| </div>
|
| <span class="case-stamp">Case Open</span>
|
| </header>
|
| <div class="case-intro-cards">
|
| <article class="intro-card crime-card">
|
| <span class="intro-card-number">01</span>
|
| <small>The Crime</small>
|
| <h3 id="caseIntroCrime"></h3>
|
| <p id="caseIntroNarrative"></p>
|
| <dl><dt>Stolen</dt><dd id="caseIntroStolen"></dd><dt>Victim</dt><dd id="caseIntroVictim"></dd></dl>
|
| </article>
|
| <article class="intro-card suspect-card">
|
| <span class="intro-card-number">02</span>
|
| <small>The Thief</small>
|
| <img id="caseIntroImage" src="/static/assets/reference/suspect_portrait_placeholder.png" alt="Current suspect portrait" />
|
| <h3 id="caseIntroAlias"></h3>
|
| <p id="caseIntroDescription"></p>
|
| </article>
|
| <article class="intro-card sightings-card">
|
| <span class="intro-card-number">03</span>
|
| <small>The Trail</small>
|
| <h3>Last Seen</h3>
|
| <ol id="caseIntroSightings"></ol>
|
| </article>
|
| </div>
|
| <footer>
|
| <p>Study the trail. The thief is already moving.</p>
|
| <button id="beginInvestigationButton" type="button">Begin Investigation</button>
|
| </footer>
|
| </section>
|
| </dialog>
|
|
|
| <dialog id="settingsDialog" class="settings-dialog">
|
| <form method="dialog" class="settings-panel">
|
| <header>
|
| <h2>Settings</h2>
|
| <button id="settingsCloseButton" type="button" aria-label="Close settings">x</button>
|
| </header>
|
|
|
| <section class="settings-grid">
|
| <label class="settings-wide">
|
| Text AI Backend
|
| <select id="providerSetting">
|
| <option value="zerogpu_transformers">ZeroGPU transformers (this Space)</option>
|
| <option value="llama_cpp_server">Local GGUF via llama.cpp</option>
|
| <option value="external_llama_cpp_server">User-started llama.cpp server</option>
|
| </select>
|
| </label>
|
| <div id="customModelSettings" class="settings-subgrid settings-wide" hidden>
|
| <label class="settings-wide">
|
| GGUF Model Path
|
| <input id="modelPathSetting" type="text" autocomplete="off" placeholder="C:\\Models\\model.gguf" />
|
| </label>
|
| <label class="settings-wide">
|
| llama-server Executable
|
| <input id="serverBinSetting" type="text" autocomplete="off" />
|
| </label>
|
| </div>
|
| <div id="llamaConnectionSettings" class="settings-subgrid settings-wide" hidden>
|
| <label>
|
| llama.cpp API URL
|
| <input id="baseUrlSetting" type="text" autocomplete="off" />
|
| </label>
|
| <label>
|
| Model ID
|
| <input id="llmModelSetting" type="text" autocomplete="off" placeholder="Model ID from /v1/models" />
|
| </label>
|
| <p id="externalServerHint" class="settings-hint settings-wide" hidden>
|
| Start and configure llama-server yourself. Phantom Grid only sends OpenAI-compatible requests to this URL.
|
| </p>
|
| </div>
|
| <label>
|
| Sound
|
| <select id="soundSetting">
|
| <option value="on">On</option>
|
| <option value="off">Off</option>
|
| </select>
|
| </label>
|
| <label>
|
| Difficulty
|
| <select id="difficultySetting">
|
| <option value="easy">Easy</option>
|
| <option value="normal">Normal</option>
|
| <option value="hard">Hard</option>
|
| </select>
|
| </label>
|
| <label>
|
| Comni Gateway URL
|
| <input id="gatewayUrlSetting" type="text" autocomplete="off" />
|
| </label>
|
| <label>
|
| Comni Launcher Path
|
| <input id="launcherPathSetting" type="text" autocomplete="off" />
|
| </label>
|
| <label>
|
| Comni Checkout Directory
|
| <input id="comniCheckoutSetting" type="text" autocomplete="off" />
|
| </label>
|
| <label>
|
| llama.cpp-omni Root
|
| <input id="omniRootSetting" type="text" autocomplete="off" />
|
| </label>
|
| <label>
|
| MiniCPM-o Model Directory
|
| <input id="modelDirSetting" type="text" autocomplete="off" />
|
| </label>
|
| <label>
|
| Quantization
|
| <select id="quantizationSetting"></select>
|
| </label>
|
| <label>
|
| Context Length
|
| <select id="contextLengthSetting">
|
| <option value="4096">4,096</option>
|
| <option value="8192">8,192</option>
|
| <option value="16384">16,384</option>
|
| <option value="24576">24,576</option>
|
| <option value="32768">32,768</option>
|
| </select>
|
| </label>
|
| <label>
|
| GPU Device
|
| <select id="gpuDeviceSetting"></select>
|
| </label>
|
| <label hidden>
|
| Voice in Text Chat
|
| <select id="witnessChatTtsSetting">
|
| <option value="0">Off — text only</option>
|
| </select>
|
| </label>
|
| <label>
|
| GPU Layers
|
| <input id="gpuLayersSetting" type="text" placeholder="auto, 0, or an explicit count" autocomplete="off" />
|
| </label>
|
| <label hidden>
|
| Witness Voice Directory
|
| <input id="voiceDirSetting" type="text" autocomplete="off" />
|
| </label>
|
| </section>
|
|
|
| <p id="llamaStatusText" class="llama-status">AI backend status unknown.</p>
|
| <div class="settings-actions">
|
| <button id="settingsSaveButton" type="button">Save</button>
|
| <button id="llamaStartButton" type="button">Start Backend</button>
|
| <button id="llamaRestartButton" type="button">Restart Backend</button>
|
| <button id="llamaStopButton" type="button">Stop Backend</button>
|
| </div>
|
| </form>
|
| </dialog>
|
|
|
| <dialog id="noticeDialog" class="notice-dialog">
|
| <form method="dialog" class="notice-panel">
|
| <header>
|
| <div><small>Public appeal at <span id="noticeJunctionLabel">selected junction</span></small><h2>Issue Public Notice</h2></div>
|
| <button id="noticeCloseButton" type="button" aria-label="Close notice">x</button>
|
| </header>
|
| <textarea id="noticeText" spellcheck="true"></textarea>
|
| <p id="lookoutMeta">The wording controls which existing witnesses recognize the appeal.</p>
|
| <div class="dialog-actions">
|
| <button id="raiseLookoutButton" type="button">Publish Notice</button>
|
| <button id="noticeCancelButton" type="button">Cancel</button>
|
| </div>
|
| </form>
|
| </dialog>
|
|
|
| <dialog id="witnessDialog" class="witness-dialog">
|
| <section class="witness-interview-shell">
|
| <header>
|
| <div>
|
| <small>Witness Interview</small>
|
| <h2 id="witnessName">Witness</h2>
|
| <p id="witnessProfile"></p>
|
| </div>
|
| <div class="interview-status"><span id="witnessConnection">Text ready</span><button id="witnessCloseButton" type="button">End Interview</button></div>
|
| </header>
|
| <div class="witness-summary" id="witnessSummary"></div>
|
| <div class="witness-transcript" id="witnessTranscript" aria-live="polite"></div>
|
| <footer>
|
| <div class="speech-controls" hidden>
|
| <button id="autoSpeechButton" type="button" hidden>Start Auto Speech</button>
|
| <button id="pushToTalkButton" type="button" hidden>Hold to Talk</button>
|
| <button id="stopAudioButton" type="button" hidden>Stop Voice</button>
|
| <meter id="micLevel" min="0" max="1" value="0" hidden></meter>
|
| </div>
|
| <div class="text-chat-row">
|
| <textarea id="witnessMessage" placeholder="Ask the witness a question..."></textarea>
|
| <button id="sendWitnessMessage" type="button">Send</button>
|
| </div>
|
| </footer>
|
| </section>
|
| </dialog>
|
|
|
| <dialog id="storyDialog" class="story-dialog">
|
| <section class="story-reveal-shell">
|
| <header><div><small>Private Case Record</small><h2>John Doe: Turn-by-Turn Story</h2></div><button id="storyCloseButton" type="button">Close</button></header>
|
| <div id="storyTimeline" class="story-timeline"></div>
|
| <footer id="storyFooter"></footer>
|
| </section>
|
| </dialog>
|
|
|
| <script type="module" src="/static/app.js?v=26"></script>
|
| </body>
|
| </html>
|
|
|