JacobLinCool Codex commited on
Commit
96da637
·
verified ·
1 Parent(s): c909baf

refactor: show builder goals in app

Browse files

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

app.py CHANGED
@@ -19,7 +19,7 @@ from hackathon_advisor.lora_training_kit import TRAINING_KIT_FILENAME, build_lor
19
  from hackathon_advisor.prize_ledger import prize_ledger
20
  from hackathon_advisor.submission_packet import build_submission_packet_markdown
21
  from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
22
- from hackathon_advisor.tools import TARGETS
23
  from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
24
 
25
 
@@ -71,6 +71,7 @@ def bootstrap() -> dict:
71
  "top_projects": [project.to_public_dict() for project in index.top_projects(limit=8)],
72
  "whitespace": [item.to_dict() for item in index.find_whitespace(limit=5)],
73
  "target_options": TARGETS,
 
74
  "default_targets": TARGETS[:3],
75
  "profile_fields": PROFILE_FIELDS,
76
  "prize_ledger": prize_ledger(runtime_status),
 
19
  from hackathon_advisor.prize_ledger import prize_ledger
20
  from hackathon_advisor.submission_packet import build_submission_packet_markdown
21
  from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
22
+ from hackathon_advisor.tools import TARGETS, target_profiles
23
  from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
24
 
25
 
 
71
  "top_projects": [project.to_public_dict() for project in index.top_projects(limit=8)],
72
  "whitespace": [item.to_dict() for item in index.find_whitespace(limit=5)],
73
  "target_options": TARGETS,
74
+ "target_profiles": target_profiles(),
75
  "default_targets": TARGETS[:3],
76
  "profile_fields": PROFILE_FIELDS,
77
  "prize_ledger": prize_ledger(runtime_status),
hackathon_advisor/agent.py CHANGED
@@ -16,6 +16,7 @@ from hackathon_advisor.tools import (
16
  ToolEvent,
17
  idea_from_text,
18
  normalize_targets,
 
19
  targets_from_state,
20
  )
21
  from hackathon_advisor.wood_map import build_wood_map
@@ -349,7 +350,8 @@ class AdvisorEngine:
349
  idea.targets = targets
350
  self._store_idea(state, idea)
351
  tool_events.append(ToolEvent("set_target", f"Set {len(targets)} target quests."))
352
- response = "The seal will now bias toward: " + (", ".join(targets) or "no specific targets")
 
353
  return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
354
 
355
  def _idea_for_optional_id(self, call: ToolCall, state: dict[str, Any]) -> Idea | None:
 
16
  ToolEvent,
17
  idea_from_text,
18
  normalize_targets,
19
+ target_label,
20
  targets_from_state,
21
  )
22
  from hackathon_advisor.wood_map import build_wood_map
 
350
  idea.targets = targets
351
  self._store_idea(state, idea)
352
  tool_events.append(ToolEvent("set_target", f"Set {len(targets)} target quests."))
353
+ labels = [target_label(target) for target in targets]
354
+ response = "The seal will now bias toward: " + (", ".join(labels) or "no specific goals")
355
  return self._result(normalized, corrections, response, state, tool_events, [], [], None, [], {})
356
 
357
  def _idea_for_optional_id(self, call: ToolCall, state: dict[str, Any]) -> Idea | None:
hackathon_advisor/demo_rehearsal.py CHANGED
@@ -5,7 +5,7 @@ from typing import Any
5
 
6
  DEMO_PROMPT = (
7
  "A local-first archive cartographer for family photos that runs offline, cites nearby Spaces, "
8
- "and exports Field Notes."
9
  )
10
  DEMO_PLAN_PROMPT = "make a build plan"
