JacobLinCool Codex commited on
Commit
50124a7
verified
1 Parent(s): 1afd4af

feat: persist advisor sessions

Browse files

Co-authored-by: Codex <noreply@openai.com>

Files changed (4) hide show
  1. README.md +6 -0
  2. static/app.js +83 -6
  3. static/index.html +1 -0
  4. 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
- renderProjects(data.top_projects || []);
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(6, minmax(0, 1fr));
188
  gap: 8px;
189
  margin-top: 10px;
190
  }
@@ -527,7 +527,7 @@ button:disabled {
527
  }
528
 
529
  .command-row {
530
- grid-template-columns: repeat(3, minmax(0, 1fr));
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 {