Spaces:
Running on Zero
Running on Zero
feat: persist advisor sessions
Browse filesCo-authored-by: Codex <noreply@openai.com>
- README.md +6 -0
- static/app.js +83 -6
- static/index.html +1 -0
- static/styles.css +2 -2
README.md
CHANGED
|
@@ -80,6 +80,12 @@ map, so the share artifact visually proves whether the page sits in an empty mar
|
|
| 80 |
The custom frontend shows optimistic ink immediately after submit. If the first streamed token is slow, a lightweight
|
| 81 |
watchdog updates the page text so the demo never sits in a silent blank state during Space startup or model routing.
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
## Tool-Call Contract
|
| 84 |
|
| 85 |
`/api/tool-contracts` exposes the JSON schemas intended for MiniCPM-style tool calling. `tool_contract_check` accepts a
|
|
|
|
| 80 |
The custom frontend shows optimistic ink immediately after submit. If the first streamed token is slow, a lightweight
|
| 81 |
watchdog updates the page text so the demo never sits in a silent blank state during Space startup or model routing.
|
| 82 |
|
| 83 |
+
## Session Persistence
|
| 84 |
+
|
| 85 |
+
The frontend stores the current advisor session in browser `localStorage`: profile notes, selected targets, idea board,
|
| 86 |
+
trace, latest build plan, and last share artifact. Refreshing the Space restores the same cockpit state; the `Reset`
|
| 87 |
+
button clears the saved session and returns to the current snapshot defaults.
|
| 88 |
+
|
| 89 |
## Tool-Call Contract
|
| 90 |
|
| 91 |
`/api/tool-contracts` exposes the JSON schemas intended for MiniCPM-style tool calling. `tool_contract_check` accepts a
|
static/app.js
CHANGED
|
@@ -20,6 +20,9 @@ const overallEl = document.querySelector("#overall");
|
|
| 20 |
const exportButton = document.querySelector("#export-artifact");
|
| 21 |
const exportTraceButton = document.querySelector("#export-trace");
|
| 22 |
const exportNotesButton = document.querySelector("#export-notes");
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
let session = {};
|
| 25 |
let clientPromise = Client.connect(window.location.origin);
|
|
@@ -57,6 +60,11 @@ exportNotesButton.addEventListener("click", async () => {
|
|
| 57 |
await exportNotes();
|
| 58 |
});
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
targetsEl.addEventListener("change", (event) => {
|
| 61 |
const target = event.target;
|
| 62 |
if (!(target instanceof HTMLInputElement) || !target.dataset.target) return;
|
|
@@ -65,6 +73,7 @@ targetsEl.addEventListener("change", (event) => {
|
|
| 65 |
);
|
| 66 |
session.targets = targetOptions.filter((option) => checked.has(option));
|
| 67 |
syncCurrentIdeaTargets();
|
|
|
|
| 68 |
renderIdeas(session.ideas || []);
|
| 69 |
});
|
| 70 |
|
|
@@ -79,6 +88,7 @@ profileEl.addEventListener("input", (event) => {
|
|
| 79 |
delete profile[target.dataset.profileField];
|
| 80 |
}
|
| 81 |
session.profile = profile;
|
|
|
|
| 82 |
});
|
| 83 |
|
| 84 |
async function runTurn(message) {
|
|
@@ -126,16 +136,12 @@ async function bootstrap() {
|
|
| 126 |
profile: {},
|
| 127 |
targets: data.default_targets || targetOptions.slice(0, 3),
|
| 128 |
};
|
|
|
|
| 129 |
renderProvenance(data);
|
| 130 |
renderTargets(session.targets);
|
| 131 |
renderProfile(session.profile);
|
| 132 |
-
|
| 133 |
renderWhitespace(data.whitespace || []);
|
| 134 |
-
renderIdeas([]);
|
| 135 |
-
renderWoodMap(null);
|
| 136 |
-
renderScore(null);
|
| 137 |
-
renderPlan([]);
|
| 138 |
-
renderTrace([]);
|
| 139 |
}
|
| 140 |
|
| 141 |
function renderProvenance(data) {
|
|
@@ -145,6 +151,76 @@ function renderProvenance(data) {
|
|
| 145 |
provenanceEl.textContent = `${data.index_algorithm || "index"} 路 snapshot ${snapshot} 路 index ${index} 路 ${digest}`;
|
| 146 |
}
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
function renderTargets(selectedTargets) {
|
| 149 |
const selected = new Set(selectedTargets || []);
|
| 150 |
targetsEl.innerHTML = "";
|
|
@@ -234,6 +310,7 @@ function handleEvent(event) {
|
|
| 234 |
}
|
| 235 |
exportTraceButton.disabled = !(session.trace?.length);
|
| 236 |
exportNotesButton.disabled = !(session.trace?.length);
|
|
|
|
| 237 |
}
|
| 238 |
}
|
| 239 |
|
|
|
|
| 20 |
const exportButton = document.querySelector("#export-artifact");
|
| 21 |
const exportTraceButton = document.querySelector("#export-trace");
|
| 22 |
const exportNotesButton = document.querySelector("#export-notes");
|
| 23 |
+
const resetButton = document.querySelector("#reset-session");
|
| 24 |
+
|
| 25 |
+
const SESSION_STORAGE_KEY = "hackathon-advisor-session-v1";
|
| 26 |
|
| 27 |
let session = {};
|
| 28 |
let clientPromise = Client.connect(window.location.origin);
|
|
|
|
| 60 |
await exportNotes();
|
| 61 |
});
|
| 62 |
|
| 63 |
+
resetButton.addEventListener("click", () => {
|
| 64 |
+
clearSavedSession();
|
| 65 |
+
window.location.reload();
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
targetsEl.addEventListener("change", (event) => {
|
| 69 |
const target = event.target;
|
| 70 |
if (!(target instanceof HTMLInputElement) || !target.dataset.target) return;
|
|
|
|
| 73 |
);
|
| 74 |
session.targets = targetOptions.filter((option) => checked.has(option));
|
| 75 |
syncCurrentIdeaTargets();
|
| 76 |
+
saveSession();
|
| 77 |
renderIdeas(session.ideas || []);
|
| 78 |
});
|
| 79 |
|
|
|
|
| 88 |
delete profile[target.dataset.profileField];
|
| 89 |
}
|
| 90 |
session.profile = profile;
|
| 91 |
+
saveSession();
|
| 92 |
});
|
| 93 |
|
| 94 |
async function runTurn(message) {
|
|
|
|
| 136 |
profile: {},
|
| 137 |
targets: data.default_targets || targetOptions.slice(0, 3),
|
| 138 |
};
|
| 139 |
+
session = normalizeSession(readSavedSession(), session);
|
| 140 |
renderProvenance(data);
|
| 141 |
renderTargets(session.targets);
|
| 142 |
renderProfile(session.profile);
|
| 143 |
+
renderRestoredSession(data);
|
| 144 |
renderWhitespace(data.whitespace || []);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
function renderProvenance(data) {
|
|
|
|
| 151 |
provenanceEl.textContent = `${data.index_algorithm || "index"} 路 snapshot ${snapshot} 路 index ${index} 路 ${digest}`;
|
| 152 |
}
|
| 153 |
|
| 154 |
+
function renderRestoredSession(data) {
|
| 155 |
+
currentArtifact = session.last_artifact || null;
|
| 156 |
+
if (currentArtifact?.seal) {
|
| 157 |
+
renderScore(currentArtifact.seal);
|
| 158 |
+
verdictEl.textContent = currentArtifact.verdict || currentArtifact.seal.verdict || "UNWRITTEN";
|
| 159 |
+
overallEl.textContent = Number(currentArtifact.overall || currentArtifact.seal.overall || 0).toFixed(1);
|
| 160 |
+
renderWoodMap(currentArtifact.wood_map || null);
|
| 161 |
+
if (currentArtifact.seal.echoes?.length) {
|
| 162 |
+
renderCitations(currentArtifact.seal.echoes);
|
| 163 |
+
} else {
|
| 164 |
+
renderProjects(data.top_projects || []);
|
| 165 |
+
}
|
| 166 |
+
exportButton.disabled = false;
|
| 167 |
+
} else {
|
| 168 |
+
renderScore(null);
|
| 169 |
+
renderWoodMap(null);
|
| 170 |
+
renderProjects(data.top_projects || []);
|
| 171 |
+
exportButton.disabled = true;
|
| 172 |
+
}
|
| 173 |
+
renderIdeas(session.ideas || []);
|
| 174 |
+
renderPlan(session.last_plan || []);
|
| 175 |
+
renderTrace(session.trace || []);
|
| 176 |
+
exportTraceButton.disabled = !(session.trace?.length);
|
| 177 |
+
exportNotesButton.disabled = !(session.trace?.length);
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
function readSavedSession() {
|
| 181 |
+
try {
|
| 182 |
+
const raw = window.localStorage.getItem(SESSION_STORAGE_KEY);
|
| 183 |
+
if (!raw) return null;
|
| 184 |
+
const parsed = JSON.parse(raw);
|
| 185 |
+
return parsed && typeof parsed === "object" ? parsed : null;
|
| 186 |
+
} catch {
|
| 187 |
+
return null;
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
function normalizeSession(savedSession, defaultSession) {
|
| 192 |
+
const normalized = { ...defaultSession };
|
| 193 |
+
if (!savedSession) return normalized;
|
| 194 |
+
normalized.profile = savedSession.profile && typeof savedSession.profile === "object" ? savedSession.profile : {};
|
| 195 |
+
const savedTargets = Array.isArray(savedSession.targets) ? savedSession.targets : defaultSession.targets;
|
| 196 |
+
normalized.targets = targetOptions.filter((option) => savedTargets.includes(option));
|
| 197 |
+
if (!normalized.targets.length && defaultSession.targets?.length) normalized.targets = [...defaultSession.targets];
|
| 198 |
+
if (Array.isArray(savedSession.ideas)) normalized.ideas = savedSession.ideas;
|
| 199 |
+
if (Array.isArray(savedSession.trace)) normalized.trace = savedSession.trace;
|
| 200 |
+
if (Array.isArray(savedSession.last_plan)) normalized.last_plan = savedSession.last_plan;
|
| 201 |
+
if (savedSession.current_idea_id) normalized.current_idea_id = savedSession.current_idea_id;
|
| 202 |
+
if (savedSession.current_whitespace) normalized.current_whitespace = savedSession.current_whitespace;
|
| 203 |
+
if (savedSession.last_tool_resolution) normalized.last_tool_resolution = savedSession.last_tool_resolution;
|
| 204 |
+
if (savedSession.last_artifact) normalized.last_artifact = savedSession.last_artifact;
|
| 205 |
+
return normalized;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
function saveSession() {
|
| 209 |
+
try {
|
| 210 |
+
window.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
|
| 211 |
+
} catch {
|
| 212 |
+
// Storage may be disabled in some embeds; the app still works in-memory.
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
function clearSavedSession() {
|
| 217 |
+
try {
|
| 218 |
+
window.localStorage.removeItem(SESSION_STORAGE_KEY);
|
| 219 |
+
} catch {
|
| 220 |
+
// Nothing else to clear when storage is unavailable.
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
function renderTargets(selectedTargets) {
|
| 225 |
const selected = new Set(selectedTargets || []);
|
| 226 |
targetsEl.innerHTML = "";
|
|
|
|
| 310 |
}
|
| 311 |
exportTraceButton.disabled = !(session.trace?.length);
|
| 312 |
exportNotesButton.disabled = !(session.trace?.length);
|
| 313 |
+
saveSession();
|
| 314 |
}
|
| 315 |
}
|
| 316 |
|
static/index.html
CHANGED
|
@@ -35,6 +35,7 @@
|
|
| 35 |
<button type="button" id="export-trace" title="Export the tool trace" disabled>JSONL</button>
|
| 36 |
<button type="button" id="export-notes" title="Export Field Notes" disabled>Notes</button>
|
| 37 |
<button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
|
|
|
|
| 38 |
</div>
|
| 39 |
<div id="corrections" class="corrections" aria-live="polite"></div>
|
| 40 |
</section>
|
|
|
|
| 35 |
<button type="button" id="export-trace" title="Export the tool trace" disabled>JSONL</button>
|
| 36 |
<button type="button" id="export-notes" title="Export Field Notes" disabled>Notes</button>
|
| 37 |
<button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
|
| 38 |
+
<button type="button" id="reset-session" title="Clear the saved session">Reset</button>
|
| 39 |
</div>
|
| 40 |
<div id="corrections" class="corrections" aria-live="polite"></div>
|
| 41 |
</section>
|
static/styles.css
CHANGED
|
@@ -184,7 +184,7 @@ button:disabled {
|
|
| 184 |
|
| 185 |
.command-row {
|
| 186 |
display: grid;
|
| 187 |
-
grid-template-columns: repeat(
|
| 188 |
gap: 8px;
|
| 189 |
margin-top: 10px;
|
| 190 |
}
|
|
@@ -527,7 +527,7 @@ button:disabled {
|
|
| 527 |
}
|
| 528 |
|
| 529 |
.command-row {
|
| 530 |
-
grid-template-columns: repeat(
|
| 531 |
}
|
| 532 |
|
| 533 |
.score-row {
|
|
|
|
| 184 |
|
| 185 |
.command-row {
|
| 186 |
display: grid;
|
| 187 |
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
| 188 |
gap: 8px;
|
| 189 |
margin-top: 10px;
|
| 190 |
}
|
|
|
|
| 527 |
}
|
| 528 |
|
| 529 |
.command-row {
|
| 530 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 531 |
}
|
| 532 |
|
| 533 |
.score-row {
|