11
  DEMO_PROFILE = {
 
5
 
6
  DEMO_PROMPT = (
7
  "A local-first archive cartographer for family photos that runs offline, cites nearby Spaces, "
8
+ "and exports build notes."
9
  )
10
  DEMO_PLAN_PROMPT = "make a build plan"
11
  DEMO_PROFILE = {
hackathon_advisor/tools.py CHANGED
@@ -17,6 +17,48 @@ TARGETS = [
17
  "Field Notes",
18
  ]
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  def normalize_targets(raw_targets: Any, default: list[str] | None = None) -> list[str]:
22
  if raw_targets is None:
@@ -111,7 +153,7 @@ class AdvisorTools:
111
  "Refresh the Space snapshot, then tune the bleed threshold against the closest echoes.",
112
  "Build the smallest happy path: input, citations, score seal, and shareable artifact.",
113
  "Add one prize hook only after the core loop is smooth enough to demo without narration.",
114
- "Record the trace and write Field Notes from the exact build decisions.",
115
  ]
116
  if any("Well" in target for target in idea.targets):
117
  plan.insert(4, "Prepare a tiny LoRA dataset from successful advisor turns before training.")
 
17
  "Field Notes",
18
  ]
19
 
20
+ TARGET_PROFILE_BY_ID = {
21
+ "Off the Grid": {
22
+ "label": "Local-first",
23
+ "description": "Favor ideas that work without proprietary inference APIs.",
24
+ },
25
+ "Well-Tuned": {
26
+ "label": "Trainable",
27
+ "description": "Shape good turns into a tiny fine-tune dataset.",
28
+ },
29
+ "Off-Brand": {
30
+ "label": "Distinct voice",
31
+ "description": "Leave room for an interface and tone people remember.",
32
+ },
33
+ "Llama Champion": {
34
+ "label": "llama.cpp path",
35
+ "description": "Prefer small-model choices that can run locally.",
36
+ },
37
+ "Sharing is Caring": {
38
+ "label": "Shareable artifact",
39
+ "description": "Make an output people can save, post, or compare.",
40
+ },
41
+ "Field Notes": {
42
+ "label": "Build notes",
43
+ "description": "Keep decisions easy to write up from the session trace.",
44
+ },
45
+ }
46
+
47
+
48
+ def target_profiles() -> list[dict[str, str]]:
49
+ return [
50
+ {
51
+ "id": target,
52
+ "label": TARGET_PROFILE_BY_ID[target]["label"],
53
+ "description": TARGET_PROFILE_BY_ID[target]["description"],
54
+ }
55
+ for target in TARGETS
56
+ ]
57
+
58
+
59
+ def target_label(target: str) -> str:
60
+ return TARGET_PROFILE_BY_ID.get(target, {}).get("label", target)
61
+
62
 
63
  def normalize_targets(raw_targets: Any, default: list[str] | None = None) -> list[str]:
64
  if raw_targets is None:
 
153
  "Refresh the Space snapshot, then tune the bleed threshold against the closest echoes.",
154
  "Build the smallest happy path: input, citations, score seal, and shareable artifact.",
155
  "Add one prize hook only after the core loop is smooth enough to demo without narration.",
156
+ "Record the trace and write build notes from the exact decisions.",
157
  ]
158
  if any("Well" in target for target in idea.targets):
159
  plan.insert(4, "Prepare a tiny LoRA dataset from successful advisor turns before training.")
static/app.js CHANGED
@@ -28,6 +28,8 @@ let session = {};
28
  let clientPromise = Client.connect(window.location.origin);
29
  let currentArtifact = null;
30
  let targetOptions = [];
 
 
31
  let profileFields = [];
32
  let turnWatchdog = null;
33
  let sawTurnToken = false;
