| <!doctype html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <meta name="color-scheme" content="dark light" /> |
| <title>JackAILocal</title> |
| <link rel="stylesheet" href="styles.css" /> |
| </head> |
| <body> |
| <div id="app" class="app-shell"> |
| <aside class="sidebar" aria-label="JackAILocal navigation"> |
| <div class="brand-card"> |
| <div class="brand-mark">J</div> |
| <div> |
| <div class="brand-name">JackAILocal</div> |
| <div class="brand-subtitle" data-i18n="brandSubtitle">Offline local AI</div> |
| </div> |
| </div> |
|
|
| <div class="language-switch" aria-label="Language"> |
| <button type="button" class="lang-btn active" data-lang="en">EN</button> |
| <button type="button" class="lang-btn" data-lang="fr">FR</button> |
| </div> |
|
|
| <div id="statusBox" class="status-card state-loading" role="status" aria-live="polite"> |
| <div class="status-header"> |
| <span class="status-dot"></span> |
| <span id="runtimeStatus" class="status-title" data-i18n="statusStarting">Starting</span> |
| </div> |
| <div id="hardwareLine" class="status-subtitle" data-i18n="statusLocalScan">Local scan...</div> |
| </div> |
|
|
| <nav class="nav-list"> |
| <button class="nav active" data-view="dashboard"><span>⌂</span><span data-i18n="navDashboard">Dashboard</span></button> |
| <button class="nav" data-view="chat"><span>◌</span><span data-i18n="navChat">Chat</span></button> |
| <button class="nav" data-view="models"><span>▣</span><span data-i18n="navModels">Models</span></button> |
| <button class="nav" data-view="documents"><span>□</span><span data-i18n="navDocuments">Documents</span></button> |
| <button class="nav" data-view="benchmark"><span>◇</span><span data-i18n="navBenchmark">Benchmark</span></button> |
| <button class="nav" data-view="settings"><span>⚙</span><span data-i18n="navSettings">Settings</span></button> |
| <button class="nav" data-view="support"><span>?</span><span data-i18n="navSupport">Support</span></button> |
| <button class="nav" data-view="scout"><span>V</span><span data-i18n="navScout">SCOUT Vision</span></button> |
| <button class="nav" data-view="voice"><span>A</span><span data-i18n="navVoice">Voice</span></button> |
| <button class="nav" data-view="field-manual"><span>F</span><span data-i18n="navFieldManual">Field Manual</span></button> |
| <button class="nav" data-view="transfers"><span>X</span><span data-i18n="navTransfers">Import / Export</span></button> |
| <button class="nav" data-view="phone"><span>P</span><span data-i18n="navPhone">Phone Access</span></button> |
| </nav> |
|
|
| <div class="sidebar-footer"> |
| <div class="mini-label" data-i18n="networkMode">Network mode</div> |
| <div class="security-pill">127.0.0.1</div> |
| <div class="mini-copy" data-i18n="loopbackOnly">Local access only</div> |
| <div id="licenseWarning" class="security-pill availability-off hidden"></div> |
| </div> |
| </aside> |
|
|
| <main class="main"> |
| <div class="app-topbar"> |
| <div> |
| <div class="eyebrow" data-i18n="eyebrow">Portable private AI</div> |
| <h1 id="viewTitle">Dashboard</h1> |
| </div> |
| <div class="top-actions"> |
| <button id="refreshAll" class="secondary" data-i18n="refreshButton">Refresh</button> |
| <button id="openSupport" class="secondary" data-i18n="supportButton">Support</button> |
| </div> |
| </div> |
|
|
| <div id="errorBanner" class="banner hidden" role="status"></div> |
|
|
| <section id="view-dashboard" class="view active"> |
| <div class="hero-panel"> |
| <div> |
| <div class="hero-kicker" data-i18n="heroKicker">Ready for offline work</div> |
| <h2 data-i18n="heroTitle">Local AI, loaded from your USB drive.</h2> |
| <p data-i18n="heroSubtitle">JackAILocal runs the launcher, runtime, models, documents, and UI locally. No cloud is required after preparation.</p> |
| </div> |
| <div class="hero-actions"> |
| <button class="primary" data-action-view="chat" data-i18n="startChatButton">Start chat</button> |
| <button class="secondary" data-action-view="models" data-i18n="checkModelsButton">Check models</button> |
| </div> |
| </div> |
|
|
| <div class="metric-grid"> |
| <article class="metric-card"> |
| <span class="metric-label" data-i18n="runtimeCard">Runtime</span> |
| <strong id="runtimeMetric">...</strong> |
| <small id="backendMetric">...</small> |
| </article> |
| <article class="metric-card"> |
| <span class="metric-label" data-i18n="hardwareCard">Hardware</span> |
| <strong id="hardwareMetric">...</strong> |
| <small id="gpuMetric">...</small> |
| </article> |
| <article class="metric-card"> |
| <span class="metric-label" data-i18n="modelCard">Recommended model</span> |
| <strong id="recommendedMetric">Auto</strong> |
| <small data-i18n="autoSelectHint">Selected by local policy</small> |
| </article> |
| <article class="metric-card"> |
| <span class="metric-label" data-i18n="privacyCard">Privacy</span> |
| <strong data-i18n="offlineLocked">Offline locked</strong> |
| <small data-i18n="localOnlyHint">Loopback only</small> |
| </article> |
| </div> |
|
|
| <div class="split-hero"> |
| <div class="panel"> |
| <h2 data-i18n="setupTitle">Preparation state</h2> |
| <div class="checklist"> |
| <div class="check-row" id="checkRuntime"><span></span><div><strong data-i18n="checkRuntimeTitle">Local API</strong><small data-i18n="checkRuntimeText">jackailocald must be running.</small></div></div> |
| <div class="check-row" id="checkModels"><span></span><div><strong data-i18n="checkModelsTitle">Model catalog</strong><small data-i18n="checkModelsText">The USB must contain configured model weights.</small></div></div> |
| <div class="check-row" id="checkSecurity"><span></span><div><strong data-i18n="checkSecurityTitle">Security mode</strong><small data-i18n="checkSecurityText">Shell tools and LAN exposure remain disabled.</small></div></div> |
| </div> |
| </div> |
| <div class="panel"> |
| <h2 data-i18n="quickActionsTitle">Quick actions</h2> |
| <div class="quick-actions"> |
| <button class="secondary" data-action-view="benchmark" data-i18n="runBenchmarkQuick">Run benchmark</button> |
| <button class="secondary" data-action-view="documents" data-i18n="openDocsQuick">Open documents</button> |
| <button class="secondary" id="supportBundleQuick" data-i18n="createSupportQuick">Create support bundle</button> |
| </div> |
| <pre id="dashboardOut" class="terminal small-terminal"></pre> |
| </div> |
| </div> |
| </section> |
|
|
| <section id="view-chat" class="view"> |
| <div class="chat-workspace"> |
| <aside class="thread-panel"> |
| <div class="thread-header"> |
| <h2 data-i18n="threadsTitle">Threads</h2> |
| <button id="newThread" class="primary compact" data-i18n="newThreadButton">New</button> |
| </div> |
| <input id="threadFilter" data-i18n-placeholder="threadFilterPlaceholder" placeholder="Filter threads" /> |
| <div id="threadList" class="thread-list"></div> |
| </aside> |
| <div class="chat-panel"> |
| <div class="chat-thread-bar"> |
| <strong id="activeThreadTitle" data-i18n="newConversation">New conversation</strong> |
| <div> |
| <button id="renameThread" class="secondary compact" data-i18n="renameButton">Rename</button> |
| <button id="deleteThread" class="secondary compact danger-button" data-i18n="deleteButton">Delete</button> |
| </div> |
| </div> |
| <div id="chatLog" class="chat-log" aria-live="polite"></div> |
| <div class="composer-card"> |
| <textarea id="prompt" spellcheck="true" data-i18n-placeholder="promptPlaceholder" placeholder="Write your request here. Enter = send, Shift+Enter = new line."></textarea> |
| <div class="composer-actions"> |
| <label class="select-label"><span data-i18n="modelLabel">Model</span><select id="modelSelect"><option value="auto">Auto</option></select></label> |
| <button id="clearChat" class="secondary" data-i18n="clearButton">Clear</button> |
| <button id="sendBtn" class="primary" data-i18n="sendButton">Send</button> |
| </div> |
| </div> |
| </div> |
| <aside class="side-panel"> |
| <h2 data-i18n="chatHelperTitle">Assistant mode</h2> |
| <p data-i18n="chatHelperText">Use Auto unless you need a specific installed model. Threads are stored on this JackAILocal drive.</p> |
| <div id="modelChips" class="chip-list"></div> |
| <div class="hint-box"><strong data-i18n="offlineNoteTitle">Offline note</strong><span data-i18n="offlineNoteText">Answers come from local models. Do not expect cloud frontier quality.</span></div> |
| </aside> |
| </div> |
| </section> |
|
|
| <section id="view-scout" class="view"> |
| <div class="section-header"> |
| <div><h2 data-i18n="scoutTitle">SCOUT Vision</h2><p data-i18n="scoutSubtitle">Analyze an image with an installed local vision model.</p></div> |
| </div> |
| <div class="split"> |
| <div class="panel"> |
| <input id="scoutImage" type="file" accept="image/*" /> |
| <img id="scoutPreview" class="image-preview hidden" alt="Selected image preview" /> |
| <textarea id="scoutPrompt" class="doc-textarea" data-i18n-placeholder="scoutPromptPlaceholder" placeholder="Describe the image and extract readable text."></textarea> |
| <button id="runScout" class="primary" data-i18n="analyzeButton">Analyze locally</button> |
| </div> |
| <div class="panel"> |
| <h2 data-i18n="scoutResultTitle">Analysis result</h2> |
| <pre id="scoutOut" class="terminal"></pre> |
| </div> |
| </div> |
| </section> |
|
|
| <section id="view-voice" class="view"> |
| <div class="section-header"> |
| <div><h2 data-i18n="voiceTitle">Voice Mode</h2><p data-i18n="voiceSubtitle">Local speech-to-text with whisper.cpp and text-to-speech with Piper.</p></div> |
| <button id="refreshVoice" class="secondary" data-i18n="refreshButton">Refresh</button> |
| </div> |
| <div class="split"> |
| <div class="panel"> |
| <h2 data-i18n="sttTitle">Speech to text</h2> |
| <p id="sttStatus" class="muted"></p> |
| <div class="button-row"> |
| <button id="startRecording" class="primary" data-i18n="recordButton">Record</button> |
| <button id="stopRecording" class="secondary" disabled data-i18n="stopButton">Stop</button> |
| </div> |
| <textarea id="voiceTranscript" class="doc-textarea" data-i18n-placeholder="transcriptPlaceholder" placeholder="Transcript"></textarea> |
| <button id="sendTranscript" class="secondary" data-i18n="sendToChatButton">Send to chat</button> |
| </div> |
| <div class="panel"> |
| <h2 data-i18n="ttsTitle">Text to speech</h2> |
| <p id="ttsStatus" class="muted"></p> |
| <textarea id="ttsText" class="doc-textarea" data-i18n-placeholder="ttsPlaceholder" placeholder="Text to speak"></textarea> |
| <button id="speakText" class="primary" data-i18n="speakButton">Speak locally</button> |
| <audio id="ttsAudio" controls class="audio-player hidden"></audio> |
| <pre id="voiceOut" class="terminal small-terminal"></pre> |
| </div> |
| </div> |
| </section> |
|
|
| <section id="view-field-manual" class="view"> |
| <div class="section-header"> |
| <div><h2 data-i18n="fieldManualTitle">Field Manual</h2><p data-i18n="fieldManualSubtitle">Offline reference cards with an Ask AI action.</p></div> |
| </div> |
| <div id="fieldManualWarning" class="banner hidden"></div> |
| <div id="fieldManualCards" class="cards"></div> |
| </section> |
|
|
| <section id="view-models" class="view"> |
| <div class="section-header"> |
| <div><h2 data-i18n="modelsTitle">Models</h2><p data-i18n="modelsSubtitle">The catalog shows what is configured. Model weights must be present in models/.</p></div> |
| <button id="refreshModels" class="secondary" data-i18n="refreshButton">Refresh</button> |
| </div> |
| <div id="modelCards" class="cards"></div> |
| <div class="panel safe-panel"> |
| <h2 data-i18n="agentTitle">LLM Config Agent</h2> |
| <p data-i18n="agentSubtitle">A real installed local LLM can review hardware, model selection, and client configuration. Preferred model: gemma4:12b.</p> |
| <div class="form-grid"> |
| <label class="select-label"><span data-i18n="agentModelLabel">Agent model</span><select id="agentModelSelect"><option value="auto">Auto</option></select></label> |
| <textarea id="agentNotes" class="doc-textarea" data-i18n-placeholder="agentNotesPlaceholder" placeholder="Client constraints, use case, budget, privacy needs..."></textarea> |
| <button id="runAgentReview" class="primary" data-i18n="agentRunButton">Run local agent review</button> |
| </div> |
| <pre id="agentOut" class="terminal small-terminal"></pre> |
| </div> |
| <div class="panel safe-panel"> |
| <h2 data-i18n="addModelTitle">Add a model to the catalog</h2> |
| <p data-i18n="addModelSubtitle">This registers a model. It does not download weights and it does not bypass builder validation.</p> |
| <div class="form-grid"> |
| <input id="addModelId" data-i18n-placeholder="addModelIdPlaceholder" placeholder="local id, example: qwen-custom" /> |
| <input id="addModelLabel" data-i18n-placeholder="addModelLabelPlaceholder" placeholder="display name" /> |
| <button id="addModelBtn" class="primary" data-i18n="addButton">Add</button> |
| </div> |
| <pre id="addModelOut" class="terminal small-terminal"></pre> |
| </div> |
| </section> |
|
|
| <section id="view-documents" class="view"> |
| <div class="section-header"> |
| <div><h2 data-i18n="documentsTitle">Documents</h2><p data-i18n="documentsSubtitle">Local storage in workspace/documents. No arbitrary disk access.</p></div> |
| <button id="reloadDocs" class="secondary" data-i18n="refreshButton">Refresh</button> |
| </div> |
| <div class="split"> |
| <div class="panel"> |
| <h2 data-i18n="createNoteTitle">Create a note</h2> |
| <input id="docName" data-i18n-placeholder="docNamePlaceholder" placeholder="document-name.txt" /> |
| <textarea id="docContent" data-i18n-placeholder="docContentPlaceholder" placeholder="Text, notes, procedures, prompts..." class="doc-textarea"></textarea> |
| <button id="saveDoc" class="primary" data-i18n="saveLocalButton">Save locally</button> |
| <pre id="docOut" class="terminal small-terminal"></pre> |
| </div> |
| <div class="panel"> |
| <h2 data-i18n="localDocsTitle">Local documents</h2> |
| <div id="docList" class="doc-list"></div> |
| </div> |
| </div> |
| </section> |
|
|
| <section id="view-transfers" class="view"> |
| <div class="section-header"> |
| <div><h2 data-i18n="transfersTitle">Encrypted Import / Export</h2><p data-i18n="transfersSubtitle">Transfer threads and documents with an AES-256-GCM encrypted pack.</p></div> |
| </div> |
| <div class="split"> |
| <div class="panel"> |
| <h2 data-i18n="exportTitle">Export</h2> |
| <input id="exportPassphrase" type="password" data-i18n-placeholder="passphrasePlaceholder" placeholder="Passphrase, minimum 8 characters" /> |
| <div class="option-stack compact-stack"> |
| <label class="option-card"><input id="exportThreads" type="checkbox" checked /><span class="option-copy"><strong data-i18n="includeThreads">Include threads</strong></span></label> |
| <label class="option-card"><input id="exportDocuments" type="checkbox" checked /><span class="option-copy"><strong data-i18n="includeDocuments">Include documents</strong></span></label> |
| <label class="option-card"><input id="exportSettings" type="checkbox" /><span class="option-copy"><strong data-i18n="includeSettings">Include safe settings</strong></span></label> |
| </div> |
| <button id="exportPack" class="primary" data-i18n="exportButton">Export encrypted pack</button> |
| </div> |
| <div class="panel"> |
| <h2 data-i18n="importTitle">Import</h2> |
| <input id="importPackFile" type="file" accept=".jackaipack,application/json" /> |
| <input id="importPassphrase" type="password" data-i18n-placeholder="passphrasePlaceholder" placeholder="Passphrase, minimum 8 characters" /> |
| <label class="select-label"><span data-i18n="importMode">Import mode</span><select id="importMode"><option value="merge">Merge</option><option value="replace">Replace threads</option></select></label> |
| <button id="importPack" class="primary" data-i18n="importButton">Import encrypted pack</button> |
| </div> |
| </div> |
| <pre id="transferOut" class="terminal small-terminal"></pre> |
| </section> |
|
|
| <section id="view-phone" class="view"> |
| <div class="section-header"> |
| <div><h2 data-i18n="phoneTitle">Phone Access</h2><p data-i18n="phoneSubtitle">Explicit local-network access with a pairing token. Disabled by default.</p></div> |
| <button id="refreshPhone" class="secondary" data-i18n="refreshButton">Refresh</button> |
| </div> |
| <div class="split"> |
| <div class="panel"> |
| <div id="phoneState" class="terminal small-terminal"></div> |
| <div class="button-row"> |
| <button id="enablePhone" class="primary" data-i18n="enablePhoneButton">Enable Phone Access</button> |
| <button id="disablePhone" class="secondary" data-i18n="disablePhoneButton">Disable Phone Access</button> |
| <button id="restartRuntime" class="secondary" data-i18n="restartRuntimeButton">Restart runtime</button> |
| </div> |
| <p class="muted" data-i18n="phoneRestartNote">A runtime restart is required after changing this mode.</p> |
| </div> |
| <div class="panel qr-panel"> |
| <div id="phoneQr"></div> |
| </div> |
| </div> |
| </section> |
|
|
| <section id="view-benchmark" class="view"> |
| <div class="section-header"> |
| <div><h2 data-i18n="benchmarkTitle">Benchmark</h2><p data-i18n="benchmarkSubtitle">Short test to validate loading, latency, and a real response.</p></div> |
| <button id="runBenchmark" class="primary" data-i18n="runTestButton">Run test</button> |
| </div> |
| <pre id="benchmarkOut" class="terminal"></pre> |
| </section> |
|
|
| <section id="view-settings" class="view"> |
| <div class="section-header"> |
| <div> |
| <h2 data-i18n="settingsTitle">Settings</h2> |
| <p data-i18n="settingsSubtitle">Options stay simple. Dangerous features remain disabled.</p> |
| </div> |
| </div> |
| <div class="settings-grid"> |
| <div class="panel settings-panel"> |
| <h2 data-i18n="modeTitle">Mode</h2> |
| <p class="muted" data-i18n="settingsModeHint">Simple mode is the default for non-technical users. Advanced mode keeps unsafe tools blocked.</p> |
| <div class="option-stack"> |
| <label class="option-card"> |
| <input type="radio" name="mode" value="simple" checked /> |
| <span class="option-copy"> |
| <strong data-i18n="simpleMode">Simple</strong> |
| <small data-i18n="simpleModeDesc">Recommended default. Minimal choices and automatic model selection.</small> |
| </span> |
| </label> |
| <label class="option-card"> |
| <input type="radio" name="mode" value="advanced" /> |
| <span class="option-copy"> |
| <strong data-i18n="advancedMode">Advanced</strong> |
| <small data-i18n="advancedModeDesc">Shows more controls without enabling shell or system tools.</small> |
| </span> |
| </label> |
| <label class="option-card"> |
| <input type="checkbox" id="offlineLock" checked /> |
| <span class="option-copy"> |
| <strong data-i18n="offlineLock">Offline lock</strong> |
| <small data-i18n="offlineLockDesc">Keeps JackAILocal bound to the local machine by default.</small> |
| </span> |
| </label> |
| <label class="option-card"> |
| <input type="checkbox" id="toolsLock" checked /> |
| <span class="option-copy"> |
| <strong data-i18n="toolsLock">Block shell/system tools</strong> |
| <small data-i18n="toolsLockDesc">Prevents agent-style system actions in the consumer build.</small> |
| </span> |
| </label> |
| </div> |
| <div class="settings-actions"> |
| <button id="saveSettings" class="primary" data-i18n="saveButton">Save</button> |
| </div> |
| <pre id="settingsOut" class="terminal small-terminal"></pre> |
| </div> |
| <div class="panel settings-panel"> |
| <h2 data-i18n="activeFeaturesTitle">Active features</h2> |
| <p class="muted" data-i18n="activeFeaturesSubtitle">Runtime capabilities are read from the local API when it is running.</p> |
| <div id="featureList" class="feature-list"></div> |
| </div> |
| <div class="panel settings-panel"> |
| <h2 data-i18n="licenseTitle">License</h2> |
| <p class="muted" data-i18n="licenseSubtitle">License status of this installation. Verified locally, no internet required.</p> |
| <div id="licenseInfo" class="feature-list"></div> |
| <div class="license-install"> |
| <input id="licenseFile" type="file" accept=".json,application/json" /> |
| <button id="installLicense" class="secondary" data-i18n="installLicenseButton">Install license file</button> |
| <div id="licenseMessage" class="muted small"></div> |
| </div> |
| <p class="muted small" data-i18n="legalDocsHint">Legal documents are in the legal/ and licenses/ folders of this installation.</p> |
| </div> |
| </div> |
| </section> |
|
|
| <section id="view-support" class="view"> |
| <div class="section-header"> |
| <div><h2 data-i18n="supportTitle">Support</h2><p data-i18n="supportSubtitle">Local diagnostics without conversations by default.</p></div> |
| <button id="supportBundle" class="primary" data-i18n="prepareReportButton">Prepare report</button> |
| </div> |
| <pre id="supportOut" class="terminal"></pre> |
| </section> |
| </main> |
| </div> |
|
|
| <div id="eulaOverlay" class="eula-overlay hidden" role="dialog" aria-modal="true" aria-labelledby="eulaTitle"> |
| <div class="panel eula-modal"> |
| <h2 id="eulaTitle" data-i18n="eulaTitle">End User License Agreement</h2> |
| <div id="eulaText" class="eula-text" tabindex="0"></div> |
| <div class="eula-actions"> |
| <button id="eulaAccept" class="primary" data-i18n="eulaAcceptButton">I accept</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script src="app-v15.js"></script> |
| </body> |
| </html> |
|
|