1273 lines
44 KiB
HTML
1273 lines
44 KiB
HTML
<!DOCTYPE html>
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<html lang="en" dir="ltr">
|
|
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>DIDACTYL</title>
|
|
|
|
<link rel="stylesheet" href="./css/client.css" />
|
|
|
|
<!-- Initialize theme BEFORE any components load -->
|
|
<script>
|
|
(function () {
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme === 'dark') {
|
|
document.documentElement.classList.add('dark-mode');
|
|
if (document.body) {
|
|
document.body.classList.add('dark-mode');
|
|
}
|
|
}
|
|
})();
|
|
</script>
|
|
<link rel="shortcut icon" type="image/x-icon" href="./favicon/favicon-dots2.ico" />
|
|
|
|
<!-- SVG.js library (required by HamburgerMorphing) -->
|
|
<script src="https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js@3.0/dist/svg.min.js"></script>
|
|
|
|
<style media="screen">
|
|
#divBody {
|
|
flex-direction: column !important;
|
|
flex-wrap: nowrap !important;
|
|
align-items: stretch !important;
|
|
justify-content: flex-start !important;
|
|
align-content: flex-start !important;
|
|
gap: 20px;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.divPanel {
|
|
border: var(--header-border);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
background: var(--secondary-color);
|
|
width: 100%;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.divPanelTitle {
|
|
font-size: 130%;
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.divPanelSubTitle {
|
|
font-size: 90%;
|
|
color: var(--primary-color);
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.divGrid2 {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.divGrid3 {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.divField {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.divFieldLabel {
|
|
font-size: 80%;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.inText,
|
|
.inNumber,
|
|
.inSelect,
|
|
.taInput,
|
|
.taOutput {
|
|
width: 100%;
|
|
border: 1px solid var(--primary-color);
|
|
border-radius: 5px;
|
|
background: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
padding: 10px;
|
|
font-size: 80%;
|
|
}
|
|
|
|
.taInput {
|
|
min-height: 120px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.taOutput {
|
|
min-height: 120px;
|
|
background: var(--muted-color);
|
|
white-space: pre-wrap;
|
|
overflow: auto;
|
|
}
|
|
|
|
.taTall {
|
|
min-height: 180px;
|
|
}
|
|
|
|
.taXL {
|
|
min-height: 260px;
|
|
}
|
|
|
|
.divRow {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.btn {
|
|
border: 2px solid var(--primary-color);
|
|
border-radius: 5px;
|
|
background: var(--secondary-color);
|
|
color: var(--primary-color);
|
|
padding: 10px 15px;
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
|
|
}
|
|
|
|
.btn:hover {
|
|
background-color: var(--accent-color);
|
|
border-color: var(--accent-color);
|
|
color: var(--secondary-color);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.divStatusBadge {
|
|
display: inline-block;
|
|
border: 1px solid var(--primary-color);
|
|
border-radius: 5px;
|
|
padding: 5px 10px;
|
|
font-size: 75%;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.divWarning {
|
|
border: 2px solid var(--accent-color);
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
font-size: 75%;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.divSummary {
|
|
border: 1px solid var(--primary-color);
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
font-size: 80%;
|
|
}
|
|
|
|
.divContextParts {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.detPart {
|
|
border: 1px solid var(--primary-color);
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
background: var(--secondary-color);
|
|
}
|
|
|
|
.detPart summary {
|
|
cursor: pointer;
|
|
font-size: 80%;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.prePart {
|
|
margin-top: 10px;
|
|
padding: 10px;
|
|
border: 1px solid var(--muted-color);
|
|
border-radius: 5px;
|
|
background: var(--secondary-color);
|
|
font-size: 75%;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.divKV {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 10px;
|
|
font-size: 80%;
|
|
}
|
|
|
|
.divKVItem {
|
|
border: 1px solid var(--primary-color);
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
min-height: 50px;
|
|
}
|
|
|
|
.spanMuted {
|
|
opacity: 0.7;
|
|
font-size: 90%;
|
|
}
|
|
|
|
.divCompareCols {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.divSectionFooter {
|
|
margin-top: 10px;
|
|
font-size: 70%;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
@media (max-width: 1000px) {
|
|
.divGrid2,
|
|
.divGrid3,
|
|
.divKV,
|
|
.divCompareCols {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<!-- ================================================================
|
|
HAMBURGER BUTTON (Fixed, separate from header)
|
|
================================================================ -->
|
|
<div id="divSvgHam" class="divHeaderButtons">
|
|
<!-- HamburgerMorphing will be injected here -->
|
|
</div>
|
|
|
|
<!-- ================================================================
|
|
HEADER
|
|
================================================================ -->
|
|
<div id="divHeader">
|
|
<div id="divHeaderFlexLeft">
|
|
<!-- Hamburger is now separate fixed element -->
|
|
</div>
|
|
|
|
<div id="divHeaderFlexCenter">
|
|
<div class="divHeaderText">DIDACTYL</div>
|
|
</div>
|
|
|
|
<div id="divHeaderFlexRight">
|
|
<!-- No button in header right - logout is in sidenav footer -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ================================================================
|
|
BODY
|
|
================================================================ -->
|
|
<div id="divBody">
|
|
<div class="divPanel">
|
|
<div class="divPanelTitle">Status Dashboard</div>
|
|
<div class="divPanelSubTitle">Agent health, runtime info, and API connectivity.</div>
|
|
<div class="divRow" style="margin-bottom: 10px;">
|
|
<div class="divField" style="min-width: 160px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="apiProtocolInput">API Protocol</label>
|
|
<select id="apiProtocolInput" class="inSelect">
|
|
<option value="http">http</option>
|
|
<option value="https">https</option>
|
|
</select>
|
|
</div>
|
|
<div class="divField" style="min-width: 220px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="apiHostInput">API Host</label>
|
|
<input id="apiHostInput" class="inText" type="text" value="127.0.0.1" />
|
|
</div>
|
|
<div class="divField" style="min-width: 160px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="apiPortInput">API Port</label>
|
|
<input id="apiPortInput" class="inNumber" type="number" min="1" max="65535" value="8484" />
|
|
</div>
|
|
<button id="btnApplyApiAddress" class="btn">Apply API Address</button>
|
|
<button id="btnRefreshStatus" class="btn">Refresh Status</button>
|
|
</div>
|
|
<div class="divRow" style="margin-bottom: 10px;">
|
|
<span id="apiStatusBadge" class="divStatusBadge">API: Unknown</span>
|
|
</div>
|
|
<div id="statusGrid" class="divKV"></div>
|
|
<div class="divField" style="margin-top: 10px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="apiDebugOutput">API Debug Log</label>
|
|
<textarea id="apiDebugOutput" class="taOutput taTall" readonly placeholder="Connection diagnostics will appear here."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divPanel">
|
|
<div class="divPanelTitle">Context Inspector</div>
|
|
<div class="divPanelSubTitle">Inspect current LLM context parts with token estimates and content.</div>
|
|
<div class="divRow" style="margin-bottom: 10px;">
|
|
<button id="btnLoadContextParts" class="btn">Load Context Parts</button>
|
|
</div>
|
|
<div id="contextSummary" class="divSummary">No context loaded.</div>
|
|
<div id="contextPartsList" class="divContextParts"></div>
|
|
</div>
|
|
|
|
<div class="divPanel">
|
|
<div class="divPanelTitle">Simple Prompt Crafting</div>
|
|
<div class="divPanelSubTitle">Run a basic system + user prompt without tools.</div>
|
|
|
|
<div class="divGrid2">
|
|
<div class="divField">
|
|
<label class="divFieldLabel" for="simpleSystemPrompt">System Prompt</label>
|
|
<textarea id="simpleSystemPrompt" class="taInput taTall" placeholder="You are Didactyl, a concise assistant."></textarea>
|
|
</div>
|
|
|
|
<div class="divField">
|
|
<label class="divFieldLabel" for="simpleUserPrompt">User Message</label>
|
|
<textarea id="simpleUserPrompt" class="taInput taTall" placeholder="Summarize the latest context changes."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divRow" style="margin-bottom: 10px;">
|
|
<div class="divField" style="min-width: 280px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="simpleModelOverride">Model Override (optional)</label>
|
|
<input id="simpleModelOverride" class="inText" type="text" placeholder="e.g. claude-haiku-4.5" />
|
|
</div>
|
|
<button id="btnRunSimple" class="btn">Run Simple Prompt</button>
|
|
</div>
|
|
|
|
<div class="divGrid3" style="margin-bottom: 10px;">
|
|
<div id="simpleModelUsed" class="divKVItem">model_used: -</div>
|
|
<div id="simpleInputTokens" class="divKVItem">input_tokens_estimate: -</div>
|
|
<div id="simpleOutputTokens" class="divKVItem">output_tokens_estimate: -</div>
|
|
</div>
|
|
|
|
<textarea id="simpleResponse" class="taOutput" readonly placeholder="Simple response output will appear here."></textarea>
|
|
</div>
|
|
|
|
<div class="divPanel">
|
|
<div class="divPanelTitle">Full Prompt with Tools</div>
|
|
<div class="divPanelSubTitle">Edit the full messages array and run with tool execution enabled.</div>
|
|
|
|
<div class="divWarning">
|
|
Tool execution is real when calling <strong>/api/prompt/run</strong>; agent tool calls can perform real side effects.
|
|
</div>
|
|
|
|
<div class="divRow" style="margin-bottom: 10px;">
|
|
<button id="btnLoadContextMessages" class="btn">Load Current Context Into Messages</button>
|
|
</div>
|
|
|
|
<div class="divField">
|
|
<label class="divFieldLabel" for="fullMessagesJson">Messages JSON Array</label>
|
|
<textarea id="fullMessagesJson" class="taInput taXL" placeholder="[{"role":"system","content":"..."},{"role":"user","content":"..."}]"></textarea>
|
|
</div>
|
|
|
|
<div class="divRow" style="margin-bottom: 10px;">
|
|
<div class="divField" style="min-width: 200px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="fullMaxTurns">Max Turns</label>
|
|
<input id="fullMaxTurns" class="inNumber" type="number" min="1" value="4" />
|
|
</div>
|
|
<div class="divField" style="min-width: 280px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="fullModelOverride">Model Override (optional)</label>
|
|
<input id="fullModelOverride" class="inText" type="text" placeholder="e.g. claude-haiku-4.5" />
|
|
</div>
|
|
<label class="divRow" style="font-size: 80%;">
|
|
<input id="fullConfirmTools" type="checkbox" />
|
|
I understand this can execute real tools.
|
|
</label>
|
|
<button id="btnRunFull" class="btn">Run Full Prompt</button>
|
|
</div>
|
|
|
|
<div class="divGrid3" style="margin-bottom: 10px;">
|
|
<div id="fullModelUsed" class="divKVItem">model_used: -</div>
|
|
<div id="fullInputTokens" class="divKVItem">total_input_tokens_estimate: -</div>
|
|
<div id="fullOutputTokens" class="divKVItem">total_output_tokens_estimate: -</div>
|
|
</div>
|
|
|
|
<div class="divGrid2">
|
|
<div class="divField">
|
|
<label class="divFieldLabel" for="fullFinalResponse">Final Response</label>
|
|
<textarea id="fullFinalResponse" class="taOutput taTall" readonly placeholder="Final response will appear here."></textarea>
|
|
</div>
|
|
<div class="divField">
|
|
<label class="divFieldLabel" for="fullTraceOutput">Turn Trace / Tool Calls</label>
|
|
<textarea id="fullTraceOutput" class="taOutput taTall" readonly placeholder="Turn trace will appear here."></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divPanel">
|
|
<div class="divPanelTitle">A/B Prompt Comparison</div>
|
|
<div class="divPanelSubTitle">Run two prompt variants side-by-side and compare outputs.</div>
|
|
|
|
<div class="divWarning">
|
|
Comparison also executes tools for each variant when model behavior chooses tool calls.
|
|
</div>
|
|
|
|
<div class="divCompareCols" style="margin-bottom: 10px;">
|
|
<div>
|
|
<div class="divField">
|
|
<label class="divFieldLabel" for="compareMessagesA">Variant A Messages JSON</label>
|
|
<textarea id="compareMessagesA" class="taInput taTall" placeholder="[{"role":"system","content":"..."},{"role":"user","content":"..."}]"></textarea>
|
|
</div>
|
|
<div class="divRow">
|
|
<div class="divField" style="min-width: 160px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="compareMaxTurnsA">Max Turns A</label>
|
|
<input id="compareMaxTurnsA" class="inNumber" type="number" min="1" value="4" />
|
|
</div>
|
|
<div class="divField" style="min-width: 220px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="compareModelA">Model A (optional)</label>
|
|
<input id="compareModelA" class="inText" type="text" placeholder="optional model override" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="divField">
|
|
<label class="divFieldLabel" for="compareMessagesB">Variant B Messages JSON</label>
|
|
<textarea id="compareMessagesB" class="taInput taTall" placeholder="[{"role":"system","content":"..."},{"role":"user","content":"..."}]"></textarea>
|
|
</div>
|
|
<div class="divRow">
|
|
<div class="divField" style="min-width: 160px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="compareMaxTurnsB">Max Turns B</label>
|
|
<input id="compareMaxTurnsB" class="inNumber" type="number" min="1" value="4" />
|
|
</div>
|
|
<div class="divField" style="min-width: 220px; margin-bottom: 0;">
|
|
<label class="divFieldLabel" for="compareModelB">Model B (optional)</label>
|
|
<input id="compareModelB" class="inText" type="text" placeholder="optional model override" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="divRow" style="margin-bottom: 10px;">
|
|
<button id="btnCompare" class="btn">Run A/B Compare</button>
|
|
</div>
|
|
|
|
<div class="divGrid2" style="margin-bottom: 10px;">
|
|
<div id="compareStatsA" class="divKVItem">Variant A stats: -</div>
|
|
<div id="compareStatsB" class="divKVItem">Variant B stats: -</div>
|
|
</div>
|
|
|
|
<div class="divCompareCols">
|
|
<div class="divField">
|
|
<label class="divFieldLabel" for="compareOutputA">Variant A Final Response</label>
|
|
<textarea id="compareOutputA" class="taOutput taTall" readonly placeholder="Variant A final response."></textarea>
|
|
</div>
|
|
<div class="divField">
|
|
<label class="divFieldLabel" for="compareOutputB">Variant B Final Response</label>
|
|
<textarea id="compareOutputB" class="taOutput taTall" readonly placeholder="Variant B final response."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<textarea id="compareTrace" class="taOutput taTall" readonly placeholder="Combined comparison trace output."></textarea>
|
|
<div class="divSectionFooter">Difference view is text-first; copy results into external diff tooling if you need strict semantic diffs.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ================================================================
|
|
FOOTER
|
|
================================================================ -->
|
|
<div id="divFooter">
|
|
<div id="divFooterLeft" class="divFooterBox"></div>
|
|
<div id="divFooterCenter" class="divFooterBox"></div>
|
|
<div id="divFooterRight" class="divFooterBox"></div>
|
|
</div>
|
|
|
|
<!-- ================================================================
|
|
SIDENAV
|
|
================================================================ -->
|
|
<div id="divSideNav">
|
|
<div id="divSideNavHeader">
|
|
<!-- No close button - use main hamburger to close -->
|
|
</div>
|
|
|
|
<div id="divSideNavBody">
|
|
<div id="divFiles"></div>
|
|
</div>
|
|
|
|
<div id="divRelaySection">
|
|
<div id="divRelaySectionTitle">
|
|
リレー
|
|
</div>
|
|
<div id="divRelayList">
|
|
Loading relays...
|
|
</div>
|
|
</div>
|
|
|
|
<div id="divVersionBar">
|
|
<span id="versionDisplay">v0.0.1</span>
|
|
<div id="divVersionBarButtons">
|
|
<button id="themeToggleButton" title="Toggle Dark/Light Mode">
|
|
<div id="themeToggleHamburgerContainer"></div>
|
|
</button>
|
|
<button id="logoutButton" title="Logout">
|
|
<div id="logoutHamburgerContainer"></div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ================================================================
|
|
REQUIRED SCRIPTS
|
|
================================================================ -->
|
|
<script src="./nostr.bundle.js"></script>
|
|
<script src="./nostr-lite.js"></script>
|
|
|
|
<script type="module">
|
|
import { initNDKPage, getPubkey, disconnect, getVersion, updateVersionDisplay } from './js/init-ndk.mjs';
|
|
import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs";
|
|
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
|
|
|
|
const versionInfo = await getVersion();
|
|
const VERSION = versionInfo.VERSION;
|
|
console.log(`[didactyl.html ${VERSION}] Loading...`);
|
|
|
|
const DEFAULT_API_PROTOCOL = 'http';
|
|
const DEFAULT_API_HOST = '127.0.0.1';
|
|
const DEFAULT_API_PORT = '8484';
|
|
|
|
let apiProtocol = DEFAULT_API_PROTOCOL;
|
|
let apiHost = DEFAULT_API_HOST;
|
|
let apiPort = DEFAULT_API_PORT;
|
|
let API_BASE_URL = `${apiProtocol}://${apiHost}:${apiPort}`;
|
|
|
|
let updateIntervalId = null;
|
|
let currentPubkey = null;
|
|
|
|
let hamburgerInstance = null;
|
|
let isNavOpen = false;
|
|
|
|
let logoutHamburger = null;
|
|
let themeToggleHamburger = null;
|
|
let isDarkMode = false;
|
|
|
|
const divBody = document.getElementById("divBody");
|
|
const divSideNav = document.getElementById("divSideNav");
|
|
const divSideNavBody = document.getElementById("divSideNavBody");
|
|
const divFooterCenter = document.getElementById("divFooterCenter");
|
|
const divFooterRight = document.getElementById("divFooterRight");
|
|
|
|
const apiStatusBadge = document.getElementById('apiStatusBadge');
|
|
const statusGrid = document.getElementById('statusGrid');
|
|
const apiProtocolInput = document.getElementById('apiProtocolInput');
|
|
const apiHostInput = document.getElementById('apiHostInput');
|
|
const apiPortInput = document.getElementById('apiPortInput');
|
|
const btnApplyApiAddress = document.getElementById('btnApplyApiAddress');
|
|
|
|
const contextSummary = document.getElementById('contextSummary');
|
|
const contextPartsList = document.getElementById('contextPartsList');
|
|
|
|
const simpleSystemPrompt = document.getElementById('simpleSystemPrompt');
|
|
const simpleUserPrompt = document.getElementById('simpleUserPrompt');
|
|
const simpleModelOverride = document.getElementById('simpleModelOverride');
|
|
const simpleResponse = document.getElementById('simpleResponse');
|
|
const simpleModelUsed = document.getElementById('simpleModelUsed');
|
|
const simpleInputTokens = document.getElementById('simpleInputTokens');
|
|
const simpleOutputTokens = document.getElementById('simpleOutputTokens');
|
|
|
|
const fullMessagesJson = document.getElementById('fullMessagesJson');
|
|
const fullMaxTurns = document.getElementById('fullMaxTurns');
|
|
const fullModelOverride = document.getElementById('fullModelOverride');
|
|
const fullConfirmTools = document.getElementById('fullConfirmTools');
|
|
const fullFinalResponse = document.getElementById('fullFinalResponse');
|
|
const fullTraceOutput = document.getElementById('fullTraceOutput');
|
|
const fullModelUsed = document.getElementById('fullModelUsed');
|
|
const fullInputTokens = document.getElementById('fullInputTokens');
|
|
const fullOutputTokens = document.getElementById('fullOutputTokens');
|
|
|
|
const compareMessagesA = document.getElementById('compareMessagesA');
|
|
const compareModelA = document.getElementById('compareModelA');
|
|
const compareMaxTurnsA = document.getElementById('compareMaxTurnsA');
|
|
const compareMessagesB = document.getElementById('compareMessagesB');
|
|
const compareModelB = document.getElementById('compareModelB');
|
|
const compareMaxTurnsB = document.getElementById('compareMaxTurnsB');
|
|
const compareStatsA = document.getElementById('compareStatsA');
|
|
const compareStatsB = document.getElementById('compareStatsB');
|
|
const compareOutputA = document.getElementById('compareOutputA');
|
|
const compareOutputB = document.getElementById('compareOutputB');
|
|
const compareTrace = document.getElementById('compareTrace');
|
|
const apiDebugOutput = document.getElementById('apiDebugOutput');
|
|
|
|
function initHamburgerMenu() {
|
|
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
hamburgerInstance.animateTo('burger');
|
|
}
|
|
|
|
function openNav() {
|
|
divSideNav.style.zIndex = 3;
|
|
divSideNav.style.width = "50vw";
|
|
isNavOpen = true;
|
|
if (hamburgerInstance) {
|
|
hamburgerInstance.animateTo('arrow_left');
|
|
}
|
|
|
|
divSideNavBody.innerHTML = `
|
|
<div style="padding: 0 20px 20px 20px; display: flex; flex-direction: column; gap: 10px; font-size: 80%;">
|
|
<div style="font-weight: bold;">Didactyl Admin</div>
|
|
<div>API: <span id="sideApiBase">${API_BASE_URL}</span></div>
|
|
<div>Use this panel to inspect status, context, and prompt outputs.</div>
|
|
<a href="./didactyl.html">didactyl.html</a>
|
|
<a href="./relays.html">relays.html</a>
|
|
<a href="./tools.html">tools.html</a>
|
|
</div>
|
|
`;
|
|
|
|
if (!logoutHamburger) {
|
|
logoutHamburger = new HamburgerMorphing('#logoutHamburgerContainer', {
|
|
size: 24,
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
logoutHamburger.animateTo('x');
|
|
}
|
|
|
|
if (!themeToggleHamburger) {
|
|
themeToggleHamburger = new HamburgerMorphing('#themeToggleHamburgerContainer', {
|
|
size: 24,
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
|
|
const savedTheme = localStorage.getItem('theme');
|
|
isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode');
|
|
const initialShape = isDarkMode ? 'moon' : 'circle';
|
|
themeToggleHamburger.animateTo(initialShape);
|
|
}
|
|
}
|
|
|
|
function closeNav() {
|
|
divSideNav.style.width = "0vw";
|
|
divSideNav.style.zIndex = -1;
|
|
isNavOpen = false;
|
|
if (hamburgerInstance) {
|
|
hamburgerInstance.animateTo('burger');
|
|
}
|
|
}
|
|
|
|
function toggleNav() {
|
|
if (isNavOpen) {
|
|
closeNav();
|
|
} else {
|
|
openNav();
|
|
}
|
|
}
|
|
|
|
function setApiStatus(isUp, text = '') {
|
|
if (isUp) {
|
|
apiStatusBadge.textContent = `API: Connected ${text}`.trim();
|
|
apiStatusBadge.style.borderColor = 'var(--primary-color)';
|
|
} else {
|
|
apiStatusBadge.textContent = `API: Offline ${text}`.trim();
|
|
apiStatusBadge.style.borderColor = 'var(--accent-color)';
|
|
}
|
|
}
|
|
|
|
function appendApiDebug(message, details = null) {
|
|
const timestamp = new Date().toISOString();
|
|
const detailText = details ? `\n${JSON.stringify(details, null, 2)}` : '';
|
|
const line = `[${timestamp}] ${message}${detailText}`;
|
|
|
|
if (apiDebugOutput) {
|
|
const existing = apiDebugOutput.value ? `${apiDebugOutput.value}\n\n` : '';
|
|
apiDebugOutput.value = `${existing}${line}`;
|
|
apiDebugOutput.scrollTop = apiDebugOutput.scrollHeight;
|
|
}
|
|
|
|
console.log('[didactyl.html][api-debug]', message, details || '');
|
|
}
|
|
|
|
function applyApiAddressFromInputs() {
|
|
const protocolRaw = (apiProtocolInput?.value || '').trim().toLowerCase() || DEFAULT_API_PROTOCOL;
|
|
const host = (apiHostInput?.value || '').trim() || DEFAULT_API_HOST;
|
|
const portRaw = (apiPortInput?.value || '').trim() || DEFAULT_API_PORT;
|
|
const port = Number(portRaw);
|
|
|
|
if (protocolRaw !== 'http' && protocolRaw !== 'https') {
|
|
throw new Error('API protocol must be http or https');
|
|
}
|
|
|
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
throw new Error('API port must be an integer between 1 and 65535');
|
|
}
|
|
|
|
apiProtocol = protocolRaw;
|
|
apiHost = host;
|
|
apiPort = String(port);
|
|
API_BASE_URL = `${apiProtocol}://${apiHost}:${apiPort}`;
|
|
|
|
const sideApiBase = document.getElementById('sideApiBase');
|
|
if (sideApiBase) {
|
|
sideApiBase.textContent = API_BASE_URL;
|
|
}
|
|
|
|
appendApiDebug('Applied API address', {
|
|
apiBaseUrl: API_BASE_URL,
|
|
protocol: apiProtocol,
|
|
host: apiHost,
|
|
port: apiPort
|
|
});
|
|
}
|
|
|
|
async function apiRequest(path, options = {}) {
|
|
const method = options.method || 'GET';
|
|
const url = `${API_BASE_URL}${path}`;
|
|
const startedAt = Date.now();
|
|
|
|
appendApiDebug('Request start', {
|
|
method,
|
|
url,
|
|
pageOrigin: window.location.origin,
|
|
pageProtocol: window.location.protocol,
|
|
online: navigator.onLine,
|
|
isSecureContext: window.isSecureContext
|
|
});
|
|
|
|
let response;
|
|
try {
|
|
response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: options.body ? JSON.stringify(options.body) : undefined
|
|
});
|
|
} catch (networkError) {
|
|
appendApiDebug('Network error during fetch', {
|
|
method,
|
|
url,
|
|
name: networkError?.name,
|
|
message: networkError?.message,
|
|
stack: networkError?.stack
|
|
});
|
|
|
|
const lowerMsg = String(networkError?.message || '').toLowerCase();
|
|
const hint = lowerMsg.includes('access')
|
|
? 'Browser/network policy blocked localhost access (ERR_ACCESS_DENIED); check browser security settings, extension policy, or localhost allowlist.'
|
|
: 'Fetch failed before HTTP response; verify API is running and browser can access the target host/port.';
|
|
throw new Error(`${networkError?.message || 'Network error'} | ${hint}`);
|
|
}
|
|
|
|
appendApiDebug('Response received', {
|
|
method,
|
|
url,
|
|
status: response.status,
|
|
ok: response.ok,
|
|
elapsedMs: Date.now() - startedAt
|
|
});
|
|
|
|
let data = null;
|
|
try {
|
|
data = await response.json();
|
|
} catch {
|
|
appendApiDebug('Response JSON parse failed', {
|
|
method,
|
|
url,
|
|
status: response.status
|
|
});
|
|
data = { success: false, error: `Invalid JSON response from ${path}` };
|
|
}
|
|
|
|
if (!response.ok || !data || data.success === false) {
|
|
const errorMessage = data?.error || `HTTP ${response.status} on ${path}`;
|
|
appendApiDebug('Request failed', {
|
|
method,
|
|
url,
|
|
error: errorMessage,
|
|
response: data
|
|
});
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
appendApiDebug('Request success', {
|
|
method,
|
|
url,
|
|
elapsedMs: Date.now() - startedAt
|
|
});
|
|
|
|
return data;
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
if (value === null || value === undefined) {
|
|
return '-';
|
|
}
|
|
return Number(value).toLocaleString();
|
|
}
|
|
|
|
function setTextAreaValue(el, value) {
|
|
el.value = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
}
|
|
|
|
function parseMessagesJson(raw) {
|
|
const parsed = JSON.parse(raw);
|
|
if (!Array.isArray(parsed)) {
|
|
throw new Error('Messages must be a JSON array.');
|
|
}
|
|
|
|
parsed.forEach((msg, index) => {
|
|
if (typeof msg !== 'object' || msg === null) {
|
|
throw new Error(`Message at index ${index} must be an object.`);
|
|
}
|
|
if (!msg.role || !msg.content) {
|
|
throw new Error(`Message at index ${index} must contain role and content.`);
|
|
}
|
|
});
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function formatTurns(turns = []) {
|
|
if (!Array.isArray(turns) || turns.length === 0) {
|
|
return 'No turn trace available.';
|
|
}
|
|
|
|
return turns.map((turn) => {
|
|
const lines = [`Turn ${turn.turn ?? '?'}:`];
|
|
|
|
if (Array.isArray(turn.tool_calls) && turn.tool_calls.length > 0) {
|
|
lines.push(` tool_calls: ${turn.tool_calls.length}`);
|
|
turn.tool_calls.forEach((toolCall, idx) => {
|
|
lines.push(` ${idx + 1}. ${toolCall.name || 'unknown_tool'}`);
|
|
lines.push(` arguments: ${typeof toolCall.arguments === 'string' ? toolCall.arguments : JSON.stringify(toolCall.arguments)}`);
|
|
lines.push(` result: ${typeof toolCall.result === 'string' ? toolCall.result : JSON.stringify(toolCall.result)}`);
|
|
});
|
|
} else {
|
|
lines.push(' tool_calls: 0');
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}).join('\n\n');
|
|
}
|
|
|
|
function renderStatus(statusData) {
|
|
const rows = [
|
|
['name', statusData.name],
|
|
['version', statusData.version],
|
|
['pubkey', statusData.pubkey],
|
|
['relay_count', formatNumber(statusData.relay_count)],
|
|
['connected_relays', formatNumber(statusData.connected_relays)],
|
|
['active_triggers', formatNumber(statusData.active_triggers)]
|
|
];
|
|
|
|
statusGrid.innerHTML = rows.map(([k, v]) => `
|
|
<div class="divKVItem"><span class="spanMuted">${k}</span><br>${v ?? '-'}</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderContextParts(data) {
|
|
contextSummary.innerHTML = `
|
|
<strong>Total chars:</strong> ${formatNumber(data.total_chars)}
|
|
|
|
|
<strong>Total estimated tokens:</strong> ${formatNumber(data.total_estimated_tokens)}
|
|
|
|
|
<strong>Parts:</strong> ${Array.isArray(data.parts) ? data.parts.length : 0}
|
|
`;
|
|
|
|
if (!Array.isArray(data.parts) || data.parts.length === 0) {
|
|
contextPartsList.innerHTML = '<div class="divSummary">No context parts returned.</div>';
|
|
return;
|
|
}
|
|
|
|
contextPartsList.innerHTML = data.parts.map((part, index) => `
|
|
<details class="detPart">
|
|
<summary>
|
|
${index + 1}. ${part.name || 'unnamed'}
|
|
<span class="spanMuted">(role: ${part.role || '-'}, chars: ${formatNumber(part.chars)}, tokens: ${formatNumber(part.estimated_tokens)})</span>
|
|
</summary>
|
|
<pre class="prePart">${(part.content || '').replace(/</g, '<').replace(/>/g, '>')}</pre>
|
|
</details>
|
|
`).join('');
|
|
}
|
|
|
|
async function refreshStatus() {
|
|
try {
|
|
const status = await apiRequest('/api/status');
|
|
setApiStatus(true);
|
|
renderStatus(status);
|
|
divFooterCenter.textContent = `Didactyl ${status.version || ''}`.trim();
|
|
divFooterRight.textContent = `Relays ${formatNumber(status.connected_relays)}/${formatNumber(status.relay_count)}`;
|
|
} catch (error) {
|
|
setApiStatus(false, `(${error.message})`);
|
|
statusGrid.innerHTML = `<div class="divKVItem">Status request failed: ${error.message}</div>`;
|
|
divFooterCenter.textContent = 'Didactyl API offline';
|
|
divFooterRight.textContent = '';
|
|
appendApiDebug('refreshStatus failed', {
|
|
apiBaseUrl: API_BASE_URL,
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
async function loadContextParts() {
|
|
contextSummary.textContent = 'Loading context...';
|
|
contextPartsList.innerHTML = '';
|
|
try {
|
|
const data = await apiRequest('/api/context/parts');
|
|
setApiStatus(true);
|
|
renderContextParts(data);
|
|
} catch (error) {
|
|
setApiStatus(false, `(${error.message})`);
|
|
contextSummary.textContent = `Failed to load context: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
async function loadContextIntoMessages() {
|
|
try {
|
|
const data = await apiRequest('/api/context/current');
|
|
setApiStatus(true);
|
|
setTextAreaValue(fullMessagesJson, data.messages || []);
|
|
} catch (error) {
|
|
setApiStatus(false, `(${error.message})`);
|
|
fullTraceOutput.value = `Failed to load context/current: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
async function runSimplePrompt() {
|
|
simpleResponse.value = 'Running simple prompt...';
|
|
simpleModelUsed.textContent = 'model_used: -';
|
|
simpleInputTokens.textContent = 'input_tokens_estimate: -';
|
|
simpleOutputTokens.textContent = 'output_tokens_estimate: -';
|
|
|
|
try {
|
|
const payload = {
|
|
system_prompt: simpleSystemPrompt.value || '',
|
|
user_message: simpleUserPrompt.value || ''
|
|
};
|
|
|
|
const model = (simpleModelOverride.value || '').trim();
|
|
if (model) {
|
|
payload.model = model;
|
|
}
|
|
|
|
const data = await apiRequest('/api/prompt/run-simple', {
|
|
method: 'POST',
|
|
body: payload
|
|
});
|
|
|
|
setApiStatus(true);
|
|
simpleResponse.value = data.response || '';
|
|
simpleModelUsed.textContent = `model_used: ${data.model_used || '-'}`;
|
|
simpleInputTokens.textContent = `input_tokens_estimate: ${formatNumber(data.input_tokens_estimate)}`;
|
|
simpleOutputTokens.textContent = `output_tokens_estimate: ${formatNumber(data.output_tokens_estimate)}`;
|
|
} catch (error) {
|
|
setApiStatus(false, `(${error.message})`);
|
|
simpleResponse.value = `Simple prompt failed: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
async function runFullPrompt() {
|
|
fullFinalResponse.value = '';
|
|
fullTraceOutput.value = 'Running full prompt...';
|
|
|
|
if (!fullConfirmTools.checked) {
|
|
fullTraceOutput.value = 'You must confirm real tool execution before running full prompt.';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const messages = parseMessagesJson(fullMessagesJson.value || '[]');
|
|
const payload = {
|
|
messages,
|
|
max_turns: Number(fullMaxTurns.value || 4)
|
|
};
|
|
|
|
const model = (fullModelOverride.value || '').trim();
|
|
if (model) {
|
|
payload.model = model;
|
|
}
|
|
|
|
const data = await apiRequest('/api/prompt/run', {
|
|
method: 'POST',
|
|
body: payload
|
|
});
|
|
|
|
setApiStatus(true);
|
|
fullFinalResponse.value = data.final_response || '';
|
|
fullTraceOutput.value = formatTurns(data.turns || []);
|
|
fullModelUsed.textContent = `model_used: ${data.model_used || '-'}`;
|
|
fullInputTokens.textContent = `total_input_tokens_estimate: ${formatNumber(data.total_input_tokens_estimate)}`;
|
|
fullOutputTokens.textContent = `total_output_tokens_estimate: ${formatNumber(data.total_output_tokens_estimate)}`;
|
|
} catch (error) {
|
|
setApiStatus(false, `(${error.message})`);
|
|
fullTraceOutput.value = `Full prompt failed: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
function createCompareVariant(messagesRaw, maxTurnsRaw, modelRaw) {
|
|
const variant = {
|
|
messages: parseMessagesJson(messagesRaw || '[]'),
|
|
max_turns: Number(maxTurnsRaw || 4)
|
|
};
|
|
|
|
const model = (modelRaw || '').trim();
|
|
if (model) {
|
|
variant.model = model;
|
|
}
|
|
|
|
return variant;
|
|
}
|
|
|
|
function renderCompareVariantStats(container, label, variant) {
|
|
container.textContent = `${label}: model=${variant.model_used || '-'}, input=${formatNumber(variant.total_input_tokens_estimate)}, output=${formatNumber(variant.total_output_tokens_estimate)}`;
|
|
}
|
|
|
|
async function runComparePrompt() {
|
|
compareOutputA.value = '';
|
|
compareOutputB.value = '';
|
|
compareTrace.value = 'Running comparison...';
|
|
|
|
try {
|
|
const payload = {
|
|
variant_a: createCompareVariant(compareMessagesA.value, compareMaxTurnsA.value, compareModelA.value),
|
|
variant_b: createCompareVariant(compareMessagesB.value, compareMaxTurnsB.value, compareModelB.value)
|
|
};
|
|
|
|
const data = await apiRequest('/api/prompt/compare', {
|
|
method: 'POST',
|
|
body: payload
|
|
});
|
|
|
|
setApiStatus(true);
|
|
|
|
const variantA = data.variant_a || {};
|
|
const variantB = data.variant_b || {};
|
|
|
|
compareOutputA.value = variantA.final_response || '';
|
|
compareOutputB.value = variantB.final_response || '';
|
|
|
|
renderCompareVariantStats(compareStatsA, 'Variant A stats', variantA);
|
|
renderCompareVariantStats(compareStatsB, 'Variant B stats', variantB);
|
|
|
|
compareTrace.value = [
|
|
'=== Variant A Trace ===',
|
|
formatTurns(variantA.turns || []),
|
|
'',
|
|
'=== Variant B Trace ===',
|
|
formatTurns(variantB.turns || [])
|
|
].join('\n');
|
|
} catch (error) {
|
|
setApiStatus(false, `(${error.message})`);
|
|
compareTrace.value = `Compare failed: ${error.message}`;
|
|
}
|
|
}
|
|
|
|
const UpdateFooter = async () => {
|
|
try {
|
|
await updateFooterRelayStatus();
|
|
await updateSidenavRelaySection();
|
|
} catch (error) {
|
|
console.error('[didactyl.html] Error updating footer:', error);
|
|
}
|
|
};
|
|
|
|
const Logout = async () => {
|
|
console.log("[didactyl.html] Starting logout process...");
|
|
|
|
if (updateIntervalId) {
|
|
clearInterval(updateIntervalId);
|
|
updateIntervalId = null;
|
|
}
|
|
|
|
disconnect();
|
|
|
|
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.logout) {
|
|
await window.NOSTR_LOGIN_LITE.logout();
|
|
}
|
|
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
|
|
if (window.indexedDB) {
|
|
const databases = await window.indexedDB.databases();
|
|
for (const db of databases) {
|
|
if (db.name) {
|
|
window.indexedDB.deleteDatabase(db.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log("[didactyl.html] Logged out, reloading page");
|
|
location.reload(true);
|
|
};
|
|
|
|
function bindUiEvents() {
|
|
const divSvgHam = document.getElementById('divSvgHam');
|
|
if (divSvgHam) {
|
|
divSvgHam.addEventListener('click', toggleNav);
|
|
}
|
|
|
|
const themeToggleButton = document.getElementById('themeToggleButton');
|
|
const logoutButton = document.getElementById('logoutButton');
|
|
|
|
if (themeToggleButton) {
|
|
themeToggleButton.addEventListener('click', () => {
|
|
isDarkMode = !isDarkMode;
|
|
if (isDarkMode) {
|
|
localStorage.setItem('theme', 'dark');
|
|
} else {
|
|
localStorage.setItem('theme', 'light');
|
|
}
|
|
localStorage.setItem('sidenavWasOpen', isNavOpen ? 'true' : 'false');
|
|
window.location.reload();
|
|
});
|
|
}
|
|
|
|
if (logoutButton) {
|
|
logoutButton.addEventListener('click', async () => {
|
|
try {
|
|
await Logout();
|
|
} catch (error) {
|
|
console.error('Logout failed:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (btnApplyApiAddress) {
|
|
btnApplyApiAddress.addEventListener('click', async () => {
|
|
try {
|
|
applyApiAddressFromInputs();
|
|
await refreshStatus();
|
|
} catch (error) {
|
|
setApiStatus(false, `(${error.message})`);
|
|
}
|
|
});
|
|
}
|
|
|
|
document.getElementById('btnRefreshStatus').addEventListener('click', refreshStatus);
|
|
document.getElementById('btnLoadContextParts').addEventListener('click', loadContextParts);
|
|
document.getElementById('btnRunSimple').addEventListener('click', runSimplePrompt);
|
|
document.getElementById('btnLoadContextMessages').addEventListener('click', loadContextIntoMessages);
|
|
document.getElementById('btnRunFull').addEventListener('click', runFullPrompt);
|
|
document.getElementById('btnCompare').addEventListener('click', runComparePrompt);
|
|
}
|
|
|
|
function seedDefaultPromptEditors() {
|
|
if (!simpleSystemPrompt.value.trim()) {
|
|
simpleSystemPrompt.value = 'You are Didactyl; answer with concise, high-signal operational guidance.';
|
|
}
|
|
|
|
if (!simpleUserPrompt.value.trim()) {
|
|
simpleUserPrompt.value = 'Summarize the currently loaded context in 5 bullet points.';
|
|
}
|
|
|
|
if (!fullMessagesJson.value.trim()) {
|
|
fullMessagesJson.value = JSON.stringify([
|
|
{
|
|
role: 'system',
|
|
content: 'You are Didactyl with tools enabled; reason carefully before choosing tools.'
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: 'Inspect context and propose the safest next admin action.'
|
|
}
|
|
], null, 2);
|
|
}
|
|
|
|
if (!compareMessagesA.value.trim()) {
|
|
compareMessagesA.value = JSON.stringify([
|
|
{
|
|
role: 'system',
|
|
content: 'You are Didactyl; respond with short, direct recommendations.'
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: 'Propose one safe action and one high-impact action.'
|
|
}
|
|
], null, 2);
|
|
}
|
|
|
|
if (!compareMessagesB.value.trim()) {
|
|
compareMessagesB.value = JSON.stringify([
|
|
{
|
|
role: 'system',
|
|
content: 'You are Didactyl; include explicit risk analysis and confidence levels.'
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: 'Propose one safe action and one high-impact action.'
|
|
}
|
|
], null, 2);
|
|
}
|
|
}
|
|
|
|
(async function main() {
|
|
console.log("[didactyl.html] Starting initialization...");
|
|
|
|
try {
|
|
initHamburgerMenu();
|
|
bindUiEvents();
|
|
seedDefaultPromptEditors();
|
|
|
|
if (apiProtocolInput && !apiProtocolInput.value.trim()) {
|
|
apiProtocolInput.value = DEFAULT_API_PROTOCOL;
|
|
}
|
|
if (apiHostInput && !apiHostInput.value.trim()) {
|
|
apiHostInput.value = DEFAULT_API_HOST;
|
|
}
|
|
if (apiPortInput && !apiPortInput.value.trim()) {
|
|
apiPortInput.value = DEFAULT_API_PORT;
|
|
}
|
|
applyApiAddressFromInputs();
|
|
|
|
await initNDKPage();
|
|
|
|
currentPubkey = await getPubkey();
|
|
console.log("[didactyl.html] Authenticated as:", currentPubkey);
|
|
|
|
initFooterRelayStatus();
|
|
initSidenavRelaySection();
|
|
await UpdateFooter();
|
|
|
|
window.addEventListener('ndkRelayActivity', (event) => {
|
|
const { relayUrl, activity, stats } = event.detail;
|
|
console.log(`[didactyl.html] Relay activity: ${relayUrl} - ${activity}`, stats);
|
|
setRelayActivityState(relayUrl, activity);
|
|
});
|
|
|
|
window.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'relayActivity') {
|
|
const { relayUrl, activity } = event.data;
|
|
console.log(`[didactyl.html] Relay activity: ${relayUrl} - ${activity}`);
|
|
setRelayActivityState(relayUrl, activity);
|
|
}
|
|
});
|
|
|
|
const sidenavWasOpen = localStorage.getItem('sidenavWasOpen');
|
|
if (sidenavWasOpen === 'true') {
|
|
localStorage.removeItem('sidenavWasOpen');
|
|
openNav();
|
|
}
|
|
|
|
updateIntervalId = setInterval(UpdateFooter, 1000);
|
|
|
|
await updateVersionDisplay();
|
|
await refreshStatus();
|
|
|
|
console.log('[didactyl.html] Initialization complete');
|
|
} catch (error) {
|
|
console.error('[didactyl.html] Initialization failed:', error);
|
|
divBody.innerHTML = `<div style="text-align: center; padding: 50px;">
|
|
<div style="font-size: 24px; margin-bottom: 20px; color: red;">❌ Initialization Error</div>
|
|
<div style="font-size: 16px; color: var(--primary-color);">${error.message}</div>
|
|
<div style="margin-top: 20px;">
|
|
<button onclick="location.reload()" style="padding: 10px 20px; font-size: 16px;">Retry</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|