From fbfe676ea00f0037966cdac5ae039fedb640bef3 Mon Sep 17 00:00:00 2001
From: reehals
Date: Sun, 4 Jan 2026 00:58:04 -0800
Subject: [PATCH 1/3] Hackbot init
---
app/(api)/_actions/hackbot/askHackbot.ts | 245 ++++++++
.../_datalib/hackbot/getHackbotContext.ts | 257 ++++++++
app/(api)/_datalib/hackbot/hackbotTypes.ts | 9 +
app/(api)/api/hackbot/route.ts | 42 ++
app/(pages)/(hackers)/(hub)/layout.tsx | 2 +
.../_components/Hackbot/HackbotWidget.tsx | 185 ++++++
.../ProjectInfo/JudgingInfo/JudgingInfo.tsx | 6 -
.../JudgingProcessAccordian..tsx | 15 +-
app/_data/hackbot_knowledge.json | 583 ++++++++++++++++++
scripts/hackbotSeed.mjs | 234 +++++++
10 files changed, 1559 insertions(+), 19 deletions(-)
create mode 100644 app/(api)/_actions/hackbot/askHackbot.ts
create mode 100644 app/(api)/_datalib/hackbot/getHackbotContext.ts
create mode 100644 app/(api)/_datalib/hackbot/hackbotTypes.ts
create mode 100644 app/(api)/api/hackbot/route.ts
create mode 100644 app/(pages)/(hackers)/_components/Hackbot/HackbotWidget.tsx
create mode 100644 app/_data/hackbot_knowledge.json
create mode 100644 scripts/hackbotSeed.mjs
diff --git a/app/(api)/_actions/hackbot/askHackbot.ts b/app/(api)/_actions/hackbot/askHackbot.ts
new file mode 100644
index 00000000..4a1f0e35
--- /dev/null
+++ b/app/(api)/_actions/hackbot/askHackbot.ts
@@ -0,0 +1,245 @@
+import { retrieveContext } from "@datalib/hackbot/getHackbotContext";
+
+export type HackbotMessageRole = "user" | "assistant" | "system";
+
+export interface HackbotMessage {
+ role: HackbotMessageRole;
+ content: string;
+}
+
+export interface HackbotResponse {
+ ok: boolean;
+ answer: string;
+ url?: string;
+ error?: string;
+ usage?: {
+ chat?: {
+ promptTokens?: number;
+ completionTokens?: number;
+ totalTokens?: number;
+ };
+ embeddings?: {
+ promptTokens?: number;
+ totalTokens?: number;
+ };
+ };
+}
+
+const MAX_USER_MESSAGE_CHARS = 200;
+const MAX_HISTORY_MESSAGES = 10;
+const MAX_ANSWER_WORDS = 180;
+
+function parseIsoToMs(value: unknown): number | null {
+ if (typeof value !== "string") return null;
+ const ms = Date.parse(value);
+ return Number.isFinite(ms) ? ms : null;
+}
+
+function truncateToWords(text: string, maxWords: number): string {
+ const words = text.trim().split(/\s+/);
+ if (words.length <= maxWords) return text.trim();
+ return words.slice(0, maxWords).join(" ") + "...";
+}
+
+function stripExternalDomains(text: string): string {
+ // Replace absolute URLs like https://hackdavis.io/path with just /path.
+ return text.replace(/https?:\/\/[^\s)]+(\/[\w#/?=&.-]*)/g, "$1");
+}
+
+export async function askHackbot(
+ messages: HackbotMessage[]
+): Promise {
+ if (!messages.length) {
+ return { ok: false, answer: "", error: "No messages provided." };
+ }
+
+ const last = messages[messages.length - 1];
+
+ if (last.role !== "user") {
+ return { ok: false, answer: "", error: "Last message must be from user." };
+ }
+
+ if (last.content.length > MAX_USER_MESSAGE_CHARS) {
+ return {
+ ok: false,
+ answer: "",
+ error: `Message too long. Please keep it under ${MAX_USER_MESSAGE_CHARS} characters.`,
+ };
+ }
+
+ const trimmedHistory = messages.slice(-MAX_HISTORY_MESSAGES);
+
+ let docs;
+ let embeddingsUsage:
+ | {
+ promptTokens?: number;
+ totalTokens?: number;
+ }
+ | undefined;
+ try {
+ // Use a single, general context size (vector-only) to avoid
+ // question-specific limits or retrieval heuristics.
+ ({ docs, usage: embeddingsUsage } = await retrieveContext(last.content, {
+ limit: 25,
+ }));
+ } catch (e) {
+ console.error("Hackbot context retrieval error", e);
+ return {
+ ok: false,
+ answer: "",
+ error:
+ "HackDavis Helper search backend is not configured (vector search unavailable). Please contact an organizer.",
+ };
+ }
+
+ if (!docs || docs.length === 0) {
+ return {
+ ok: false,
+ answer: "",
+ error:
+ "HackDavis Helper could not find any context documents in its vector index. Please contact an organizer.",
+ };
+ }
+
+ // Present event context in chronological order so the model doesn't
+ // “pick a few” out of order when asked for itinerary/timeline questions.
+ const sortedDocs = (() => {
+ const eventDocs = docs.filter((d: any) => d.type === "event");
+ const otherDocs = docs.filter((d: any) => d.type !== "event");
+
+ eventDocs.sort((a: any, b: any) => {
+ const aMs = parseIsoToMs(a.startISO);
+ const bMs = parseIsoToMs(b.startISO);
+
+ if (aMs === null && bMs === null) return 0;
+ if (aMs === null) return 1;
+ if (bMs === null) return -1;
+ return aMs - bMs;
+ });
+
+ return [...eventDocs, ...otherDocs];
+ })();
+
+ const primaryUrl =
+ sortedDocs.find((d) => d.type === "event" && d.url)?.url ??
+ sortedDocs.find((d) => d.url)?.url;
+
+ const contextSummary = sortedDocs
+ .map((d, index) => {
+ const header = `${index + 1}) [type=${d.type}, title="${d.title}"${
+ d.url ? `, url="${d.url}"` : ""
+ }]`;
+ return `${header}\n${d.text}`;
+ })
+ .join("\n\n");
+
+ const systemPrompt =
+ "You are HackDavis Helper, an assistant for the HackDavis hackathon. " +
+ 'You have a friendly personality and introduce yourself as "Hacky". ' +
+ 'You may respond warmly to simple greetings (like "hi" or "hello") by saying something like: "Hi, I am Hacky! I can help with questions about HackDavis." ' +
+ 'You should happily answer high-level questions like "What is HackDavis?" as long as they are clearly about the HackDavis hackathon. ' +
+ "Only refuse questions that are clearly unrelated to HackDavis or hackathons (for example, general trivia, homework, or other topics with no mention of HackDavis). " +
+ 'For clearly unrelated questions, respond in a brief, friendly way such as: "Sorry, I can only answer questions about HackDavis." and optionally add a short follow-up like: "Do you have any questions about HackDavis?" ' +
+ 'For all other questions that mention HackDavis or obviously refer to the event (including "What is HackDavis?"), provide a concise, helpful answer based on your general knowledge of the event and the provided context. ' +
+ "Keep every answer under 100 words. Prefer short, direct answers. " +
+ "First, silently pick the single most relevant context document by matching the user’s key terms to the document title (especially for event questions). " +
+ "If multiple events look plausible (similar names), ask one short clarifying question instead of guessing. " +
+ "For time/location questions, strongly prefer documents with type=event. " +
+ "When listing multiple schedule items (timeline/schedule/agenda/itinerary), format your answer as a bullet list (one item per line) using only items found in the context. " +
+ "If the user asks for itinerary/timeline, order items chronologically by the start time in the context. Do not present a random subset if more relevant items are available in the context. " +
+ "When giving times or locations, you MUST only use times, dates, and locations that explicitly appear in the provided context text. Do NOT use generic knowledge about hackathons. " +
+ 'If a question is asking "When is" or "What time is" a specific event, and the context contains both a "Starts" line and an "Ends" line for that event, answer with the full range (for example: "The Closing Ceremony is from 3:00 PM to 4:00 PM Pacific Time."). ' +
+ "If only a start time is present, answer with the start time. If only an end time is present, answer with the end time. Do not answer with only the end time when both are available. " +
+ 'In particular, never say that a hackathon "ends on the same day it starts" or that it ends at 11:59 PM unless that exact wording appears in the context. ' +
+ 'If you cannot find an explicit time or place for what the user asked, say: "I do not know the exact time from the current schedule." ' +
+ "Do not include any URLs in your answer text. The UI will show a separate “More info” link when available. " +
+ 'Never invent domains such as "hackdavis.com" or new anchors. ' +
+ "Write like a helpful human: use contractions, avoid robotic phrases, and answer in 1–3 short sentences unless the user asks for steps. " +
+ "Never generate code or answer homework, programming, or general knowledge questions.";
+
+ // Prepare messages for the chat model (Ollama, GPT-compatible schema)
+ const chatMessages = [
+ { role: "system", content: systemPrompt },
+ {
+ role: "system",
+ content: `Context documents about HackDavis (use these to answer):\n\n${contextSummary}`,
+ },
+ ...trimmedHistory.map((m) => ({
+ role: m.role,
+ content: m.content,
+ })),
+ ];
+
+ const ollamaUrl = process.env.OLLAMA_URL || "http://localhost:11434";
+
+ try {
+ const startedAt = Date.now();
+ const response = await fetch(`${ollamaUrl}/api/chat`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ model: "llama3.2",
+ messages: chatMessages,
+ stream: false,
+ }),
+ });
+
+ if (!response.ok) {
+ return {
+ ok: false,
+ answer: "",
+ error: "Upstream model error. Please try again later.",
+ };
+ }
+
+ const data = await response.json();
+ const rawAnswer: string = data?.message?.content?.toString() ?? "";
+
+ const promptTokens =
+ typeof data?.prompt_eval_count === "number"
+ ? data.prompt_eval_count
+ : undefined;
+ const completionTokens =
+ typeof data?.eval_count === "number" ? data.eval_count : undefined;
+ const totalTokens =
+ typeof promptTokens === "number" && typeof completionTokens === "number"
+ ? promptTokens + completionTokens
+ : undefined;
+
+ console.log("[hackbot][ollama][chat]", {
+ model: data?.model ?? "unknown",
+ promptTokens,
+ completionTokens,
+ totalTokens,
+ ms: Date.now() - startedAt,
+ });
+
+ const answer = truncateToWords(
+ stripExternalDomains(rawAnswer),
+ MAX_ANSWER_WORDS
+ );
+
+ return {
+ ok: true,
+ answer,
+ url: primaryUrl,
+ usage: {
+ chat: {
+ promptTokens,
+ completionTokens,
+ totalTokens,
+ },
+ embeddings: embeddingsUsage,
+ },
+ };
+ } catch (e) {
+ console.error("Hackbot error", e);
+ return {
+ ok: false,
+ answer: "",
+ error: "Something went wrong. Please try again later.",
+ };
+ }
+}
diff --git a/app/(api)/_datalib/hackbot/getHackbotContext.ts b/app/(api)/_datalib/hackbot/getHackbotContext.ts
new file mode 100644
index 00000000..ec5b5eaf
--- /dev/null
+++ b/app/(api)/_datalib/hackbot/getHackbotContext.ts
@@ -0,0 +1,257 @@
+import { HackDoc, HackDocType } from "./hackbotTypes";
+import { getDatabase } from "@utils/mongodb/mongoClient.mjs";
+import { ObjectId } from "mongodb";
+
+export interface RetrievedContext {
+ docs: HackDoc[];
+ usage?: {
+ promptTokens?: number;
+ totalTokens?: number;
+ };
+}
+
+function formatEventDateTime(raw: unknown): string | null {
+ let date: Date | null = null;
+
+ if (raw instanceof Date) {
+ date = raw;
+ } else if (typeof raw === "string") {
+ date = new Date(raw);
+ } else if (raw && typeof raw === "object" && "$date" in (raw as any)) {
+ date = new Date((raw as any).$date);
+ }
+
+ if (!date || Number.isNaN(date.getTime())) return null;
+
+ return date.toLocaleString("en-US", {
+ timeZone: "America/Los_Angeles",
+ weekday: "short",
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ });
+}
+
+function formatLiveEventDoc(event: any): {
+ title: string;
+ text: string;
+ url: string;
+ startISO?: string;
+ endISO?: string;
+} {
+ const title = String(event?.name || "Event");
+ const type = event?.type ? String(event.type) : "";
+ const start = formatEventDateTime(event?.start_time);
+ const end = formatEventDateTime(event?.end_time);
+ const location = event?.location ? String(event.location) : "";
+ const host = event?.host ? String(event.host) : "";
+ const tags = Array.isArray(event?.tags) ? event.tags.map(String) : [];
+
+ const parts = [
+ `Event: ${title}`,
+ type ? `Type: ${type}` : "",
+ // Machine-readable anchors to allow reliable chronological ordering.
+ event?.start_time instanceof Date
+ ? `StartISO: ${event.start_time.toISOString()}`
+ : "",
+ event?.end_time instanceof Date
+ ? `EndISO: ${event.end_time.toISOString()}`
+ : "",
+ start ? `Starts (Pacific Time): ${start}` : "",
+ end ? `Ends (Pacific Time): ${end}` : "",
+ location ? `Location: ${location}` : "",
+ host ? `Host: ${host}` : "",
+ tags.length ? `Tags: ${tags.join(", ")}` : "",
+ ].filter(Boolean);
+
+ return {
+ title,
+ text: parts.join("\n"),
+ url: "/hackers/hub/schedule",
+ startISO:
+ event?.start_time instanceof Date
+ ? event.start_time.toISOString()
+ : undefined,
+ endISO:
+ event?.end_time instanceof Date
+ ? event.end_time.toISOString()
+ : undefined,
+ };
+}
+
+async function getQueryEmbedding(query: string): Promise<{
+ embedding: number[];
+ usage?: {
+ promptTokens?: number;
+ totalTokens?: number;
+ };
+} | null> {
+ const ollamaUrl = process.env.OLLAMA_URL || "http://localhost:11434";
+
+ try {
+ const startedAt = Date.now();
+ const res = await fetch(`${ollamaUrl}/api/embeddings`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ model: "llama3.2", prompt: query }),
+ });
+
+ if (!res.ok) {
+ console.error(
+ "[hackbot][embeddings] Upstream error",
+ res.status,
+ res.statusText
+ );
+ return null;
+ }
+
+ const data = await res.json();
+ if (!data || !Array.isArray(data.embedding)) {
+ console.error("[hackbot][embeddings] Invalid response shape");
+ return null;
+ }
+
+ const promptTokens =
+ typeof data?.prompt_eval_count === "number"
+ ? data.prompt_eval_count
+ : undefined;
+ const totalTokens =
+ typeof data?.eval_count === "number"
+ ? data.eval_count
+ : typeof promptTokens === "number"
+ ? promptTokens
+ : undefined;
+
+ console.log("[hackbot][ollama][embeddings]", {
+ model: data?.model ?? "unknown",
+ promptTokens,
+ totalTokens,
+ ms: Date.now() - startedAt,
+ });
+
+ return {
+ embedding: data.embedding as number[],
+ usage: {
+ promptTokens,
+ totalTokens,
+ },
+ };
+ } catch (err) {
+ console.error("[hackbot][embeddings] Failed to get embedding", err);
+ return null;
+ }
+}
+
+export async function retrieveContext(
+ query: string,
+ opts?: { limit?: number; preferredTypes?: HackDocType[] }
+): Promise {
+ const limit = opts?.limit ?? 25;
+ const trimmed = query.trim();
+
+ // Vector-only search over hackbot_docs in MongoDB.
+ try {
+ const embeddingResult = await getQueryEmbedding(trimmed);
+ if (!embeddingResult) {
+ console.error(
+ "[hackbot][retrieve] No embedding available for query; vector search required."
+ );
+ throw new Error("Embedding unavailable");
+ }
+
+ const embedding = embeddingResult.embedding;
+
+ const db = await getDatabase();
+ const collection = db.collection("hackbot_docs");
+
+ const preferredTypes = opts?.preferredTypes?.length
+ ? Array.from(new Set(opts.preferredTypes))
+ : null;
+
+ const numCandidates = Math.min(200, Math.max(50, limit * 10));
+
+ const vectorResults = await collection
+ .aggregate([
+ {
+ $vectorSearch: {
+ index: "hackbot_vector_index",
+ queryVector: embedding,
+ path: "embedding",
+ numCandidates,
+ limit,
+ ...(preferredTypes
+ ? {
+ filter: {
+ type: { $in: preferredTypes },
+ },
+ }
+ : {}),
+ },
+ },
+ ])
+ .toArray();
+
+ if (!vectorResults.length) {
+ console.warn("[hackbot][retrieve] Vector search returned no results.");
+ return { docs: [] };
+ }
+
+ const docs: HackDoc[] = vectorResults.map((doc: any) => ({
+ id: String(doc._id),
+ type: doc.type,
+ title: doc.title,
+ text: doc.text,
+ url: doc.url ?? undefined,
+ }));
+
+ // Hydrate event docs from the live `events` collection so the answer
+ // always reflects the current schedule (times/locations), even if the
+ // vector index was seeded earlier.
+ const eventsCollection = db.collection("events");
+ await Promise.all(
+ docs.map(async (d) => {
+ if (d.type !== "event") return;
+
+ const suffix = d.id.startsWith("event-")
+ ? d.id.slice("event-".length)
+ : "";
+ let event: any | null = null;
+
+ if (suffix && ObjectId.isValid(suffix)) {
+ event = await eventsCollection.findOne({ _id: new ObjectId(suffix) });
+ }
+
+ if (!event && d.title) {
+ event = await eventsCollection.findOne({ name: d.title });
+ }
+
+ if (!event) return;
+
+ const live = formatLiveEventDoc(event);
+ d.title = live.title;
+ d.text = live.text;
+ d.url = live.url;
+
+ // Attach sortable timestamps for server-side ordering.
+ (d as any).startISO = live.startISO;
+ (d as any).endISO = live.endISO;
+ })
+ );
+
+ console.log("[hackbot][retrieve][vector]", {
+ query: trimmed,
+ docIds: docs.map((d) => d.id),
+ titles: docs.map((d) => d.title),
+ });
+
+ return { docs, usage: embeddingResult.usage };
+ } catch (err) {
+ console.error(
+ "[hackbot][retrieve] Vector search failed (no fallback).",
+ err
+ );
+ throw err;
+ }
+}
diff --git a/app/(api)/_datalib/hackbot/hackbotTypes.ts b/app/(api)/_datalib/hackbot/hackbotTypes.ts
new file mode 100644
index 00000000..779f4633
--- /dev/null
+++ b/app/(api)/_datalib/hackbot/hackbotTypes.ts
@@ -0,0 +1,9 @@
+export type HackDocType = 'event' | 'track' | 'judging' | 'submission';
+
+export interface HackDoc {
+ id: string;
+ type: HackDocType;
+ title: string;
+ text: string;
+ url?: string;
+}
diff --git a/app/(api)/api/hackbot/route.ts b/app/(api)/api/hackbot/route.ts
new file mode 100644
index 00000000..e5fceec1
--- /dev/null
+++ b/app/(api)/api/hackbot/route.ts
@@ -0,0 +1,42 @@
+import { NextRequest, NextResponse } from "next/server";
+import { askHackbot, HackbotMessage } from "@actions/hackbot/askHackbot";
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const messages = (body?.messages ?? []) as HackbotMessage[];
+
+ if (!Array.isArray(messages) || messages.length === 0) {
+ return NextResponse.json(
+ {
+ ok: false,
+ answer: "",
+ error: "Invalid request body. Expected { messages: [...] }.",
+ },
+ { status: 400 }
+ );
+ }
+
+ const result = await askHackbot(messages);
+
+ return NextResponse.json(
+ {
+ ok: result.ok,
+ answer: result.answer,
+ url: result.url,
+ usage: result.usage ?? null,
+ error: result.error ?? null,
+ },
+ { status: result.ok ? 200 : 400 }
+ );
+ } catch (e) {
+ return NextResponse.json(
+ {
+ ok: false,
+ answer: "",
+ error: "Invalid JSON body.",
+ },
+ { status: 400 }
+ );
+ }
+}
diff --git a/app/(pages)/(hackers)/(hub)/layout.tsx b/app/(pages)/(hackers)/(hub)/layout.tsx
index 602aadd8..9e806735 100644
--- a/app/(pages)/(hackers)/(hub)/layout.tsx
+++ b/app/(pages)/(hackers)/(hub)/layout.tsx
@@ -1,5 +1,6 @@
import ProtectedDisplay from '@components/ProtectedDisplay/ProtectedDisplay';
import Navbar from '@components/Navbar/Navbar';
+import HackbotWidget from '@pages/(hackers)/_components/Hackbot/HackbotWidget';
export default function Layout({ children }: { children: React.ReactNode }) {
return (
@@ -9,6 +10,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
>
{children}
+
);
}
diff --git a/app/(pages)/(hackers)/_components/Hackbot/HackbotWidget.tsx b/app/(pages)/(hackers)/_components/Hackbot/HackbotWidget.tsx
new file mode 100644
index 00000000..a05b701a
--- /dev/null
+++ b/app/(pages)/(hackers)/_components/Hackbot/HackbotWidget.tsx
@@ -0,0 +1,185 @@
+'use client';
+
+import { useState } from 'react';
+import { Button } from '@pages/_globals/components/ui/button';
+
+export type HackbotChatMessage = {
+ role: 'user' | 'assistant';
+ content: string;
+ url?: string;
+};
+
+const MAX_USER_MESSAGE_CHARS = 200;
+
+export default function HackbotWidget() {
+ const [open, setOpen] = useState(false);
+ const [messages, setMessages] = useState([]);
+ const [input, setInput] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const canSend =
+ !loading &&
+ input.trim().length > 0 &&
+ input.trim().length <= MAX_USER_MESSAGE_CHARS;
+
+ const toggleOpen = () => {
+ setOpen((prev) => !prev);
+ setError(null);
+ };
+
+ const sendMessage = async () => {
+ if (!canSend) return;
+
+ const userMessage: HackbotChatMessage = {
+ role: 'user',
+ content: input.trim(),
+ };
+
+ setMessages((prev) => [...prev, userMessage]);
+ setInput('');
+ setError(null);
+ setLoading(true);
+
+ try {
+ const response = await fetch('/api/hackbot', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ messages: [
+ ...messages.map((m) => ({ role: m.role, content: m.content })),
+ { role: 'user', content: userMessage.content },
+ ],
+ }),
+ });
+
+ const data = await response.json();
+
+ if (!data.ok) {
+ setError(data.error || 'Something went wrong.');
+ }
+
+ const assistantMessage: HackbotChatMessage = {
+ role: 'assistant',
+ content: data.answer || 'Sorry, I could not answer that.',
+ url: data.url || undefined,
+ };
+
+ setMessages((prev) => [...prev, assistantMessage]);
+ } catch (err) {
+ setError('Network error. Please try again.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ await sendMessage();
+ };
+
+ return (
+
+ {open && (
+
+
+
+
+ {messages.length === 0 && (
+
+ Try asking: "When does hacking end?" or "Where is the opening
+ ceremony?"
+
+ )}
+
+ {messages.map((m, idx) => (
+
+ ))}
+
+
+
+
+ )}
+
+
+
+ );
+}
diff --git a/app/(pages)/(hackers)/_components/ProjectInfo/JudgingInfo/JudgingInfo.tsx b/app/(pages)/(hackers)/_components/ProjectInfo/JudgingInfo/JudgingInfo.tsx
index 887e78c4..b6abb0b0 100644
--- a/app/(pages)/(hackers)/_components/ProjectInfo/JudgingInfo/JudgingInfo.tsx
+++ b/app/(pages)/(hackers)/_components/ProjectInfo/JudgingInfo/JudgingInfo.tsx
@@ -4,7 +4,6 @@ import ProjectInfoAccordion, {
AccordionItemInt,
} from '../ProjectInfoAccordion/ProjectInfoAccordion';
import SubmissionDue from './JudgingSteps/SubmissionDue/SubmissionDue';
-import ImportantAnnouncement from './JudgingSteps/ImportantAnnouncement/ImportantAnnouncement';
import DemoTime from './JudgingSteps/DemoTime/DemoTime';
import Break from './JudgingSteps/Break/Break';
import ClosingCeremony from './JudgingSteps/ClosingCeremony/ClosingCeremony';
@@ -18,11 +17,6 @@ const accordionItems: AccordionItemInt[] = [
title: 'Submission Due',
content: ,
},
- {
- subtitle: '11:30 - 12:00 AM',
- title: 'Important Announcement',
- content: ,
- },
{
subtitle: '12:00 - 2:00 PM',
title: 'Demo Time',
diff --git a/app/(pages)/(hackers)/_components/ProjectInfo/JudgingProcess/JudgingProcessAccordian..tsx b/app/(pages)/(hackers)/_components/ProjectInfo/JudgingProcess/JudgingProcessAccordian..tsx
index 77463f74..7790d074 100644
--- a/app/(pages)/(hackers)/_components/ProjectInfo/JudgingProcess/JudgingProcessAccordian..tsx
+++ b/app/(pages)/(hackers)/_components/ProjectInfo/JudgingProcess/JudgingProcessAccordian..tsx
@@ -6,7 +6,6 @@ import styles from './JudgingProcessAccordian.module.scss';
import { CgChevronLeft } from 'react-icons/cg';
import { PiStarFourFill } from 'react-icons/pi';
import Step1 from 'public/hackers/project-info/Step1.svg';
-import Step2 from 'public/hackers/project-info/Step2.svg';
// import Step3 from 'public/hackers/project-info/Step3.svg';
// import Step4 from 'public/hackers/project-info/Step4.svg';
import Step5 from 'public/hackers/project-info/Step5.svg';
@@ -31,22 +30,12 @@ const JudgingProcessAccordian = () => {
),
},
{
- step: '11:00-11:30 AM',
- question: 'Important Announcement',
- answer: (
-
-
-
Register for the event.
-
- ),
- },
- {
- step: '11:30 - 1:30 PM',
+ step: '12:00 - 2:00 PM',
question: 'Demo Time',
answer: ,
},
{
- step: '1:30 - 2:30 PM',
+ step: '2:00 - 3:00 PM',
question: 'Break',
answer: ,
},
diff --git a/app/_data/hackbot_knowledge.json b/app/_data/hackbot_knowledge.json
new file mode 100644
index 00000000..bdd6680f
--- /dev/null
+++ b/app/_data/hackbot_knowledge.json
@@ -0,0 +1,583 @@
+{
+ "meta": {
+ "year": 2025,
+ "timezone": "America/Los_Angeles"
+ },
+ "validation": {
+ "domains": [
+ "swe",
+ "business",
+ "aiml",
+ "hardware",
+ "design",
+ "medtech"
+ ],
+ "tracks": [
+ "Best Hack for Social Good",
+ "Best Beginner Hack",
+ "Best Interdisciplinary Hack",
+ "Most Creative Hack",
+ "Best Hack for Social Justice",
+ "Best Hardware Hack",
+ "Most Technically Challenging Hack",
+ "Best Open Data Hack",
+ "Best AI/ML Hack",
+ "Best UI/UX Design",
+ "Best User Research",
+ "Best Statistical Model",
+ "Best Medical Hack",
+ "Best Entrepreneurship Hack",
+ "Hacker's Choice Award",
+ "Best Hack for California GovOps Agency",
+ "Best Hack for NAMI Yolo",
+ "Best Hack for Fourth and Hope",
+ "Best Use of Cerebras API",
+ "Best Use of Vectara",
+ "Best Use of Gemini API",
+ "Best Use of MongoDB Atlas",
+ "Best .Tech Domain Name",
+ "Best Use of Auth0",
+ "Best Use of Snowflake API",
+ "Best Assistive Technology"
+ ]
+ },
+ "tracks": [
+ "Best Hack for Social Good",
+ "Best Beginner Hack",
+ "Best Interdisciplinary Hack",
+ "Most Creative Hack",
+ "Best Hack for Social Justice",
+ "Best Hardware Hack",
+ "Most Technically Challenging Hack",
+ "Best Open Data Hack",
+ "Best AI/ML Hack",
+ "Best UI/UX Design",
+ "Best User Research",
+ "Best Statistical Model",
+ "Best Medical Hack",
+ "Best Entrepreneurship Hack",
+ "Hacker's Choice Award",
+ "Best Hack for California GovOps Agency",
+ "Best Hack for NAMI Yolo",
+ "Best Hack for Fourth and Hope",
+ "Best Use of Cerebras API",
+ "Best Use of Vectara",
+ "Best Use of Gemini API",
+ "Best Use of MongoDB Atlas",
+ "Best .Tech Domain Name",
+ "Best Use of Auth0",
+ "Best Use of Snowflake API",
+ "Best Assistive Technology"
+ ],
+ "events": {
+ "raw": [
+ {
+ "name": "Check-in Starts",
+ "type": "GENERAL",
+ "start_time": {
+ "$date": "2025-04-19T14:30:00.000Z"
+ }
+ },
+ {
+ "name": "Team Mixer",
+ "type": "ACTIVITIES",
+ "start_time": {
+ "$date": "2025-04-19T15:30:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-19T17:00:00.000Z"
+ },
+ "location": "ARC Ballroom A"
+ },
+ {
+ "name": "Opening Ceremony",
+ "type": "GENERAL",
+ "start_time": {
+ "$date": "2025-04-19T17:00:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-19T18:00:00.000Z"
+ }
+ },
+ {
+ "name": "Hacking Begins",
+ "type": "GENERAL",
+ "start_time": {
+ "$date": "2025-04-19T18:00:00.000Z"
+ }
+ },
+ {
+ "name": "Check-in Closes",
+ "type": "GENERAL",
+ "start_time": {
+ "$date": "2025-04-19T23:00:00.000Z"
+ }
+ },
+ {
+ "name": "Closing Ceremony",
+ "type": "GENERAL",
+ "start_time": {
+ "$date": "2025-04-20T22:00:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T23:00:00.000Z"
+ }
+ },
+ {
+ "name": "Hacking Ends",
+ "type": "GENERAL",
+ "start_time": {
+ "$date": "2025-04-20T18:00:00.000Z"
+ }
+ },
+ {
+ "name": "Spaghetti & Marshmallow",
+ "type": "ACTIVITIES",
+ "start_time": {
+ "$date": "2025-04-19T21:00:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-19T22:00:00.000Z"
+ },
+ "location": "North Wing"
+ },
+ {
+ "name": "Bracelet Making",
+ "type": "ACTIVITIES",
+ "start_time": {
+ "$date": "2025-04-19T22:30:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T00:30:00.000Z"
+ },
+ "location": "North Wing"
+ },
+ {
+ "name": "Therapy Dogs",
+ "type": "ACTIVITIES",
+ "start_time": {
+ "$date": "2025-04-19T22:30:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T00:00:00.000Z"
+ },
+ "location": "South Wing"
+ },
+ {
+ "name": "Jeopardy/Kahoot",
+ "type": "ACTIVITIES",
+ "start_time": {
+ "$date": "2025-04-20T04:30:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T05:30:00.000Z"
+ },
+ "location": "ARC Ballroom B"
+ },
+ {
+ "name": "Demos",
+ "type": "GENERAL",
+ "start_time": {
+ "$date": "2025-04-20T19:00:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T21:00:00.000Z"
+ }
+ },
+ {
+ "name": "Panel Judging",
+ "type": "GENERAL",
+ "start_time": {
+ "$date": "2025-04-20T21:00:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T22:00:00.000Z"
+ }
+ },
+ {
+ "name": "Hackathons 101",
+ "type": "WORKSHOPS",
+ "start_time": {
+ "$date": "2025-04-19T18:40:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-19T20:00:00.000Z"
+ },
+ "location": "ARC Ballroom A",
+ "host": "HackDavis",
+ "tags": [
+ "beginner",
+ "developer",
+ "designer",
+ "pm",
+ "other"
+ ]
+ },
+ {
+ "name": "Surprise Mini-Event!",
+ "type": "WORKSHOPS",
+ "start_time": {
+ "$date": "2025-04-20T03:10:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T04:00:00.000Z"
+ },
+ "location": "ARC Ballroom A",
+ "host": "Major League Hacking",
+ "tags": [
+ "beginner",
+ "developer",
+ "pm",
+ "designer",
+ "other"
+ ]
+ },
+ {
+ "name": "Dinner Starts",
+ "type": "MEALS",
+ "start_time": {
+ "$date": "2025-04-20T02:00:00.000Z"
+ }
+ },
+ {
+ "name": "Brunch Starts",
+ "type": "MEALS",
+ "start_time": {
+ "$date": "2025-04-20T16:30:00.000Z"
+ }
+ },
+ {
+ "name": "Getting Started with Git & GitHub",
+ "type": "WORKSHOPS",
+ "start_time": {
+ "$date": "2025-04-19T20:10:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-19T22:00:00.000Z"
+ },
+ "location": "ARC Ballroom A",
+ "host": "UC Davis DataLab",
+ "tags": [
+ "beginner",
+ "developer"
+ ]
+ },
+ {
+ "name": "GitHub Copilot Workshop",
+ "type": "WORKSHOPS",
+ "start_time": {
+ "$date": "2025-04-19T22:10:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-19T23:00:00.000Z"
+ },
+ "location": "ARC Ballroom A",
+ "host": "Major League Hacking",
+ "tags": [
+ "beginner",
+ "developer",
+ "pm"
+ ]
+ },
+ {
+ "name": "Tech Together Meetup",
+ "type": "WORKSHOPS",
+ "start_time": {
+ "$date": "2025-04-19T23:10:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T00:00:00.000Z"
+ },
+ "location": "ARC Ballroom A",
+ "host": "Major League Hacking",
+ "tags": [
+ "beginner",
+ "developer",
+ "pm",
+ "designer",
+ "other"
+ ]
+ },
+ {
+ "name": "Intro to UI/UX",
+ "type": "WORKSHOPS",
+ "start_time": {
+ "$date": "2025-04-19T23:10:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T00:00:00.000Z"
+ },
+ "location": "ARC Ballroom B",
+ "host": "Design Interactive",
+ "tags": [
+ "beginner",
+ "designer"
+ ]
+ },
+ {
+ "name": "Lunch Starts",
+ "type": "MEALS",
+ "start_time": {
+ "$date": "2025-04-19T20:00:00.000Z"
+ }
+ },
+ {
+ "name": "Software Development",
+ "type": "WORKSHOPS",
+ "start_time": {
+ "$date": "2025-04-19T20:10:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-19T21:00:00.000Z"
+ },
+ "location": "ARC Ballroom B",
+ "host": "CodeLab",
+ "tags": [
+ "beginner",
+ "developer",
+ "pm"
+ ]
+ },
+ {
+ "name": "Intro to Freepik AI Suite",
+ "type": "WORKSHOPS",
+ "start_time": {
+ "$date": "2025-04-20T00:10:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T01:00:00.000Z"
+ },
+ "location": "ARC Ballroom B",
+ "host": "Freepik",
+ "tags": [
+ "developer",
+ "pm",
+ "designer",
+ "other"
+ ]
+ },
+ {
+ "name": "Break",
+ "type": "GENERAL",
+ "start_time": {
+ "$date": "2025-04-20T18:00:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T19:00:00.000Z"
+ }
+ },
+ {
+ "name": "Hacking with LLMs",
+ "type": "WORKSHOPS",
+ "start_time": {
+ "$date": "2025-04-19T21:10:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-19T22:00:00.000Z"
+ },
+ "location": "ARC Ballroom B",
+ "host": "Miguel Acevedo, Founder @ Marble",
+ "tags": [
+ "beginner",
+ "developer",
+ "pm"
+ ]
+ },
+ {
+ "name": "Intro to Letta AI Agents Framework",
+ "type": "WORKSHOPS",
+ "start_time": {
+ "$date": "2025-04-20T00:10:00.000Z"
+ },
+ "end_time": {
+ "$date": "2025-04-20T01:00:00.000Z"
+ },
+ "location": "ARC Ballroom A",
+ "host": "Letta",
+ "tags": [
+ "developer"
+ ]
+ }
+ ],
+ "key": [
+ {
+ "id": "check-in-starts",
+ "name": "Check-in Starts",
+ "category": "schedule",
+ "type": "GENERAL",
+ "start": "2025-04-19T14:30:00.000Z",
+ "end": null,
+ "location": null,
+ "host": null,
+ "url": "/hackers/hub/schedule"
+ },
+ {
+ "id": "opening-ceremony",
+ "name": "Opening Ceremony",
+ "category": "schedule",
+ "type": "GENERAL",
+ "start": "2025-04-19T17:00:00.000Z",
+ "end": "2025-04-19T18:00:00.000Z",
+ "location": null,
+ "host": null,
+ "url": "/hackers/hub/schedule"
+ },
+ {
+ "id": "hacking-begins",
+ "name": "Hacking Begins",
+ "category": "schedule",
+ "type": "GENERAL",
+ "start": "2025-04-19T18:00:00.000Z",
+ "end": null,
+ "location": null,
+ "host": null,
+ "url": "/hackers/hub/schedule"
+ },
+ {
+ "id": "hacking-ends",
+ "name": "Hacking Ends",
+ "category": "schedule",
+ "type": "GENERAL",
+ "start": "2025-04-20T18:00:00.000Z",
+ "end": null,
+ "location": null,
+ "host": null,
+ "summary": "Hacking for HackDavis ends on Sunday at 11:00 AM Pacific Time. All work must be completed and ready for submission before this deadline.",
+ "url": "/hackers/hub/schedule"
+ },
+ {
+ "id": "closing-ceremony",
+ "name": "Closing Ceremony",
+ "category": "schedule",
+ "type": "GENERAL",
+ "start": "2025-04-20T22:00:00.000Z",
+ "end": "2025-04-20T23:00:00.000Z",
+ "location": null,
+ "host": null,
+ "url": "/hackers/hub/schedule"
+ }
+ ]
+ },
+ "judging": {
+ "id": "judging-overview",
+ "title": "Judging Process Overview",
+ "url": "/hackers/hub/project-info#judging",
+ "summary": "Judging day timeline (Pacific Time): 11:00 AM submissions due on Devpost; 12:00-2:00 PM demo time with judges; 2:00-3:00 PM break; 3:00-4:00 PM closing ceremony. Rubric: 60% track-specific, 20% social good, 10% creativity, 10% presentation.",
+ "timeline": [
+ {
+ "time": "11:00 AM",
+ "title": "Submission Due",
+ "details": {
+ "rubric": [
+ {
+ "percentage": 60,
+ "criterion": "Track-Specific"
+ },
+ {
+ "percentage": 20,
+ "criterion": "Social Good"
+ },
+ {
+ "percentage": 10,
+ "criterion": "Creativity"
+ },
+ {
+ "percentage": 10,
+ "criterion": "Presentation"
+ }
+ ]
+ }
+ },
+ {
+ "time": "12:00 - 2:00 PM",
+ "title": "Demo Time",
+ "details": {
+ "sections": [
+ {
+ "title": "TIMELINE"
+ },
+ {
+ "title": "JUDGES"
+ },
+ {
+ "title": "POST DEMO"
+ }
+ ]
+ }
+ },
+ {
+ "time": "2:00 - 3:00 PM",
+ "title": "Break",
+ "details": {
+ "hackerChoice": {
+ "title": "Hacker's Choice Award",
+ "description": "Once demos end, you will have about an hour to visit other teams and vote for the Hacker's Choice Award. You can also look at projects in the gallery on Devpost.",
+ "notes": [
+ "Panels of judges will be choosing the winners from the top 5 projects shortlisted for each track after demos.",
+ "When feature flag 'hackers-choice-link' is enabled, a link is shown for submitting votes."
+ ],
+ "voteLink": "https://forms.gle/6SktCxAFAvYZ1hKz5"
+ }
+ }
+ },
+ {
+ "time": "3:00 - 4:00 PM",
+ "title": "Closing Ceremony",
+ "details": {
+ "notes": [
+ "If your team needs to leave before or during closing ceremony, please inform someone at the Director Table.",
+ "If your team wins a prize and is not at the venue, HackDavis will contact you via email after the event to get your prize to you."
+ ]
+ }
+ }
+ ]
+ },
+ "submission": {
+ "id": "submission-overview",
+ "title": "Submission Process Overview",
+ "url": "/hackers/hub/project-info#submission",
+ "summary": "Submission steps: (1) Log in or sign up on Devpost and join the HackDavis hackathon. (2) Register for the event. (3) Create a project (only one teammate needs to create it). (4) Invite teammates. (5) Fill out project details. (6) Submit the project on Devpost before the deadline.",
+ "steps": [
+ {
+ "step": 1,
+ "title": "Login to Devpost",
+ "description": "When you click on the Devpost link, you should see the HackDavis Devpost page. Click \"Join Hackathon\". Log in or sign up for a Devpost account if you don't have one already."
+ },
+ {
+ "step": 2,
+ "title": "Register for the Event",
+ "description": "Register for the event on Devpost from the HackDavis hackathon page."
+ },
+ {
+ "step": 3,
+ "title": "Create a Project",
+ "description": "Click \"Create project\". Only one person per team has to create a project and complete the following steps."
+ },
+ {
+ "step": 4,
+ "title": "Invite Teammates",
+ "description": "Invite your teammates to join the Devpost project so everyone is correctly associated with the submission."
+ },
+ {
+ "step": 5,
+ "title": "Fill Out Details",
+ "description": "Fill out the required information such as project overview, description, technical details, and any other requested fields."
+ },
+ {
+ "step": 6,
+ "title": "Submit Project",
+ "description": "Once all information is filled out and teammates are added, submit the project on Devpost before the deadline."
+ }
+ ],
+ "tips": {
+ "devpost": {
+ "checklist": [
+ "Picked four relevant prize tracks.",
+ "Added your GitHub and/or Figma links.",
+ "Inserted a demo video."
+ ],
+ "url": "https://hackdavis-2025.devpost.com/"
+ }
+ }
+ }
+}
diff --git a/scripts/hackbotSeed.mjs b/scripts/hackbotSeed.mjs
new file mode 100644
index 00000000..d5599a79
--- /dev/null
+++ b/scripts/hackbotSeed.mjs
@@ -0,0 +1,234 @@
+import { getClient } from '../app/(api)/_utils/mongodb/mongoClient.mjs';
+import fs from 'fs';
+import path from 'path';
+import readline from 'readline';
+
+const HACKBOT_COLLECTION = 'hackbot_docs';
+const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+});
+
+function askQuestion(question) {
+ return new Promise((resolve) => {
+ rl.question(question, (answer) => {
+ resolve(answer);
+ });
+ });
+}
+
+function loadEventsFallbackFromKnowledge() {
+ const knowledgePath = path.join(
+ path.dirname(new URL(import.meta.url).pathname),
+ '..',
+ 'app',
+ '_data',
+ 'hackbot_knowledge.json'
+ );
+
+ const raw = fs.readFileSync(knowledgePath, 'utf8');
+ const knowledge = JSON.parse(raw);
+ return Array.isArray(knowledge?.events?.raw) ? knowledge.events.raw : [];
+}
+
+async function embedText(text) {
+ const res = await fetch(`${OLLAMA_URL}/api/embeddings`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ model: 'llama3.2',
+ prompt: text,
+ }),
+ });
+
+ if (!res.ok) {
+ throw new Error(`Ollama embeddings error: ${res.status} ${res.statusText}`);
+ }
+
+ const data = await res.json();
+ if (!data || !Array.isArray(data.embedding)) {
+ throw new Error('Invalid embeddings response from Ollama');
+ }
+
+ return data.embedding;
+}
+
+function formatEventDateTime(raw) {
+ const iso =
+ typeof raw === 'string' ? raw : raw && raw.$date ? raw.$date : undefined;
+
+ if (!iso) return null;
+
+ const date = new Date(iso);
+ if (Number.isNaN(date.getTime())) return null;
+
+ return date.toLocaleString('en-US', {
+ timeZone: 'America/Los_Angeles',
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
+ });
+}
+
+function buildDocsFromEvents(events) {
+ if (!Array.isArray(events)) return [];
+
+ return events.map((ev) => {
+ const id = String(ev._id?.$oid || ev._id || ev.name);
+ const title = String(ev.name || 'Event');
+
+ const start = formatEventDateTime(ev.start_time);
+ const end = formatEventDateTime(ev.end_time);
+ const location = ev.location || '';
+ const host = ev.host || '';
+ const type = ev.type || '';
+
+ const parts = [
+ `Event: ${title}`,
+ type ? `Type: ${type}` : '',
+ start ? `Starts (Pacific Time): ${start}` : '',
+ end ? `Ends (Pacific Time): ${end}` : '',
+ location ? `Location: ${location}` : '',
+ host ? `Host: ${host}` : '',
+ Array.isArray(ev.tags) && ev.tags.length
+ ? `Tags: ${ev.tags.join(', ')}`
+ : '',
+ ].filter(Boolean);
+
+ return {
+ _id: `event-${id}`,
+ type: 'event',
+ title,
+ text: parts.join('\n'),
+ url: '/hackers/hub/schedule',
+ };
+ });
+}
+
+function buildStaticDocs() {
+ const judging = {
+ _id: 'judging-overview',
+ type: 'judging',
+ title: 'Judging Process Overview',
+ text:
+ 'Judging day timeline (Pacific Time):\n' +
+ '- 11:00 AM: submissions due on Devpost\n' +
+ '- 12:00–2:00 PM: demo time with judges\n' +
+ '- 2:00–3:00 PM: break\n' +
+ '- 3:00–4:00 PM: closing ceremony\n\n' +
+ 'Rubric: 60% track-specific, 20% social good, 10% creativity, 10% presentation.',
+ url: '/hackers/hub/project-info#judging',
+ };
+
+ const submission = {
+ _id: 'submission-overview',
+ type: 'submission',
+ title: 'Submission Process Overview',
+ text: 'Submission steps: (1) Log in or sign up on Devpost and join the HackDavis hackathon. (2) Register for the event. (3) Create a project (only one teammate needs to create it). (4) Invite teammates. (5) Fill out project details. (6) Submit the project on Devpost before the deadline.',
+ url: '/hackers/hub/project-info#submission',
+ };
+
+ return [judging, submission];
+}
+
+async function seedHackbotDocs({ wipe }) {
+ const client = await getClient();
+ try {
+ await client.connect();
+ } catch (err) {
+ console.error(
+ 'MongoDB connection failed. Check MONGODB_URI and make sure your Mongo/Atlas Local deployment is running.\n' +
+ `Details: ${err.message}`
+ );
+ return;
+ }
+
+ const db = client.db();
+ const collection = db.collection(HACKBOT_COLLECTION);
+
+ if (wipe === 'y') {
+ await collection.deleteMany({});
+ console.log(`Wiped collection: ${HACKBOT_COLLECTION}`);
+ }
+
+ let events = [];
+ try {
+ events = await db.collection('events').find({}).toArray();
+ } catch (err) {
+ console.warn(
+ 'Failed to load events from MongoDB; falling back to hackbot_knowledge.json:',
+ err.message
+ );
+ }
+
+ if (!Array.isArray(events) || events.length === 0) {
+ events = loadEventsFallbackFromKnowledge();
+ }
+
+ const docs = [...buildDocsFromEvents(events), ...buildStaticDocs()];
+
+ console.log(`Preparing to embed and upsert ${docs.length} hackbot docs...`);
+
+ let successCount = 0;
+ for (const doc of docs) {
+ try {
+ const embedding = await embedText(doc.text);
+
+ await collection.updateOne(
+ { _id: doc._id },
+ {
+ $set: {
+ type: doc.type,
+ title: doc.title,
+ text: doc.text,
+ url: doc.url || null,
+ embedding,
+ },
+ },
+ { upsert: true }
+ );
+
+ successCount += 1;
+ console.log(`Upserted doc ${doc._id}`);
+ } catch (err) {
+ console.error(`Failed to upsert doc ${doc._id}:`, err.message);
+ }
+ }
+
+ console.log(
+ `Done. Successfully upserted ${successCount}/${docs.length} docs.`
+ );
+
+ await client.close();
+}
+
+async function gatherInputAndRun() {
+ try {
+ let wipe = '';
+ while (wipe !== 'y' && wipe !== 'n') {
+ // eslint-disable-next-line no-await-in-loop
+ wipe = (
+ await askQuestion(
+ `Seed collection "${HACKBOT_COLLECTION}" from app/_data (events + judging/submission docs). Wipe existing docs first? (y/n): `
+ )
+ ).toLowerCase();
+ if (wipe !== 'y' && wipe !== 'n') {
+ console.log('Please enter either "y" or "n".');
+ }
+ }
+
+ rl.close();
+
+ await seedHackbotDocs({ wipe });
+ } catch (err) {
+ console.error('Error while seeding hackbot docs:', err);
+ rl.close();
+ }
+}
+
+// Run when invoked via `node --env-file=".env" scripts/hackbotSeed.mjs`
+await gatherInputAndRun();
From 28de7b25ae429bf4afc286394290bed9dbc1c0e6 Mon Sep 17 00:00:00 2001
From: reehals
Date: Fri, 30 Jan 2026 00:00:03 -0800
Subject: [PATCH 2/3] Lint fixes
---
app/(api)/_actions/hackbot/askHackbot.ts | 98 ++++++++---------
.../_datalib/hackbot/getHackbotContext.ts | 104 +++++++++---------
app/(api)/api/hackbot/route.ts | 12 +-
.../ProjectInfo/JudgingInfo/JudgingInfo.tsx | 6 +
app/_data/hackbot_knowledge.json | 72 ++----------
5 files changed, 124 insertions(+), 168 deletions(-)
diff --git a/app/(api)/_actions/hackbot/askHackbot.ts b/app/(api)/_actions/hackbot/askHackbot.ts
index 4a1f0e35..512a1556 100644
--- a/app/(api)/_actions/hackbot/askHackbot.ts
+++ b/app/(api)/_actions/hackbot/askHackbot.ts
@@ -1,6 +1,6 @@
-import { retrieveContext } from "@datalib/hackbot/getHackbotContext";
+import { retrieveContext } from '@datalib/hackbot/getHackbotContext';
-export type HackbotMessageRole = "user" | "assistant" | "system";
+export type HackbotMessageRole = 'user' | 'assistant' | 'system';
export interface HackbotMessage {
role: HackbotMessageRole;
@@ -30,7 +30,7 @@ const MAX_HISTORY_MESSAGES = 10;
const MAX_ANSWER_WORDS = 180;
function parseIsoToMs(value: unknown): number | null {
- if (typeof value !== "string") return null;
+ if (typeof value !== 'string') return null;
const ms = Date.parse(value);
return Number.isFinite(ms) ? ms : null;
}
@@ -38,31 +38,31 @@ function parseIsoToMs(value: unknown): number | null {
function truncateToWords(text: string, maxWords: number): string {
const words = text.trim().split(/\s+/);
if (words.length <= maxWords) return text.trim();
- return words.slice(0, maxWords).join(" ") + "...";
+ return words.slice(0, maxWords).join(' ') + '...';
}
function stripExternalDomains(text: string): string {
// Replace absolute URLs like https://hackdavis.io/path with just /path.
- return text.replace(/https?:\/\/[^\s)]+(\/[\w#/?=&.-]*)/g, "$1");
+ return text.replace(/https?:\/\/[^\s)]+(\/[\w#/?=&.-]*)/g, '$1');
}
export async function askHackbot(
messages: HackbotMessage[]
): Promise {
if (!messages.length) {
- return { ok: false, answer: "", error: "No messages provided." };
+ return { ok: false, answer: '', error: 'No messages provided.' };
}
const last = messages[messages.length - 1];
- if (last.role !== "user") {
- return { ok: false, answer: "", error: "Last message must be from user." };
+ if (last.role !== 'user') {
+ return { ok: false, answer: '', error: 'Last message must be from user.' };
}
if (last.content.length > MAX_USER_MESSAGE_CHARS) {
return {
ok: false,
- answer: "",
+ answer: '',
error: `Message too long. Please keep it under ${MAX_USER_MESSAGE_CHARS} characters.`,
};
}
@@ -83,29 +83,29 @@ export async function askHackbot(
limit: 25,
}));
} catch (e) {
- console.error("Hackbot context retrieval error", e);
+ console.error('Hackbot context retrieval error', e);
return {
ok: false,
- answer: "",
+ answer: '',
error:
- "HackDavis Helper search backend is not configured (vector search unavailable). Please contact an organizer.",
+ 'HackDavis Helper search backend is not configured (vector search unavailable). Please contact an organizer.',
};
}
if (!docs || docs.length === 0) {
return {
ok: false,
- answer: "",
+ answer: '',
error:
- "HackDavis Helper could not find any context documents in its vector index. Please contact an organizer.",
+ 'HackDavis Helper could not find any context documents in its vector index. Please contact an organizer.',
};
}
// Present event context in chronological order so the model doesn't
// “pick a few” out of order when asked for itinerary/timeline questions.
const sortedDocs = (() => {
- const eventDocs = docs.filter((d: any) => d.type === "event");
- const otherDocs = docs.filter((d: any) => d.type !== "event");
+ const eventDocs = docs.filter((d: any) => d.type === 'event');
+ const otherDocs = docs.filter((d: any) => d.type !== 'event');
eventDocs.sort((a: any, b: any) => {
const aMs = parseIsoToMs(a.startISO);
@@ -121,47 +121,47 @@ export async function askHackbot(
})();
const primaryUrl =
- sortedDocs.find((d) => d.type === "event" && d.url)?.url ??
+ sortedDocs.find((d) => d.type === 'event' && d.url)?.url ??
sortedDocs.find((d) => d.url)?.url;
const contextSummary = sortedDocs
.map((d, index) => {
const header = `${index + 1}) [type=${d.type}, title="${d.title}"${
- d.url ? `, url="${d.url}"` : ""
+ d.url ? `, url="${d.url}"` : ''
}]`;
return `${header}\n${d.text}`;
})
- .join("\n\n");
+ .join('\n\n');
const systemPrompt =
- "You are HackDavis Helper, an assistant for the HackDavis hackathon. " +
+ 'You are HackDavis Helper, an assistant for the HackDavis hackathon. ' +
'You have a friendly personality and introduce yourself as "Hacky". ' +
'You may respond warmly to simple greetings (like "hi" or "hello") by saying something like: "Hi, I am Hacky! I can help with questions about HackDavis." ' +
'You should happily answer high-level questions like "What is HackDavis?" as long as they are clearly about the HackDavis hackathon. ' +
- "Only refuse questions that are clearly unrelated to HackDavis or hackathons (for example, general trivia, homework, or other topics with no mention of HackDavis). " +
+ 'Only refuse questions that are clearly unrelated to HackDavis or hackathons (for example, general trivia, homework, or other topics with no mention of HackDavis). ' +
'For clearly unrelated questions, respond in a brief, friendly way such as: "Sorry, I can only answer questions about HackDavis." and optionally add a short follow-up like: "Do you have any questions about HackDavis?" ' +
'For all other questions that mention HackDavis or obviously refer to the event (including "What is HackDavis?"), provide a concise, helpful answer based on your general knowledge of the event and the provided context. ' +
- "Keep every answer under 100 words. Prefer short, direct answers. " +
- "First, silently pick the single most relevant context document by matching the user’s key terms to the document title (especially for event questions). " +
- "If multiple events look plausible (similar names), ask one short clarifying question instead of guessing. " +
- "For time/location questions, strongly prefer documents with type=event. " +
- "When listing multiple schedule items (timeline/schedule/agenda/itinerary), format your answer as a bullet list (one item per line) using only items found in the context. " +
- "If the user asks for itinerary/timeline, order items chronologically by the start time in the context. Do not present a random subset if more relevant items are available in the context. " +
- "When giving times or locations, you MUST only use times, dates, and locations that explicitly appear in the provided context text. Do NOT use generic knowledge about hackathons. " +
+ 'Keep every answer under 100 words. Prefer short, direct answers. ' +
+ 'First, silently pick the single most relevant context document by matching the user’s key terms to the document title (especially for event questions). ' +
+ 'If multiple events look plausible (similar names), ask one short clarifying question instead of guessing. ' +
+ 'For time/location questions, strongly prefer documents with type=event. ' +
+ 'When listing multiple schedule items (timeline/schedule/agenda/itinerary), format your answer as a bullet list (one item per line) using only items found in the context. ' +
+ 'If the user asks for itinerary/timeline, order items chronologically by the start time in the context. Do not present a random subset if more relevant items are available in the context. ' +
+ 'When giving times or locations, you MUST only use times, dates, and locations that explicitly appear in the provided context text. Do NOT use generic knowledge about hackathons. ' +
'If a question is asking "When is" or "What time is" a specific event, and the context contains both a "Starts" line and an "Ends" line for that event, answer with the full range (for example: "The Closing Ceremony is from 3:00 PM to 4:00 PM Pacific Time."). ' +
- "If only a start time is present, answer with the start time. If only an end time is present, answer with the end time. Do not answer with only the end time when both are available. " +
+ 'If only a start time is present, answer with the start time. If only an end time is present, answer with the end time. Do not answer with only the end time when both are available. ' +
'In particular, never say that a hackathon "ends on the same day it starts" or that it ends at 11:59 PM unless that exact wording appears in the context. ' +
'If you cannot find an explicit time or place for what the user asked, say: "I do not know the exact time from the current schedule." ' +
- "Do not include any URLs in your answer text. The UI will show a separate “More info” link when available. " +
+ 'Do not include any URLs in your answer text. The UI will show a separate “More info” link when available. ' +
'Never invent domains such as "hackdavis.com" or new anchors. ' +
- "Write like a helpful human: use contractions, avoid robotic phrases, and answer in 1–3 short sentences unless the user asks for steps. " +
- "Never generate code or answer homework, programming, or general knowledge questions.";
+ 'Write like a helpful human: use contractions, avoid robotic phrases, and answer in 1–3 short sentences unless the user asks for steps. ' +
+ 'Never generate code or answer homework, programming, or general knowledge questions.';
// Prepare messages for the chat model (Ollama, GPT-compatible schema)
const chatMessages = [
- { role: "system", content: systemPrompt },
+ { role: 'system', content: systemPrompt },
{
- role: "system",
+ role: 'system',
content: `Context documents about HackDavis (use these to answer):\n\n${contextSummary}`,
},
...trimmedHistory.map((m) => ({
@@ -170,17 +170,17 @@ export async function askHackbot(
})),
];
- const ollamaUrl = process.env.OLLAMA_URL || "http://localhost:11434";
+ const ollamaUrl = process.env.OLLAMA_URL || 'http://localhost:11434';
try {
const startedAt = Date.now();
const response = await fetch(`${ollamaUrl}/api/chat`, {
- method: "POST",
+ method: 'POST',
headers: {
- "Content-Type": "application/json",
+ 'Content-Type': 'application/json',
},
body: JSON.stringify({
- model: "llama3.2",
+ model: 'llama3.2',
messages: chatMessages,
stream: false,
}),
@@ -189,27 +189,27 @@ export async function askHackbot(
if (!response.ok) {
return {
ok: false,
- answer: "",
- error: "Upstream model error. Please try again later.",
+ answer: '',
+ error: 'Upstream model error. Please try again later.',
};
}
const data = await response.json();
- const rawAnswer: string = data?.message?.content?.toString() ?? "";
+ const rawAnswer: string = data?.message?.content?.toString() ?? '';
const promptTokens =
- typeof data?.prompt_eval_count === "number"
+ typeof data?.prompt_eval_count === 'number'
? data.prompt_eval_count
: undefined;
const completionTokens =
- typeof data?.eval_count === "number" ? data.eval_count : undefined;
+ typeof data?.eval_count === 'number' ? data.eval_count : undefined;
const totalTokens =
- typeof promptTokens === "number" && typeof completionTokens === "number"
+ typeof promptTokens === 'number' && typeof completionTokens === 'number'
? promptTokens + completionTokens
: undefined;
- console.log("[hackbot][ollama][chat]", {
- model: data?.model ?? "unknown",
+ console.log('[hackbot][ollama][chat]', {
+ model: data?.model ?? 'unknown',
promptTokens,
completionTokens,
totalTokens,
@@ -235,11 +235,11 @@ export async function askHackbot(
},
};
} catch (e) {
- console.error("Hackbot error", e);
+ console.error('Hackbot error', e);
return {
ok: false,
- answer: "",
- error: "Something went wrong. Please try again later.",
+ answer: '',
+ error: 'Something went wrong. Please try again later.',
};
}
}
diff --git a/app/(api)/_datalib/hackbot/getHackbotContext.ts b/app/(api)/_datalib/hackbot/getHackbotContext.ts
index ec5b5eaf..6aafc529 100644
--- a/app/(api)/_datalib/hackbot/getHackbotContext.ts
+++ b/app/(api)/_datalib/hackbot/getHackbotContext.ts
@@ -1,6 +1,6 @@
-import { HackDoc, HackDocType } from "./hackbotTypes";
-import { getDatabase } from "@utils/mongodb/mongoClient.mjs";
-import { ObjectId } from "mongodb";
+import { HackDoc, HackDocType } from './hackbotTypes';
+import { getDatabase } from '@utils/mongodb/mongoClient.mjs';
+import { ObjectId } from 'mongodb';
export interface RetrievedContext {
docs: HackDoc[];
@@ -15,22 +15,22 @@ function formatEventDateTime(raw: unknown): string | null {
if (raw instanceof Date) {
date = raw;
- } else if (typeof raw === "string") {
+ } else if (typeof raw === 'string') {
date = new Date(raw);
- } else if (raw && typeof raw === "object" && "$date" in (raw as any)) {
+ } else if (raw && typeof raw === 'object' && '$date' in (raw as any)) {
date = new Date((raw as any).$date);
}
if (!date || Number.isNaN(date.getTime())) return null;
- return date.toLocaleString("en-US", {
- timeZone: "America/Los_Angeles",
- weekday: "short",
- month: "short",
- day: "numeric",
- year: "numeric",
- hour: "numeric",
- minute: "2-digit",
+ return date.toLocaleString('en-US', {
+ timeZone: 'America/Los_Angeles',
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
});
}
@@ -41,35 +41,35 @@ function formatLiveEventDoc(event: any): {
startISO?: string;
endISO?: string;
} {
- const title = String(event?.name || "Event");
- const type = event?.type ? String(event.type) : "";
+ const title = String(event?.name || 'Event');
+ const type = event?.type ? String(event.type) : '';
const start = formatEventDateTime(event?.start_time);
const end = formatEventDateTime(event?.end_time);
- const location = event?.location ? String(event.location) : "";
- const host = event?.host ? String(event.host) : "";
+ const location = event?.location ? String(event.location) : '';
+ const host = event?.host ? String(event.host) : '';
const tags = Array.isArray(event?.tags) ? event.tags.map(String) : [];
const parts = [
`Event: ${title}`,
- type ? `Type: ${type}` : "",
+ type ? `Type: ${type}` : '',
// Machine-readable anchors to allow reliable chronological ordering.
event?.start_time instanceof Date
? `StartISO: ${event.start_time.toISOString()}`
- : "",
+ : '',
event?.end_time instanceof Date
? `EndISO: ${event.end_time.toISOString()}`
- : "",
- start ? `Starts (Pacific Time): ${start}` : "",
- end ? `Ends (Pacific Time): ${end}` : "",
- location ? `Location: ${location}` : "",
- host ? `Host: ${host}` : "",
- tags.length ? `Tags: ${tags.join(", ")}` : "",
+ : '',
+ start ? `Starts (Pacific Time): ${start}` : '',
+ end ? `Ends (Pacific Time): ${end}` : '',
+ location ? `Location: ${location}` : '',
+ host ? `Host: ${host}` : '',
+ tags.length ? `Tags: ${tags.join(', ')}` : '',
].filter(Boolean);
return {
title,
- text: parts.join("\n"),
- url: "/hackers/hub/schedule",
+ text: parts.join('\n'),
+ url: '/hackers/hub/schedule',
startISO:
event?.start_time instanceof Date
? event.start_time.toISOString()
@@ -88,19 +88,19 @@ async function getQueryEmbedding(query: string): Promise<{
totalTokens?: number;
};
} | null> {
- const ollamaUrl = process.env.OLLAMA_URL || "http://localhost:11434";
+ const ollamaUrl = process.env.OLLAMA_URL || 'http://localhost:11434';
try {
const startedAt = Date.now();
const res = await fetch(`${ollamaUrl}/api/embeddings`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ model: "llama3.2", prompt: query }),
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ model: 'llama3.2', prompt: query }),
});
if (!res.ok) {
console.error(
- "[hackbot][embeddings] Upstream error",
+ '[hackbot][embeddings] Upstream error',
res.status,
res.statusText
);
@@ -109,23 +109,23 @@ async function getQueryEmbedding(query: string): Promise<{
const data = await res.json();
if (!data || !Array.isArray(data.embedding)) {
- console.error("[hackbot][embeddings] Invalid response shape");
+ console.error('[hackbot][embeddings] Invalid response shape');
return null;
}
const promptTokens =
- typeof data?.prompt_eval_count === "number"
+ typeof data?.prompt_eval_count === 'number'
? data.prompt_eval_count
: undefined;
const totalTokens =
- typeof data?.eval_count === "number"
+ typeof data?.eval_count === 'number'
? data.eval_count
- : typeof promptTokens === "number"
+ : typeof promptTokens === 'number'
? promptTokens
: undefined;
- console.log("[hackbot][ollama][embeddings]", {
- model: data?.model ?? "unknown",
+ console.log('[hackbot][ollama][embeddings]', {
+ model: data?.model ?? 'unknown',
promptTokens,
totalTokens,
ms: Date.now() - startedAt,
@@ -139,7 +139,7 @@ async function getQueryEmbedding(query: string): Promise<{
},
};
} catch (err) {
- console.error("[hackbot][embeddings] Failed to get embedding", err);
+ console.error('[hackbot][embeddings] Failed to get embedding', err);
return null;
}
}
@@ -156,15 +156,15 @@ export async function retrieveContext(
const embeddingResult = await getQueryEmbedding(trimmed);
if (!embeddingResult) {
console.error(
- "[hackbot][retrieve] No embedding available for query; vector search required."
+ '[hackbot][retrieve] No embedding available for query; vector search required.'
);
- throw new Error("Embedding unavailable");
+ throw new Error('Embedding unavailable');
}
const embedding = embeddingResult.embedding;
const db = await getDatabase();
- const collection = db.collection("hackbot_docs");
+ const collection = db.collection('hackbot_docs');
const preferredTypes = opts?.preferredTypes?.length
? Array.from(new Set(opts.preferredTypes))
@@ -176,9 +176,9 @@ export async function retrieveContext(
.aggregate([
{
$vectorSearch: {
- index: "hackbot_vector_index",
+ index: 'hackbot_vector_index',
queryVector: embedding,
- path: "embedding",
+ path: 'embedding',
numCandidates,
limit,
...(preferredTypes
@@ -194,7 +194,7 @@ export async function retrieveContext(
.toArray();
if (!vectorResults.length) {
- console.warn("[hackbot][retrieve] Vector search returned no results.");
+ console.warn('[hackbot][retrieve] Vector search returned no results.');
return { docs: [] };
}
@@ -209,14 +209,14 @@ export async function retrieveContext(
// Hydrate event docs from the live `events` collection so the answer
// always reflects the current schedule (times/locations), even if the
// vector index was seeded earlier.
- const eventsCollection = db.collection("events");
+ const eventsCollection = db.collection('events');
await Promise.all(
docs.map(async (d) => {
- if (d.type !== "event") return;
+ if (d.type !== 'event') return;
- const suffix = d.id.startsWith("event-")
- ? d.id.slice("event-".length)
- : "";
+ const suffix = d.id.startsWith('event-')
+ ? d.id.slice('event-'.length)
+ : '';
let event: any | null = null;
if (suffix && ObjectId.isValid(suffix)) {
@@ -240,7 +240,7 @@ export async function retrieveContext(
})
);
- console.log("[hackbot][retrieve][vector]", {
+ console.log('[hackbot][retrieve][vector]', {
query: trimmed,
docIds: docs.map((d) => d.id),
titles: docs.map((d) => d.title),
@@ -249,7 +249,7 @@ export async function retrieveContext(
return { docs, usage: embeddingResult.usage };
} catch (err) {
console.error(
- "[hackbot][retrieve] Vector search failed (no fallback).",
+ '[hackbot][retrieve] Vector search failed (no fallback).',
err
);
throw err;
diff --git a/app/(api)/api/hackbot/route.ts b/app/(api)/api/hackbot/route.ts
index e5fceec1..355dd9a9 100644
--- a/app/(api)/api/hackbot/route.ts
+++ b/app/(api)/api/hackbot/route.ts
@@ -1,5 +1,5 @@
-import { NextRequest, NextResponse } from "next/server";
-import { askHackbot, HackbotMessage } from "@actions/hackbot/askHackbot";
+import { NextRequest, NextResponse } from 'next/server';
+import { askHackbot, HackbotMessage } from '@actions/hackbot/askHackbot';
export async function POST(request: NextRequest) {
try {
@@ -10,8 +10,8 @@ export async function POST(request: NextRequest) {
return NextResponse.json(
{
ok: false,
- answer: "",
- error: "Invalid request body. Expected { messages: [...] }.",
+ answer: '',
+ error: 'Invalid request body. Expected { messages: [...] }.',
},
{ status: 400 }
);
@@ -33,8 +33,8 @@ export async function POST(request: NextRequest) {
return NextResponse.json(
{
ok: false,
- answer: "",
- error: "Invalid JSON body.",
+ answer: '',
+ error: 'Invalid JSON body.',
},
{ status: 400 }
);
diff --git a/app/(pages)/(hackers)/_components/ProjectInfo/JudgingInfo/JudgingInfo.tsx b/app/(pages)/(hackers)/_components/ProjectInfo/JudgingInfo/JudgingInfo.tsx
index b6abb0b0..91f49381 100644
--- a/app/(pages)/(hackers)/_components/ProjectInfo/JudgingInfo/JudgingInfo.tsx
+++ b/app/(pages)/(hackers)/_components/ProjectInfo/JudgingInfo/JudgingInfo.tsx
@@ -10,6 +10,7 @@ import ClosingCeremony from './JudgingSteps/ClosingCeremony/ClosingCeremony';
import ResourceHelp from '../../StarterKit/Resources/ResourceHelp';
import StarterKitSlide from '../../StarterKit/StarterKitSlide';
import styles from './JudgingInfo.module.scss';
+import ImportantAnnouncement from './JudgingSteps/ImportantAnnouncement/ImportantAnnouncement';
const accordionItems: AccordionItemInt[] = [
{
@@ -17,6 +18,11 @@ const accordionItems: AccordionItemInt[] = [
title: 'Submission Due',
content: ,
},
+ {
+ subtitle: '11:30 - 12:00 AM',
+ title: 'Important Announcement',
+ content: ,
+ },
{
subtitle: '12:00 - 2:00 PM',
title: 'Demo Time',
diff --git a/app/_data/hackbot_knowledge.json b/app/_data/hackbot_knowledge.json
index bdd6680f..ded36442 100644
--- a/app/_data/hackbot_knowledge.json
+++ b/app/_data/hackbot_knowledge.json
@@ -4,14 +4,7 @@
"timezone": "America/Los_Angeles"
},
"validation": {
- "domains": [
- "swe",
- "business",
- "aiml",
- "hardware",
- "design",
- "medtech"
- ],
+ "domains": ["swe", "business", "aiml", "hardware", "design", "medtech"],
"tracks": [
"Best Hack for Social Good",
"Best Beginner Hack",
@@ -205,13 +198,7 @@
},
"location": "ARC Ballroom A",
"host": "HackDavis",
- "tags": [
- "beginner",
- "developer",
- "designer",
- "pm",
- "other"
- ]
+ "tags": ["beginner", "developer", "designer", "pm", "other"]
},
{
"name": "Surprise Mini-Event!",
@@ -224,13 +211,7 @@
},
"location": "ARC Ballroom A",
"host": "Major League Hacking",
- "tags": [
- "beginner",
- "developer",
- "pm",
- "designer",
- "other"
- ]
+ "tags": ["beginner", "developer", "pm", "designer", "other"]
},
{
"name": "Dinner Starts",
@@ -257,10 +238,7 @@
},
"location": "ARC Ballroom A",
"host": "UC Davis DataLab",
- "tags": [
- "beginner",
- "developer"
- ]
+ "tags": ["beginner", "developer"]
},
{
"name": "GitHub Copilot Workshop",
@@ -273,11 +251,7 @@
},
"location": "ARC Ballroom A",
"host": "Major League Hacking",
- "tags": [
- "beginner",
- "developer",
- "pm"
- ]
+ "tags": ["beginner", "developer", "pm"]
},
{
"name": "Tech Together Meetup",
@@ -290,13 +264,7 @@
},
"location": "ARC Ballroom A",
"host": "Major League Hacking",
- "tags": [
- "beginner",
- "developer",
- "pm",
- "designer",
- "other"
- ]
+ "tags": ["beginner", "developer", "pm", "designer", "other"]
},
{
"name": "Intro to UI/UX",
@@ -309,10 +277,7 @@
},
"location": "ARC Ballroom B",
"host": "Design Interactive",
- "tags": [
- "beginner",
- "designer"
- ]
+ "tags": ["beginner", "designer"]
},
{
"name": "Lunch Starts",
@@ -332,11 +297,7 @@
},
"location": "ARC Ballroom B",
"host": "CodeLab",
- "tags": [
- "beginner",
- "developer",
- "pm"
- ]
+ "tags": ["beginner", "developer", "pm"]
},
{
"name": "Intro to Freepik AI Suite",
@@ -349,12 +310,7 @@
},
"location": "ARC Ballroom B",
"host": "Freepik",
- "tags": [
- "developer",
- "pm",
- "designer",
- "other"
- ]
+ "tags": ["developer", "pm", "designer", "other"]
},
{
"name": "Break",
@@ -377,11 +333,7 @@
},
"location": "ARC Ballroom B",
"host": "Miguel Acevedo, Founder @ Marble",
- "tags": [
- "beginner",
- "developer",
- "pm"
- ]
+ "tags": ["beginner", "developer", "pm"]
},
{
"name": "Intro to Letta AI Agents Framework",
@@ -394,9 +346,7 @@
},
"location": "ARC Ballroom A",
"host": "Letta",
- "tags": [
- "developer"
- ]
+ "tags": ["developer"]
}
],
"key": [
From b3627aaf518327f873bbcc0a3149184efe6dded2 Mon Sep 17 00:00:00 2001
From: reehals
Date: Fri, 30 Jan 2026 15:49:56 -0800
Subject: [PATCH 3/3] Update to google also
---
.github/workflows/production.yaml | 34 +
.github/workflows/staging.yaml | 34 +
app/(api)/_actions/hackbot/askHackbot.ts | 442 +++++++--
.../_datalib/hackbot/getHackbotContext.ts | 216 ++++-
app/(api)/api/hackbot/stream/route.ts | 199 ++++
.../_components/Hackbot/HackbotWidget.tsx | 152 ++-
package-lock.json | 917 +++++++++++++++++-
package.json | 4 +
scripts/hackbotSeedCI.mjs | 286 ++++++
9 files changed, 2131 insertions(+), 153 deletions(-)
create mode 100644 app/(api)/api/hackbot/stream/route.ts
create mode 100644 scripts/hackbotSeedCI.mjs
diff --git a/.github/workflows/production.yaml b/.github/workflows/production.yaml
index 19823802..17e42a2f 100644
--- a/.github/workflows/production.yaml
+++ b/.github/workflows/production.yaml
@@ -5,7 +5,27 @@ on:
types: [published]
jobs:
+ check_hackbot_knowledge:
+ name: Check if hackbot_knowledge.json changed
+ runs-on: ubuntu-latest
+ outputs:
+ changed: ${{ steps.filter.outputs.hackbot_knowledge }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
+
+ - name: Check file changes
+ uses: dorny/paths-filter@v2
+ id: filter
+ with:
+ filters: |
+ hackbot_knowledge:
+ - 'app/_data/hackbot_knowledge.json'
+
deploy_staging:
+ needs: check_hackbot_knowledge
name: Deploy Production
runs-on: ubuntu-latest
environment:
@@ -29,6 +49,15 @@ jobs:
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}
+ - name: Seed hackbot documentation
+ if: needs.check_hackbot_knowledge.outputs.changed == 'true'
+ run: npm run hackbot:seed
+ env:
+ MONGODB_URI: ${{ secrets.MONGODB_URI }}
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
+ GOOGLE_EMBEDDING_MODEL: ${{ vars.GOOGLE_EMBEDDING_MODEL }}
+ HACKBOT_MODE: google
+
- name: Lint and build code, then publish to Vercel
run: npx vercel --token ${{ secrets.VERCEL_TOKEN }} -n ${{ vars.VERCEL_PROJECT }} --yes --prod
env:
@@ -41,6 +70,11 @@ jobs:
SENDER_EMAIL: ${{ vars.SENDER_EMAIL }}
SENDER_PWD: ${{ secrets.SENDER_PWD }}
CHECK_IN_CODE: ${{ secrets.CHECK_IN_CODE }}
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
+ GOOGLE_MODEL: ${{ vars.GOOGLE_MODEL }}
+ GOOGLE_EMBEDDING_MODEL: ${{ vars.GOOGLE_EMBEDDING_MODEL }}
+ GOOGLE_MAX_TOKENS: ${{ vars.GOOGLE_MAX_TOKENS }}
+ HACKBOT_MODE: google
- name: Success
run: echo "🚀 Deploy successful - BLAST OFF WOO! (woot woot) !!! 🐕 🐕 🐕 🚀 "
\ No newline at end of file
diff --git a/.github/workflows/staging.yaml b/.github/workflows/staging.yaml
index e201a8f5..7a354917 100644
--- a/.github/workflows/staging.yaml
+++ b/.github/workflows/staging.yaml
@@ -7,7 +7,27 @@ on:
workflow_dispatch:
jobs:
+ check_hackbot_knowledge:
+ name: Check if hackbot_knowledge.json changed
+ runs-on: ubuntu-latest
+ outputs:
+ changed: ${{ steps.filter.outputs.hackbot_knowledge }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
+
+ - name: Check file changes
+ uses: dorny/paths-filter@v2
+ id: filter
+ with:
+ filters: |
+ hackbot_knowledge:
+ - 'app/_data/hackbot_knowledge.json'
+
deploy_staging:
+ needs: check_hackbot_knowledge
name: Deploy Staging
runs-on: ubuntu-latest
environment:
@@ -31,6 +51,15 @@ jobs:
env:
MONGODB_URI: ${{ secrets.MONGODB_URI }}
+ - name: Seed hackbot documentation
+ if: needs.check_hackbot_knowledge.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
+ run: npm run hackbot:seed
+ env:
+ MONGODB_URI: ${{ secrets.MONGODB_URI }}
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
+ GOOGLE_EMBEDDING_MODEL: ${{ vars.GOOGLE_EMBEDDING_MODEL }}
+ HACKBOT_MODE: google
+
- name: Lint and build code, then publish to Vercel
run: npx vercel --debug --token ${{ secrets.VERCEL_TOKEN }} -n ${{ vars.VERCEL_PROJECT }} --yes --prod
env:
@@ -43,6 +72,11 @@ jobs:
SENDER_EMAIL: ${{ vars.SENDER_EMAIL }}
SENDER_PWD: ${{ secrets.SENDER_PWD }}
CHECK_IN_CODE: ${{ secrets.CHECK_IN_CODE }}
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
+ GOOGLE_MODEL: ${{ vars.GOOGLE_MODEL }}
+ GOOGLE_EMBEDDING_MODEL: ${{ vars.GOOGLE_EMBEDDING_MODEL }}
+ GOOGLE_MAX_TOKENS: ${{ vars.GOOGLE_MAX_TOKENS }}
+ HACKBOT_MODE: google
- name: Success
run: echo "🚀 Deploy successful - BLAST OFF WOO! (woot woot) !!! 🐕 🐕 🐕 🚀 "
diff --git a/app/(api)/_actions/hackbot/askHackbot.ts b/app/(api)/_actions/hackbot/askHackbot.ts
index 512a1556..8c8210e5 100644
--- a/app/(api)/_actions/hackbot/askHackbot.ts
+++ b/app/(api)/_actions/hackbot/askHackbot.ts
@@ -2,6 +2,57 @@ import { retrieveContext } from '@datalib/hackbot/getHackbotContext';
export type HackbotMessageRole = 'user' | 'assistant' | 'system';
+interface RetryOptions {
+ maxAttempts: number;
+ delayMs: number;
+ backoffMultiplier: number;
+ retryableErrors?: string[];
+}
+
+async function retryWithBackoff(
+ fn: () => Promise,
+ options: RetryOptions
+): Promise {
+ const {
+ maxAttempts,
+ delayMs,
+ backoffMultiplier,
+ retryableErrors = ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'],
+ } = options;
+
+ let lastError: Error | null = null;
+
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+ try {
+ return await fn();
+ } catch (err: any) {
+ lastError = err;
+
+ const isRetryable =
+ retryableErrors.some((code) => err.message?.includes(code)) ||
+ err.status === 429 || // Rate limit
+ err.status === 500 || // Server error
+ err.status === 502 || // Bad gateway
+ err.status === 503 || // Service unavailable
+ err.status === 504; // Gateway timeout
+
+ if (!isRetryable || attempt === maxAttempts) {
+ throw err;
+ }
+
+ const delay = delayMs * Math.pow(backoffMultiplier, attempt - 1);
+ console.log(
+ `[hackbot][retry] Attempt ${attempt}/${maxAttempts} failed. Retrying in ${delay}ms...`,
+ err.message
+ );
+
+ await new Promise((resolve) => setTimeout(resolve, delay));
+ }
+ }
+
+ throw lastError;
+}
+
export interface HackbotMessage {
role: HackbotMessageRole;
content: string;
@@ -77,11 +128,8 @@ export async function askHackbot(
}
| undefined;
try {
- // Use a single, general context size (vector-only) to avoid
- // question-specific limits or retrieval heuristics.
- ({ docs, usage: embeddingsUsage } = await retrieveContext(last.content, {
- limit: 25,
- }));
+ // Use adaptive context retrieval - limit determined by query complexity
+ ({ docs, usage: embeddingsUsage } = await retrieveContext(last.content));
} catch (e) {
console.error('Hackbot context retrieval error', e);
return {
@@ -134,32 +182,59 @@ export async function askHackbot(
.join('\n\n');
const systemPrompt =
- 'You are HackDavis Helper, an assistant for the HackDavis hackathon. ' +
- 'You have a friendly personality and introduce yourself as "Hacky". ' +
- 'You may respond warmly to simple greetings (like "hi" or "hello") by saying something like: "Hi, I am Hacky! I can help with questions about HackDavis." ' +
- 'You should happily answer high-level questions like "What is HackDavis?" as long as they are clearly about the HackDavis hackathon. ' +
- 'Only refuse questions that are clearly unrelated to HackDavis or hackathons (for example, general trivia, homework, or other topics with no mention of HackDavis). ' +
- 'For clearly unrelated questions, respond in a brief, friendly way such as: "Sorry, I can only answer questions about HackDavis." and optionally add a short follow-up like: "Do you have any questions about HackDavis?" ' +
- 'For all other questions that mention HackDavis or obviously refer to the event (including "What is HackDavis?"), provide a concise, helpful answer based on your general knowledge of the event and the provided context. ' +
- 'Keep every answer under 100 words. Prefer short, direct answers. ' +
- 'First, silently pick the single most relevant context document by matching the user’s key terms to the document title (especially for event questions). ' +
- 'If multiple events look plausible (similar names), ask one short clarifying question instead of guessing. ' +
- 'For time/location questions, strongly prefer documents with type=event. ' +
- 'When listing multiple schedule items (timeline/schedule/agenda/itinerary), format your answer as a bullet list (one item per line) using only items found in the context. ' +
- 'If the user asks for itinerary/timeline, order items chronologically by the start time in the context. Do not present a random subset if more relevant items are available in the context. ' +
- 'When giving times or locations, you MUST only use times, dates, and locations that explicitly appear in the provided context text. Do NOT use generic knowledge about hackathons. ' +
- 'If a question is asking "When is" or "What time is" a specific event, and the context contains both a "Starts" line and an "Ends" line for that event, answer with the full range (for example: "The Closing Ceremony is from 3:00 PM to 4:00 PM Pacific Time."). ' +
- 'If only a start time is present, answer with the start time. If only an end time is present, answer with the end time. Do not answer with only the end time when both are available. ' +
- 'In particular, never say that a hackathon "ends on the same day it starts" or that it ends at 11:59 PM unless that exact wording appears in the context. ' +
- 'If you cannot find an explicit time or place for what the user asked, say: "I do not know the exact time from the current schedule." ' +
- 'Do not include any URLs in your answer text. The UI will show a separate “More info” link when available. ' +
- 'Never invent domains such as "hackdavis.com" or new anchors. ' +
- 'Write like a helpful human: use contractions, avoid robotic phrases, and answer in 1–3 short sentences unless the user asks for steps. ' +
- 'Never generate code or answer homework, programming, or general knowledge questions.';
-
- // Prepare messages for the chat model (Ollama, GPT-compatible schema)
+ 'You are HackDavis Helper ("Hacky"), an AI assistant for the HackDavis hackathon. ' +
+ // CRITICAL CONSTRAINTS
+ 'CRITICAL: Your response MUST be under 200 tokens (~150 words). Be extremely concise. ' +
+ 'CRITICAL: Only answer questions about HackDavis. Refuse unrelated topics politely. ' +
+ 'CRITICAL: Only use facts from the provided context. Never invent times, dates, or locations. ' +
+ // PERSONALITY
+ 'You are friendly, helpful, and conversational. Use contractions ("you\'re", "it\'s") and avoid robotic phrasing. ' +
+ // HANDLING GREETINGS
+ 'For simple greetings ("hi", "hello"), respond warmly: "Hi, I\'m Hacky! I can help with questions about HackDavis." Keep it brief (1 sentence). ' +
+ // HANDLING QUESTIONS
+ 'For questions about HackDavis: ' +
+ '1. First, silently identify the most relevant context document by matching key terms to document titles. ' +
+ '2. If multiple documents seem relevant (e.g., similar event names), ask ONE short clarifying question instead of guessing. ' +
+ '3. Answer directly in 2-3 sentences using only context facts. ' +
+ '4. For time/location questions: Use only explicit times and locations from context. If both start and end times exist, provide the full range ("3:00 PM to 4:00 PM"). ' +
+ '5. For schedule/timeline questions: Format as a bullet list, ordered chronologically. Include only items from context. ' +
+ // WHAT NOT TO DO
+ 'Do NOT: ' +
+ '- Invent times, dates, locations, or URLs not in context. ' +
+ '- Include URLs in your answer text (UI shows separate "More info" link). ' +
+ '- Use generic hackathon knowledge; only use provided context. ' +
+ '- Answer coding, homework, or general knowledge questions. ' +
+ '- Say "based on the context" or "according to the documents" (just answer directly). ' +
+ // HANDLING UNKNOWNS
+ 'If you cannot find an answer in context, say: "I don\'t have that information. Please ask an organizer or check the HackDavis website." ' +
+ // REFUSING UNRELATED QUESTIONS
+ 'For unrelated questions (not about HackDavis), say: "Sorry, I can only answer questions about HackDavis. Do you have any questions about the event?"';
+
+ // Few-shot examples to demonstrate desired format
+ const fewShotExamples = [
+ {
+ role: 'user' as const,
+ content: 'When does hacking end?',
+ },
+ {
+ role: 'assistant' as const,
+ content: 'Hacking ends on Sunday, April 20 at 11:00 AM Pacific Time.',
+ },
+ {
+ role: 'user' as const,
+ content: 'What workshops are available?',
+ },
+ {
+ role: 'assistant' as const,
+ content:
+ 'We have several workshops including:\n• Hackathons 101 (Sat 11:40 AM)\n• Getting Started with Git & GitHub (Sat 1:10 PM)\n• Intro to UI/UX (Sat 4:10 PM)\n• Hacking with LLMs (Sat 2:10 PM)',
+ },
+ ];
+
+ // Prepare messages for the chat model
const chatMessages = [
{ role: 'system', content: systemPrompt },
+ ...fewShotExamples,
{
role: 'system',
content: `Context documents about HackDavis (use these to answer):\n\n${contextSummary}`,
@@ -170,76 +245,265 @@ export async function askHackbot(
})),
];
- const ollamaUrl = process.env.OLLAMA_URL || 'http://localhost:11434';
+ const mode = process.env.HACKBOT_MODE || 'google';
- try {
- const startedAt = Date.now();
- const response = await fetch(`${ollamaUrl}/api/chat`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- model: 'llama3.2',
- messages: chatMessages,
- stream: false,
- }),
- });
+ if (mode === 'google') {
+ // Google AI implementation with Vercel AI SDK
+ try {
+ const { generateText } = await import('ai');
+ const { google } = await import('@ai-sdk/google');
+
+ const startedAt = Date.now();
+ const model = process.env.GOOGLE_MODEL || 'gemini-1.5-flash';
+ const maxTokens = parseInt(process.env.GOOGLE_MAX_TOKENS || '200', 10);
+
+ const { text, usage } = await retryWithBackoff(
+ () =>
+ generateText({
+ model: google(model),
+ messages: chatMessages.map((m) => ({
+ role: m.role as 'system' | 'user' | 'assistant',
+ content: m.content,
+ })),
+ maxTokens,
+ }),
+ {
+ maxAttempts: 2,
+ delayMs: 2000,
+ backoffMultiplier: 2,
+ }
+ );
+
+ console.log('[hackbot][google][chat]', {
+ model,
+ promptTokens: usage.promptTokens,
+ completionTokens: usage.completionTokens,
+ totalTokens: usage.totalTokens,
+ ms: Date.now() - startedAt,
+ });
+
+ const answer = truncateToWords(
+ stripExternalDomains(text),
+ MAX_ANSWER_WORDS
+ );
+
+ return {
+ ok: true,
+ answer,
+ url: primaryUrl,
+ usage: {
+ chat: {
+ promptTokens: usage.promptTokens,
+ completionTokens: usage.completionTokens,
+ totalTokens: usage.totalTokens,
+ },
+ embeddings: embeddingsUsage,
+ },
+ };
+ } catch (e: any) {
+ console.error('[hackbot][google] Error', e);
+
+ // Differentiate error types for better UX
+ if (e.status === 429) {
+ return {
+ ok: false,
+ answer: '',
+ error: 'Too many requests. Please wait a moment and try again.',
+ };
+ }
+
+ if (
+ e.message?.includes('ECONNREFUSED') ||
+ e.message?.includes('ETIMEDOUT')
+ ) {
+ return {
+ ok: false,
+ answer: '',
+ error:
+ 'Cannot reach AI service. Please check your connection or try again later.',
+ };
+ }
+
+ if (e.status === 401 || e.message?.includes('API key')) {
+ return {
+ ok: false,
+ answer: '',
+ error:
+ 'AI service configuration error. Please contact an organizer.',
+ };
+ }
- if (!response.ok) {
return {
ok: false,
answer: '',
- error: 'Upstream model error. Please try again later.',
+ error: 'Something went wrong. Please try again in a moment.',
};
}
+ } else if (mode === 'openai') {
+ // OpenAI implementation with Vercel AI SDK
+ try {
+ const { generateText } = await import('ai');
+ const { openai } = await import('@ai-sdk/openai');
- const data = await response.json();
- const rawAnswer: string = data?.message?.content?.toString() ?? '';
-
- const promptTokens =
- typeof data?.prompt_eval_count === 'number'
- ? data.prompt_eval_count
- : undefined;
- const completionTokens =
- typeof data?.eval_count === 'number' ? data.eval_count : undefined;
- const totalTokens =
- typeof promptTokens === 'number' && typeof completionTokens === 'number'
- ? promptTokens + completionTokens
- : undefined;
-
- console.log('[hackbot][ollama][chat]', {
- model: data?.model ?? 'unknown',
- promptTokens,
- completionTokens,
- totalTokens,
- ms: Date.now() - startedAt,
- });
+ const startedAt = Date.now();
+ const model = process.env.OPENAI_MODEL || 'gpt-4o';
+ const maxTokens = parseInt(process.env.OPENAI_MAX_TOKENS || '200', 10);
- const answer = truncateToWords(
- stripExternalDomains(rawAnswer),
- MAX_ANSWER_WORDS
- );
+ const { text, usage } = await retryWithBackoff(
+ () =>
+ generateText({
+ model: openai(model),
+ messages: chatMessages.map((m) => ({
+ role: m.role as 'system' | 'user' | 'assistant',
+ content: m.content,
+ })),
+ maxTokens,
+ }),
+ {
+ maxAttempts: 2,
+ delayMs: 2000,
+ backoffMultiplier: 2,
+ }
+ );
- return {
- ok: true,
- answer,
- url: primaryUrl,
- usage: {
- chat: {
- promptTokens,
- completionTokens,
- totalTokens,
+ console.log('[hackbot][openai][chat]', {
+ model,
+ promptTokens: usage.promptTokens,
+ completionTokens: usage.completionTokens,
+ totalTokens: usage.totalTokens,
+ ms: Date.now() - startedAt,
+ });
+
+ const answer = truncateToWords(
+ stripExternalDomains(text),
+ MAX_ANSWER_WORDS
+ );
+
+ return {
+ ok: true,
+ answer,
+ url: primaryUrl,
+ usage: {
+ chat: {
+ promptTokens: usage.promptTokens,
+ completionTokens: usage.completionTokens,
+ totalTokens: usage.totalTokens,
+ },
+ embeddings: embeddingsUsage,
},
- embeddings: embeddingsUsage,
- },
- };
- } catch (e) {
- console.error('Hackbot error', e);
- return {
- ok: false,
- answer: '',
- error: 'Something went wrong. Please try again later.',
- };
+ };
+ } catch (e: any) {
+ console.error('[hackbot][openai] Error', e);
+
+ // Differentiate error types for better UX
+ if (e.status === 429) {
+ return {
+ ok: false,
+ answer: '',
+ error: 'Too many requests. Please wait a moment and try again.',
+ };
+ }
+
+ if (
+ e.message?.includes('ECONNREFUSED') ||
+ e.message?.includes('ETIMEDOUT')
+ ) {
+ return {
+ ok: false,
+ answer: '',
+ error:
+ 'Cannot reach AI service. Please check your connection or try again later.',
+ };
+ }
+
+ if (e.status === 401 || e.message?.includes('API key')) {
+ return {
+ ok: false,
+ answer: '',
+ error:
+ 'AI service configuration error. Please contact an organizer.',
+ };
+ }
+
+ return {
+ ok: false,
+ answer: '',
+ error: 'Something went wrong. Please try again in a moment.',
+ };
+ }
+ } else {
+ // Ollama implementation (local dev fallback)
+ const ollamaUrl = process.env.OLLAMA_URL || 'http://localhost:11434';
+
+ try {
+ const startedAt = Date.now();
+ const response = await fetch(`${ollamaUrl}/api/chat`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ model: 'llama3.2',
+ messages: chatMessages,
+ stream: false,
+ }),
+ });
+
+ if (!response.ok) {
+ return {
+ ok: false,
+ answer: '',
+ error: 'Upstream model error. Please try again later.',
+ };
+ }
+
+ const data = await response.json();
+ const rawAnswer: string = data?.message?.content?.toString() ?? '';
+
+ const promptTokens =
+ typeof data?.prompt_eval_count === 'number'
+ ? data.prompt_eval_count
+ : undefined;
+ const completionTokens =
+ typeof data?.eval_count === 'number' ? data.eval_count : undefined;
+ const totalTokens =
+ typeof promptTokens === 'number' && typeof completionTokens === 'number'
+ ? promptTokens + completionTokens
+ : undefined;
+
+ console.log('[hackbot][ollama][chat]', {
+ model: data?.model ?? 'unknown',
+ promptTokens,
+ completionTokens,
+ totalTokens,
+ ms: Date.now() - startedAt,
+ });
+
+ const answer = truncateToWords(
+ stripExternalDomains(rawAnswer),
+ MAX_ANSWER_WORDS
+ );
+
+ return {
+ ok: true,
+ answer,
+ url: primaryUrl,
+ usage: {
+ chat: {
+ promptTokens,
+ completionTokens,
+ totalTokens,
+ },
+ embeddings: embeddingsUsage,
+ },
+ };
+ } catch (e) {
+ console.error('[hackbot][ollama] Error', e);
+ return {
+ ok: false,
+ answer: '',
+ error: 'Something went wrong. Please try again later.',
+ };
+ }
}
}
diff --git a/app/(api)/_datalib/hackbot/getHackbotContext.ts b/app/(api)/_datalib/hackbot/getHackbotContext.ts
index 6aafc529..8af3579f 100644
--- a/app/(api)/_datalib/hackbot/getHackbotContext.ts
+++ b/app/(api)/_datalib/hackbot/getHackbotContext.ts
@@ -10,6 +10,97 @@ export interface RetrievedContext {
};
}
+interface QueryComplexity {
+ type: 'simple' | 'moderate' | 'complex';
+ docLimit: number;
+ reason: string;
+}
+
+interface RetryOptions {
+ maxAttempts: number;
+ delayMs: number;
+ backoffMultiplier: number;
+ retryableErrors?: string[];
+}
+
+async function retryWithBackoff(
+ fn: () => Promise,
+ options: RetryOptions
+): Promise {
+ const {
+ maxAttempts,
+ delayMs,
+ backoffMultiplier,
+ retryableErrors = ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'],
+ } = options;
+
+ let lastError: Error | null = null;
+
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
+ try {
+ return await fn();
+ } catch (err: any) {
+ lastError = err;
+
+ const isRetryable =
+ retryableErrors.some((code) => err.message?.includes(code)) ||
+ err.status === 429 || // Rate limit
+ err.status === 500 || // Server error
+ err.status === 502 || // Bad gateway
+ err.status === 503 || // Service unavailable
+ err.status === 504; // Gateway timeout
+
+ if (!isRetryable || attempt === maxAttempts) {
+ throw err;
+ }
+
+ const delay = delayMs * Math.pow(backoffMultiplier, attempt - 1);
+ console.log(
+ `[hackbot][retry] Attempt ${attempt}/${maxAttempts} failed. Retrying in ${delay}ms...`,
+ err.message
+ );
+
+ await new Promise((resolve) => setTimeout(resolve, delay));
+ }
+ }
+
+ throw lastError;
+}
+
+function analyzeQueryComplexity(query: string): QueryComplexity {
+ const trimmed = query.trim().toLowerCase();
+ const words = trimmed.split(/\s+/);
+
+ // Simple greeting or single fact
+ if (words.length <= 5) {
+ if (/^(hi|hello|hey|thanks|thank you|ok|okay)/.test(trimmed)) {
+ return { type: 'simple', docLimit: 5, reason: 'greeting' };
+ }
+ if (/^(what|when|where|who)\s+(is|are)/.test(trimmed)) {
+ return { type: 'simple', docLimit: 10, reason: 'single fact question' };
+ }
+ }
+
+ // Timeline/schedule queries (need more docs)
+ if (
+ /\b(schedule|timeline|agenda|itinerary|all events|list)\b/.test(trimmed) ||
+ (words.length >= 3 && /\b(what|show|tell)\b/.test(trimmed))
+ ) {
+ return { type: 'complex', docLimit: 30, reason: 'schedule/list query' };
+ }
+
+ // Multiple questions or comparisons
+ if (
+ /\b(and|or|versus|vs|compare)\b/.test(trimmed) ||
+ (trimmed.match(/\?/g) || []).length > 1
+ ) {
+ return { type: 'complex', docLimit: 25, reason: 'multi-part query' };
+ }
+
+ // Moderate: specific event or detail
+ return { type: 'moderate', docLimit: 15, reason: 'specific detail query' };
+}
+
function formatEventDateTime(raw: unknown): string | null {
let date: Date | null = null;
@@ -81,7 +172,84 @@ function formatLiveEventDoc(event: any): {
};
}
-async function getQueryEmbedding(query: string): Promise<{
+async function getGoogleEmbedding(query: string): Promise<{
+ embedding: number[];
+ usage?: {
+ promptTokens?: number;
+ totalTokens?: number;
+ };
+} | null> {
+ try {
+ const { embed } = await import('ai');
+ const { google } = await import('@ai-sdk/google');
+
+ const startedAt = Date.now();
+ const model = process.env.GOOGLE_EMBEDDING_MODEL || 'text-embedding-004';
+
+ const { embedding, usage } = await embed({
+ model: google.textEmbeddingModel(model),
+ value: query,
+ });
+
+ console.log('[hackbot][google][embeddings]', {
+ model,
+ tokens: usage.tokens,
+ ms: Date.now() - startedAt,
+ });
+
+ return {
+ embedding,
+ usage: {
+ promptTokens: usage.tokens,
+ totalTokens: usage.tokens,
+ },
+ };
+ } catch (err) {
+ console.error('[hackbot][embeddings][google] Failed', err);
+ return null;
+ }
+}
+
+async function getOpenAIEmbedding(query: string): Promise<{
+ embedding: number[];
+ usage?: {
+ promptTokens?: number;
+ totalTokens?: number;
+ };
+} | null> {
+ try {
+ const { embed } = await import('ai');
+ const { openai } = await import('@ai-sdk/openai');
+
+ const startedAt = Date.now();
+ const model =
+ process.env.OPENAI_EMBEDDING_MODEL || 'text-embedding-3-small';
+
+ const { embedding, usage } = await embed({
+ model: openai.embedding(model),
+ value: query,
+ });
+
+ console.log('[hackbot][openai][embeddings]', {
+ model,
+ tokens: usage.tokens,
+ ms: Date.now() - startedAt,
+ });
+
+ return {
+ embedding,
+ usage: {
+ promptTokens: usage.tokens,
+ totalTokens: usage.tokens,
+ },
+ };
+ } catch (err) {
+ console.error('[hackbot][embeddings][openai] Failed', err);
+ return null;
+ }
+}
+
+async function getOllamaEmbedding(query: string): Promise<{
embedding: number[];
usage?: {
promptTokens?: number;
@@ -139,26 +307,64 @@ async function getQueryEmbedding(query: string): Promise<{
},
};
} catch (err) {
- console.error('[hackbot][embeddings] Failed to get embedding', err);
+ console.error('[hackbot][embeddings][ollama] Failed', err);
return null;
}
}
+async function getQueryEmbedding(query: string): Promise<{
+ embedding: number[];
+ usage?: {
+ promptTokens?: number;
+ totalTokens?: number;
+ };
+} | null> {
+ const mode = process.env.HACKBOT_MODE || 'google';
+
+ if (mode === 'google') {
+ return getGoogleEmbedding(query);
+ } else if (mode === 'openai') {
+ return getOpenAIEmbedding(query);
+ } else {
+ return getOllamaEmbedding(query);
+ }
+}
+
export async function retrieveContext(
query: string,
opts?: { limit?: number; preferredTypes?: HackDocType[] }
): Promise {
- const limit = opts?.limit ?? 25;
const trimmed = query.trim();
+ // Analyze query complexity if no explicit limit provided
+ let limit = opts?.limit;
+ if (!limit) {
+ const complexity = analyzeQueryComplexity(trimmed);
+ limit = complexity.docLimit;
+ console.log('[hackbot][retrieve][adaptive]', {
+ query: trimmed,
+ complexity: complexity.type,
+ docLimit: limit,
+ reason: complexity.reason,
+ });
+ }
+
// Vector-only search over hackbot_docs in MongoDB.
try {
- const embeddingResult = await getQueryEmbedding(trimmed);
+ const embeddingResult = await retryWithBackoff(
+ () => getQueryEmbedding(trimmed),
+ {
+ maxAttempts: 3,
+ delayMs: 1000,
+ backoffMultiplier: 2,
+ }
+ );
+
if (!embeddingResult) {
console.error(
'[hackbot][retrieve] No embedding available for query; vector search required.'
);
- throw new Error('Embedding unavailable');
+ throw new Error('Embedding unavailable after retries');
}
const embedding = embeddingResult.embedding;
diff --git a/app/(api)/api/hackbot/stream/route.ts b/app/(api)/api/hackbot/stream/route.ts
new file mode 100644
index 00000000..db730fb9
--- /dev/null
+++ b/app/(api)/api/hackbot/stream/route.ts
@@ -0,0 +1,199 @@
+import { streamText } from 'ai';
+import { google } from '@ai-sdk/google';
+import { openai } from '@ai-sdk/openai';
+import { retrieveContext } from '@datalib/hackbot/getHackbotContext';
+import { HackbotMessage } from '@actions/hackbot/askHackbot';
+
+const MAX_USER_MESSAGE_CHARS = 200;
+const MAX_HISTORY_MESSAGES = 10;
+
+function parseIsoToMs(value: unknown): number | null {
+ if (typeof value !== 'string') return null;
+ const ms = Date.parse(value);
+ return Number.isFinite(ms) ? ms : null;
+}
+
+function stripExternalDomains(text: string): string {
+ return text.replace(/https?:\/\/[^\s)]+(\/[\w#/?=&.-]*)/g, '$1');
+}
+
+export async function POST(request: Request) {
+ try {
+ const { messages } = await request.json();
+
+ if (!Array.isArray(messages) || messages.length === 0) {
+ return Response.json({ error: 'Invalid request' }, { status: 400 });
+ }
+
+ const lastMessage = messages[messages.length - 1];
+
+ if (lastMessage.role !== 'user') {
+ return Response.json(
+ { error: 'Last message must be from user.' },
+ { status: 400 }
+ );
+ }
+
+ if (lastMessage.content.length > MAX_USER_MESSAGE_CHARS) {
+ return Response.json(
+ {
+ error: `Message too long. Please keep it under ${MAX_USER_MESSAGE_CHARS} characters.`,
+ },
+ { status: 400 }
+ );
+ }
+
+ // Retrieve context using adaptive retrieval
+ let docs;
+ try {
+ ({ docs } = await retrieveContext(lastMessage.content));
+ } catch (e) {
+ console.error('[hackbot][stream] Context retrieval error', e);
+ return Response.json(
+ { error: 'Search backend unavailable. Please contact an organizer.' },
+ { status: 500 }
+ );
+ }
+
+ if (!docs || docs.length === 0) {
+ return Response.json(
+ { error: 'No context found. Please contact an organizer.' },
+ { status: 500 }
+ );
+ }
+
+ // Sort docs chronologically (events first)
+ const sortedDocs = (() => {
+ const eventDocs = docs.filter((d: any) => d.type === 'event');
+ const otherDocs = docs.filter((d: any) => d.type !== 'event');
+
+ eventDocs.sort((a: any, b: any) => {
+ const aMs = parseIsoToMs(a.startISO);
+ const bMs = parseIsoToMs(b.startISO);
+
+ if (aMs === null && bMs === null) return 0;
+ if (aMs === null) return 1;
+ if (bMs === null) return -1;
+ return aMs - bMs;
+ });
+
+ return [...eventDocs, ...otherDocs];
+ })();
+
+ const contextSummary = sortedDocs
+ .map((d, index) => {
+ const header = `${index + 1}) [type=${d.type}, title="${d.title}"${
+ d.url ? `, url="${d.url}"` : ''
+ }]`;
+ return `${header}\n${d.text}`;
+ })
+ .join('\n\n');
+
+ const systemPrompt =
+ 'You are HackDavis Helper ("Hacky"), an AI assistant for the HackDavis hackathon. ' +
+ 'CRITICAL: Your response MUST be under 200 tokens (~150 words). Be extremely concise. ' +
+ 'CRITICAL: Only answer questions about HackDavis. Refuse unrelated topics politely. ' +
+ 'CRITICAL: Only use facts from the provided context. Never invent times, dates, or locations. ' +
+ 'You are friendly, helpful, and conversational. Use contractions ("you\'re", "it\'s") and avoid robotic phrasing. ' +
+ 'For simple greetings ("hi", "hello"), respond warmly: "Hi, I\'m Hacky! I can help with questions about HackDavis." Keep it brief (1 sentence). ' +
+ 'For questions about HackDavis: ' +
+ '1. First, silently identify the most relevant context document by matching key terms to document titles. ' +
+ '2. If multiple documents seem relevant (e.g., similar event names), ask ONE short clarifying question instead of guessing. ' +
+ '3. Answer directly in 2-3 sentences using only context facts. ' +
+ '4. For time/location questions: Use only explicit times and locations from context. If both start and end times exist, provide the full range ("3:00 PM to 4:00 PM"). ' +
+ '5. For schedule/timeline questions: Format as a bullet list, ordered chronologically. Include only items from context. ' +
+ 'Do NOT: ' +
+ '- Invent times, dates, locations, or URLs not in context. ' +
+ '- Include URLs in your answer text (UI shows separate "More info" link). ' +
+ '- Use generic hackathon knowledge; only use provided context. ' +
+ '- Answer coding, homework, or general knowledge questions. ' +
+ '- Say "based on the context" or "according to the documents" (just answer directly). ' +
+ 'If you cannot find an answer in context, say: "I don\'t have that information. Please ask an organizer or check the HackDavis website." ' +
+ 'For unrelated questions (not about HackDavis), say: "Sorry, I can only answer questions about HackDavis. Do you have any questions about the event?"';
+
+ const fewShotExamples = [
+ {
+ role: 'user' as const,
+ content: 'When does hacking end?',
+ },
+ {
+ role: 'assistant' as const,
+ content: 'Hacking ends on Sunday, April 20 at 11:00 AM Pacific Time.',
+ },
+ {
+ role: 'user' as const,
+ content: 'What workshops are available?',
+ },
+ {
+ role: 'assistant' as const,
+ content:
+ 'We have several workshops including:\n• Hackathons 101 (Sat 11:40 AM)\n• Getting Started with Git & GitHub (Sat 1:10 PM)\n• Intro to UI/UX (Sat 4:10 PM)\n• Hacking with LLMs (Sat 2:10 PM)',
+ },
+ ];
+
+ const chatMessages = [
+ { role: 'system', content: systemPrompt },
+ ...fewShotExamples,
+ {
+ role: 'system',
+ content: `Context documents about HackDavis (use these to answer):\n\n${contextSummary}`,
+ },
+ ...messages.slice(-MAX_HISTORY_MESSAGES),
+ ];
+
+ // Stream response using Vercel AI SDK
+ const mode = process.env.HACKBOT_MODE || 'google';
+ let result;
+
+ if (mode === 'google') {
+ const model = process.env.GOOGLE_MODEL || 'gemini-1.5-flash';
+ const maxTokens = parseInt(process.env.GOOGLE_MAX_TOKENS || '200', 10);
+
+ result = streamText({
+ model: google(model),
+ messages: chatMessages.map((m: any) => ({
+ role: m.role as 'system' | 'user' | 'assistant',
+ content: m.content,
+ })),
+ maxTokens,
+ });
+ } else {
+ // OpenAI fallback
+ const model = process.env.OPENAI_MODEL || 'gpt-4o';
+ const maxTokens = parseInt(process.env.OPENAI_MAX_TOKENS || '200', 10);
+
+ result = streamText({
+ model: openai(model),
+ messages: chatMessages.map((m: any) => ({
+ role: m.role as 'system' | 'user' | 'assistant',
+ content: m.content,
+ })),
+ maxTokens,
+ });
+ }
+
+ return result.toDataStreamResponse();
+ } catch (error: any) {
+ console.error('[hackbot][stream] Error', error);
+
+ // Differentiate error types
+ if (error.status === 429) {
+ return Response.json(
+ { error: 'Too many requests. Please wait a moment and try again.' },
+ { status: 429 }
+ );
+ }
+
+ if (error.status === 401 || error.message?.includes('API key')) {
+ return Response.json(
+ { error: 'AI service configuration error. Please contact an organizer.' },
+ { status: 500 }
+ );
+ }
+
+ return Response.json(
+ { error: 'Something went wrong. Please try again in a moment.' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/(pages)/(hackers)/_components/Hackbot/HackbotWidget.tsx b/app/(pages)/(hackers)/_components/Hackbot/HackbotWidget.tsx
index a05b701a..66e7ff50 100644
--- a/app/(pages)/(hackers)/_components/Hackbot/HackbotWidget.tsx
+++ b/app/(pages)/(hackers)/_components/Hackbot/HackbotWidget.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { Button } from '@pages/_globals/components/ui/button';
export type HackbotChatMessage = {
@@ -10,19 +10,56 @@ export type HackbotChatMessage = {
};
const MAX_USER_MESSAGE_CHARS = 200;
+const STORAGE_KEY = 'hackbot_chat_history';
+const MAX_STORED_MESSAGES = 20;
export default function HackbotWidget() {
const [open, setOpen] = useState(false);
- const [messages, setMessages] = useState([]);
+ const [messages, setMessages] = useState(() => {
+ // Load from localStorage on mount
+ if (typeof window !== 'undefined') {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (stored) {
+ const parsed = JSON.parse(stored);
+ if (Array.isArray(parsed)) {
+ return parsed.slice(-MAX_STORED_MESSAGES);
+ }
+ }
+ } catch (err) {
+ console.error('[hackbot] Failed to load history', err);
+ }
+ }
+ return [];
+ });
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
+ // Persist messages to localStorage whenever they change
+ useEffect(() => {
+ if (typeof window !== 'undefined' && messages.length > 0) {
+ try {
+ const toStore = messages.slice(-MAX_STORED_MESSAGES);
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
+ } catch (err) {
+ console.error('[hackbot] Failed to save history', err);
+ }
+ }
+ }, [messages]);
+
const canSend =
!loading &&
input.trim().length > 0 &&
input.trim().length <= MAX_USER_MESSAGE_CHARS;
+ const clearHistory = () => {
+ setMessages([]);
+ if (typeof window !== 'undefined') {
+ localStorage.removeItem(STORAGE_KEY);
+ }
+ };
+
const toggleOpen = () => {
setOpen((prev) => !prev);
setError(null);
@@ -41,8 +78,15 @@ export default function HackbotWidget() {
setError(null);
setLoading(true);
+ // Add placeholder for assistant response
+ const assistantPlaceholder: HackbotChatMessage = {
+ role: 'assistant',
+ content: '',
+ };
+ setMessages((prev) => [...prev, assistantPlaceholder]);
+
try {
- const response = await fetch('/api/hackbot', {
+ const response = await fetch('/api/hackbot/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -55,22 +99,51 @@ export default function HackbotWidget() {
}),
});
- const data = await response.json();
-
- if (!data.ok) {
- setError(data.error || 'Something went wrong.');
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error || 'Stream failed');
}
- const assistantMessage: HackbotChatMessage = {
- role: 'assistant',
- content: data.answer || 'Sorry, I could not answer that.',
- url: data.url || undefined,
- };
+ const reader = response.body?.getReader();
+ const decoder = new TextDecoder();
+ let accumulatedText = '';
+
+ if (reader) {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
- setMessages((prev) => [...prev, assistantMessage]);
- } catch (err) {
- setError('Network error. Please try again.');
- } finally {
+ const chunk = decoder.decode(value, { stream: true });
+ const lines = chunk.split('\n');
+
+ for (const line of lines) {
+ if (line.startsWith('0:')) {
+ // Vercel AI SDK data stream format
+ const content = line.slice(2).trim().replace(/^"(.*)"$/, '$1');
+ if (content) {
+ accumulatedText += content;
+
+ // Update the last message with accumulated text
+ setMessages((prev) => {
+ const updated = [...prev];
+ updated[updated.length - 1] = {
+ ...updated[updated.length - 1],
+ content: accumulatedText,
+ };
+ return updated;
+ });
+ }
+ }
+ }
+ }
+ }
+
+ setLoading(false);
+ } catch (err: any) {
+ console.error('[hackbot] Stream error', err);
+ setError(err.message || 'Network error. Please try again.');
+ // Remove placeholder message on error
+ setMessages((prev) => prev.slice(0, -1));
setLoading(false);
}
};
@@ -92,13 +165,25 @@ export default function HackbotWidget() {
judging, or submissions.
-
+
+ {messages.length > 0 && (
+
+ )}
+
+
@@ -164,10 +249,27 @@ export default function HackbotWidget() {
disabled={!canSend}
className="h-7 px-3 text-[11px]"
>
- {loading ? 'Thinking...' : 'Send'}
+ {loading ? (
+
+ ● Thinking...
+
+ ) : (
+ 'Send'
+ )}
- {error && {error}
}
+ {error && (
+
+
{error}
+
+
+ )}
)}
diff --git a/package-lock.json b/package-lock.json
index b62b8b5b..647693ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
"name": "hackdavis-hacker-hub",
"version": "0.1.0",
"dependencies": {
+ "@ai-sdk/google": "^1.0.0",
+ "@ai-sdk/openai": "^1.0.0",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@fortawesome/free-brands-svg-icons": "^6.5.1",
@@ -20,6 +22,7 @@
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@szhsin/react-accordion": "^1.4.0",
+ "ai": "^3.4.0",
"bcryptjs": "^2.4.3",
"chart.js": "^4.4.9",
"class-variance-authority": "^0.7.1",
@@ -87,6 +90,358 @@
"typescript": "^5"
}
},
+ "node_modules/@ai-sdk/google": {
+ "version": "1.2.22",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-1.2.22.tgz",
+ "integrity": "sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "1.1.3",
+ "@ai-sdk/provider-utils": "2.2.8"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.0.0"
+ }
+ },
+ "node_modules/@ai-sdk/openai": {
+ "version": "1.3.24",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-1.3.24.tgz",
+ "integrity": "sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "1.1.3",
+ "@ai-sdk/provider-utils": "2.2.8"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.0.0"
+ }
+ },
+ "node_modules/@ai-sdk/provider": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.3.tgz",
+ "integrity": "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "json-schema": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ai-sdk/provider-utils": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz",
+ "integrity": "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "1.1.3",
+ "nanoid": "^3.3.8",
+ "secure-json-parse": "^2.7.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.23.8"
+ }
+ },
+ "node_modules/@ai-sdk/react": {
+ "version": "0.0.70",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-0.0.70.tgz",
+ "integrity": "sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider-utils": "1.0.22",
+ "@ai-sdk/ui-utils": "0.0.50",
+ "swr": "^2.2.5",
+ "throttleit": "2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "zod": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider": {
+ "version": "0.0.26",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
+ "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "json-schema": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider-utils": {
+ "version": "1.0.22",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
+ "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "0.0.26",
+ "eventsource-parser": "^1.1.2",
+ "nanoid": "^3.3.7",
+ "secure-json-parse": "^2.7.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@ai-sdk/solid": {
+ "version": "0.0.54",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/solid/-/solid-0.0.54.tgz",
+ "integrity": "sha512-96KWTVK+opdFeRubqrgaJXoNiDP89gNxFRWUp0PJOotZW816AbhUf4EnDjBjXTLjXL1n0h8tGSE9sZsRkj9wQQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider-utils": "1.0.22",
+ "@ai-sdk/ui-utils": "0.0.50"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "solid-js": "^1.7.7"
+ },
+ "peerDependenciesMeta": {
+ "solid-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@ai-sdk/solid/node_modules/@ai-sdk/provider": {
+ "version": "0.0.26",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
+ "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "json-schema": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ai-sdk/solid/node_modules/@ai-sdk/provider-utils": {
+ "version": "1.0.22",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
+ "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "0.0.26",
+ "eventsource-parser": "^1.1.2",
+ "nanoid": "^3.3.7",
+ "secure-json-parse": "^2.7.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@ai-sdk/svelte": {
+ "version": "0.0.57",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/svelte/-/svelte-0.0.57.tgz",
+ "integrity": "sha512-SyF9ItIR9ALP9yDNAD+2/5Vl1IT6kchgyDH8xkmhysfJI6WrvJbtO1wdQ0nylvPLcsPoYu+cAlz1krU4lFHcYw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider-utils": "1.0.22",
+ "@ai-sdk/ui-utils": "0.0.50",
+ "sswr": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "svelte": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@ai-sdk/svelte/node_modules/@ai-sdk/provider": {
+ "version": "0.0.26",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
+ "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "json-schema": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ai-sdk/svelte/node_modules/@ai-sdk/provider-utils": {
+ "version": "1.0.22",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
+ "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "0.0.26",
+ "eventsource-parser": "^1.1.2",
+ "nanoid": "^3.3.7",
+ "secure-json-parse": "^2.7.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@ai-sdk/ui-utils": {
+ "version": "0.0.50",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-0.0.50.tgz",
+ "integrity": "sha512-Z5QYJVW+5XpSaJ4jYCCAVG7zIAuKOOdikhgpksneNmKvx61ACFaf98pmOd+xnjahl0pIlc/QIe6O4yVaJ1sEaw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "0.0.26",
+ "@ai-sdk/provider-utils": "1.0.22",
+ "json-schema": "^0.4.0",
+ "secure-json-parse": "^2.7.0",
+ "zod-to-json-schema": "^3.23.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@ai-sdk/ui-utils/node_modules/@ai-sdk/provider": {
+ "version": "0.0.26",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
+ "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "json-schema": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ai-sdk/ui-utils/node_modules/@ai-sdk/provider-utils": {
+ "version": "1.0.22",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
+ "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "0.0.26",
+ "eventsource-parser": "^1.1.2",
+ "nanoid": "^3.3.7",
+ "secure-json-parse": "^2.7.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@ai-sdk/vue": {
+ "version": "0.0.59",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-0.0.59.tgz",
+ "integrity": "sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider-utils": "1.0.22",
+ "@ai-sdk/ui-utils": "0.0.50",
+ "swrv": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.4"
+ },
+ "peerDependenciesMeta": {
+ "vue": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@ai-sdk/vue/node_modules/@ai-sdk/provider": {
+ "version": "0.0.26",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
+ "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "json-schema": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@ai-sdk/vue/node_modules/@ai-sdk/provider-utils": {
+ "version": "1.0.22",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
+ "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "0.0.26",
+ "eventsource-parser": "^1.1.2",
+ "nanoid": "^3.3.7",
+ "secure-json-parse": "^2.7.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -491,18 +846,18 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
- "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.25.9",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
- "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -642,12 +997,12 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
- "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
+ "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.27.0"
+ "@babel/types": "^7.28.6"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -2032,13 +2387,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
- "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
+ "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.25.9",
- "@babel/helper-validator-identifier": "^7.25.9"
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -3569,6 +3924,17 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -3588,9 +3954,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
@@ -4031,6 +4397,15 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/@panva/hkdf": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
@@ -5007,6 +5382,16 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@sveltejs/acorn-typescript": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz",
+ "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==",
+ "license": "MIT",
+ "peer": true,
+ "peerDependencies": {
+ "acorn": "^8.9.0"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -5127,6 +5512,19 @@
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
+ "node_modules/@types/diff-match-patch": {
+ "version": "1.0.36",
+ "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
+ "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -5588,11 +5986,132 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.27",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz",
+ "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/shared": "3.5.27",
+ "entities": "^7.0.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-core/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.27",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz",
+ "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-core": "3.5.27",
+ "@vue/shared": "3.5.27"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.27",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz",
+ "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/compiler-core": "3.5.27",
+ "@vue/compiler-dom": "3.5.27",
+ "@vue/compiler-ssr": "3.5.27",
+ "@vue/shared": "3.5.27",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.6",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.27",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz",
+ "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.27",
+ "@vue/shared": "3.5.27"
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.27",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz",
+ "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/shared": "3.5.27"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.27",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz",
+ "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/reactivity": "3.5.27",
+ "@vue/shared": "3.5.27"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.27",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz",
+ "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/reactivity": "3.5.27",
+ "@vue/runtime-core": "3.5.27",
+ "@vue/shared": "3.5.27",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.27",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz",
+ "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.27",
+ "@vue/shared": "3.5.27"
+ },
+ "peerDependencies": {
+ "vue": "3.5.27"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.27",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz",
+ "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
- "devOptional": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -5633,6 +6152,89 @@
"node": ">= 14"
}
},
+ "node_modules/ai": {
+ "version": "3.4.33",
+ "resolved": "https://registry.npmjs.org/ai/-/ai-3.4.33.tgz",
+ "integrity": "sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "0.0.26",
+ "@ai-sdk/provider-utils": "1.0.22",
+ "@ai-sdk/react": "0.0.70",
+ "@ai-sdk/solid": "0.0.54",
+ "@ai-sdk/svelte": "0.0.57",
+ "@ai-sdk/ui-utils": "0.0.50",
+ "@ai-sdk/vue": "0.0.59",
+ "@opentelemetry/api": "1.9.0",
+ "eventsource-parser": "1.1.2",
+ "json-schema": "^0.4.0",
+ "jsondiffpatch": "0.6.0",
+ "secure-json-parse": "^2.7.0",
+ "zod-to-json-schema": "^3.23.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "openai": "^4.42.0",
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "sswr": "^2.1.0",
+ "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0",
+ "zod": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "openai": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "sswr": {
+ "optional": true
+ },
+ "svelte": {
+ "optional": true
+ },
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ai/node_modules/@ai-sdk/provider": {
+ "version": "0.0.26",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
+ "integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "json-schema": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ai/node_modules/@ai-sdk/provider-utils": {
+ "version": "1.0.22",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
+ "integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@ai-sdk/provider": "0.0.26",
+ "eventsource-parser": "^1.1.2",
+ "nanoid": "^3.3.7",
+ "secure-json-parse": "^2.7.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "zod": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -5751,7 +6353,6 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
- "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -6035,7 +6636,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
- "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -7073,9 +7673,9 @@
}
},
"node_modules/csstype": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/csv-parse": {
@@ -7279,6 +7879,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -7303,6 +7912,13 @@
"node": ">=8"
}
},
+ "node_modules/devalue": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
+ "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -7319,6 +7935,12 @@
"node": ">=0.3.1"
}
},
+ "node_modules/diff-match-patch": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
+ "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
+ "license": "Apache-2.0"
+ },
"node_modules/diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -8854,6 +9476,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/esm-env": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
+ "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/espree": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -8899,6 +9528,16 @@
"node": ">=0.10"
}
},
+ "node_modules/esrap": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.2.tgz",
+ "integrity": "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ }
+ },
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@@ -8922,6 +9561,13 @@
"node": ">=4.0"
}
},
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -8932,6 +9578,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventsource-parser": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
+ "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -10247,6 +10902,16 @@
"node": ">=8"
}
},
+ "node_modules/is-reference": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+ "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/estree": "^1.0.6"
+ }
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -11759,6 +12424,12 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
+ "node_modules/json-schema": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
+ "license": "(AFL-2.1 OR BSD-3-Clause)"
+ },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -11786,6 +12457,35 @@
"json5": "lib/cli.js"
}
},
+ "node_modules/jsondiffpatch": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz",
+ "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/diff-match-patch": "^1.0.36",
+ "chalk": "^5.3.0",
+ "diff-match-patch": "^1.0.5"
+ },
+ "bin": {
+ "jsondiffpatch": "bin/jsondiffpatch.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/jsondiffpatch/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@@ -11955,6 +12655,13 @@
"node": ">=18"
}
},
+ "node_modules/locate-character": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
+ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -12084,6 +12791,16 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -12474,9 +13191,9 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.8",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
@@ -13653,9 +14370,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.3",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
- "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
@@ -13672,7 +14389,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.8",
+ "nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -14684,6 +15401,12 @@
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"license": "MIT"
},
+ "node_modules/secure-json-parse": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
+ "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -15069,6 +15792,18 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
+ "node_modules/sswr": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/sswr/-/sswr-2.2.0.tgz",
+ "integrity": "sha512-clTszLPZkmycALTHD1mXGU+mOtA/MIoLgS1KGTTzFNVm9rytQVykgRaP+z1zl572cz0bTqj4rFVoC2N+IGK4Sg==",
+ "license": "MIT",
+ "dependencies": {
+ "swrev": "^4.0.0"
+ },
+ "peerDependencies": {
+ "svelte": "^4.0.0 || ^5.0.0"
+ }
+ },
"node_modules/stable-hash": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz",
@@ -15462,6 +16197,61 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/svelte": {
+ "version": "5.49.1",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.49.1.tgz",
+ "integrity": "sha512-jj95WnbKbXsXXngYj28a4zx8jeZx50CN/J4r0CEeax2pbfdsETv/J1K8V9Hbu3DCXnpHz5qAikICuxEooi7eNQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@sveltejs/acorn-typescript": "^1.0.5",
+ "@types/estree": "^1.0.5",
+ "acorn": "^8.12.1",
+ "aria-query": "^5.3.1",
+ "axobject-query": "^4.1.0",
+ "clsx": "^2.1.1",
+ "devalue": "^5.6.2",
+ "esm-env": "^1.2.1",
+ "esrap": "^2.2.2",
+ "is-reference": "^3.0.3",
+ "locate-character": "^3.0.0",
+ "magic-string": "^0.30.11",
+ "zimmerframe": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/swr": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.8.tgz",
+ "integrity": "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/swrev": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz",
+ "integrity": "sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==",
+ "license": "MIT"
+ },
+ "node_modules/swrv": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/swrv/-/swrv-1.1.0.tgz",
+ "integrity": "sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "vue": ">=3.2.26 < 4"
+ }
+ },
"node_modules/synckit": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz",
@@ -15765,6 +16555,18 @@
"node": ">=0.8"
}
},
+ "node_modules/throttleit": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
+ "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
@@ -16289,6 +17091,15 @@
"integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==",
"license": "MIT"
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -16344,6 +17155,28 @@
"spdx-expression-parse": "^3.0.0"
}
},
+ "node_modules/vue": {
+ "version": "3.5.27",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz",
+ "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.27",
+ "@vue/compiler-sfc": "3.5.27",
+ "@vue/runtime-dom": "3.5.27",
+ "@vue/server-renderer": "3.5.27",
+ "@vue/shared": "3.5.27"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@@ -16799,14 +17632,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zimmerframe": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
+ "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/zod": {
- "version": "3.24.2",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
- "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+ },
+ "node_modules/zod-to-json-schema": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
+ "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "zod": "^3.25 || ^4"
+ }
}
}
}
diff --git a/package.json b/package.json
index dfadea92..9937e0a0 100644
--- a/package.json
+++ b/package.json
@@ -13,14 +13,18 @@
"seed": "run-script-os",
"seed:nix": "node --env-file='.env' \"scripts/dbSeed.mjs\"",
"seed:windows": "node --env-file=\".\\.env\" \".\\scripts\\dbSeed.mjs\"",
+ "hackbot:seed": "node --env-file='.env' scripts/hackbotSeedCI.mjs",
"test": "run-script-os",
"test:nix": "echo 'module.exports = { presets: [[\"@babel/preset-env\"]] };' > babel.config.js && npx jest --detectOpenHandles && rm babel.config.js",
"test:windows": "echo module.exports = { presets: [['@babel/preset-env']] }; > babel.config.js && npx jest --detectOpenHandles && rm babel.config.js",
"lint": "next lint"
},
"dependencies": {
+ "@ai-sdk/google": "^1.0.0",
+ "@ai-sdk/openai": "^1.0.0",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
+ "ai": "^3.4.0",
"@fortawesome/free-brands-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
diff --git a/scripts/hackbotSeedCI.mjs b/scripts/hackbotSeedCI.mjs
new file mode 100644
index 00000000..55acd6b6
--- /dev/null
+++ b/scripts/hackbotSeedCI.mjs
@@ -0,0 +1,286 @@
+import { getClient } from '../app/(api)/_utils/mongodb/mongoClient.mjs';
+import fs from 'fs';
+import path from 'path';
+
+const HACKBOT_COLLECTION = 'hackbot_docs';
+
+// Load from environment or use Google AI by default in CI
+const EMBEDDING_MODE = process.env.HACKBOT_MODE || 'google';
+const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY;
+const GOOGLE_EMBEDDING_MODEL =
+ process.env.GOOGLE_EMBEDDING_MODEL || 'text-embedding-004';
+const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
+const OPENAI_EMBEDDING_MODEL =
+ process.env.OPENAI_EMBEDDING_MODEL || 'text-embedding-3-small';
+const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434';
+
+function loadEventsFallbackFromKnowledge() {
+ const knowledgePath = path.join(
+ path.dirname(new URL(import.meta.url).pathname),
+ '..',
+ 'app',
+ '_data',
+ 'hackbot_knowledge.json'
+ );
+
+ const raw = fs.readFileSync(knowledgePath, 'utf8');
+ const knowledge = JSON.parse(raw);
+ return Array.isArray(knowledge?.events?.raw) ? knowledge.events.raw : [];
+}
+
+async function embedTextGoogle(text) {
+ const response = await fetch(
+ `https://generativelanguage.googleapis.com/v1beta/models/${GOOGLE_EMBEDDING_MODEL}:embedContent?key=${GOOGLE_API_KEY}`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ content: {
+ parts: [{ text }],
+ },
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(
+ `Google AI embeddings error: ${response.status} ${errorText}`
+ );
+ }
+
+ const data = await response.json();
+ return data.embedding.values;
+}
+
+async function embedTextOpenAI(text) {
+ const response = await fetch('https://api.openai.com/v1/embeddings', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${OPENAI_API_KEY}`,
+ },
+ body: JSON.stringify({
+ model: OPENAI_EMBEDDING_MODEL,
+ input: text,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`OpenAI embeddings error: ${response.status} ${errorText}`);
+ }
+
+ const data = await response.json();
+ return data.data[0].embedding;
+}
+
+async function embedTextOllama(text) {
+ const res = await fetch(`${OLLAMA_URL}/api/embeddings`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ model: 'llama3.2',
+ prompt: text,
+ }),
+ });
+
+ if (!res.ok) {
+ throw new Error(`Ollama embeddings error: ${res.status} ${res.statusText}`);
+ }
+
+ const data = await res.json();
+ if (!data || !Array.isArray(data.embedding)) {
+ throw new Error('Invalid embeddings response from Ollama');
+ }
+
+ return data.embedding;
+}
+
+async function embedText(text) {
+ if (EMBEDDING_MODE === 'google') {
+ return embedTextGoogle(text);
+ } else if (EMBEDDING_MODE === 'openai') {
+ return embedTextOpenAI(text);
+ } else {
+ return embedTextOllama(text);
+ }
+}
+
+function formatEventDateTime(raw) {
+ const iso =
+ typeof raw === 'string' ? raw : raw && raw.$date ? raw.$date : undefined;
+
+ if (!iso) return null;
+
+ const date = new Date(iso);
+ if (Number.isNaN(date.getTime())) return null;
+
+ return date.toLocaleString('en-US', {
+ timeZone: 'America/Los_Angeles',
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
+ });
+}
+
+function buildDocsFromEvents(events) {
+ if (!Array.isArray(events)) return [];
+
+ return events.map((ev) => {
+ const id = String(ev._id?.$oid || ev._id || ev.name);
+ const title = String(ev.name || 'Event');
+
+ const start = formatEventDateTime(ev.start_time);
+ const end = formatEventDateTime(ev.end_time);
+ const location = ev.location || '';
+ const host = ev.host || '';
+ const type = ev.type || '';
+
+ const parts = [
+ `Event: ${title}`,
+ type ? `Type: ${type}` : '',
+ start ? `Starts (Pacific Time): ${start}` : '',
+ end ? `Ends (Pacific Time): ${end}` : '',
+ location ? `Location: ${location}` : '',
+ host ? `Host: ${host}` : '',
+ Array.isArray(ev.tags) && ev.tags.length
+ ? `Tags: ${ev.tags.join(', ')}`
+ : '',
+ ].filter(Boolean);
+
+ return {
+ _id: `event-${id}`,
+ type: 'event',
+ title,
+ text: parts.join('\n'),
+ url: '/hackers/hub/schedule',
+ };
+ });
+}
+
+function buildStaticDocs() {
+ const judging = {
+ _id: 'judging-overview',
+ type: 'judging',
+ title: 'Judging Process Overview',
+ text:
+ 'Judging day timeline (Pacific Time):\n' +
+ '- 11:00 AM: submissions due on Devpost\n' +
+ '- 12:00–2:00 PM: demo time with judges\n' +
+ '- 2:00–3:00 PM: break\n' +
+ '- 3:00–4:00 PM: closing ceremony\n\n' +
+ 'Rubric: 60% track-specific, 20% social good, 10% creativity, 10% presentation.',
+ url: '/hackers/hub/project-info#judging',
+ };
+
+ const submission = {
+ _id: 'submission-overview',
+ type: 'submission',
+ title: 'Submission Process Overview',
+ text: 'Submission steps: (1) Log in or sign up on Devpost and join the HackDavis hackathon. (2) Register for the event. (3) Create a project (only one teammate needs to create it). (4) Invite teammates. (5) Fill out project details. (6) Submit the project on Devpost before the deadline.',
+ url: '/hackers/hub/project-info#submission',
+ };
+
+ return [judging, submission];
+}
+
+async function seedHackbotDocs() {
+ console.log('[hackbotSeedCI] Starting seeding process...');
+ console.log(`[hackbotSeedCI] Embedding mode: ${EMBEDDING_MODE}`);
+
+ const client = await getClient();
+
+ try {
+ await client.connect();
+ console.log('[hackbotSeedCI] Connected to MongoDB');
+ } catch (err) {
+ console.error('[hackbotSeedCI] MongoDB connection failed:', err.message);
+ process.exit(1);
+ }
+
+ const db = client.db();
+ const collection = db.collection(HACKBOT_COLLECTION);
+
+ // Always wipe in CI to ensure clean state
+ await collection.deleteMany({});
+ console.log(`[hackbotSeedCI] Wiped collection: ${HACKBOT_COLLECTION}`);
+
+ // Load events from live collection
+ let events = [];
+ try {
+ events = await db.collection('events').find({}).toArray();
+ console.log(`[hackbotSeedCI] Loaded ${events.length} events from database`);
+ } catch (err) {
+ console.warn(
+ '[hackbotSeedCI] Failed to load events, using fallback:',
+ err.message
+ );
+ events = loadEventsFallbackFromKnowledge();
+ }
+
+ if (!Array.isArray(events) || events.length === 0) {
+ events = loadEventsFallbackFromKnowledge();
+ console.log(
+ `[hackbotSeedCI] Using ${events.length} events from fallback`
+ );
+ }
+
+ const docs = [...buildDocsFromEvents(events), ...buildStaticDocs()];
+ console.log(
+ `[hackbotSeedCI] Preparing to embed and upsert ${docs.length} docs`
+ );
+
+ let successCount = 0;
+ for (const doc of docs) {
+ try {
+ const embedding = await embedText(doc.text);
+
+ await collection.updateOne(
+ { _id: doc._id },
+ {
+ $set: {
+ type: doc.type,
+ title: doc.title,
+ text: doc.text,
+ url: doc.url || null,
+ embedding,
+ },
+ },
+ { upsert: true }
+ );
+
+ successCount += 1;
+ console.log(`[hackbotSeedCI] Upserted doc ${doc._id}`);
+ } catch (err) {
+ console.error(
+ `[hackbotSeedCI] Failed to upsert doc ${doc._id}:`,
+ err.message
+ );
+ // Don't exit on individual doc failure
+ }
+ }
+
+ console.log(
+ `[hackbotSeedCI] Done. Successfully upserted ${successCount}/${docs.length} docs.`
+ );
+
+ await client.close();
+
+ if (successCount < docs.length) {
+ console.error(
+ `[hackbotSeedCI] Some docs failed. Success: ${successCount}/${docs.length}`
+ );
+ process.exit(1);
+ }
+}
+
+// Run
+seedHackbotDocs().catch((err) => {
+ console.error('[hackbotSeedCI] Fatal error:', err);
+ process.exit(1);
+});