Conversation
…CI/CD - OpenAI streaming chat endpoint with get_events and provide_links tools - Server actions for knowledge CRUD, reseed, import, usage metrics - Event filtering/formatting utilities with timezone-aware LA time handling - System prompt builder with profile-aware personalization and prefix caching - Vector search context retrieval with retry/backoff - Knowledge base JSON (55 entries: FAQ, tracks, judging, submission, general) - CI/CD seed scripts for hackbot_knowledge to hackbot_docs - Auth session extended with position, is_beginner, name fields - Tailwind hackbot-slide-in animation keyframe - Dependencies: ai@6, @ai-sdk/openai
…ation Chat widget with streaming responses, retry logic, and resize handle. Event cards (full for workshops/activities, compact for meals/general). Markdown text renderer, session-gated wrapper, cascading animations. Layout integration in (hackers) route group.
…o hackbot-user-widget
There was a problem hiding this comment.
Pull request overview
Adds the HackBot “HackDavis Helper” chat widget to the hackers section, including streaming chat UX (events + links), a minimal markdown renderer, and small server-streaming refinements to support the UI.
Changes:
- Adds the HackBot widget UI (panel, header, message list, input, event cards) and mounts it in the
(hackers)layout with session gating. - Updates the HackBot streaming API to optionally disable the
get_eventstool based on query intent and tweaks streaming behavior for tool-first responses. - Adjusts stream request validation to tolerate tool-only assistant messages and increases the allowed total history size.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| app/(pages)/(hackers)/layout.tsx | Mounts the HackBot widget wrapper in the hackers route group and passes an initial profile derived from the server session. |
| app/(pages)/(hackers)/_components/Hackbot/MarkdownText.tsx | Adds a lightweight inline markdown renderer (bold/italic + line breaks). |
| app/(pages)/(hackers)/_components/Hackbot/HackbotWidgetWrapper.tsx | Client-side session gating for showing the widget only to hacker/admin users. |
| app/(pages)/(hackers)/_components/Hackbot/HackbotWidget.tsx | Main chat widget: streaming fetch, retries, resize, localStorage history, and event/link buffering. |
| app/(pages)/(hackers)/_components/Hackbot/HackbotMessageList.tsx | Renders chat bubbles, typing/retry indicators, links, and event cards/rows. |
| app/(pages)/(hackers)/_components/Hackbot/HackbotInputForm.tsx | Input textarea, send button, character counter, and suggestions accordion. |
| app/(pages)/(hackers)/_components/Hackbot/HackbotHeader.tsx | Chat header UI and close button. |
| app/(pages)/(hackers)/_components/Hackbot/HackbotEventCard.tsx | Event card UI with add/remove personal-schedule actions. |
| app/(api)/api/hackbot/stream/route.ts | Builds tools dynamically (conditionally includes get_events) and wires streaming response. |
| app/(api)/_utils/hackbot/stream/responseStream.ts | Alters text-suppression logic after tool results to support tool-first UX. |
| app/(api)/_utils/hackbot/stream/request.ts | Accepts empty assistant messages in history by dropping them; increases total history character cap. |
| app/(api)/_utils/hackbot/stream/intent.ts | Adds heuristic intent detection to disable get_events for non-schedule factual queries. |
| .claude/settings.json | Adds Claude tool permission settings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const content = line | ||
| .slice(2) | ||
| .trim() | ||
| .replace(/^"([\s\S]*)"$/, '$1') | ||
| .replace(/\\n/g, '\n') | ||
| .replace(/\\"/g, '"'); |
There was a problem hiding this comment.
Streaming text parsing is currently calling .trim() on each 0: text delta and manually stripping quotes/escapes. Because streaming deltas often start with a leading space (e.g. " world"), .trim() will remove significant whitespace and can concatenate words; the ad-hoc unescaping can also corrupt valid JSON-escaped sequences. Prefer JSON.parse(line.slice(2)) (no trim) inside a try/catch, and only append the parsed string as-is.
| const content = line | |
| .slice(2) | |
| .trim() | |
| .replace(/^"([\s\S]*)"$/, '$1') | |
| .replace(/\\n/g, '\n') | |
| .replace(/\\"/g, '"'); | |
| let content: string; | |
| try { | |
| const parsedContent = JSON.parse(line.slice(2)); | |
| if (typeof parsedContent !== 'string') { | |
| continue; | |
| } | |
| content = parsedContent; | |
| } catch { | |
| continue; | |
| } |
| while (keepReading) { | ||
| // eslint-disable-next-line no-await-in-loop | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| keepReading = false; | ||
| } else { | ||
| const chunk = decoder.decode(value, { stream: true }); | ||
| for (const line of chunk.split('\n')) { |
There was a problem hiding this comment.
The stream reader splits each received chunk on \n and processes lines immediately, but network chunking can split a logical line across reads. This can cause missed/garbled prefixes (0:, a:) and JSON parse failures. Keep a persistent buffer string across reader.read() calls, append new decoded text, and only process complete lines (leaving the trailing partial line in the buffer for the next iteration).
| while (keepReading) { | |
| // eslint-disable-next-line no-await-in-loop | |
| const { done, value } = await reader.read(); | |
| if (done) { | |
| keepReading = false; | |
| } else { | |
| const chunk = decoder.decode(value, { stream: true }); | |
| for (const line of chunk.split('\n')) { | |
| let lineBuffer = ''; | |
| while (keepReading) { | |
| // eslint-disable-next-line no-await-in-loop | |
| const { done, value } = await reader.read(); | |
| lineBuffer += done | |
| ? decoder.decode() | |
| : decoder.decode(value, { stream: true }); | |
| const lines = lineBuffer.split('\n'); | |
| if (done) { | |
| keepReading = false; | |
| lineBuffer = ''; | |
| } else { | |
| lineBuffer = lines.pop() ?? ''; | |
| } | |
| for (const line of lines) { |
| const reader = response.body?.getReader(); | ||
| const decoder = new TextDecoder(); | ||
|
|
||
| if (reader) { | ||
| let keepReading = true; |
There was a problem hiding this comment.
If response.body?.getReader() is unavailable (e.g., streaming not supported in a given runtime/browser), the code currently treats the request as successful but will leave the assistant message empty. Consider explicitly handling the !reader case (e.g., fall back to await response.text() or throw to trigger retry/error state).
| style={{ color: style?.text ?? '#003D3D' }} | ||
| > | ||
| {/* Left: datetime, location, tags */} | ||
| <div className="flex-1 min-w-0 space-y-1 space-between"> |
There was a problem hiding this comment.
space-between is not a Tailwind utility class (and there doesn't appear to be a project-defined .space-between class), so it has no effect here. If the intent is vertical distribution, use flex flex-col justify-between; otherwise remove the stray class to avoid confusion.
| <div className="flex-1 min-w-0 space-y-1 space-between"> | |
| <div className="flex-1 min-w-0 space-y-1"> |
| {/* Right: type badge, hosted by, recommended */} | ||
| <div className="shrink-0 flex flex-col items-end gap-1.5 pt-0.5 space-between"> | ||
| {style && ( | ||
| <span |
There was a problem hiding this comment.
space-between is not a Tailwind utility class (and there doesn't appear to be a project-defined .space-between class), so it has no effect here. If the intent is vertical distribution, use flex flex-col justify-between; otherwise remove the stray class to avoid confusion.
Add HackBot chat widget with event cards, markdown, and layout integration
MERGE ONLY AFTER #441 IS MERGED
Closes #443