JacobLinCool Codex commited on
Commit
902a11f
·
verified ·
1 Parent(s): 36ed450

feat: add page-number citations

Browse files

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

hackathon_advisor/agent.py CHANGED
@@ -398,7 +398,9 @@ class AdvisorEngine:
398
  f"{score.verdict} at {score.overall}/10. Push the AI necessity harder: make the model decide, rank, "
399
  "or personalize something a static app cannot."
400
  )
401
- citations = "; ".join(f"page {idx + 1}: {project.title}" for idx, project in enumerate(projects[:3]))
 
 
402
  return (
403
  f"The ink bleeds around {idea.title}. Closest echoes: {citations}. The seal reads "
404
  f"{score.verdict} at {score.overall}/10. Keep the audience, but change the mechanism or artifact so the "
 
398
  f"{score.verdict} at {score.overall}/10. Push the AI necessity harder: make the model decide, rank, "
399
  "or personalize something a static app cannot."
400
  )
401
+ citations = "; ".join(
402
+ f"page {hit.page_number}: {hit.project.title}" for hit in score.echoes[:3]
403
+ )
404
  return (
405
  f"The ink bleeds around {idea.title}. Closest echoes: {citations}. The seal reads "
406
  f"{score.verdict} at {score.overall}/10. Keep the audience, but change the mechanism or artifact so the "
hackathon_advisor/data.py CHANGED
@@ -87,6 +87,7 @@ class SearchHit:
87
  project: Project
88
  score: float
89
  matched_terms: tuple[str, ...]
 
90
 
91
 
92
  @dataclass(frozen=True)
@@ -218,7 +219,10 @@ class ProjectIndex:
218
  query_doc = Counter(query_terms)
219
  query_norm = self._norm(query_doc)
220
  hits: list[SearchHit] = []
221
- for project, doc, doc_norm in zip(self.projects, self._documents, self._norms, strict=True):
 
 
 
222
  if doc_norm == 0.0 or query_norm == 0.0:
223
  continue
224
  raw = 0.0
@@ -233,7 +237,14 @@ class ProjectIndex:
233
  title_bonus = sum(0.08 for term in matched if term in tokenize(project.title))
234
  tag_bonus = sum(0.05 for term in matched if term in tokenize(" ".join(project.tags)))
235
  score = raw / (query_norm * doc_norm) + title_bonus + tag_bonus
236
- hits.append(SearchHit(project=project, score=score, matched_terms=tuple(sorted(matched))))
 
 
 
 
 
 
 
237
  hits.sort(key=lambda hit: (hit.score, hit.project.likes), reverse=True)
238
  return hits[:limit]
239
 
 
87
  project: Project
88
  score: float
89
  matched_terms: tuple[str, ...]
90
+ page_number: int
91
 
92
 
93
  @dataclass(frozen=True)
 
219
  query_doc = Counter(query_terms)
220
  query_norm = self._norm(query_doc)
221
  hits: list[SearchHit] = []
222
+ for page_number, (project, doc, doc_norm) in enumerate(
223
+ zip(self.projects, self._documents, self._norms, strict=True),
224
+ start=1,
225
+ ):
226
  if doc_norm == 0.0 or query_norm == 0.0:
227
  continue
228
  raw = 0.0
 
237
  title_bonus = sum(0.08 for term in matched if term in tokenize(project.title))
238
  tag_bonus = sum(0.05 for term in matched if term in tokenize(" ".join(project.tags)))
239
  score = raw / (query_norm * doc_norm) + title_bonus + tag_bonus
240
+ hits.append(
241
+ SearchHit(
242
+ project=project,
243
+ score=score,
244
+ matched_terms=tuple(sorted(matched)),
245
+ page_number=page_number,
246
+ )
247
+ )
248
  hits.sort(key=lambda hit: (hit.score, hit.project.likes), reverse=True)
249
  return hits[:limit]
250
 
hackathon_advisor/field_notes.py CHANGED
@@ -67,10 +67,11 @@ def build_field_notes_markdown(session: dict[str, Any], metadata: dict[str, Any]
67
  title = _clean(dot.get("title"))
68
  score = _clean(dot.get("score"))
69
  url = _clean(dot.get("url"))
 
70
  if url:
71
- lines.append(f"- [{title}]({url}) - echo score {score}")
72
  else:
73
- lines.append(f"- {title} - echo score {score}")
74
 
75
  if last_artifact:
76
  lines.extend(
@@ -107,10 +108,14 @@ def _idea_section(index: int, idea: dict[str, Any]) -> list[str]:
107
  url = _clean(project.get("url") or project.get("host") or "")
108
  matched = ", ".join(str(term) for term in echo.get("matched_terms") or [])
109
  score_text = _clean(echo.get("score"))
 
110
  if url:
111
- lines.append(f" - [{title}]({url}) - score {score_text}; matched {matched or 'no shared terms'}")
 
 
 
112
  else:
113
- lines.append(f" - {title} - score {score_text}; matched {matched or 'no shared terms'}")
114
  lines.append("")
115
  return lines
116
 
 
67
  title = _clean(dot.get("title"))
68
  score = _clean(dot.get("score"))
69
  url = _clean(dot.get("url"))
70
+ page = _clean(dot.get("page_number")) or "?"
71
  if url:
72
+ lines.append(f"- Page {page}: [{title}]({url}) - echo score {score}")
73
  else:
74
+ lines.append(f"- Page {page}: {title} - echo score {score}")
75
 
76
  if last_artifact:
77
  lines.extend(
 
108
  url = _clean(project.get("url") or project.get("host") or "")
109
  matched = ", ".join(str(term) for term in echo.get("matched_terms") or [])
110
  score_text = _clean(echo.get("score"))
111
+ page = _clean(echo.get("page_number")) or "?"
112
  if url:
113
+ lines.append(
114
+ f" - Page {page}: [{title}]({url}) - score {score_text}; "
115
+ f"matched {matched or 'no shared terms'}"
116
+ )
117
  else:
118
+ lines.append(f" - Page {page}: {title} - score {score_text}; matched {matched or 'no shared terms'}")
119
  lines.append("")
120
  return lines
121
 
hackathon_advisor/scoring.py CHANGED
@@ -40,6 +40,7 @@ class ScoreCard:
40
  "echoes": [
41
  {
42
  "score": round(hit.score, 3),
 
43
  "matched_terms": list(hit.matched_terms),
44
  "project": hit.project.to_public_dict(),
45
  }
 
40
  "echoes": [
41
  {
42
  "score": round(hit.score, 3),
43
+ "page_number": hit.page_number,
44
  "matched_terms": list(hit.matched_terms),
45
  "project": hit.project.to_public_dict(),
46
  }
hackathon_advisor/wood_map.py CHANGED
@@ -43,6 +43,7 @@ def _echo_dot(hit: SearchHit) -> dict[str, Any]:
43
  dot = _project_dot(hit.project, "echo")
44
  dot["score"] = round(hit.score, 3)
45
  dot["matched_terms"] = list(hit.matched_terms)
 
46
  dot["radius"] = max(5, min(9, round(4 + hit.score * 14)))
47
  return dot
48
 
 
43
  dot = _project_dot(hit.project, "echo")
44
  dot["score"] = round(hit.score, 3)
45
  dot["matched_terms"] = list(hit.matched_terms)
46
+ dot["page_number"] = hit.page_number
47
  dot["radius"] = max(5, min(9, round(4 + hit.score * 14)))
48
  return dot
49
 
static/app.js CHANGED
@@ -198,7 +198,11 @@ function handleEvent(event) {
198
  session = event.state || {};
199
  session.profile = session.profile || {};
200
  session.targets = Array.isArray(session.targets) ? session.targets : [];
201
- if (event.projects?.length) renderProjects(event.projects);
 
 
 
 
202
  if (event.whitespace?.length) renderWhitespace(event.whitespace);
203
  renderTargets(session.targets);
204
  renderProfile(session.profile);
@@ -315,6 +319,30 @@ function renderProjects(projects) {
315
  }
316
  }
317
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  function renderWhitespace(items) {
319
  whitespaceEl.innerHTML = "";
320
  if (!items.length) {
@@ -414,6 +442,7 @@ function exportArtifact(artifact) {
414
  ctx.font = "28px Georgia, serif";
415
  ctx.fillStyle = "#6b4e35";
416
  wrapText(ctx, artifact.caption || "", 82, 252, 720, 36);
 
417
 
418
  ctx.save();
419
  ctx.translate(930, 226);
@@ -528,6 +557,21 @@ function drawWoodMap(ctx, map, x, y, width, height, verdict) {
528
  ctx.restore();
529
  }
530
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  function wrapText(ctx, text, x, y, maxWidth, lineHeight, align = "left") {
532
  const words = String(text).split(/\s+/);
533
  let line = "";
 
198
  session = event.state || {};
199
  session.profile = session.profile || {};
200
  session.targets = Array.isArray(session.targets) ? session.targets : [];
201
+ if (event.score?.echoes?.length) {
202
+ renderCitations(event.score.echoes);
203
+ } else if (event.projects?.length) {
204
+ renderProjects(event.projects);
205
+ }
206
  if (event.whitespace?.length) renderWhitespace(event.whitespace);
207
  renderTargets(session.targets);
208
  renderProfile(session.profile);
 
319
  }
320
  }
321
 
322
+ function renderCitations(echoes) {
323
+ projectsEl.innerHTML = "";
324
+ if (!echoes.length) {
325
+ projectsEl.innerHTML = `<div class="empty">No red ink yet.</div>`;
326
+ return;
327
+ }
328
+ for (const echo of echoes.slice(0, 5)) {
329
+ const project = echo.project || {};
330
+ const item = document.createElement("a");
331
+ item.className = "project citation";
332
+ item.href = project.url || project.host || "#";
333
+ item.target = "_blank";
334
+ item.rel = "noreferrer";
335
+ item.title = project.title || project.id || "Project citation";
336
+ const matched = (echo.matched_terms || []).slice(0, 5).join(", ") || "no shared terms";
337
+ item.innerHTML = `
338
+ <strong>Page ${escapeHtml(echo.page_number || "?")} · ${escapeHtml(project.title || project.id || "Untitled")}</strong>
339
+ <p>${escapeHtml(project.summary || project.id || "")}</p>
340
+ <span>${Number(echo.score || 0).toFixed(3)} · ${escapeHtml(matched)}</span>
341
+ `;
342
+ projectsEl.append(item);
343
+ }
344
+ }
345
+
346
  function renderWhitespace(items) {
347
  whitespaceEl.innerHTML = "";
348
  if (!items.length) {
 
442
  ctx.font = "28px Georgia, serif";
443
  ctx.fillStyle = "#6b4e35";
444
  wrapText(ctx, artifact.caption || "", 82, 252, 720, 36);
445
+ drawCitationList(ctx, seal.echoes || [], 742, 330, 330);
446
 
447
  ctx.save();
448
  ctx.translate(930, 226);
 
557
  ctx.restore();
558
  }
559
 
560
+ function drawCitationList(ctx, echoes, x, y, maxWidth) {
561
+ if (!echoes.length) return;
562
+ ctx.save();
563
+ ctx.fillStyle = "#6b4e35";
564
+ ctx.font = "800 18px Inter, sans-serif";
565
+ ctx.fillText("CLOSEST PAGES", x, y);
566
+ ctx.font = "700 15px Inter, sans-serif";
567
+ echoes.slice(0, 3).forEach((echo, index) => {
568
+ const project = echo.project || {};
569
+ const label = `Page ${echo.page_number || "?"}: ${project.title || project.id || "Untitled"}`;
570
+ wrapText(ctx, label, x, y + 24 + index * 26, maxWidth, 18);
571
+ });
572
+ ctx.restore();
573
+ }
574
+
575
  function wrapText(ctx, text, x, y, maxWidth, lineHeight, align = "left") {
576
  const words = String(text).split(/\s+/);
577
  let line = "";
static/styles.css CHANGED
@@ -322,6 +322,15 @@ button:disabled {
322
  line-height: 1.35;
323
  }
324
 
 
 
 
 
 
 
 
 
 
325
  .idea span {
326
  display: inline-block;
327
  margin-top: 6px;
 
322
  line-height: 1.35;
323
  }
324
 
325
+ .project span {
326
+ display: inline-block;
327
+ margin-top: 6px;
328
+ color: #70401e;
329
+ font-size: 0.74rem;
330
+ line-height: 1.25;
331
+ font-weight: 900;
332
+ }
333
+
334
  .idea span {
335
  display: inline-block;
336
  margin-top: 6px;
tests/test_agent.py CHANGED
@@ -31,6 +31,8 @@ def test_agent_scores_and_persists_idea() -> None:
31
  assert result.state["last_artifact"]["title"] == result.artifact["title"]
32
  assert result.artifact["wood_map"]["caption"]
33
  assert {dot["kind"] for dot in result.artifact["wood_map"]["dots"]} >= {"idea", "echo", "inked"}
 
 
34
  assert result.response
35
 
36
 
 
31
  assert result.state["last_artifact"]["title"] == result.artifact["title"]
32
  assert result.artifact["wood_map"]["caption"]
33
  assert {dot["kind"] for dot in result.artifact["wood_map"]["dots"]} >= {"idea", "echo", "inked"}
34
+ assert result.score.to_dict()["echoes"][0]["page_number"] >= 1
35
+ assert "page " in result.response
36
  assert result.response
37
 
38
 
tests/test_data.py CHANGED
@@ -11,6 +11,7 @@ def test_project_index_searches_snapshot() -> None:
11
 
12
  assert hits
13
  assert hits[0].project.id.startswith("build-small-hackathon/")
 
14
  assert index.index_algorithm == "tfidf-sparse-v1"
15
 
16
 
 
11
 
12
  assert hits
13
  assert hits[0].project.id.startswith("build-small-hackathon/")
14
+ assert hits[0].page_number >= 1
15
  assert index.index_algorithm == "tfidf-sparse-v1"
16
 
17
 
tests/test_field_notes.py CHANGED
@@ -31,6 +31,7 @@ def test_field_notes_markdown_contains_session_decisions() -> None:
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 "## Wood Map" in markdown
35
  assert "echo score" in markdown
36
  assert "Planner call: `make_plan`" 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
36
  assert "echo score" in markdown
37
  assert "Planner call: `make_plan`" in markdown