@@ -134,7 +136,11 @@ async function runTurn(message) {
134
  async function bootstrap() {
135
  const response = await fetch("/api/bootstrap");
136
  const data = await response.json();
137
- targetOptions = data.target_options || [];
 
 
 
 
138
  profileFields = data.profile_fields || [];
139
  session = {
140
  profile: {},
@@ -262,6 +268,22 @@ function normalizeSession(savedSession, defaultSession) {
262
  return normalized;
263
  }
264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  function saveSession() {
266
  try {
267
  window.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
@@ -282,15 +304,24 @@ function renderTargets(selectedTargets) {
282
  const selected = new Set(selectedTargets || []);
283
  targetsEl.innerHTML = "";
284
  if (!targetOptions.length) {
285
- targetsEl.innerHTML = `<div class="empty">No seals loaded.</div>`;
286
  return;
287
  }
288
  for (const option of targetOptions) {
 
289
  const label = document.createElement("label");
290
  label.className = "target-toggle";
291
  label.innerHTML = `
292
- <input type="checkbox" data-target="${escapeAttribute(option)}" ${selected.has(option) ? "checked" : ""} />
293
- <span>${escapeHtml(option)}</span>
 
 
 
 
 
 
 
 
294
  `;
295
  targetsEl.append(label);
296
  }
@@ -378,7 +409,7 @@ function renderIdeas(ideas) {
378
  }
379
  for (const idea of visibleIdeas(ideas)) {
380
  const score = idea.score?.overall ? Number(idea.score.overall).toFixed(1) : "0.0";
381
- const targets = (idea.targets || []).slice(0, 3).join(" · ");
382
  const item = document.createElement("div");
383
  item.className = "idea";
384
  item.innerHTML = `
@@ -398,6 +429,10 @@ function visibleIdeas(ideas) {
398
  return current ? [current, ...remaining] : ideas.slice(-4).reverse();
399
  }
400
 
 
 
 
 
401
  function renderScore(score) {
402
  const rows = [
403
  ["Originality", score?.originality || 0],
 
28
  let clientPromise = Client.connect(window.location.origin);
29
  let currentArtifact = null;
30
  let targetOptions = [];
31
+ let targetProfiles = [];
32
+ let targetProfileById = new Map();
33
  let profileFields = [];
34
  let turnWatchdog = null;
35
  let sawTurnToken = false;
 
136
  async function bootstrap() {
137
  const response = await fetch("/api/bootstrap");
138
  const data = await response.json();
139
+ const rawProfiles = Array.isArray(data.target_profiles) ? data.target_profiles : [];
140
+ const rawOptions = Array.isArray(data.target_options) ? data.target_options : [];
141
+ targetProfiles = normalizeTargetProfiles(rawProfiles, rawOptions);
142
+ targetOptions = targetProfiles.map((target) => target.id);
143
+ targetProfileById = new Map(targetProfiles.map((target) => [target.id, target]));
144
  profileFields = data.profile_fields || [];
145
  session = {
146
  profile: {},
 
268
  return normalized;
269
  }
270
 
271
+ function normalizeTargetProfiles(profiles, options) {
272
+ const byId = new Map(
273
+ profiles
274
+ .filter((profile) => profile && typeof profile.id === "string")
275
+ .map((profile) => [
276
+ profile.id,
277
+ {
278
+ id: profile.id,
279
+ label: String(profile.label || profile.id),
280
+ description: String(profile.description || ""),
281
+ },
282
+ ]),
283
+ );
284
+ return options.map((id) => byId.get(id) || { id, label: id, description: "" });
285
+ }
286
+
287
  function saveSession() {
288
  try {
289
  window.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
 
304
  const selected = new Set(selectedTargets || []);
305
  targetsEl.innerHTML = "";
306
  if (!targetOptions.length) {
307
+ targetsEl.innerHTML = `<div class="empty">No goals loaded.</div>`;
308
  return;
309
  }
310
  for (const option of targetOptions) {
311
+ const profile = targetProfileById.get(option) || { label: option, description: "" };
312
  const label = document.createElement("label");
313
  label.className = "target-toggle";
314
  label.innerHTML = `
315
+ <input
316
+ type="checkbox"
317
+ data-target="${escapeAttribute(option)}"
318
+ aria-label="${escapeAttribute(profile.label)}"
319
+ ${selected.has(option) ? "checked" : ""}
320
+ />
321
+ <span class="target-copy">
322
+ <strong>${escapeHtml(profile.label)}</strong>
323
+ ${profile.description ? `<small>${escapeHtml(profile.description)}</small>` : ""}
324
+ </span>
325
  `;
326
  targetsEl.append(label);
327
  }
 
409
  }
410
  for (const idea of visibleIdeas(ideas)) {
411
  const score = idea.score?.overall ? Number(idea.score.overall).toFixed(1) : "0.0";
412
+ const targets = (idea.targets || []).slice(0, 3).map(targetDisplayName).join(" · ");
413
  const item = document.createElement("div");
414
  item.className = "idea";
415
  item.innerHTML = `
 
429
  return current ? [current, ...remaining] : ideas.slice(-4).reverse();
430
  }
431
 
432
+ function targetDisplayName(target) {
433
+ return targetProfileById.get(target)?.label || target;
434
+ }
435
+
436
  function renderScore(score) {
437
  const rows = [
438
  ["Originality", score?.originality || 0],
static/index.html CHANGED
@@ -33,7 +33,7 @@
33
  </button>
34
  <button type="button" data-command="make a build plan" title="Draft a build plan">Plan</button>
35
  <button type="button" data-command="compare ideas" title="Compare the idea board">Rank</button>
36
- <button type="button" id="export-notes" title="Export Field Notes" disabled>Notes</button>
37
  <button type="button" id="export-chapter" title="Export the Almanac chapter" disabled>Chapter</button>
38
  <button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
39
  <button type="button" id="reset-session" title="Clear the saved session">Reset</button>
@@ -54,7 +54,7 @@
54
  <div id="wood-map" class="wood-map"></div>
55
  </article>
56
  <article>
57
- <h2>Targets</h2>
58
  <div id="targets" class="target-list"></div>
59
  </article>
60
  <article>
 
33
  </button>
34
  <button type="button" data-command="make a build plan" title="Draft a build plan">Plan</button>
35
  <button type="button" data-command="compare ideas" title="Compare the idea board">Rank</button>
36
+ <button type="button" id="export-notes" title="Export build notes" disabled>Notes</button>
37
  <button type="button" id="export-chapter" title="Export the Almanac chapter" disabled>Chapter</button>
38
  <button type="button" id="export-artifact" title="Export the current fate page" disabled>PNG</button>
39
  <button type="button" id="reset-session" title="Clear the saved session">Reset</button>
 
54
  <div id="wood-map" class="wood-map"></div>
55
  </article>
56
  <article>
57
+ <h2>Goals</h2>
58
  <div id="targets" class="target-list"></div>
59
  </article>
60
  <article>
static/styles.css CHANGED
@@ -363,16 +363,16 @@ button:disabled {
363
  }
364
 
365
  .target-list {
366
- grid-template-columns: repeat(2, minmax(0, 1fr));
367
  gap: 7px;
368
  }
369
 
370
  .target-toggle {
371
  min-width: 0;
372
- min-height: 36px;
373
  display: flex;
374
- align-items: center;
375
- gap: 7px;
376
  color: #2a170d;
377
  font-size: 0.76rem;
378
  line-height: 1.2;
@@ -384,11 +384,27 @@ button:disabled {
384
  flex: 0 0 auto;
385
  width: 16px;
386
  height: 16px;
 
387
  accent-color: var(--leaf);
388
  }
389
 
390
- .target-toggle span {
391
  min-width: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  }
393
 
394
  .profile-field {
 
363
  }
364
 
365
  .target-list {
366
+ grid-template-columns: 1fr;
367
  gap: 7px;
368
  }
369
 
370
  .target-toggle {
371
  min-width: 0;
372
+ min-height: 50px;
373
  display: flex;
374
+ align-items: flex-start;
375
+ gap: 8px;
376
  color: #2a170d;
377
  font-size: 0.76rem;
378
  line-height: 1.2;
 
384
  flex: 0 0 auto;
385
  width: 16px;
386
  height: 16px;
387
+ margin-top: 2px;
388
  accent-color: var(--leaf);
389
  }
390
 
391
+ .target-copy {
392
  min-width: 0;
393
+ display: grid;
394
+ gap: 3px;
395
+ }
396
+
397
+ .target-copy strong {
398
+ color: #2a170d;
399
+ font-size: 0.8rem;
400
+ line-height: 1.2;
401
+ }
402
+
403
+ .target-copy small {
404
+ color: var(--muted-ink);
405
+ font-size: 0.72rem;
406
+ line-height: 1.25;
407
+ font-weight: 800;
408
  }
409
 
410
  .profile-field {
tests/test_agent.py CHANGED
@@ -137,6 +137,7 @@ def test_planner_profile_and_targets_update_state() -> None:
137
 
138
  assert targeted.state["profile"]["skills"] == "frontend"
139
  assert targeted.state["targets"] == ["Off the Grid", "Field Notes"]
 
140
 
141
 
142
  def test_session_targets_apply_to_new_and_current_ideas() -> None:
 
137
 
138
  assert targeted.state["profile"]["skills"] == "frontend"
139
  assert targeted.state["targets"] == ["Off the Grid", "Field Notes"]
140
+ assert "Local-first, Build notes" in targeted.response
141
 
142
 
143
  def test_session_targets_apply_to_new_and_current_ideas() -> None:
tests/test_app.py CHANGED
@@ -41,6 +41,9 @@ def test_bootstrap_exposes_index_metadata() -> None:
41
  assert payload["runtime"]["tool_count"] >= 8
42
  assert payload["top_projects"]
43
  assert payload["default_targets"] == payload["target_options"][:3]
 
 
 
44
  assert "skills" in payload["profile_fields"]
45
  assert payload["prize_ledger"]["tiny_titan_eligible"] is True
46
 
@@ -68,7 +71,7 @@ def test_field_notes_endpoint_exports_markdown() -> None:
68
  assert "Skills: frontend" in payload
69
  assert "Targets: Field Notes" in payload
70
  assert "## Turn Trace" in payload
71
- assert "Record the trace and write Field Notes" in payload
72
 
73
 
74
  def test_chapter_endpoint_exports_markdown() -> None:
 
41
  assert payload["runtime"]["tool_count"] >= 8
42
  assert payload["top_projects"]
43
  assert payload["default_targets"] == payload["target_options"][:3]
44
+ assert [target["id"] for target in payload["target_profiles"]] == payload["target_options"]
45
+ assert payload["target_profiles"][0]["label"] == "Local-first"
46
+ assert "description" in payload["target_profiles"][0]
47
  assert "skills" in payload["profile_fields"]
48
  assert payload["prize_ledger"]["tiny_titan_eligible"] is True
49
 
 
71
  assert "Skills: frontend" in payload
72
  assert "Targets: Field Notes" in payload
73
  assert "## Turn Trace" in payload
74
+ assert "Record the trace and write build notes" in payload
75
 
76
 
77
  def test_chapter_endpoint_exports_markdown() -> None:
tests/test_field_notes.py CHANGED
@@ -29,7 +29,7 @@ def test_field_notes_markdown_contains_session_decisions() -> None:
29
  assert "Targets: Field Notes" in markdown
30
  assert "A local-first archive cartographer for family photos" in markdown
31
  assert "## Build Plan" in markdown
32
- assert "Record the trace and write Field Notes" in markdown
33
  assert "Closest cited Spaces" in markdown
34
  assert "Page " in markdown
35
  assert "## Wood Map" in markdown
 
29
  assert "Targets: Field Notes" in markdown
30
  assert "A local-first archive cartographer for family photos" in markdown
31
  assert "## Build Plan" in markdown
32
+ assert "Record the trace and write build notes" in markdown
33
  assert "Closest cited Spaces" in markdown
34
  assert "Page " in markdown
35
  assert "## Wood Map" in markdown