From 36ce12ee54769fd03d31b58a508b9b09dc24194d Mon Sep 17 00:00:00 2001 From: theg1239 <52027622+theg1239@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:31:38 +0000 Subject: [PATCH 1/5] Add Study Brain foundation with schema, agent, and study-plan UI Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- app/(app)/past_papers/[code]/[exam]/page.tsx | 5 +- app/(app)/past_papers/[code]/page.tsx | 4 + app/(app)/study-plan/page.tsx | 53 ++++ app/(app)/syllabus/course/[code]/page.tsx | 3 + .../study-brain/study-plan-builder.tsx | 220 ++++++++++++++ app/components/study-brain/study-plan-cta.tsx | 70 +++++ db/schema.ts | 221 ++++++++++++++ db/types.ts | 21 ++ .../migration.sql | 215 ++++++++++++++ lib/study-brain/research/providers.ts | 45 +++ lib/study-brain/research/queries.ts | 32 ++ lib/study-brain/schemas.ts | 85 ++++++ worker/src/index.ts | 2 + worker/src/router.ts | 3 +- worker/src/study-brain-agent.ts | 277 ++++++++++++++++++ worker/src/types.ts | 25 ++ worker/wrangler.jsonc | 8 + 17 files changed, 1287 insertions(+), 2 deletions(-) create mode 100644 app/(app)/study-plan/page.tsx create mode 100644 app/components/study-brain/study-plan-builder.tsx create mode 100644 app/components/study-brain/study-plan-cta.tsx create mode 100644 drizzle/20260619120000_study_brain_foundation/migration.sql create mode 100644 lib/study-brain/research/providers.ts create mode 100644 lib/study-brain/research/queries.ts create mode 100644 lib/study-brain/schemas.ts create mode 100644 worker/src/study-brain-agent.ts diff --git a/app/(app)/past_papers/[code]/[exam]/page.tsx b/app/(app)/past_papers/[code]/[exam]/page.tsx index fd9fb2cf..2b09af89 100644 --- a/app/(app)/past_papers/[code]/[exam]/page.tsx +++ b/app/(app)/past_papers/[code]/[exam]/page.tsx @@ -21,6 +21,7 @@ import { getCourseSyllabusPath, } from "@/lib/seo"; import CourseHeader from "@/app/components/past_papers/course-header"; +import StudyPlanCta from "@/app/components/study-brain/study-plan-cta"; import CoursePaperGrid from "@/app/components/past_papers/course-paper-grid"; import { DESKTOP_SELECT_ALL_HOST_ID, @@ -191,7 +192,9 @@ async function CourseExamContent({ ]} /> -
+ + +

Open the dedicated {label} collection for {course.code} when you want a focused set of papers for one exam pattern instead of the full course list. diff --git a/app/(app)/past_papers/[code]/page.tsx b/app/(app)/past_papers/[code]/page.tsx index 7d488393..04fcefe2 100644 --- a/app/(app)/past_papers/[code]/page.tsx +++ b/app/(app)/past_papers/[code]/page.tsx @@ -34,6 +34,7 @@ import { } from "@/app/components/past_papers/course-paper-grid-controls"; import CoursePagination from "@/app/components/past_papers/course-pagination"; import CourseVisitTracker from "@/app/components/past_papers/course-visit-tracker"; +import StudyPlanCta from "@/app/components/study-brain/study-plan-cta"; import { campusValues, course as courseTable, @@ -602,6 +603,7 @@ async function CoursePastPapersPageContent({ noteCount={course.noteCount} syllabusId={null} > + + + }> ; +}) { + const [params, courses] = await Promise.all([ + searchParams ?? Promise.resolve({} as SearchParams), + getCoursePickerRecords(), + ]); + + return ( + +

+ +
+ +
+
+ + ); +} diff --git a/app/(app)/syllabus/course/[code]/page.tsx b/app/(app)/syllabus/course/[code]/page.tsx index 4c9fc42d..e1544c25 100644 --- a/app/(app)/syllabus/course/[code]/page.tsx +++ b/app/(app)/syllabus/course/[code]/page.tsx @@ -9,6 +9,7 @@ import ViewTracker from "@/app/components/view-tracker"; import { LazySyllabusInlineEditor } from "@/app/components/moderation/lazy-editors"; import StructuredData from "@/app/components/seo/structured-data"; import DirectionalTransition from "@/app/components/common/directional-transition"; +import StudyPlanCta from "@/app/components/study-brain/study-plan-cta"; import { getCourseByCodeAny } from "@/lib/data/courses"; import { getCourseDetailByCode } from "@/lib/data/course-catalog"; import { getSubjectByCourseCode } from "@/lib/data/resources"; @@ -189,6 +190,8 @@ async function CourseSyllabusContent({ + +
= [ + { id: "past_papers", label: "Past-paper practice", description: "Prioritize questions and patterns." }, + { id: "videos", label: "Video explanations", description: "Use good lectures when they save time." }, + { id: "notes", label: "Notes and reading", description: "Keep it text-first and skimmable." }, + { id: "solved_examples", label: "Solved examples", description: "Work through examples before papers." }, + { id: "quick_summaries", label: "Quick summaries", description: "Fast revision blocks first." }, + { id: "mixed", label: "Mixed mode", description: "Let ExamCooker balance the queue." }, +]; + +function normalize(value: string) { + return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); +} + +export default function StudyPlanBuilder({ + courses, + initialCourse = "", + initialExam = "", + initialSlot = "", +}: Props) { + const [courseQuery, setCourseQuery] = useState(initialCourse); + const [selectedCourseCode, setSelectedCourseCode] = useState(initialCourse.toUpperCase()); + const [examType, setExamType] = useState(initialExam.toUpperCase()); + const [slot, setSlot] = useState(initialSlot.toUpperCase()); + const [preferences, setPreferences] = useState(["past_papers", "mixed"]); + + const selectedCourse = useMemo( + () => courses.find((course) => course.code.toUpperCase() === selectedCourseCode), + [courses, selectedCourseCode], + ); + + const matches = useMemo(() => { + const query = normalize(courseQuery); + if (!query) return courses.slice(0, 8); + return courses + .filter((course) => { + const haystack = normalize(`${course.code} ${course.title} ${course.aliases.join(" ")}`); + return haystack.includes(query); + }) + .slice(0, 8); + }, [courseQuery, courses]); + + const togglePreference = (id: StudyPreferenceMode) => { + setPreferences((current) => + current.includes(id) + ? current.filter((item) => item !== id) + : [...current, id], + ); + }; + + return ( +
+
+
+ + Study Brain +
+

+ Build the queue that makes sense for your exam. +

+

+ Choose the course, exam, slot, and how you actually study. ExamCooker + will use syllabus topics, previous papers, slot reports, and web research + without making it feel like AI homework. +

+ +
+ + +
+ {matches.map((course) => { + const active = course.code.toUpperCase() === selectedCourseCode; + return ( + + ); + })} +
+ +
+ + +
+
+
+ + +
+ ); +} diff --git a/app/components/study-brain/study-plan-cta.tsx b/app/components/study-brain/study-plan-cta.tsx new file mode 100644 index 00000000..a73f6be0 --- /dev/null +++ b/app/components/study-brain/study-plan-cta.tsx @@ -0,0 +1,70 @@ +import Link from "next/link"; +import { ArrowRight, Clock3, Sparkles } from "lucide-react"; + +type Props = { + courseCode?: string; + examType?: string; + slot?: string; + variant?: "compact" | "panel"; +}; + +function buildStudyPlanHref({ courseCode, examType, slot }: Props) { + const params = new URLSearchParams(); + if (courseCode) params.set("course", courseCode); + if (examType) params.set("exam", examType); + if (slot) params.set("slot", slot); + const query = params.toString(); + return `/study-plan${query ? `?${query}` : ""}`; +} + +export default function StudyPlanCta({ + courseCode, + examType, + slot, + variant = "panel", +}: Props) { + const href = buildStudyPlanHref({ courseCode, examType, slot }); + + if (variant === "compact") { + return ( + + + Plan study + + ); + } + + return ( +
+
+
+
+ +
+
+

+ Exam soon? +

+

+ Build a plan from your syllabus, slot intel, and papers. +

+

+ Pick what is coming, choose how you study, and let ExamCooker line up + the high-yield stuff first. No calendar cosplay required. +

+
+
+ + Plan my study + + +
+
+ ); +} diff --git a/db/schema.ts b/db/schema.ts index 8aa8f10c..76a77aa5 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -614,6 +614,227 @@ export const upcomingExam = cockroachTable( table.scheduledAt.asc(), ), ], + ); + +export const examInstance = cockroachTable( + "ExamInstance", + { + id: string().$defaultFn(createId).primaryKey(), + courseId: string() + .notNull() + .references(() => course.id, { onDelete: "cascade", onUpdate: "cascade" }), + courseCode: string().notNull(), + examType: examType(), + semester: semester().default("UNKNOWN").notNull(), + campus: campus().default("VELLORE").notNull(), + slot: string(), + scheduledAt: timestamp({ mode: "date", precision: 3 }), + status: string().default("upcoming").notNull(), + createdAt: createdAtColumn(), + updatedAt: updatedAtColumn(), + }, + (table) => [ + index("ExamInstance_courseId_idx").using("btree", table.courseId.asc()), + index("ExamInstance_course_exam_slot_idx").using( + "btree", + table.courseCode.asc(), + table.examType.asc(), + table.semester.asc(), + table.campus.asc(), + table.slot.asc(), + ), + index("ExamInstance_scheduledAt_idx").using("btree", table.scheduledAt.asc()), + ], +); + +export const syllabusTopicExtraction = cockroachTable( + "SyllabusTopicExtraction", + { + id: string().$defaultFn(createId).primaryKey(), + syllabusId: string() + .notNull() + .references(() => syllabi.id, { onDelete: "cascade", onUpdate: "cascade" }), + sourceHash: string().notNull(), + model: string().notNull(), + promptVersion: string().notNull(), + status: string().default("queued").notNull(), + topics: jsonb().$type(), + error: string(), + createdAt: createdAtColumn(), + updatedAt: updatedAtColumn(), + }, + (table) => [ + index("SyllabusTopicExtraction_syllabusId_idx").using("btree", table.syllabusId.asc()), + uniqueIndex("SyllabusTopicExtraction_source_key").using( + "btree", + table.syllabusId.asc(), + table.sourceHash.asc(), + table.model.asc(), + table.promptVersion.asc(), + ), + ], +); + +export const canonicalTopic = cockroachTable( + "CanonicalTopic", + { + id: string().$defaultFn(createId).primaryKey(), + courseId: string() + .notNull() + .references(() => course.id, { onDelete: "cascade", onUpdate: "cascade" }), + syllabusId: string().references(() => syllabi.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + moduleLabel: string(), + title: string().notNull(), + aliases: string().array().$defaultFn(() => []), + embedding: jsonb().$type(), + createdAt: createdAtColumn(), + updatedAt: updatedAtColumn(), + }, + (table) => [ + index("CanonicalTopic_courseId_idx").using("btree", table.courseId.asc()), + index("CanonicalTopic_title_trgm_idx").using("gin", table.title.asc()), + ], +); + +export const examSignal = cockroachTable( + "ExamSignal", + { + id: string().$defaultFn(createId).primaryKey(), + examInstanceId: string().references(() => examInstance.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + userId: string().references(() => user.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + topicId: string().references(() => canonicalTopic.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + signalType: string().notNull(), + source: string().notNull(), + rawText: string(), + confidence: int4().default(1).notNull(), + metadata: jsonb().$type>(), + createdAt: createdAtColumn(), + updatedAt: updatedAtColumn(), + }, + (table) => [ + index("ExamSignal_examInstanceId_idx").using("btree", table.examInstanceId.asc()), + index("ExamSignal_topicId_idx").using("btree", table.topicId.asc()), + index("ExamSignal_userId_idx").using("btree", table.userId.asc()), + index("ExamSignal_type_createdAt_idx").using( + "btree", + table.signalType.asc(), + table.createdAt.asc(), + ), + ], +); + +export const studyPlanRun = cockroachTable( + "StudyPlanRun", + { + id: string().$defaultFn(createId).primaryKey(), + userId: string() + .notNull() + .references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" }), + courseId: string().references(() => course.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + examInstanceId: string().references(() => examInstance.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + courseCode: string().notNull(), + examType: examType(), + syllabusId: string().references(() => syllabi.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + selectedTopics: jsonb().$type().notNull(), + studyPreferences: jsonb().$type().notNull(), + status: string().default("queued").notNull(), + plan: jsonb().$type(), + error: string(), + cost: jsonb().$type>(), + createdAt: createdAtColumn(), + updatedAt: updatedAtColumn(), + }, + (table) => [ + index("StudyPlanRun_userId_updatedAt_idx").using( + "btree", + table.userId.asc(), + table.updatedAt.asc(), + ), + index("StudyPlanRun_course_exam_idx").using( + "btree", + table.courseCode.asc(), + table.examType.asc(), + ), + index("StudyPlanRun_status_idx").using("btree", table.status.asc()), + ], +); + +export const slotIntelligenceSummary = cockroachTable( + "SlotIntelligenceSummary", + { + id: string().$defaultFn(createId).primaryKey(), + courseId: string() + .notNull() + .references(() => course.id, { onDelete: "cascade", onUpdate: "cascade" }), + examType: examType(), + semester: semester().default("UNKNOWN").notNull(), + campus: campus().default("VELLORE").notNull(), + slot: string(), + summary: jsonb().$type().notNull(), + confidence: int4().default(1).notNull(), + generatedAt: timestamp({ mode: "date", precision: 3 }).$defaultFn(now).notNull(), + createdAt: createdAtColumn(), + updatedAt: updatedAtColumn(), + }, + (table) => [ + uniqueIndex("SlotIntelligenceSummary_scope_key").using( + "btree", + table.courseId.asc(), + table.examType.asc(), + table.semester.asc(), + table.campus.asc(), + table.slot.asc(), + ), + ], +); + +export const contributionLedger = cockroachTable( + "ContributionLedger", + { + id: string().$defaultFn(createId).primaryKey(), + userId: string() + .notNull() + .references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" }), + signalId: string().references(() => examSignal.id, { + onDelete: "set null", + onUpdate: "cascade", + }), + creditDelta: int4().default(0).notNull(), + reputationDelta: int4().default(0).notNull(), + reason: string().notNull(), + status: string().default("pending").notNull(), + createdAt: createdAtColumn(), + updatedAt: updatedAtColumn(), + }, + (table) => [ + index("ContributionLedger_userId_createdAt_idx").using( + "btree", + table.userId.asc(), + table.createdAt.asc(), + ), + index("ContributionLedger_signalId_idx").using("btree", table.signalId.asc()), + ], ); export const viewHistory = cockroachTable( diff --git a/db/types.ts b/db/types.ts index 02b1360c..e57ed085 100644 --- a/db/types.ts +++ b/db/types.ts @@ -11,6 +11,10 @@ import { accounts, comment, course, + canonicalTopic, + contributionLedger, + examInstance, + examSignal, forum, forumPost, module, @@ -19,8 +23,11 @@ import { sessions, studyChat, studyMessage, + studyPlanRun, subject, syllabi, + slotIntelligenceSummary, + syllabusTopicExtraction, tag, upcomingExam, user, @@ -31,6 +38,10 @@ import { export type Account = InferSelectModel; export type Comment = InferSelectModel; export type Course = InferSelectModel; +export type CanonicalTopic = InferSelectModel; +export type ContributionLedger = InferSelectModel; +export type ExamInstance = InferSelectModel; +export type ExamSignal = InferSelectModel; export type Forum = InferSelectModel; export type ForumPost = InferSelectModel; export type Module = InferSelectModel; @@ -39,8 +50,11 @@ export type PastPaper = InferSelectModel; export type Session = InferSelectModel; export type StudyChat = InferSelectModel; export type StudyMessage = InferSelectModel; +export type StudyPlanRun = InferSelectModel; export type Subject = InferSelectModel; export type Syllabus = InferSelectModel; +export type SlotIntelligenceSummary = InferSelectModel; +export type SyllabusTopicExtraction = InferSelectModel; export type Tag = InferSelectModel; export type UpcomingExam = InferSelectModel; export type User = InferSelectModel; @@ -50,6 +64,10 @@ export type Vote = InferSelectModel; export type NewAccount = InferInsertModel; export type NewComment = InferInsertModel; export type NewCourse = InferInsertModel; +export type NewCanonicalTopic = InferInsertModel; +export type NewContributionLedger = InferInsertModel; +export type NewExamInstance = InferInsertModel; +export type NewExamSignal = InferInsertModel; export type NewForum = InferInsertModel; export type NewForumPost = InferInsertModel; export type NewModule = InferInsertModel; @@ -58,8 +76,11 @@ export type NewPastPaper = InferInsertModel; export type NewSession = InferInsertModel; export type NewStudyChat = InferInsertModel; export type NewStudyMessage = InferInsertModel; +export type NewStudyPlanRun = InferInsertModel; export type NewSubject = InferInsertModel; export type NewSyllabus = InferInsertModel; +export type NewSlotIntelligenceSummary = InferInsertModel; +export type NewSyllabusTopicExtraction = InferInsertModel; export type NewTag = InferInsertModel; export type NewUpcomingExam = InferInsertModel; export type NewUser = InferInsertModel; diff --git a/drizzle/20260619120000_study_brain_foundation/migration.sql b/drizzle/20260619120000_study_brain_foundation/migration.sql new file mode 100644 index 00000000..1586c7a5 --- /dev/null +++ b/drizzle/20260619120000_study_brain_foundation/migration.sql @@ -0,0 +1,215 @@ +CREATE TABLE IF NOT EXISTS "ExamInstance" ( + "id" STRING PRIMARY KEY, + "courseId" STRING NOT NULL, + "courseCode" STRING NOT NULL, + "examType" "ExamType", + "semester" "Semester" DEFAULT 'UNKNOWN' NOT NULL, + "campus" "Campus" DEFAULT 'VELLORE' NOT NULL, + "slot" STRING, + "scheduledAt" timestamp(3), + "status" STRING DEFAULT 'upcoming' NOT NULL, + "createdAt" timestamp(3) DEFAULT current_timestamp() NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + CONSTRAINT "ExamInstance_courseId_fkey" + FOREIGN KEY ("courseId") + REFERENCES "Course"("id") + ON DELETE CASCADE + ON UPDATE CASCADE +); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ExamInstance_courseId_idx" + ON "ExamInstance" ("courseId"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ExamInstance_course_exam_slot_idx" + ON "ExamInstance" ("courseCode", "examType", "semester", "campus", "slot"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ExamInstance_scheduledAt_idx" + ON "ExamInstance" ("scheduledAt"); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "SyllabusTopicExtraction" ( + "id" STRING PRIMARY KEY, + "syllabusId" STRING NOT NULL, + "sourceHash" STRING NOT NULL, + "model" STRING NOT NULL, + "promptVersion" STRING NOT NULL, + "status" STRING DEFAULT 'queued' NOT NULL, + "topics" jsonb, + "error" STRING, + "createdAt" timestamp(3) DEFAULT current_timestamp() NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + CONSTRAINT "SyllabusTopicExtraction_syllabusId_fkey" + FOREIGN KEY ("syllabusId") + REFERENCES "syllabi"("id") + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT "SyllabusTopicExtraction_source_key" + UNIQUE ("syllabusId", "sourceHash", "model", "promptVersion") +); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "SyllabusTopicExtraction_syllabusId_idx" + ON "SyllabusTopicExtraction" ("syllabusId"); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "CanonicalTopic" ( + "id" STRING PRIMARY KEY, + "courseId" STRING NOT NULL, + "syllabusId" STRING, + "moduleLabel" STRING, + "title" STRING NOT NULL, + "aliases" STRING[] DEFAULT ARRAY[]:::STRING[], + "embedding" jsonb, + "createdAt" timestamp(3) DEFAULT current_timestamp() NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + CONSTRAINT "CanonicalTopic_courseId_fkey" + FOREIGN KEY ("courseId") + REFERENCES "Course"("id") + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT "CanonicalTopic_syllabusId_fkey" + FOREIGN KEY ("syllabusId") + REFERENCES "syllabi"("id") + ON DELETE SET NULL + ON UPDATE CASCADE +); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "CanonicalTopic_courseId_idx" + ON "CanonicalTopic" ("courseId"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "CanonicalTopic_title_trgm_idx" + ON "CanonicalTopic" USING gin ("title"); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "ExamSignal" ( + "id" STRING PRIMARY KEY, + "examInstanceId" STRING, + "userId" STRING, + "topicId" STRING, + "signalType" STRING NOT NULL, + "source" STRING NOT NULL, + "rawText" STRING, + "confidence" INT4 DEFAULT 1 NOT NULL, + "metadata" jsonb, + "createdAt" timestamp(3) DEFAULT current_timestamp() NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + CONSTRAINT "ExamSignal_examInstanceId_fkey" + FOREIGN KEY ("examInstanceId") + REFERENCES "ExamInstance"("id") + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT "ExamSignal_userId_fkey" + FOREIGN KEY ("userId") + REFERENCES "User"("id") + ON DELETE SET NULL + ON UPDATE CASCADE, + CONSTRAINT "ExamSignal_topicId_fkey" + FOREIGN KEY ("topicId") + REFERENCES "CanonicalTopic"("id") + ON DELETE SET NULL + ON UPDATE CASCADE +); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ExamSignal_examInstanceId_idx" + ON "ExamSignal" ("examInstanceId"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ExamSignal_topicId_idx" + ON "ExamSignal" ("topicId"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ExamSignal_userId_idx" + ON "ExamSignal" ("userId"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ExamSignal_type_createdAt_idx" + ON "ExamSignal" ("signalType", "createdAt"); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "StudyPlanRun" ( + "id" STRING PRIMARY KEY, + "userId" STRING NOT NULL, + "courseId" STRING, + "examInstanceId" STRING, + "courseCode" STRING NOT NULL, + "examType" "ExamType", + "syllabusId" STRING, + "selectedTopics" jsonb NOT NULL, + "studyPreferences" jsonb NOT NULL, + "status" STRING DEFAULT 'queued' NOT NULL, + "plan" jsonb, + "error" STRING, + "cost" jsonb, + "createdAt" timestamp(3) DEFAULT current_timestamp() NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + CONSTRAINT "StudyPlanRun_userId_fkey" + FOREIGN KEY ("userId") + REFERENCES "User"("id") + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT "StudyPlanRun_courseId_fkey" + FOREIGN KEY ("courseId") + REFERENCES "Course"("id") + ON DELETE SET NULL + ON UPDATE CASCADE, + CONSTRAINT "StudyPlanRun_examInstanceId_fkey" + FOREIGN KEY ("examInstanceId") + REFERENCES "ExamInstance"("id") + ON DELETE SET NULL + ON UPDATE CASCADE, + CONSTRAINT "StudyPlanRun_syllabusId_fkey" + FOREIGN KEY ("syllabusId") + REFERENCES "syllabi"("id") + ON DELETE SET NULL + ON UPDATE CASCADE +); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "StudyPlanRun_userId_updatedAt_idx" + ON "StudyPlanRun" ("userId", "updatedAt"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "StudyPlanRun_course_exam_idx" + ON "StudyPlanRun" ("courseCode", "examType"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "StudyPlanRun_status_idx" + ON "StudyPlanRun" ("status"); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "SlotIntelligenceSummary" ( + "id" STRING PRIMARY KEY, + "courseId" STRING NOT NULL, + "examType" "ExamType", + "semester" "Semester" DEFAULT 'UNKNOWN' NOT NULL, + "campus" "Campus" DEFAULT 'VELLORE' NOT NULL, + "slot" STRING, + "summary" jsonb NOT NULL, + "confidence" INT4 DEFAULT 1 NOT NULL, + "generatedAt" timestamp(3) DEFAULT current_timestamp() NOT NULL, + "createdAt" timestamp(3) DEFAULT current_timestamp() NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + CONSTRAINT "SlotIntelligenceSummary_courseId_fkey" + FOREIGN KEY ("courseId") + REFERENCES "Course"("id") + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT "SlotIntelligenceSummary_scope_key" + UNIQUE ("courseId", "examType", "semester", "campus", "slot") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "ContributionLedger" ( + "id" STRING PRIMARY KEY, + "userId" STRING NOT NULL, + "signalId" STRING, + "creditDelta" INT4 DEFAULT 0 NOT NULL, + "reputationDelta" INT4 DEFAULT 0 NOT NULL, + "reason" STRING NOT NULL, + "status" STRING DEFAULT 'pending' NOT NULL, + "createdAt" timestamp(3) DEFAULT current_timestamp() NOT NULL, + "updatedAt" timestamp(3) NOT NULL, + CONSTRAINT "ContributionLedger_userId_fkey" + FOREIGN KEY ("userId") + REFERENCES "User"("id") + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT "ContributionLedger_signalId_fkey" + FOREIGN KEY ("signalId") + REFERENCES "ExamSignal"("id") + ON DELETE SET NULL + ON UPDATE CASCADE +); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ContributionLedger_userId_createdAt_idx" + ON "ContributionLedger" ("userId", "createdAt"); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ContributionLedger_signalId_idx" + ON "ContributionLedger" ("signalId"); diff --git a/lib/study-brain/research/providers.ts b/lib/study-brain/research/providers.ts new file mode 100644 index 00000000..14680cd8 --- /dev/null +++ b/lib/study-brain/research/providers.ts @@ -0,0 +1,45 @@ +export type ResearchSearchProviderId = "parallel" | "exa"; + +export type ResearchSourceType = + | "official" + | "university" + | "docs" + | "tutorial" + | "video" + | "forum" + | "unknown"; + +export type ResearchSearchInput = { + query: string; + topicTitle: string; + courseCode: string; + courseTitle: string; + maxResults: number; +}; + +export type ResearchSearchResult = { + title: string; + url: string; + snippet: string | null; + sourceType: ResearchSourceType; + provider: ResearchSearchProviderId; + providerScore?: number; + publishedAt?: string | null; +}; + +export type ResearchSearchProvider = { + id: ResearchSearchProviderId; + search(input: ResearchSearchInput): Promise; +}; + +export type ResearchBudget = { + maxSearches: number; + maxFetchedPages: number; + maxSynthesisCalls: number; +}; + +export const DEFAULT_RESEARCH_BUDGET: ResearchBudget = { + maxSearches: 8, + maxFetchedPages: 12, + maxSynthesisCalls: 3, +}; diff --git a/lib/study-brain/research/queries.ts b/lib/study-brain/research/queries.ts new file mode 100644 index 00000000..32f44669 --- /dev/null +++ b/lib/study-brain/research/queries.ts @@ -0,0 +1,32 @@ +import type { StudyPreferenceMode } from "@/lib/study-brain/schemas"; + +type BuildResearchQueriesInput = { + courseCode: string; + courseTitle: string; + topicTitle: string; + preferences: StudyPreferenceMode[]; +}; + +export function buildResearchQueries(input: BuildResearchQueriesInput) { + const baseTopic = `${input.courseTitle} ${input.topicTitle}`.replace(/\s+/g, " ").trim(); + const queries = new Set([ + `${baseTopic} explained for engineering students`, + `${baseTopic} solved problems examples`, + `${input.topicTitle} ${input.courseTitle} university notes`, + ]); + + if (input.preferences.includes("videos")) { + queries.add(`${baseTopic} lecture video`); + } + + if (input.preferences.includes("past_papers")) { + queries.add(`${input.courseCode} ${input.topicTitle} previous year questions`); + queries.add(`${baseTopic} exam questions solved`); + } + + if (input.preferences.includes("quick_summaries")) { + queries.add(`${baseTopic} quick revision summary`); + } + + return Array.from(queries).slice(0, 6); +} diff --git a/lib/study-brain/schemas.ts b/lib/study-brain/schemas.ts new file mode 100644 index 00000000..e9d90aaa --- /dev/null +++ b/lib/study-brain/schemas.ts @@ -0,0 +1,85 @@ +import { z } from "zod"; +import { campusValues, examTypeValues, semesterValues } from "@/db/enums"; + +export const StudyPreferenceModeSchema = z.enum([ + "videos", + "past_papers", + "notes", + "solved_examples", + "quick_summaries", + "mixed", +]); + +export const StudyPlanPrioritySchema = z.enum([ + "critical", + "high", + "medium", + "low", +]); + +export const StudyPlanEvidenceSchema = z.object({ + type: z.enum(["syllabus", "past_paper", "slot_report", "web", "resource"]), + label: z.string().min(1), + confidence: z.enum(["high", "medium", "low"]), +}); + +export const StudyPlanTaskSchema = z.object({ + kind: z.enum(["read", "watch", "practice", "revise", "skim"]), + title: z.string().min(1), + url: z.string().url().optional(), + source: z.string().optional(), + estimatedMinutes: z.number().int().min(1).max(240), + skipIfShortOnTime: z.boolean().optional(), +}); + +export const StudyPlanSectionSchema = z.object({ + topicId: z.string().optional(), + topicTitle: z.string().min(1), + priority: StudyPlanPrioritySchema, + estimatedMinutes: z.number().int().min(1).max(600), + reason: z.string().min(1), + evidence: z.array(StudyPlanEvidenceSchema), + tasks: z.array(StudyPlanTaskSchema), +}); + +export const StudyPlanSchema = z.object({ + title: z.string().min(1), + context: z.object({ + courseCode: z.string().min(1), + courseTitle: z.string().min(1), + examType: z.enum(examTypeValues).nullable(), + slot: z.string().nullable().optional(), + syllabusName: z.string().nullable().optional(), + }), + evidenceSummary: z.object({ + syllabusTopicsUsed: z.number().int().min(0), + pastPapersUsed: z.number().int().min(0), + earlierSlotSignalsUsed: z.number().int().min(0), + webResourcesUsed: z.number().int().min(0), + }), + totalEstimatedMinutes: z.number().int().min(0), + sections: z.array(StudyPlanSectionSchema), + warnings: z.array(z.string()), +}); + +export const StudyBrainPlanRequestSchema = z.object({ + courseCode: z.string().min(1), + examType: z.enum(examTypeValues).nullable(), + semester: z.enum(semesterValues).default("UNKNOWN"), + campus: z.enum(campusValues).default("VELLORE"), + slot: z.string().trim().max(20).nullable().optional(), + syllabusId: z.string().nullable().optional(), + selectedTopics: z.array( + z.object({ + topicId: z.string().nullable().optional(), + title: z.string().min(1), + rawText: z.string().optional(), + }), + ), + preferences: z.array(StudyPreferenceModeSchema).min(1), +}); + +export type StudyPreferenceMode = z.infer; +export type StudyPlan = z.infer; +export type StudyPlanSection = z.infer; +export type StudyBrainPlanRequest = z.infer; diff --git a/worker/src/index.ts b/worker/src/index.ts index 2ad60248..c0204956 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -1,10 +1,12 @@ /// import { ExamCookerCommandAgent } from "./command-agent"; +import { ExamCookerStudyBrainAgent } from "./study-brain-agent"; import { routeWorkerRequest } from "./router"; import type { Env } from "./types"; export { ExamCookerCommandAgent }; +export { ExamCookerStudyBrainAgent }; export default { async fetch(request: Request, env: Env) { diff --git a/worker/src/router.ts b/worker/src/router.ts index ea574eba..0db0b9cb 100644 --- a/worker/src/router.ts +++ b/worker/src/router.ts @@ -14,9 +14,10 @@ export async function routeWorkerRequest(request: Request, env: Env) { if (pathname === "/health") { return jsonResponse({ ok: true, - service: "examcooker-command-agent", + service: "examcooker-agent-worker", routes: [ "/agents/ExamCookerCommandAgent/global", + "/agents/ExamCookerStudyBrainAgent/global", ], }); } diff --git a/worker/src/study-brain-agent.ts b/worker/src/study-brain-agent.ts new file mode 100644 index 00000000..680e1283 --- /dev/null +++ b/worker/src/study-brain-agent.ts @@ -0,0 +1,277 @@ +import { Agent, callable } from "agents"; +import { emptyResponse, jsonResponse, readJsonPayload } from "./http"; +import type { + Env, + StudyBrainAgentState, + StudyBrainPlanRunInput, +} from "./types"; + +type StudyBrainRunRow = { + id: string; + status: string; + inputJson: string; + resultJson: string | null; + error: string | null; + createdAt: number; + updatedAt: number; +}; + +const STUDY_BRAIN_RUN_LIMIT = 2000; + +function ensureStudyBrainSchema(agent: Agent) { + agent.sql` + CREATE TABLE IF NOT EXISTS study_brain_runs ( + id TEXT PRIMARY KEY, + user_key TEXT, + course_code TEXT NOT NULL, + exam_type TEXT, + slot TEXT, + status TEXT NOT NULL, + input_json TEXT NOT NULL, + result_json TEXT, + error TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + `; + + agent.sql` + CREATE INDEX IF NOT EXISTS study_brain_runs_lookup_idx + ON study_brain_runs (user_key, updated_at DESC) + `; + + agent.sql` + CREATE INDEX IF NOT EXISTS study_brain_runs_course_idx + ON study_brain_runs (course_code, exam_type, slot, updated_at DESC) + `; +} + +function normalizeString(value: unknown) { + return typeof value === "string" ? value.trim() : ""; +} + +function normalizeNullableString(value: unknown) { + const normalized = normalizeString(value); + return normalized || null; +} + +function normalizePlanInput(input: StudyBrainPlanRunInput) { + const courseCode = normalizeString(input.courseCode).toUpperCase(); + if (!courseCode) { + throw new Error("courseCode is required."); + } + + const selectedTopics = Array.isArray(input.selectedTopics) + ? input.selectedTopics + .map((topic) => ({ + topicId: normalizeNullableString(topic.topicId), + title: normalizeString(topic.title), + rawText: normalizeString(topic.rawText), + })) + .filter((topic) => topic.title) + .slice(0, 24) + : []; + + return { + userKey: normalizeNullableString(input.userKey), + courseCode, + examType: normalizeNullableString(input.examType), + semester: normalizeString(input.semester) || "UNKNOWN", + campus: normalizeString(input.campus) || "VELLORE", + slot: normalizeNullableString(input.slot)?.toUpperCase() ?? null, + syllabusId: normalizeNullableString(input.syllabusId), + selectedTopics, + preferences: Array.isArray(input.preferences) + ? input.preferences.map(normalizeString).filter(Boolean).slice(0, 8) + : [], + }; +} + +function pruneStudyBrainRuns(agent: Agent) { + agent.sql` + DELETE FROM study_brain_runs + WHERE id NOT IN ( + SELECT id + FROM study_brain_runs + ORDER BY updated_at DESC + LIMIT ${STUDY_BRAIN_RUN_LIMIT} + ) + `; +} + +export class ExamCookerStudyBrainAgent extends Agent { + initialState: StudyBrainAgentState = { + runsCreated: 0, + lastRunId: null, + lastStatus: null, + updatedAt: null, + }; + + async onStart() { + ensureStudyBrainSchema(this); + await this.scheduleEvery(86_400, "pruneRuns", undefined, { + _idempotent: true, + }); + } + + async onRequest(request: Request) { + if (request.method === "OPTIONS") { + return emptyResponse({ status: 204 }); + } + + const { pathname } = new URL(request.url); + const action = pathname.split("/").filter(Boolean).at(-1); + + if (action === "runs" && request.method === "POST") { + return this.handleCreateRun(request); + } + + if (action === "runs" && request.method === "GET") { + return this.handleListRuns(request); + } + + return jsonResponse({ error: "Unknown study brain action." }, { status: 404 }); + } + + async pruneRuns() { + ensureStudyBrainSchema(this); + pruneStudyBrainRuns(this); + } + + @callable() + async createPlanRun(input: StudyBrainPlanRunInput) { + ensureStudyBrainSchema(this); + const normalized = normalizePlanInput(input); + const timestamp = Date.now(); + const id = crypto.randomUUID(); + + this.sql` + INSERT INTO study_brain_runs ( + id, + user_key, + course_code, + exam_type, + slot, + status, + input_json, + result_json, + error, + created_at, + updated_at + ) + VALUES ( + ${id}, + ${normalized.userKey}, + ${normalized.courseCode}, + ${normalized.examType}, + ${normalized.slot}, + ${"queued"}, + ${JSON.stringify(normalized)}, + ${null}, + ${null}, + ${timestamp}, + ${timestamp} + ) + `; + + const nextState = { + runsCreated: this.state.runsCreated + 1, + lastRunId: id, + lastStatus: "queued", + updatedAt: new Date(timestamp).toISOString(), + }; + this.setState(nextState); + pruneStudyBrainRuns(this); + + return { + run: { + id, + status: "queued", + input: normalized, + createdAt: timestamp, + updatedAt: timestamp, + }, + state: nextState, + source: "cloudflare-study-brain-agent", + }; + } + + @callable() + async getPlanRun(input: { id?: string }) { + ensureStudyBrainSchema(this); + const id = normalizeString(input.id); + if (!id) return null; + + const rows = this.sql` + SELECT + id, + status, + input_json AS inputJson, + result_json AS resultJson, + error, + created_at AS createdAt, + updated_at AS updatedAt + FROM study_brain_runs + WHERE id = ${id} + LIMIT 1 + `; + + const row = rows[0]; + if (!row) return null; + + return { + id: row.id, + status: row.status, + input: JSON.parse(row.inputJson) as unknown, + result: row.resultJson ? (JSON.parse(row.resultJson) as unknown) : null, + error: row.error, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; + } + + private async handleCreateRun(request: Request) { + const payload = (await readJsonPayload(request)) as StudyBrainPlanRunInput | null; + + try { + return jsonResponse(await this.createPlanRun(payload ?? {})); + } catch (error) { + return jsonResponse( + { error: error instanceof Error ? error.message : "Invalid study brain run." }, + { status: 400 }, + ); + } + } + + private async handleListRuns(request: Request) { + ensureStudyBrainSchema(this); + const userKey = new URL(request.url).searchParams.get("userKey")?.trim() || null; + const rows = this.sql` + SELECT + id, + status, + input_json AS inputJson, + result_json AS resultJson, + error, + created_at AS createdAt, + updated_at AS updatedAt + FROM study_brain_runs + WHERE ${userKey} IS NULL OR user_key = ${userKey} + ORDER BY updated_at DESC + LIMIT 20 + `; + + return jsonResponse({ + runs: rows.map((row) => ({ + id: row.id, + status: row.status, + input: JSON.parse(row.inputJson) as unknown, + result: row.resultJson ? (JSON.parse(row.resultJson) as unknown) : null, + error: row.error, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + })), + source: "cloudflare-study-brain-agent", + }); + } +} diff --git a/worker/src/types.ts b/worker/src/types.ts index 9845a927..470733e7 100644 --- a/worker/src/types.ts +++ b/worker/src/types.ts @@ -8,6 +8,7 @@ export type CommandResolver = "openai" | "local" | "semantic-cache"; export type Env = { ExamCookerCommandAgent: DurableObjectNamespace; + ExamCookerStudyBrainAgent: DurableObjectNamespace; CLOUDFLARE_COMMAND_AGENT_ADMIN_TOKEN?: string; OPENAI_API_KEY?: string; OPENAI_COMMAND_MODEL?: string; @@ -91,6 +92,30 @@ export type TrustedCommandUserContext = { role: "user" | "moderator" | null; }; +export type StudyBrainAgentState = { + runsCreated: number; + lastRunId: string | null; + lastStatus: string | null; + updatedAt: string | null; +}; + +export type StudyBrainPlanRunInput = { + userKey?: string; + userToken?: string | null; + courseCode?: string; + examType?: string | null; + semester?: string; + campus?: string; + slot?: string | null; + syllabusId?: string | null; + selectedTopics?: Array<{ + topicId?: string | null; + title: string; + rawText?: string; + }>; + preferences?: string[]; +}; + export type CommandPreferenceRequestInput = { userKey?: string; userToken?: string | null; diff --git a/worker/wrangler.jsonc b/worker/wrangler.jsonc index 930376de..749f62ef 100644 --- a/worker/wrangler.jsonc +++ b/worker/wrangler.jsonc @@ -9,6 +9,10 @@ { "name": "ExamCookerCommandAgent", "class_name": "ExamCookerCommandAgent" + }, + { + "name": "ExamCookerStudyBrainAgent", + "class_name": "ExamCookerStudyBrainAgent" } ] }, @@ -16,6 +20,10 @@ { "tag": "v1", "new_sqlite_classes": ["ExamCookerCommandAgent"] + }, + { + "tag": "v2", + "new_sqlite_classes": ["ExamCookerStudyBrainAgent"] } ], "vars": { From 6e61e5c5e943e97a5315ff0609777c4bee0339dc Mon Sep 17 00:00:00 2001 From: theg1239 <52027622+theg1239@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:36:37 +0000 Subject: [PATCH 2/5] fix: sync pnpm lockfile overrides Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 1196 +++--------------------------------------------- 1 file changed, 71 insertions(+), 1125 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86db47e1..7678fe33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,20 +1,16 @@ lockfileVersion: '9.0' settings: - autoInstallPeers: true + autoInstallPeers: false excludeLinksFromLockfile: false overrides: - dompurify@>=3.1.3 <=3.3.1: '>=3.3.2' - minimatch@<3.1.3: '>=3.1.3' - minimatch@<3.1.4: '>=3.1.4' - pdfjs-dist@<=4.1.392: '>=4.2.67' - tar@<7.5.7: '>=7.5.7' - tar@<7.5.8: '>=7.5.8' - tar@<=7.5.10: '>=7.5.11' - tar@<=7.5.2: '>=7.5.3' - tar@<=7.5.3: '>=7.5.4' - tar@<=7.5.9: '>=7.5.10' + fast-uri: 3.1.2 + fast-xml-builder: 1.1.7 + hono: 4.12.16 + ip-address: 10.1.1 + postcss: 8.5.13 + uuid: 14.0.0 importers: @@ -55,10 +51,10 @@ importers: version: 8.0.2(@capacitor/core@8.3.1) '@embedpdf/core': specifier: 2.14.3 - version: 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + version: 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/engines': specifier: 2.14.3 - version: 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + version: 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/models': specifier: 2.14.3 version: 2.14.3 @@ -67,19 +63,19 @@ importers: version: 2.14.3 '@embedpdf/plugin-document-manager': specifier: 2.14.3 - version: 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + version: 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/plugin-render': specifier: 2.14.3 - version: 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + version: 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/plugin-scroll': specifier: 2.14.3 - version: 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + version: 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/plugin-viewport': specifier: 2.14.3 - version: 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + version: 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/plugin-zoom': specifier: 2.14.3 - version: 2.14.3(e04da8b39211c4fdfdad1391317399fe) + version: 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@embedpdf/plugin-scroll@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@fortawesome/fontawesome-svg-core': specifier: ^7.1.0 version: 7.1.0 @@ -100,7 +96,7 @@ importers: version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) '@next/third-parties': specifier: 16.2.0 - version: 16.2.0(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + version: 16.2.0(next@16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) '@openai/agents': specifier: ^0.8.5 version: 0.8.5(@cfworker/json-schema@4.1.1)(ws@8.20.0)(zod@4.3.6) @@ -136,7 +132,7 @@ importers: version: 1.36.1 agents: specifier: ^0.12.3 - version: 0.12.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(@cloudflare/workers-types@4.20260508.1)(ai@6.0.66(zod@4.3.6))(react@19.2.4)(rolldown@1.0.2)(zod@4.3.6) + version: 0.12.3(@babel/runtime@7.29.2)(@cloudflare/workers-types@4.20260508.1)(ai@6.0.66(zod@4.3.6))(react@19.2.4)(zod@4.3.6) ai: specifier: ^6.0.66 version: 6.0.66(zod@4.3.6) @@ -178,10 +174,10 @@ importers: version: 0.544.0(react@19.2.4) next: specifier: 16.2.6 - version: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-auth: specifier: 4.24.14 - version: 4.24.14(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 4.24.14(next@16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) ogl: specifier: ^1.0.11 version: 1.0.11 @@ -259,7 +255,7 @@ importers: specifier: 1.0.0-rc.1 version: 1.0.0-rc.1 postcss: - specifier: ^8.5.13 + specifier: 8.5.13 version: 8.5.13 tailwindcss: specifier: ^4.1.14 @@ -274,27 +270,6 @@ importers: specifier: ^4.90.0 version: 4.90.0(@cloudflare/workers-types@4.20260508.1) - packages/capacitor-native-tab: - devDependencies: - '@capacitor/android': - specifier: ^8.3.1 - version: 8.3.1(@capacitor/core@8.3.1) - '@capacitor/core': - specifier: ^8.3.1 - version: 8.3.1 - '@capacitor/ios': - specifier: ^8.3.1 - version: 8.3.1(@capacitor/core@8.3.1) - rimraf: - specifier: ^5.0.5 - version: 5.0.10 - rollup: - specifier: ^4.9.5 - version: 4.60.4 - typescript: - specifier: ^6.0.3 - version: 6.0.3 - packages/cli: dependencies: '@clack/prompts': @@ -417,14 +392,6 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.3': - resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} @@ -433,10 +400,6 @@ packages: resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.29.3': resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==} engines: {node: '>=6.9.0'} @@ -451,16 +414,6 @@ packages: resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.27.1': resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} engines: {node: '>=6.9.0'} @@ -487,14 +440,6 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} - engines: {node: '>=6.9.0'} - '@babel/parser@7.29.3': resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} @@ -761,18 +706,9 @@ packages: svelte: '>=5 <6' vue: '>=3.2.0' - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -1268,7 +1204,7 @@ packages: resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} peerDependencies: - hono: ^4 + hono: 4.12.16 '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -1461,10 +1397,6 @@ packages: resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==} engines: {node: '>=16.0.0'} - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -1519,12 +1451,6 @@ packages: '@cfworker/json-schema': optional: true - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - '@next/env@16.2.6': resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} @@ -1688,9 +1614,6 @@ packages: resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} engines: {node: '>=14'} - '@oxc-project/types@0.132.0': - resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} - '@panva/hkdf@1.2.1': resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} @@ -1700,10 +1623,6 @@ packages: '@pdf-lib/upng@1.0.1': resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -1972,101 +1891,6 @@ packages: '@types/react-dom': optional: true - '@rolldown/binding-android-arm64@1.0.2': - resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.0.2': - resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.0.2': - resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.0.2': - resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.0.2': - resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.0.2': - resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.0.2': - resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.0.2': - resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.0.2': - resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.0.2': - resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.0.2': - resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.2': - resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.0.2': - resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@rolldown/plugin-babel@0.2.3': resolution: {integrity: sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==} engines: {node: '>=22.12.0 || ^24.0.0'} @@ -2084,147 +1908,6 @@ packages: vite: optional: true - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@rollup/rollup-android-arm-eabi@4.60.4': - resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.60.4': - resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.60.4': - resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.60.4': - resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.60.4': - resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.4': - resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': - resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.60.4': - resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.60.4': - resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.60.4': - resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.60.4': - resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.4': - resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.60.4': - resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.60.4': - resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.60.4': - resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.60.4': - resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.60.4': - resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.60.4': - resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.60.4': - resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.60.4': - resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.4': - resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.60.4': - resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.60.4': - resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.60.4': - resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.4': - resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} - cpu: [x64] - os: [win32] - '@shikijs/core@3.23.0': resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} @@ -2276,11 +1959,6 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 - '@sveltejs/acorn-typescript@1.0.10': - resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} - peerDependencies: - acorn: ^8.9.0 - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -2379,9 +2057,6 @@ packages: '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -2569,35 +2244,6 @@ packages: resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} engines: {node: '>= 20'} - '@vue/compiler-core@3.5.34': - resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} - - '@vue/compiler-dom@3.5.34': - resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} - - '@vue/compiler-sfc@3.5.34': - resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} - - '@vue/compiler-ssr@3.5.34': - resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} - - '@vue/reactivity@3.5.34': - resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} - - '@vue/runtime-core@3.5.34': - resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} - - '@vue/runtime-dom@3.5.34': - resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} - - '@vue/server-renderer@3.5.34': - resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} - peerDependencies: - vue: 3.5.34 - - '@vue/shared@3.5.34': - resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} - '@xmldom/xmldom@0.9.10': resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} engines: {node: '>=14.6'} @@ -2606,11 +2252,6 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -2650,11 +2291,6 @@ packages: ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -2679,10 +2315,6 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} - aria-query@5.3.1: - resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} - engines: {node: '>= 0.4'} - astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -2695,16 +2327,9 @@ packages: resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} engines: {node: '>=4'} - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -2712,11 +2337,6 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.31: - resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} - engines: {node: '>=6.0.0'} - hasBin: true - baseline-browser-mapping@2.9.19: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true @@ -2736,18 +2356,10 @@ packages: resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} engines: {node: '>= 5.10.0'} - brace-expansion@2.1.1: - resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} - brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -2766,9 +2378,6 @@ packages: caniuse-lite@1.0.30001766: resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} - caniuse-lite@1.0.30001793: - resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} - capacitor-native-tab@1.0.4: resolution: {integrity: sha512-/vEKDmnmHDY31VYpg/5/h4nyCshVUT0sKTHZaxUVEP1iMS1MQ+MO4U7gSrYztz6jnPiclLwFLtvzbt/3Gew7pA==} peerDependencies: @@ -2856,9 +2465,6 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -3095,9 +2701,6 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - devalue@5.8.1: - resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -3232,15 +2835,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.360: - resolution: {integrity: sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==} - elementtree@0.1.7: resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==} engines: {node: '>= 0.4.0'} @@ -3251,9 +2848,6 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -3266,10 +2860,6 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - entities@7.0.1: - resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} - engines: {node: '>=0.12'} - env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -3315,23 +2905,9 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - esm-env@1.2.2: - resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - - esrap@2.2.9: - resolution: {integrity: sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==} - peerDependencies: - '@typescript-eslint/types': ^8.2.0 - peerDependenciesMeta: - '@typescript-eslint/types': - optional: true - estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -3399,10 +2975,6 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3431,10 +3003,6 @@ packages: resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} engines: {node: '>=10'} - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -3458,11 +3026,6 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -3580,8 +3143,8 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + ip-address@10.1.1: + resolution: {integrity: sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -3616,9 +3179,6 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -3626,9 +3186,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -3662,11 +3219,6 @@ packages: json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} @@ -3772,9 +3324,6 @@ packages: load-script@1.0.0: resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==} - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - lodash-es@4.18.1: resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} @@ -3791,16 +3340,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.3.5: resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} engines: {node: 20 || >=22} - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -4039,10 +3582,6 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} - minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -4054,11 +3593,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4113,9 +3647,6 @@ packages: sass: optional: true - node-releases@2.0.44: - resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} - oauth@0.9.15: resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} @@ -4213,10 +3744,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -4291,18 +3818,10 @@ packages: points-on-path@0.2.1: resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.13: resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} - engines: {node: ^10 || ^12 || >=14} - postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -4504,10 +4023,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - rimraf@5.0.10: - resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} - hasBin: true - rimraf@6.1.3: resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} engines: {node: 20 || >=22} @@ -4516,16 +4031,6 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown@1.0.2: - resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - rollup@4.60.4: - resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -4606,10 +4111,6 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -4642,10 +4143,6 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -4693,10 +4190,6 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} - svelte@5.55.9: - resolution: {integrity: sha512-fTjjT8cHLDwigcu2j3pv7Jq04LklXevPB8uBgyHNiTXv+RMNvVnrjS4UEYrLMkhuq1vpCodHjiW+z/95SDs/fg==} - engines: {node: '>=18'} - tailwind-merge@3.4.0: resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} @@ -4819,12 +4312,6 @@ packages: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -4848,13 +4335,8 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} - hasBin: true - - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true vary@1.1.2: @@ -4896,14 +4378,6 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - vue@3.5.34: - resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -4934,10 +4408,6 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -4989,9 +4459,6 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -5016,9 +4483,6 @@ packages: youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - zimmerframe@1.1.4: - resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} - zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -5190,30 +4654,8 @@ snapshots: '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.3': {} - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color + js-tokens: 4.0.0 + picocolors: 1.1.1 '@babel/generator@7.29.1': dependencies: @@ -5227,21 +4669,12 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.3 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.2 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)': + '@babel/helper-create-class-features-plugin@7.29.3': dependencies: - '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-replace-supers': 7.28.6 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 '@babel/traverse': 7.29.0 semver: 6.3.1 @@ -5257,31 +4690,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - '@babel/helper-optimise-call-expression@7.27.1': dependencies: '@babel/types': 7.29.0 '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + '@babel/helper-replace-supers@7.28.6': dependencies: - '@babel/core': 7.29.0 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 '@babel/traverse': 7.29.0 @@ -5299,29 +4715,20 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.29.2': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 - '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-proposal-decorators@7.29.0': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-create-class-features-plugin': 7.29.3 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-decorators': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-decorators@7.28.6': dependencies: - '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 '@babel/runtime-corejs3@7.29.2': @@ -5474,17 +4881,15 @@ snapshots: '@drizzle-team/brocli@0.11.0': {} - '@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3))': + '@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@embedpdf/engines': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + '@embedpdf/engines': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/models': 2.14.3 preact: 10.29.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - svelte: 5.55.9 - vue: 3.5.34(typescript@6.0.3) - '@embedpdf/engines@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3))': + '@embedpdf/engines@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@embedpdf/fonts-arabic': 1.0.0 '@embedpdf/fonts-hebrew': 1.0.0 @@ -5498,8 +4903,6 @@ snapshots: preact: 10.29.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - svelte: 5.55.9 - vue: 3.5.34(typescript@6.0.3) '@embedpdf/fonts-arabic@1.0.0': {} @@ -5519,80 +4922,54 @@ snapshots: '@embedpdf/pdfium@2.14.3': {} - '@embedpdf/plugin-document-manager@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3))': + '@embedpdf/plugin-document-manager@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@embedpdf/core': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + '@embedpdf/core': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/models': 2.14.3 preact: 10.29.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - svelte: 5.55.9 - vue: 3.5.34(typescript@6.0.3) - '@embedpdf/plugin-render@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3))': + '@embedpdf/plugin-render@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@embedpdf/core': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + '@embedpdf/core': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/models': 2.14.3 preact: 10.29.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - svelte: 5.55.9 - vue: 3.5.34(typescript@6.0.3) - '@embedpdf/plugin-scroll@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3))': + '@embedpdf/plugin-scroll@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@embedpdf/core': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + '@embedpdf/core': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/models': 2.14.3 - '@embedpdf/plugin-viewport': 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + '@embedpdf/plugin-viewport': 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) preact: 10.29.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - svelte: 5.55.9 - vue: 3.5.34(typescript@6.0.3) - '@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3))': + '@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@embedpdf/core': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + '@embedpdf/core': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/models': 2.14.3 preact: 10.29.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - svelte: 5.55.9 - vue: 3.5.34(typescript@6.0.3) - '@embedpdf/plugin-zoom@2.14.3(e04da8b39211c4fdfdad1391317399fe)': + '@embedpdf/plugin-zoom@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@embedpdf/plugin-scroll@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@embedpdf/core': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + '@embedpdf/core': 2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@embedpdf/models': 2.14.3 - '@embedpdf/plugin-scroll': 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) - '@embedpdf/plugin-viewport': 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(svelte@5.55.9)(vue@3.5.34(typescript@6.0.3)) + '@embedpdf/plugin-scroll': 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@embedpdf/plugin-viewport@2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@embedpdf/plugin-viewport': 2.14.3(@embedpdf/core@2.14.3(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(preact@10.29.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) preact: 10.29.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - svelte: 5.55.9 - vue: 3.5.34(typescript@6.0.3) - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.25.12': optional: true @@ -6030,15 +5407,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.2.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.3 @@ -6088,7 +5456,7 @@ snapshots: dependencies: '@hono/node-server': 1.19.14(hono@4.12.16) ajv: 8.20.0 - ajv-formats: 3.0.1(ajv@8.20.0) + ajv-formats: 3.0.1 content-type: 1.0.5 cors: 2.8.6 cross-spawn: 7.0.6 @@ -6108,13 +5476,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 - optional: true - '@next/env@16.2.6': {} '@next/swc-darwin-arm64@16.2.6': @@ -6141,9 +5502,9 @@ snapshots: '@next/swc-win32-x64-msvc@16.2.6': optional: true - '@next/third-parties@16.2.0(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': + '@next/third-parties@16.2.0(next@16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)': dependencies: - next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 third-party-capital: 1.0.20 @@ -6278,8 +5639,6 @@ snapshots: '@opentelemetry/semantic-conventions@1.40.0': {} - '@oxc-project/types@0.132.0': {} - '@panva/hkdf@1.2.1': {} '@pdf-lib/standard-fonts@1.0.0': @@ -6290,9 +5649,6 @@ snapshots: dependencies: pako: 1.0.11 - '@pkgjs/parseargs@0.11.0': - optional: true - '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -6529,140 +5885,12 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@rolldown/binding-android-arm64@1.0.2': - optional: true - - '@rolldown/binding-darwin-arm64@1.0.2': - optional: true - - '@rolldown/binding-darwin-x64@1.0.2': - optional: true - - '@rolldown/binding-freebsd-x64@1.0.2': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.0.2': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.0.2': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.0.2': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.0.2': - optional: true - - '@rolldown/binding-linux-x64-musl@1.0.2': - optional: true - - '@rolldown/binding-openharmony-arm64@1.0.2': - optional: true - - '@rolldown/binding-wasm32-wasi@1.0.2': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.0.2': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.0.2': - optional: true - - '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.2)': + '@rolldown/plugin-babel@0.2.3(@babel/runtime@7.29.2)': dependencies: - '@babel/core': 7.29.0 picomatch: 4.0.4 - rolldown: 1.0.2 optionalDependencies: '@babel/runtime': 7.29.2 - '@rolldown/pluginutils@1.0.1': {} - - '@rollup/rollup-android-arm-eabi@4.60.4': - optional: true - - '@rollup/rollup-android-arm64@4.60.4': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.4': - optional: true - - '@rollup/rollup-darwin-x64@4.60.4': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.4': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.4': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.4': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.60.4': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.60.4': - optional: true - - '@rollup/rollup-linux-x64-musl@4.60.4': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.4': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.4': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.60.4': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.60.4': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.4': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.60.4': - optional: true - '@shikijs/core@3.23.0': dependencies: '@shikijs/types': 3.23.0 @@ -6733,10 +5961,6 @@ snapshots: mermaid: 11.14.0 react: 19.2.4 - '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': - dependencies: - acorn: 8.16.0 - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -6812,11 +6036,6 @@ snapshots: '@tweenjs/tween.js@23.1.3': {} - '@tybys/wasm-util@0.10.2': - dependencies: - tslib: 2.8.1 - optional: true - '@types/d3-array@3.2.2': {} '@types/d3-axis@3.0.6': @@ -6995,7 +6214,8 @@ snapshots: fflate: 0.8.2 meshoptimizer: 1.1.1 - '@types/trusted-types@2.0.7': {} + '@types/trusted-types@2.0.7': + optional: true '@types/unist@2.0.11': {} @@ -7037,60 +6257,6 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vue/compiler-core@3.5.34': - dependencies: - '@babel/parser': 7.29.3 - '@vue/shared': 3.5.34 - entities: 7.0.1 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - - '@vue/compiler-dom@3.5.34': - dependencies: - '@vue/compiler-core': 3.5.34 - '@vue/shared': 3.5.34 - - '@vue/compiler-sfc@3.5.34': - dependencies: - '@babel/parser': 7.29.3 - '@vue/compiler-core': 3.5.34 - '@vue/compiler-dom': 3.5.34 - '@vue/compiler-ssr': 3.5.34 - '@vue/shared': 3.5.34 - estree-walker: 2.0.2 - magic-string: 0.30.21 - postcss: 8.5.15 - source-map-js: 1.2.1 - - '@vue/compiler-ssr@3.5.34': - dependencies: - '@vue/compiler-dom': 3.5.34 - '@vue/shared': 3.5.34 - - '@vue/reactivity@3.5.34': - dependencies: - '@vue/shared': 3.5.34 - - '@vue/runtime-core@3.5.34': - dependencies: - '@vue/reactivity': 3.5.34 - '@vue/shared': 3.5.34 - - '@vue/runtime-dom@3.5.34': - dependencies: - '@vue/reactivity': 3.5.34 - '@vue/runtime-core': 3.5.34 - '@vue/shared': 3.5.34 - csstype: 3.2.3 - - '@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3))': - dependencies: - '@vue/compiler-ssr': 3.5.34 - '@vue/shared': 3.5.34 - vue: 3.5.34(typescript@6.0.3) - - '@vue/shared@3.5.34': {} - '@xmldom/xmldom@0.9.10': {} accepts@2.0.0: @@ -7098,16 +6264,14 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn@8.16.0: {} - agent-base@7.1.4: {} - agents@0.12.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(@cloudflare/workers-types@4.20260508.1)(ai@6.0.66(zod@4.3.6))(react@19.2.4)(rolldown@1.0.2)(zod@4.3.6): + agents@0.12.3(@babel/runtime@7.29.2)(@cloudflare/workers-types@4.20260508.1)(ai@6.0.66(zod@4.3.6))(react@19.2.4)(zod@4.3.6): dependencies: - '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-proposal-decorators': 7.29.0 '@cfworker/json-schema': 4.1.1 '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) - '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.0)(@babel/runtime@7.29.2)(rolldown@1.0.2) + '@rolldown/plugin-babel': 0.2.3(@babel/runtime@7.29.2) ai: 6.0.66(zod@4.3.6) cron-schedule: 6.0.0 mimetext: 3.0.28 @@ -7133,8 +6297,8 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.6 - ajv-formats@3.0.1(ajv@8.20.0): - optionalDependencies: + ajv-formats@3.0.1: + dependencies: ajv: 8.20.0 ajv@8.20.0: @@ -7158,26 +6322,18 @@ snapshots: dependencies: tslib: 2.8.1 - aria-query@5.3.1: {} - astral-regex@2.0.0: {} at-least-node@1.0.0: {} attr-accept@2.2.5: {} - axobject-query@4.1.0: {} - bail@2.0.2: {} - balanced-match@1.0.2: {} - balanced-match@4.0.4: {} base64-js@1.5.1: {} - baseline-browser-mapping@2.10.31: {} - baseline-browser-mapping@2.9.19: {} big-integer@1.6.52: {} @@ -7202,22 +6358,10 @@ snapshots: dependencies: big-integer: 1.6.52 - brace-expansion@2.1.1: - dependencies: - balanced-match: 1.0.2 - brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 - browserslist@4.28.2: - dependencies: - baseline-browser-mapping: 2.10.31 - caniuse-lite: 1.0.30001793 - electron-to-chromium: 1.5.360 - node-releases: 2.0.44 - update-browserslist-db: 1.2.3(browserslist@4.28.2) - buffer-crc32@0.2.13: {} bytes@3.1.2: {} @@ -7234,8 +6378,6 @@ snapshots: caniuse-lite@1.0.30001766: {} - caniuse-lite@1.0.30001793: {} - capacitor-native-tab@1.0.4(@capacitor/core@8.3.1): dependencies: '@capacitor/core': 8.3.1 @@ -7311,8 +6453,6 @@ snapshots: content-type@1.0.5: {} - convert-source-map@2.0.0: {} - cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -7556,8 +6696,6 @@ snapshots: detect-node-es@1.1.0: {} - devalue@5.8.1: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -7591,12 +6729,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - eastasianwidth@0.2.0: {} - ee-first@1.1.1: {} - electron-to-chromium@1.5.360: {} - elementtree@0.1.7: dependencies: sax: 1.1.4 @@ -7605,8 +6739,6 @@ snapshots: emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} enhanced-resolve@5.18.4: @@ -7616,8 +6748,6 @@ snapshots: entities@6.0.1: {} - entities@7.0.1: {} - env-paths@2.2.1: {} error-stack-parser-es@1.0.5: {} @@ -7723,16 +6853,8 @@ snapshots: escape-string-regexp@5.0.0: {} - esm-env@1.2.2: {} - - esrap@2.2.9: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - estree-util-is-identifier-name@3.0.0: {} - estree-walker@2.0.2: {} - etag@1.8.1: {} event-target-polyfill@0.0.4: {} @@ -7748,7 +6870,7 @@ snapshots: express-rate-limit@8.4.1(express@5.2.1): dependencies: express: 5.2.1 - ip-address: 10.1.0 + ip-address: 10.1.1 express@5.2.1: dependencies: @@ -7827,11 +6949,6 @@ snapshots: transitivePeerDependencies: - supports-color - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - forwarded@0.2.0: {} fresh@2.0.0: {} @@ -7856,8 +6973,6 @@ snapshots: fuse.js@7.1.0: {} - gensync@1.0.0-beta.2: {} - get-caller-file@2.0.5: {} get-east-asian-width@1.5.0: {} @@ -7886,15 +7001,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.9 - minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -8089,7 +7195,7 @@ snapshots: internmap@2.0.3: {} - ip-address@10.1.0: {} + ip-address@10.1.1: {} ipaddr.js@1.9.1: {} @@ -8112,22 +7218,12 @@ snapshots: is-promise@4.0.0: {} - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.8 - is-wsl@2.2.0: dependencies: is-docker: 2.2.1 isexe@2.0.0: {} - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jiti@2.6.1: {} jose@4.15.9: {} @@ -8148,8 +7244,6 @@ snapshots: json-schema@0.4.0: {} - json5@2.2.3: {} - jsonfile@6.2.1: dependencies: universalify: 2.0.1 @@ -8230,8 +7324,6 @@ snapshots: load-script@1.0.0: {} - locate-character@3.0.0: {} - lodash-es@4.18.1: {} lodash@4.18.1: {} @@ -8244,14 +7336,8 @@ snapshots: dependencies: js-tokens: 4.0.0 - lru-cache@10.4.3: {} - lru-cache@11.3.5: {} - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -8465,7 +7551,7 @@ snapshots: roughjs: 4.6.6 stylis: 4.4.0 ts-dedent: 2.2.0 - uuid: 11.1.1 + uuid: 14.0.0 meshoptimizer@1.1.1: {} @@ -8737,10 +7823,6 @@ snapshots: dependencies: brace-expansion: 5.0.5 - minimatch@9.0.9: - dependencies: - brace-expansion: 2.1.1 - minipass@7.1.3: {} minizlib@3.1.0: @@ -8749,8 +7831,6 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.11: {} - nanoid@3.3.12: {} nanoid@5.1.11: {} @@ -8773,31 +7853,31 @@ snapshots: negotiator@1.0.0: {} - next-auth@4.24.14(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next-auth@4.24.14(next@16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@babel/runtime': 7.29.2 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.28.3 preact-render-to-string: 5.2.6(preact@10.28.3) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - uuid: 8.3.2 + uuid: 14.0.0 - next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.2.6(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.6 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.9.19 caniuse-lite: 1.0.30001766 - postcss: 8.4.31 + postcss: 8.5.13 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) + styled-jsx: 5.1.6(react@19.2.4) optionalDependencies: '@next/swc-darwin-arm64': 16.2.6 '@next/swc-darwin-x64': 16.2.6 @@ -8813,8 +7893,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - node-releases@2.0.44: {} - oauth@0.9.15: {} object-assign@4.1.1: {} @@ -8900,11 +7978,6 @@ snapshots: path-key@3.1.1: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.3 - path-scurry@2.0.2: dependencies: lru-cache: 11.3.5 @@ -8979,19 +8052,7 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - postcss@8.4.31: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.13: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postcss@8.5.15: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 @@ -9254,10 +8315,6 @@ snapshots: resolve-pkg-maps@1.0.0: {} - rimraf@5.0.10: - dependencies: - glob: 10.5.0 - rimraf@6.1.3: dependencies: glob: 13.0.6 @@ -9265,58 +8322,6 @@ snapshots: robust-predicates@3.0.3: {} - rolldown@1.0.2: - dependencies: - '@oxc-project/types': 0.132.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.2 - '@rolldown/binding-darwin-arm64': 1.0.2 - '@rolldown/binding-darwin-x64': 1.0.2 - '@rolldown/binding-freebsd-x64': 1.0.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 - '@rolldown/binding-linux-arm64-gnu': 1.0.2 - '@rolldown/binding-linux-arm64-musl': 1.0.2 - '@rolldown/binding-linux-ppc64-gnu': 1.0.2 - '@rolldown/binding-linux-s390x-gnu': 1.0.2 - '@rolldown/binding-linux-x64-gnu': 1.0.2 - '@rolldown/binding-linux-x64-musl': 1.0.2 - '@rolldown/binding-openharmony-arm64': 1.0.2 - '@rolldown/binding-wasm32-wasi': 1.0.2 - '@rolldown/binding-win32-arm64-msvc': 1.0.2 - '@rolldown/binding-win32-x64-msvc': 1.0.2 - - rollup@4.60.4: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.4 - '@rollup/rollup-android-arm64': 4.60.4 - '@rollup/rollup-darwin-arm64': 4.60.4 - '@rollup/rollup-darwin-x64': 4.60.4 - '@rollup/rollup-freebsd-arm64': 4.60.4 - '@rollup/rollup-freebsd-x64': 4.60.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 - '@rollup/rollup-linux-arm-musleabihf': 4.60.4 - '@rollup/rollup-linux-arm64-gnu': 4.60.4 - '@rollup/rollup-linux-arm64-musl': 4.60.4 - '@rollup/rollup-linux-loong64-gnu': 4.60.4 - '@rollup/rollup-linux-loong64-musl': 4.60.4 - '@rollup/rollup-linux-ppc64-gnu': 4.60.4 - '@rollup/rollup-linux-ppc64-musl': 4.60.4 - '@rollup/rollup-linux-riscv64-gnu': 4.60.4 - '@rollup/rollup-linux-riscv64-musl': 4.60.4 - '@rollup/rollup-linux-s390x-gnu': 4.60.4 - '@rollup/rollup-linux-x64-gnu': 4.60.4 - '@rollup/rollup-linux-x64-musl': 4.60.4 - '@rollup/rollup-openbsd-x64': 4.60.4 - '@rollup/rollup-openharmony-arm64': 4.60.4 - '@rollup/rollup-win32-arm64-msvc': 4.60.4 - '@rollup/rollup-win32-ia32-msvc': 4.60.4 - '@rollup/rollup-win32-x64-gnu': 4.60.4 - '@rollup/rollup-win32-x64-msvc': 4.60.4 - fsevents: 2.3.3 - roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -9455,8 +8460,6 @@ snapshots: signal-exit@3.0.7: {} - signal-exit@4.1.0: {} - sisteransi@1.0.5: {} slice-ansi@4.0.0: @@ -9502,12 +8505,6 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.2.0 - string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -9541,38 +8538,15 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): + styled-jsx@5.1.6(react@19.2.4): dependencies: client-only: 0.0.1 react: 19.2.4 - optionalDependencies: - '@babel/core': 7.29.0 stylis@4.4.0: {} supports-color@10.2.2: {} - svelte@5.55.9: - dependencies: - '@jridgewell/remapping': 2.3.5 - '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) - '@types/estree': 1.0.8 - '@types/trusted-types': 2.0.7 - acorn: 8.16.0 - aria-query: 5.3.1 - axobject-query: 4.1.0 - clsx: 2.1.1 - devalue: 5.8.1 - esm-env: 1.2.2 - esrap: 2.2.9 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.21 - zimmerframe: 1.1.4 - transitivePeerDependencies: - - '@typescript-eslint/types' - tailwind-merge@3.4.0: {} tailwindcss-animate@1.0.7(tailwindcss@4.2.1): @@ -9691,12 +8665,6 @@ snapshots: untildify@4.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.2): - dependencies: - browserslist: 4.28.2 - escalade: 3.2.0 - picocolors: 1.1.1 - use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): dependencies: react: 19.2.4 @@ -9714,9 +8682,7 @@ snapshots: util-deprecate@1.0.2: {} - uuid@11.1.1: {} - - uuid@8.3.2: {} + uuid@14.0.0: {} vary@1.1.2: {} @@ -9761,16 +8727,6 @@ snapshots: vscode-uri@3.1.0: {} - vue@3.5.34(typescript@6.0.3): - dependencies: - '@vue/compiler-dom': 3.5.34 - '@vue/compiler-sfc': 3.5.34 - '@vue/runtime-dom': 3.5.34 - '@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@6.0.3)) - '@vue/shared': 3.5.34 - optionalDependencies: - typescript: 6.0.3 - web-namespaces@2.0.1: {} web-vitals@5.2.0: {} @@ -9810,12 +8766,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.2.0 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -9841,8 +8791,6 @@ snapshots: y18n@5.0.8: {} - yallist@3.1.1: {} - yallist@4.0.0: {} yallist@5.0.0: {} @@ -9876,8 +8824,6 @@ snapshots: cookie: 1.1.1 youch-core: 0.3.3 - zimmerframe@1.1.4: {} - zod-to-json-schema@3.25.2(zod@4.3.6): dependencies: zod: 4.3.6 From d273c4610dae83beb73a0ac7d21678e00543038c Mon Sep 17 00:00:00 2001 From: theg1239 <52027622+theg1239@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:43:02 +0000 Subject: [PATCH 3/5] fix: rework study brain UI to match ExamCooker design system Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- .../study-brain/study-plan-builder.tsx | 269 ++++++++++-------- app/components/study-brain/study-plan-cta.tsx | 56 ++-- 2 files changed, 180 insertions(+), 145 deletions(-) diff --git a/app/components/study-brain/study-plan-builder.tsx b/app/components/study-brain/study-plan-builder.tsx index 2bfac916..bcd3ac32 100644 --- a/app/components/study-brain/study-plan-builder.tsx +++ b/app/components/study-brain/study-plan-builder.tsx @@ -2,9 +2,12 @@ import { useMemo, useState } from "react"; import Link from "next/link"; -import { ArrowRight, Check, Clock3, FileText, Search, Sparkles } from "lucide-react"; +import { useRouter } from "next/navigation"; import type { CourseSearchRecord } from "@/lib/data/course-catalog"; import { examTypeValues } from "@/db/enums"; +import { examTypeLabel } from "@/lib/exam-slug"; +import { getCoursePastPapersPath } from "@/lib/seo"; +import { GradientText } from "@/app/components/landing/landing"; import type { StudyPreferenceMode } from "@/lib/study-brain/schemas"; type Props = { @@ -17,31 +20,46 @@ type Props = { const preferenceOptions: Array<{ id: StudyPreferenceMode; label: string; - description: string; }> = [ - { id: "past_papers", label: "Past-paper practice", description: "Prioritize questions and patterns." }, - { id: "videos", label: "Video explanations", description: "Use good lectures when they save time." }, - { id: "notes", label: "Notes and reading", description: "Keep it text-first and skimmable." }, - { id: "solved_examples", label: "Solved examples", description: "Work through examples before papers." }, - { id: "quick_summaries", label: "Quick summaries", description: "Fast revision blocks first." }, - { id: "mixed", label: "Mixed mode", description: "Let ExamCooker balance the queue." }, + { id: "past_papers", label: "Past papers" }, + { id: "videos", label: "Videos" }, + { id: "notes", label: "Notes" }, + { id: "solved_examples", label: "Solved examples" }, + { id: "quick_summaries", label: "Quick summaries" }, + { id: "mixed", label: "Mixed" }, ]; function normalize(value: string) { return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); } +const chipBase = + "inline-flex h-9 shrink-0 items-center gap-1.5 border px-3 text-sm font-semibold transition"; +const chipActive = + "border-[#5FC4E7] bg-[#5FC4E7]/25 text-black dark:border-[#3BF4C7]/60 dark:bg-[#3BF4C7]/15 dark:text-[#3BF4C7]"; +const chipIdle = + "border-black/15 bg-white text-black hover:border-black/30 dark:border-[#D5D5D5]/15 dark:bg-[#0C1222] dark:text-[#D5D5D5] dark:hover:border-[#D5D5D5]/40"; + +const sectionLabel = + "text-lg font-bold uppercase tracking-wider text-black dark:text-[#D5D5D5] sm:text-xl"; + export default function StudyPlanBuilder({ courses, initialCourse = "", initialExam = "", initialSlot = "", }: Props) { + const { prefetch } = useRouter(); const [courseQuery, setCourseQuery] = useState(initialCourse); - const [selectedCourseCode, setSelectedCourseCode] = useState(initialCourse.toUpperCase()); + const [selectedCourseCode, setSelectedCourseCode] = useState( + initialCourse.toUpperCase(), + ); const [examType, setExamType] = useState(initialExam.toUpperCase()); const [slot, setSlot] = useState(initialSlot.toUpperCase()); - const [preferences, setPreferences] = useState(["past_papers", "mixed"]); + const [preferences, setPreferences] = useState([ + "past_papers", + "mixed", + ]); const selectedCourse = useMemo( () => courses.find((course) => course.code.toUpperCase() === selectedCourseCode), @@ -50,13 +68,15 @@ export default function StudyPlanBuilder({ const matches = useMemo(() => { const query = normalize(courseQuery); - if (!query) return courses.slice(0, 8); + if (!query) return courses.slice(0, 6); return courses .filter((course) => { - const haystack = normalize(`${course.code} ${course.title} ${course.aliases.join(" ")}`); + const haystack = normalize( + `${course.code} ${course.title} ${course.aliases.join(" ")}`, + ); return haystack.includes(query); }) - .slice(0, 8); + .slice(0, 6); }, [courseQuery, courses]); const togglePreference = (id: StudyPreferenceMode) => { @@ -67,108 +87,117 @@ export default function StudyPlanBuilder({ ); }; + const readyToContinue = Boolean(selectedCourse && examType); + return ( -
-
-
- - Study Brain -
-

- Build the queue that makes sense for your exam. +
+
+ + Study plan + +

+ What do you wanna study?

-

- Choose the course, exam, slot, and how you actually study. ExamCooker - will use syllabus topics, previous papers, slot reports, and web research - without making it feel like AI homework. +

+ Pick the course, exam, and slot. ExamCooker uses the syllabus, previous + papers, and earlier-slot reports to line up a queue with time estimates — + high-yield first, skip-if-cooked last.

+
-
- +
+

Course

+
+ { + setCourseQuery(event.target.value); + setSelectedCourseCode(""); + }} + placeholder="Mechanics, BCME102L, DSA..." + className="h-full min-w-0 flex-1 bg-transparent px-3 text-sm text-black focus:outline-none placeholder:text-black/50 dark:text-[#D5D5D5] dark:placeholder:text-[#D5D5D5]/60 sm:text-base" + /> +
-
+ {matches.length > 0 && ( +
{matches.map((course) => { const active = course.code.toUpperCase() === selectedCourseCode; return ( ); })}
+ )} +
-
- -

+
+ +
- + {!readyToContinue && ( + + Pick a course and exam to continue + + )} + +
); } diff --git a/app/components/study-brain/study-plan-cta.tsx b/app/components/study-brain/study-plan-cta.tsx index a73f6be0..7bf0f6fd 100644 --- a/app/components/study-brain/study-plan-cta.tsx +++ b/app/components/study-brain/study-plan-cta.tsx @@ -1,5 +1,4 @@ import Link from "next/link"; -import { ArrowRight, Clock3, Sparkles } from "lucide-react"; type Props = { courseCode?: string; @@ -29,42 +28,39 @@ export default function StudyPlanCta({ return ( - Plan study ); } return ( -
-
-
-
- -
-
-

- Exam soon? -

-

- Build a plan from your syllabus, slot intel, and papers. -

-

- Pick what is coming, choose how you study, and let ExamCooker line up - the high-yield stuff first. No calendar cosplay required. -

-
-
- - Plan my study - - + +
+ + Study plan + +

+ Exam soon? Build a plan from the syllabus, slot reports, and papers. +

+

+ Pick what is coming, choose how you study, and ExamCooker lines up the + high-yield work first. It is not too late. +

-
+ + Plan my study + + → + + + ); } From 1396f8cbe7fe47dcbbdec09cc8aad5076c08050d Mon Sep 17 00:00:00 2001 From: theg1239 Date: Fri, 26 Jun 2026 00:59:36 +0530 Subject: [PATCH 4/5] feat: rework study plan experience --- app/(app)/past_papers/[code]/[exam]/page.tsx | 2 +- app/(app)/past_papers/[code]/page.tsx | 2 +- app/(app)/study-plan/page.tsx | 51 +- app/(app)/syllabus/course/[code]/page.tsx | 2 +- .../study-brain/study-plan-builder.tsx | 259 ----------- app/components/study-brain/study-plan-cta.tsx | 66 --- app/components/study-plan/composer.tsx | 436 ++++++++++++++++++ app/components/study-plan/copy.ts | 28 ++ app/components/study-plan/generating.tsx | 128 +++++ app/components/study-plan/plan-cta.tsx | 64 +++ app/components/study-plan/plan-view.tsx | 295 ++++++++++++ app/components/study-plan/sample-plan.ts | 318 +++++++++++++ .../study-plan/study-plan-experience.tsx | 102 ++++ app/components/study-plan/upcoming-exams.tsx | 75 +++ app/globals.css | 95 ++++ 15 files changed, 1589 insertions(+), 334 deletions(-) delete mode 100644 app/components/study-brain/study-plan-builder.tsx delete mode 100644 app/components/study-brain/study-plan-cta.tsx create mode 100644 app/components/study-plan/composer.tsx create mode 100644 app/components/study-plan/copy.ts create mode 100644 app/components/study-plan/generating.tsx create mode 100644 app/components/study-plan/plan-cta.tsx create mode 100644 app/components/study-plan/plan-view.tsx create mode 100644 app/components/study-plan/sample-plan.ts create mode 100644 app/components/study-plan/study-plan-experience.tsx create mode 100644 app/components/study-plan/upcoming-exams.tsx diff --git a/app/(app)/past_papers/[code]/[exam]/page.tsx b/app/(app)/past_papers/[code]/[exam]/page.tsx index 2b09af89..993b5369 100644 --- a/app/(app)/past_papers/[code]/[exam]/page.tsx +++ b/app/(app)/past_papers/[code]/[exam]/page.tsx @@ -21,7 +21,7 @@ import { getCourseSyllabusPath, } from "@/lib/seo"; import CourseHeader from "@/app/components/past_papers/course-header"; -import StudyPlanCta from "@/app/components/study-brain/study-plan-cta"; +import StudyPlanCta from "@/app/components/study-plan/plan-cta"; import CoursePaperGrid from "@/app/components/past_papers/course-paper-grid"; import { DESKTOP_SELECT_ALL_HOST_ID, diff --git a/app/(app)/past_papers/[code]/page.tsx b/app/(app)/past_papers/[code]/page.tsx index 04fcefe2..7f55099e 100644 --- a/app/(app)/past_papers/[code]/page.tsx +++ b/app/(app)/past_papers/[code]/page.tsx @@ -34,7 +34,7 @@ import { } from "@/app/components/past_papers/course-paper-grid-controls"; import CoursePagination from "@/app/components/past_papers/course-pagination"; import CourseVisitTracker from "@/app/components/past_papers/course-visit-tracker"; -import StudyPlanCta from "@/app/components/study-brain/study-plan-cta"; +import StudyPlanCta from "@/app/components/study-plan/plan-cta"; import { campusValues, course as courseTable, diff --git a/app/(app)/study-plan/page.tsx b/app/(app)/study-plan/page.tsx index 3088aae0..37ba1fbe 100644 --- a/app/(app)/study-plan/page.tsx +++ b/app/(app)/study-plan/page.tsx @@ -1,10 +1,21 @@ import type { Metadata } from "next"; import DirectionalTransition from "@/app/components/common/directional-transition"; -import StudyPlanBuilder from "@/app/components/study-brain/study-plan-builder"; +import StudyPlanExperience from "@/app/components/study-plan/study-plan-experience"; import StructuredData from "@/app/components/seo/structured-data"; -import { getCoursePickerRecords } from "@/lib/data/course-catalog"; +import { + getCoursePickerRecords, + getUpcomingExamsCourseGrid, + type CourseSearchRecord, +} from "@/lib/data/course-catalog"; import { buildBreadcrumbList } from "@/lib/structured-data"; +type CoursePick = { + id: string; + code: string; + title: string; + paperCount: number; +}; + type SearchParams = { course?: string; exam?: string; @@ -23,14 +34,36 @@ export default async function StudyPlanPage({ }: { searchParams?: Promise; }) { - const [params, courses] = await Promise.all([ + const [params, courses, upcoming] = await Promise.all([ searchParams ?? Promise.resolve({} as SearchParams), getCoursePickerRecords(), + getUpcomingExamsCourseGrid(), ]); + // Resolve the curated upcoming-exam courses to full search records (keeping + // the curated order) so they can seed the planner as one-tap entry points. + const byCode = new Map(courses.map((course) => [course.code, course])); + const suggested: CoursePick[] = upcoming + .map((item) => byCode.get(item.code)) + .filter((course): course is CourseSearchRecord => Boolean(course)); + + // A second band of real entry points: the courses with the most material that + // aren't already shown as upcoming exams. Always full, always useful. + const upcomingCodes = new Set(upcoming.map((item) => item.code)); + const popularPicks: CoursePick[] = courses + .filter((course) => !upcomingCodes.has(course.code) && course.paperCount > 0) + .sort((a, b) => b.paperCount - a.paperCount) + .slice(0, 12) + .map((course) => ({ + id: course.id, + code: course.code, + title: course.title, + paperCount: course.paperCount, + })); + return ( -
+
-
- +
+ = [ - { id: "past_papers", label: "Past papers" }, - { id: "videos", label: "Videos" }, - { id: "notes", label: "Notes" }, - { id: "solved_examples", label: "Solved examples" }, - { id: "quick_summaries", label: "Quick summaries" }, - { id: "mixed", label: "Mixed" }, -]; - -function normalize(value: string) { - return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); -} - -const chipBase = - "inline-flex h-9 shrink-0 items-center gap-1.5 border px-3 text-sm font-semibold transition"; -const chipActive = - "border-[#5FC4E7] bg-[#5FC4E7]/25 text-black dark:border-[#3BF4C7]/60 dark:bg-[#3BF4C7]/15 dark:text-[#3BF4C7]"; -const chipIdle = - "border-black/15 bg-white text-black hover:border-black/30 dark:border-[#D5D5D5]/15 dark:bg-[#0C1222] dark:text-[#D5D5D5] dark:hover:border-[#D5D5D5]/40"; - -const sectionLabel = - "text-lg font-bold uppercase tracking-wider text-black dark:text-[#D5D5D5] sm:text-xl"; - -export default function StudyPlanBuilder({ - courses, - initialCourse = "", - initialExam = "", - initialSlot = "", -}: Props) { - const { prefetch } = useRouter(); - const [courseQuery, setCourseQuery] = useState(initialCourse); - const [selectedCourseCode, setSelectedCourseCode] = useState( - initialCourse.toUpperCase(), - ); - const [examType, setExamType] = useState(initialExam.toUpperCase()); - const [slot, setSlot] = useState(initialSlot.toUpperCase()); - const [preferences, setPreferences] = useState([ - "past_papers", - "mixed", - ]); - - const selectedCourse = useMemo( - () => courses.find((course) => course.code.toUpperCase() === selectedCourseCode), - [courses, selectedCourseCode], - ); - - const matches = useMemo(() => { - const query = normalize(courseQuery); - if (!query) return courses.slice(0, 6); - return courses - .filter((course) => { - const haystack = normalize( - `${course.code} ${course.title} ${course.aliases.join(" ")}`, - ); - return haystack.includes(query); - }) - .slice(0, 6); - }, [courseQuery, courses]); - - const togglePreference = (id: StudyPreferenceMode) => { - setPreferences((current) => - current.includes(id) - ? current.filter((item) => item !== id) - : [...current, id], - ); - }; - - const readyToContinue = Boolean(selectedCourse && examType); - - return ( -
-
- - Study plan - -

- What do you wanna study? -

-

- Pick the course, exam, and slot. ExamCooker uses the syllabus, previous - papers, and earlier-slot reports to line up a queue with time estimates — - high-yield first, skip-if-cooked last. -

-
- -
-

Course

-
- { - setCourseQuery(event.target.value); - setSelectedCourseCode(""); - }} - placeholder="Mechanics, BCME102L, DSA..." - className="h-full min-w-0 flex-1 bg-transparent px-3 text-sm text-black focus:outline-none placeholder:text-black/50 dark:text-[#D5D5D5] dark:placeholder:text-[#D5D5D5]/60 sm:text-base" - /> -
- - {matches.length > 0 && ( -
- {matches.map((course) => { - const active = course.code.toUpperCase() === selectedCourseCode; - return ( - - ); - })} -
- )} -
- -
-
-

Exam & slot

-
- {examTypeValues.map((value) => { - const active = examType === value; - return ( - - ); - })} -
- -
- -
-

How you study

-
- {preferenceOptions.map((option) => { - const active = preferences.includes(option.id); - return ( - - ); - })} -
-

- Pick as many as you like. ExamCooker weights the queue toward what you - actually want to do at 2am. -

-
-
- -
- - What happens next - -
    -
  1. 1. Confirm the matched syllabus instead of trusting filename guesses.
  2. -
  3. 2. Pick the exact topics coming for your exam, plus custom ones.
  4. -
  5. 3. Earlier-slot reports and past papers come before any web research.
  6. -
  7. 4. Get a prioritized queue with clocks, evidence, and skip labels.
  8. -
-
- - Continue - - - {!readyToContinue && ( - - Pick a course and exam to continue - - )} -
-
-
- ); -} diff --git a/app/components/study-brain/study-plan-cta.tsx b/app/components/study-brain/study-plan-cta.tsx deleted file mode 100644 index 7bf0f6fd..00000000 --- a/app/components/study-brain/study-plan-cta.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import Link from "next/link"; - -type Props = { - courseCode?: string; - examType?: string; - slot?: string; - variant?: "compact" | "panel"; -}; - -function buildStudyPlanHref({ courseCode, examType, slot }: Props) { - const params = new URLSearchParams(); - if (courseCode) params.set("course", courseCode); - if (examType) params.set("exam", examType); - if (slot) params.set("slot", slot); - const query = params.toString(); - return `/study-plan${query ? `?${query}` : ""}`; -} - -export default function StudyPlanCta({ - courseCode, - examType, - slot, - variant = "panel", -}: Props) { - const href = buildStudyPlanHref({ courseCode, examType, slot }); - - if (variant === "compact") { - return ( - - Plan study - - ); - } - - return ( - -
- - Study plan - -

- Exam soon? Build a plan from the syllabus, slot reports, and papers. -

-

- Pick what is coming, choose how you study, and ExamCooker lines up the - high-yield work first. It is not too late. -

-
- - Plan my study - - → - - - - ); -} diff --git a/app/components/study-plan/composer.tsx b/app/components/study-plan/composer.tsx new file mode 100644 index 00000000..672ad4ea --- /dev/null +++ b/app/components/study-plan/composer.tsx @@ -0,0 +1,436 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { ArrowRight, Plus, Search, X } from "lucide-react"; +import { examTypeValues, type ExamType } from "@/db/enums"; +import { examTypeLabel } from "@/lib/exam-slug"; +import type { CourseSearchRecord } from "@/lib/data/course-catalog"; +import { GradientText } from "@/app/components/landing/landing"; +import type { StudyPreferenceMode } from "@/lib/study-brain/schemas"; +import type { ComposerConfig } from "./sample-plan"; +import { BUILD_VERBS, pickFrom } from "./copy"; + +type Props = { + courses: CourseSearchRecord[]; + initialCourseCode?: string; + initialExam?: ExamType | ""; + initialSlot?: string; + initialPreferences?: StudyPreferenceMode[]; + initialTopics?: string[]; + onCourseChange?: (course: CourseSearchRecord | null) => void; + onBuild: (config: ComposerConfig) => void; +}; + +const PRIMARY_EXAMS: ExamType[] = ["CAT_1", "CAT_2", "FAT", "MID", "QUIZ"]; +const MORE_EXAMS: ExamType[] = examTypeValues.filter( + (value) => !PRIMARY_EXAMS.includes(value), +); + +const PREFERENCES: Array<{ id: StudyPreferenceMode; label: string }> = [ + { id: "past_papers", label: "Past papers" }, + { id: "videos", label: "Videos" }, + { id: "notes", label: "Notes" }, + { id: "solved_examples", label: "Solved examples" }, + { id: "quick_summaries", label: "Summaries" }, + { id: "mixed", label: "A bit of everything" }, +]; + +const TOPIC_SUGGESTIONS = ["Numericals", "Derivations", "Definitions", "Diagrams"]; + +const chipBase = + "ec-press inline-flex h-9 items-center gap-1.5 border px-3.5 text-sm font-semibold transition-colors"; +const chipActive = + "border-black bg-[#5FC4E7] text-black dark:border-[#5FC4E7]/60 dark:bg-[#5FC4E7]/15 dark:text-[#D5D5D5]"; +const chipIdle = + "border-black/15 bg-white/60 text-black/70 hover:border-black/40 hover:bg-white dark:border-[#D5D5D5]/15 dark:bg-transparent dark:text-[#D5D5D5]/70 dark:hover:border-[#D5D5D5]/40 dark:hover:bg-white/[0.04]"; + +function normalize(value: string) { + return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); +} + +function Label({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ); +} + +export default function Composer({ + courses, + initialCourseCode = "", + initialExam = "", + initialSlot = "", + initialPreferences, + initialTopics, + onCourseChange, + onBuild, +}: Props) { + const initialCourse = useMemo( + () => + courses.find( + (course) => + course.code.toUpperCase() === initialCourseCode.toUpperCase(), + ) ?? null, + [courses, initialCourseCode], + ); + + const [courseQuery, setCourseQuery] = useState( + initialCourse ? `${initialCourse.code} ${initialCourse.title}` : "", + ); + const [selectedCourse, setSelectedCourse] = useState( + initialCourse, + ); + const [examType, setExamType] = useState(initialExam); + const [slot, setSlot] = useState(initialSlot.toUpperCase()); + const [preferences, setPreferences] = useState( + initialPreferences ?? ["past_papers", "mixed"], + ); + const [topics, setTopics] = useState(initialTopics ?? []); + const [topicDraft, setTopicDraft] = useState(""); + const [showMoreExams, setShowMoreExams] = useState( + Boolean(initialExam) && MORE_EXAMS.includes(initialExam as ExamType), + ); + + const buildVerb = pickFrom(BUILD_VERBS, `${selectedCourse?.code ?? ""}:${examType}`); + + useEffect(() => { + onCourseChange?.(selectedCourse); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedCourse]); + + const matches = useMemo(() => { + const query = normalize(courseQuery); + if (!query) return []; + return courses + .filter((course) => + normalize( + `${course.code} ${course.title} ${course.aliases.join(" ")}`, + ).includes(query), + ) + .slice(0, 6); + }, [courseQuery, courses]); + + const togglePreference = (id: StudyPreferenceMode) => { + setPreferences((current) => + current.includes(id) + ? current.filter((item) => item !== id) + : [...current, id], + ); + }; + + const addTopic = (raw: string) => { + const value = raw.trim(); + if (!value) return; + setTopics((current) => + current.some((topic) => topic.toLowerCase() === value.toLowerCase()) || + current.length >= 12 + ? current + : [...current, value], + ); + setTopicDraft(""); + }; + + const ready = Boolean(selectedCourse && examType); + + const handleBuild = () => { + if (!selectedCourse || !examType) return; + onBuild({ course: selectedCourse, examType, slot, preferences, topics }); + }; + + return ( +
+
+
+

+ Make a study plan. +

+

+ Pick your course, exam, and slot. ExamCooker orders what to study using + the syllabus, past papers, and what earlier slots reported. +

+
+ + {/* Course */} +
+ + {selectedCourse ? ( +
+
+

+ {selectedCourse.code} +

+

+ {selectedCourse.title} +

+

+ {selectedCourse.paperCount} paper + {selectedCourse.paperCount === 1 ? "" : "s"}, {selectedCourse.noteCount}{" "} + note{selectedCourse.noteCount === 1 ? "" : "s"} +

+
+ +
+ ) : ( + <> +
+ + setCourseQuery(event.target.value)} + placeholder="Search a course or code" + className="h-full min-w-0 flex-1 bg-transparent px-3 text-base text-black focus:outline-none placeholder:text-black/35 dark:text-[#D5D5D5] dark:placeholder:text-[#D5D5D5]/40" + /> + {courseQuery ? ( + + ) : null} +
+ {!courseQuery.trim() ? ( +

+ Search for a course, or pick an upcoming exam below. +

+ ) : matches.length > 0 ? ( +
+ {matches.map((course, index) => ( + + ))} +
+ ) : ( +

+ No match. Try the course code, like BCSE202L. +

+ )} + + )} +
+ + {selectedCourse ? ( + <> + {/* Exam */} +
+ +
+ {PRIMARY_EXAMS.map((value) => ( + + ))} + {showMoreExams ? ( + MORE_EXAMS.map((value) => ( + + )) + ) : ( + + )} +
+
+ + Slot + + setSlot(event.target.value.toUpperCase())} + placeholder="optional" + aria-label="Exam slot" + className="h-full min-w-0 flex-1 bg-transparent px-3 text-sm font-semibold text-black focus:outline-none placeholder:font-normal placeholder:text-black/35 dark:text-[#D5D5D5] dark:placeholder:text-[#D5D5D5]/35" + /> +
+
+ + {/* Topics */} +
+ +
+ setTopicDraft(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + addTopic(topicDraft); + } + }} + placeholder="Add a topic and press enter" + aria-label="Add a topic" + className="h-full min-w-0 flex-1 bg-transparent text-sm text-black focus:outline-none placeholder:text-black/40 dark:text-[#D5D5D5] dark:placeholder:text-[#D5D5D5]/40" + /> + +
+ {topics.length > 0 ? ( +
+ {topics.map((topic) => ( + + {topic} + + + ))} +
+ ) : ( +
+ + Try + + {TOPIC_SUGGESTIONS.map((suggestion) => ( + + ))} +
+ )} +
+ + {/* Style */} +
+ +
+ {PREFERENCES.map((option) => ( + + ))} +
+
+ + {/* Build */} +
+ + {!ready ? ( +

+ Choose an exam to build your plan. +

+ ) : null} +
+ + ) : null} +
+ ); +} diff --git a/app/components/study-plan/copy.ts b/app/components/study-plan/copy.ts new file mode 100644 index 00000000..6c7a2c4a --- /dev/null +++ b/app/components/study-plan/copy.ts @@ -0,0 +1,28 @@ +// Light, deterministic copy variation so the feature isn't one rigid branded +// label repeated everywhere. Kept plain and literal on purpose — no slogans, +// no em dashes, no exaggerated lines. + +function hashSeed(seed: string): number { + let h = 2166136261; + for (let i = 0; i < seed.length; i += 1) { + h ^= seed.charCodeAt(i); + h = Math.imul(h, 16777619); + } + return Math.abs(h); +} + +/** Deterministic pick so SSR and client agree (no hydration drift). */ +export function pickFrom(pool: readonly T[], seed: string): T { + return pool[hashSeed(seed) % pool.length] as T; +} + +export const PLAN_TITLES = ["Study plan", "Your plan", "Prep plan"] as const; + +export const BUILD_VERBS = ["Build plan", "Make plan", "Build the plan"] as const; + +export const CTA_HOOKS = [ + "Build a study plan for this exam.", + "Plan your prep for this course.", + "Turn this course into a study plan.", + "Make a plan before the exam.", +] as const; diff --git a/app/components/study-plan/generating.tsx b/app/components/study-plan/generating.tsx new file mode 100644 index 00000000..16a4828d --- /dev/null +++ b/app/components/study-plan/generating.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { Check, Loader2 } from "lucide-react"; +import { examTypeLabel } from "@/lib/exam-slug"; +import type { ComposerConfig } from "./sample-plan"; + +type Props = { + config: ComposerConfig; + onDone: () => void; +}; + +export default function Generating({ config, onDone }: Props) { + const examLabel = config.examType ? examTypeLabel(config.examType) : ""; + + const stages = useMemo(() => { + const list: string[] = ["Matching the syllabus"]; + list.push( + config.course.paperCount > 0 + ? `Reading past papers${examLabel ? ` for ${examLabel}` : ""}` + : "Reading the syllabus modules", + ); + list.push("Checking earlier slots"); + list.push("Lining up resources"); + list.push("Ordering by priority"); + return list; + }, [config.course.paperCount, examLabel]); + + const [active, setActive] = useState(0); + + useEffect(() => { + const reduce = + typeof window !== "undefined" && + window.matchMedia?.("(prefers-reduced-motion: reduce)").matches; + const step = reduce ? 130 : 720; + let index = 0; + let timer: ReturnType; + + const tick = () => { + index += 1; + setActive(index); + if (index < stages.length) { + timer = setTimeout(tick, step); + } else { + timer = setTimeout(onDone, reduce ? 120 : 540); + } + }; + + timer = setTimeout(tick, step); + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const progress = Math.min(100, Math.round((active / stages.length) * 100)); + + return ( +
+
+
+

+ Building your plan +

+

+ {config.course.code} + {examLabel ? ` · ${examLabel}` : ""} + {config.slot ? ` · slot ${config.slot}` : ""} +

+
+ +
+
+
+ +
    + {stages.map((label, index) => { + const done = index < active; + const isActive = index === active; + return ( +
  1. + + {done ? ( + + ) : isActive ? ( + + ) : null} + + + {label} + +
  2. + ); + })} +
+
+ ); +} diff --git a/app/components/study-plan/plan-cta.tsx b/app/components/study-plan/plan-cta.tsx new file mode 100644 index 00000000..c3b7e1e4 --- /dev/null +++ b/app/components/study-plan/plan-cta.tsx @@ -0,0 +1,64 @@ +import Link from "next/link"; +import { CTA_HOOKS, pickFrom } from "./copy"; + +type Props = { + courseCode?: string; + examType?: string; + slot?: string; + variant?: "compact" | "panel"; +}; + +function buildHref({ courseCode, examType, slot }: Props) { + const params = new URLSearchParams(); + if (courseCode) params.set("course", courseCode); + if (examType) params.set("exam", examType); + if (slot) params.set("slot", slot); + const query = params.toString(); + return `/study-plan${query ? `?${query}` : ""}`; +} + +export default function PlanCta({ + courseCode, + examType, + slot, + variant = "panel", +}: Props) { + const href = buildHref({ courseCode, examType, slot }); + const seed = courseCode ?? "exam-cooker"; + + if (variant === "compact") { + return ( + + Make a plan + + ); + } + + return ( + +
+

+ {pickFrom(CTA_HOOKS, seed)} +

+

+ Say what is coming and how you study. ExamCooker orders the high-yield + work first, using the syllabus, past papers, and earlier slots. +

+
+ + Make a plan + + → + + + + ); +} diff --git a/app/components/study-plan/plan-view.tsx b/app/components/study-plan/plan-view.tsx new file mode 100644 index 00000000..f3c728e3 --- /dev/null +++ b/app/components/study-plan/plan-view.tsx @@ -0,0 +1,295 @@ +"use client"; + +import type { ComponentType } from "react"; +import Link from "next/link"; +import { + ArrowLeft, + BookOpen, + ExternalLink, + FastForward, + PenLine, + Play, + RotateCcw, +} from "lucide-react"; +import { examTypeLabel } from "@/lib/exam-slug"; +import { getCoursePastPapersPath } from "@/lib/seo"; +import { GradientText } from "@/app/components/landing/landing"; +import type { StudyPlan, StudyPlanSection } from "@/lib/study-brain/schemas"; +import { formatMinutes } from "./sample-plan"; +import { PLAN_TITLES, pickFrom } from "./copy"; + +type Priority = StudyPlanSection["priority"]; +type TaskKind = StudyPlanSection["tasks"][number]["kind"]; + +type Props = { + plan: StudyPlan; + onReset: () => void; +}; + +const PRIORITY_META: Record = { + critical: { + label: "Start here", + card: "border-2 border-black/10 bg-white dark:border-[#D5D5D5]/12 dark:bg-white/[0.04]", + }, + high: { + label: "High yield", + card: "border-2 border-[#5FC4E7]/70 bg-white dark:border-[#5FC4E7]/25 dark:bg-white/[0.03]", + }, + medium: { + label: "Worth a pass", + card: "border border-black/12 bg-white dark:border-[#D5D5D5]/12 dark:bg-white/[0.02]", + }, + low: { + label: "Optional", + card: "border border-dashed border-black/20 bg-transparent dark:border-[#D5D5D5]/15", + }, +}; + +const KIND_META: Record; label: string }> = { + read: { Icon: BookOpen, label: "Read" }, + watch: { Icon: Play, label: "Watch" }, + practice: { Icon: PenLine, label: "Practice" }, + revise: { Icon: RotateCcw, label: "Revise" }, + skim: { Icon: FastForward, label: "Skim" }, +}; + +function humanJoin(parts: string[]): string { + if (parts.length === 0) return "the syllabus"; + if (parts.length === 1) return parts[0] as string; + if (parts.length === 2) return `${parts[0]} and ${parts[1]}`; + return `${parts.slice(0, -1).join(", ")}, and ${parts[parts.length - 1]}`; +} + +function Stat({ value, label }: { value: string | number; label: string }) { + return ( +
+ + {value} + + + {label} + +
+ ); +} + +export default function PlanView({ plan, onReset }: Props) { + const { context, evidenceSummary } = plan; + const examLabel = context.examType ? examTypeLabel(context.examType) : ""; + const title = pickFrom(PLAN_TITLES, context.courseCode); + + const builtFromParts: string[] = ["the syllabus"]; + if (evidenceSummary.pastPapersUsed > 0) { + builtFromParts.push( + `${evidenceSummary.pastPapersUsed} past paper${evidenceSummary.pastPapersUsed === 1 ? "" : "s"}`, + ); + } + if (evidenceSummary.earlierSlotSignalsUsed > 0) { + builtFromParts.push( + `${evidenceSummary.earlierSlotSignalsUsed} earlier-slot report${evidenceSummary.earlierSlotSignalsUsed === 1 ? "" : "s"}`, + ); + } + if (evidenceSummary.webResourcesUsed > 0) { + builtFromParts.push( + `${evidenceSummary.webResourcesUsed} resource${evidenceSummary.webResourcesUsed === 1 ? "" : "s"}`, + ); + } + + return ( +
+ {/* Top bar */} +
+ + + Preview + +
+ + {/* Header */} +
+
+

+ {title} +

+

+ {context.courseTitle} +

+

+ {context.courseCode} + {examLabel ? ` · ${examLabel}` : ""} + {context.slot ? ` · slot ${context.slot}` : ""} +

+
+ + {/* Stat strip */} +
+
+ + + + {evidenceSummary.earlierSlotSignalsUsed > 0 ? ( + + ) : null} + +
+

+ Built from {humanJoin(builtFromParts)}. Highest priority first. +

+
+ + {/* Sections */} +
+ {plan.sections.map((section, index) => { + const meta = PRIORITY_META[section.priority]; + const isCritical = section.priority === "critical"; + return ( +
+ {isCritical ? ( + + ) : null} +
+
+

+ {isCritical ? ( + {meta.label} + ) : ( + + {meta.label} + + )} +

+

+ {section.topicTitle} +

+
+ + {formatMinutes(section.estimatedMinutes)} + +
+ +

+ {section.reason} +

+ + {section.evidence.length > 0 ? ( +
+ {section.evidence.map((item, evidenceIndex) => ( + + {item.label} + + ))} +
+ ) : null} + +
    + {section.tasks.map((task, taskIndex) => { + const kind = KIND_META[task.kind]; + const Icon = kind.Icon; + return ( +
  • + + + +
    + {task.url ? ( + + {task.title} + + + ) : ( + + {task.title} + + )} +

    + {kind.label} + {task.source ? `, ${task.source}` : ""} + {task.skipIfShortOnTime ? ", skippable" : ""} +

    +
    + + {task.estimatedMinutes}m + +
  • + ); + })} +
+
+ ); + })} +
+ + {/* Warnings */} + {plan.warnings.length > 0 ? ( +
+ {plan.warnings.map((warning) => ( +

+ {warning} +

+ ))} +
+ ) : null} + + {/* After the exam */} +
+

+ After the exam +

+

+ You can mark what actually came up from a list of topics. It takes a + few seconds and helps the plan for students in later slots. +

+
+ + {/* Footer */} +
+ + Open {context.courseCode} past papers + + + +
+
+ ); +} diff --git a/app/components/study-plan/sample-plan.ts b/app/components/study-plan/sample-plan.ts new file mode 100644 index 00000000..04c56e7e --- /dev/null +++ b/app/components/study-plan/sample-plan.ts @@ -0,0 +1,318 @@ +import type { ExamType } from "@/db/enums"; +import type { CourseSearchRecord } from "@/lib/data/course-catalog"; +import { examTypeLabel } from "@/lib/exam-slug"; +import type { + StudyPlan, + StudyPlanSection, + StudyPreferenceMode, +} from "@/lib/study-brain/schemas"; + +type StudyPlanTask = StudyPlanSection["tasks"][number]; +type StudyPlanEvidence = StudyPlanSection["evidence"][number]; +type Priority = StudyPlanSection["priority"]; + +export type ComposerConfig = { + course: CourseSearchRecord; + examType: ExamType | ""; + slot: string; + preferences: StudyPreferenceMode[]; + topics: string[]; +}; + +// --- deterministic helpers ---------------------------------------------------- + +function seedNumber(seed: string): number { + let h = 2166136261; + for (let i = 0; i < seed.length; i += 1) { + h ^= seed.charCodeAt(i); + h = Math.imul(h, 16777619); + } + return Math.abs(h); +} + +function pick(pool: readonly T[], seed: string): T { + return pool[seedNumber(seed) % pool.length] as T; +} + +export function formatMinutes(total: number): string { + if (total <= 0) return "0 min"; + const hours = Math.floor(total / 60); + const mins = total % 60; + if (hours === 0) return `${mins} min`; + if (mins === 0) return `${hours}h`; + return `${hours}h ${mins}m`; +} + +// --- content pools ------------------------------------------------------------ + +const DEFAULT_TOPICS = [ + "Core theory", + "Common numericals", + "Definitions and short answers", + "Derivations and proofs", + "Repeated paper questions", +]; + +const REASONS: Record = { + critical: [ + "Appeared in your last two {exam} papers.", + "High weightage on the syllabus, and it repeats most years.", + ], + high: [ + "Comes up in most {exam} cycles.", + "Overlaps with what earlier slots reported.", + ], + medium: [ + "Shows up occasionally. A quick pass is enough.", + "Good marks for the time if the basics feel shaky.", + ], + low: [ + "Lower priority this cycle.", + "Optional if you have time left at the end.", + ], +}; + +const PRIORITY_BY_INDEX: Priority[] = [ + "critical", + "high", + "high", + "medium", + "low", +]; + +function youtubeSearch(query: string): string { + return `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}`; +} + +function prefersAny( + prefs: StudyPreferenceMode[], + target: StudyPreferenceMode, +): boolean { + return prefs.includes(target) || prefs.includes("mixed"); +} + +// --- section + task assembly -------------------------------------------------- + +function buildTasks( + topic: string, + priority: Priority, + config: ComposerConfig, + examLabel: string, +): StudyPlanTask[] { + const { preferences, course } = config; + const lower = topic.toLowerCase(); + const prefTasks: StudyPlanTask[] = []; + + if (course.paperCount > 0 && prefersAny(preferences, "past_papers")) { + prefTasks.push({ + kind: "practice", + title: `Work the ${examLabel} questions on ${lower}`, + source: "Past papers", + estimatedMinutes: 22, + }); + } + if (prefersAny(preferences, "videos")) { + prefTasks.push({ + kind: "watch", + title: `Watch a walkthrough of ${lower}`, + source: "YouTube", + url: youtubeSearch(`${course.code} ${topic}`), + estimatedMinutes: 15, + }); + } + if (prefersAny(preferences, "notes") && course.noteCount > 0) { + prefTasks.push({ + kind: "read", + title: `Skim the clearest notes on ${lower}`, + source: "Notes", + estimatedMinutes: 12, + }); + } + if (prefersAny(preferences, "solved_examples")) { + prefTasks.push({ + kind: "practice", + title: "Redo three solved examples without peeking", + source: "Solved sets", + estimatedMinutes: 18, + }); + } + if (prefersAny(preferences, "quick_summaries")) { + prefTasks.push({ + kind: "revise", + title: "Run the one-page summary out loud", + estimatedMinutes: 8, + skipIfShortOnTime: true, + }); + } + + if (prefTasks.length === 0) { + prefTasks.push({ + kind: "read", + title: `Read through ${lower} once`, + source: "Syllabus", + estimatedMinutes: 20, + }); + } + + // Keep the core work, then close with a short recall pass that is the first + // thing to drop when time runs out (except for the must-do topic). + const tasks = prefTasks.slice(0, 3); + tasks.push({ + kind: "revise", + title: "Quick recall pass", + estimatedMinutes: 8, + skipIfShortOnTime: priority !== "critical", + }); + + return tasks; +} + +function buildEvidence( + index: number, + priority: Priority, + config: ComposerConfig, + examLabel: string, +): { evidence: StudyPlanEvidence[]; usedWebResource: boolean } { + const { course, slot, preferences } = config; + const evidence: StudyPlanEvidence[] = [ + { + type: "syllabus", + label: `Module ${index + 1} on the syllabus`, + confidence: "high", + }, + ]; + + if (course.paperCount > 0) { + const seen = Math.max(1, Math.min(course.paperCount, 2 + (index % 3))); + evidence.push({ + type: "past_paper", + label: `Seen in ${seen} past ${examLabel} paper${seen === 1 ? "" : "s"}`, + confidence: course.paperCount >= 4 ? "high" : "medium", + }); + } + + if (slot && index < 2) { + const family = slot.charAt(0).toUpperCase(); + evidence.push({ + type: "slot_report", + label: `Reported by ${family}-slot students`, + confidence: "medium", + }); + } + + let usedWebResource = false; + if ( + (prefersAny(preferences, "videos") || prefersAny(preferences, "notes")) && + priority !== "low" + ) { + usedWebResource = true; + evidence.push({ + type: index % 2 === 0 ? "resource" : "web", + label: index % 2 === 0 ? "Matched note + lecture" : "Vetted web resource", + confidence: "medium", + }); + } + + return { evidence, usedWebResource }; +} + +function buildSection( + topic: string, + index: number, + config: ComposerConfig, + examLabel: string, +): { section: StudyPlanSection; usedWebResource: boolean } { + const priority = PRIORITY_BY_INDEX[index] ?? "low"; + const tasks = buildTasks(topic, priority, config, examLabel); + const estimatedMinutes = Math.min( + 600, + tasks.reduce((sum, task) => sum + task.estimatedMinutes, 0), + ); + const { evidence, usedWebResource } = buildEvidence( + index, + priority, + config, + examLabel, + ); + const reason = pick(REASONS[priority], `${config.course.code}:${topic}`).replace( + "{exam}", + examLabel, + ); + + return { + section: { + topicId: undefined, + topicTitle: topic, + priority, + estimatedMinutes, + reason, + evidence, + tasks, + }, + usedWebResource, + }; +} + +/** + * Builds a realistic, schema-valid plan from the composer choices. This stands + * in for the worker's generated plan until the research agent is wired up — + * it reads from real course counts where we have them and is clearly labelled + * as a preview in the UI so nothing is presented as confirmed fact. + */ +export function buildSamplePlan(config: ComposerConfig): StudyPlan { + const { course, examType, slot } = config; + const examLabel = examType ? examTypeLabel(examType) : "exam"; + const sourceTopics = + config.topics.length > 0 ? config.topics.slice(0, 5) : DEFAULT_TOPICS; + + let webResourcesUsed = 0; + const sections = sourceTopics.map((topic, index) => { + const { section, usedWebResource } = buildSection( + topic, + index, + config, + examLabel, + ); + if (usedWebResource) webResourcesUsed += 1; + return section; + }); + + const totalEstimatedMinutes = sections.reduce( + (sum, section) => sum + section.estimatedMinutes, + 0, + ); + + const warnings: string[] = []; + if (!slot) { + warnings.push( + "Add your slot and ExamCooker can fold in earlier-slot reports too.", + ); + } + if (course.paperCount === 0) { + warnings.push( + "No past papers for this course yet, so a few calls lean on the syllabus alone.", + ); + } + + return { + title: `${course.code} ${examLabel}`, + context: { + courseCode: course.code, + courseTitle: course.title, + examType: examType || null, + slot: slot || null, + syllabusName: null, + }, + evidenceSummary: { + syllabusTopicsUsed: sections.length, + pastPapersUsed: Math.min(course.paperCount, 6), + earlierSlotSignalsUsed: slot + ? 3 + (seedNumber(course.code + slot) % 5) + : 0, + webResourcesUsed, + }, + totalEstimatedMinutes, + sections, + warnings, + }; +} diff --git a/app/components/study-plan/study-plan-experience.tsx b/app/components/study-plan/study-plan-experience.tsx new file mode 100644 index 00000000..56977305 --- /dev/null +++ b/app/components/study-plan/study-plan-experience.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { CourseSearchRecord } from "@/lib/data/course-catalog"; +import { parseExamTypeInput } from "@/lib/exam-slug"; +import type { StudyPlan } from "@/lib/study-brain/schemas"; +import Composer from "./composer"; +import Generating from "./generating"; +import PlanView from "./plan-view"; +import UpcomingExams from "./upcoming-exams"; +import { buildSamplePlan, type ComposerConfig } from "./sample-plan"; + +type CoursePick = { + id: string; + code: string; + title: string; + paperCount: number; +}; + +type Props = { + courses: CourseSearchRecord[]; + suggested: CoursePick[]; + popular: CoursePick[]; + initialCourse?: string; + initialExam?: string; + initialSlot?: string; +}; + +type Phase = "compose" | "building" | "plan"; + +function scrollToTop() { + if (typeof window !== "undefined") { + window.scrollTo({ top: 0, behavior: "smooth" }); + } +} + +export default function StudyPlanExperience({ + courses, + suggested, + popular, + initialCourse = "", + initialExam = "", + initialSlot = "", +}: Props) { + const [phase, setPhase] = useState("compose"); + const [config, setConfig] = useState(null); + const [plan, setPlan] = useState(null); + const [quickCourse, setQuickCourse] = useState(null); + const [hasCourse, setHasCourse] = useState(false); + + useEffect(() => { + scrollToTop(); + }, [phase]); + + const handleBuild = (next: ComposerConfig) => { + setConfig(next); + setPlan(buildSamplePlan(next)); + setPhase("building"); + }; + + const handlePick = (code: string) => { + setQuickCourse(code); + scrollToTop(); + }; + + if (phase === "building" && config) { + return setPhase("plan")} />; + } + + if (phase === "plan" && plan) { + return setPhase("compose")} />; + } + + const seedCourse = quickCourse ?? config?.course.code ?? initialCourse; + + return ( +
+ setHasCourse(Boolean(course))} + onBuild={handleBuild} + /> + {!hasCourse ? ( + <> + + + + ) : null} +
+ ); +} diff --git a/app/components/study-plan/upcoming-exams.tsx b/app/components/study-plan/upcoming-exams.tsx new file mode 100644 index 00000000..0b95b93f --- /dev/null +++ b/app/components/study-plan/upcoming-exams.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { ArrowRight } from "lucide-react"; + +type CourseCard = { + id: string; + code: string; + title: string; + paperCount: number; +}; + +type Props = { + courses: CourseCard[]; + onPick: (code: string) => void; + title?: string; + hint?: string; +}; + +// Same card grammar as the rest of the site (course-grid-card.tsx): filled cyan +// in light, dark surface in dark, gray mono code, big paper count. These double +// as one-tap entry points into the planner. +export default function CoursePickGrid({ + courses, + onPick, + title = "Upcoming exams", + hint = "Pick one to start", +}: Props) { + if (courses.length === 0) return null; + + return ( +
+
+

+ {title} +

+ + {hint} + +
+
+ {courses.map((course, index) => ( + + ))} +
+
+ ); +} diff --git a/app/globals.css b/app/globals.css index c3c60adc..5cf4da1c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -224,6 +224,101 @@ overflow: hidden; } +/* ── Study plan surface ──────────────────────────────────────────────── */ +@keyframes sp-rise { + from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Staggered page-load reveal. Drive cascade with `--sp-i` per element. */ +.sp-rise { + animation: sp-rise 560ms var(--ec-ease-out-soft) both; + animation-delay: calc(var(--sp-i, 0) * 70ms); +} + +@keyframes sp-fade { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Phase transition (compose → building → plan). */ +.sp-fade { + animation: sp-fade 440ms var(--ec-ease-out-soft) both; +} + +/* Brand-blue focus ring — keeps the accent off the mint used app-wide. */ +.sp-focus { + transition: box-shadow 220ms var(--ec-ease-out-soft), border-color 220ms ease; +} + +.sp-focus:focus-within { + box-shadow: 0 0 0 2px rgb(39 186 236 / 0.42); + border-color: rgb(39 186 236 / 0.60); +} + +/* Soft brand-blue aura for atmosphere behind the hero block. */ +.sp-aura { + position: relative; + isolation: isolate; +} + +.sp-aura::before { + content: ""; + position: absolute; + z-index: -1; + left: -6%; + top: -60%; + width: min(540px, 96%); + height: 320px; + background: radial-gradient(56% 56% at 26% 44%, rgb(39 186 236 / 0.16), transparent 72%); + filter: blur(22px); + pointer-events: none; +} + +.dark .sp-aura::before { + background: radial-gradient(56% 56% at 26% 44%, rgb(39 186 236 / 0.20), transparent 72%); +} + +/* Signature gradient hairline. */ +.sp-rule { + height: 2px; + width: 56px; + border-radius: 2px; + background: linear-gradient(to right, #253ee0, #27baec); +} + +/* Gradient sweep that wipes across the build CTA on hover. */ +.sp-cta-sheen { + position: relative; + overflow: hidden; +} + +.sp-cta-sheen::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(110deg, transparent 20%, rgb(255 255 255 / 0.35) 50%, transparent 80%); + transform: translateX(-120%); + transition: transform 620ms var(--ec-ease-out-soft); + pointer-events: none; +} + +@media (hover: hover) { + .sp-cta-sheen:hover::after { + transform: translateX(120%); + } +} + +@media (prefers-reduced-motion: reduce) { + .sp-rise, + .sp-fade { + animation: none; + } + .sp-cta-sheen::after { + display: none; + } +} + @media (prefers-reduced-motion: reduce) { .ec-press, .ec-card-lift, From ceb6c69eb187e951aa068eea347d68cbe0cde6e2 Mon Sep 17 00:00:00 2001 From: theg1239 Date: Mon, 29 Jun 2026 05:25:23 +0530 Subject: [PATCH 5/5] fix(study-brain): require signed tokens for agent runs Protect the new Study Brain Durable Object run store with the same signed user-token model used by the command agent. POST /runs now rejects unauthenticated creation, GET /runs requires a trusted userKey and filters by it, scheduleEvery relies on its default idempotent behavior, and the new canonical-topic title index uses btree instead of a plain-string GIN index. --- db/schema.ts | 2 +- .../migration.sql | 4 +- worker/src/command-agent.ts | 2 +- worker/src/http.ts | 2 +- worker/src/study-brain-agent.ts | 88 ++++++++++++++++--- 5 files changed, 83 insertions(+), 15 deletions(-) diff --git a/db/schema.ts b/db/schema.ts index 76a77aa5..7fa2c77b 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -695,7 +695,7 @@ export const canonicalTopic = cockroachTable( }, (table) => [ index("CanonicalTopic_courseId_idx").using("btree", table.courseId.asc()), - index("CanonicalTopic_title_trgm_idx").using("gin", table.title.asc()), + index("CanonicalTopic_title_idx").using("btree", table.title.asc()), ], ); diff --git a/drizzle/20260619120000_study_brain_foundation/migration.sql b/drizzle/20260619120000_study_brain_foundation/migration.sql index 1586c7a5..56164aab 100644 --- a/drizzle/20260619120000_study_brain_foundation/migration.sql +++ b/drizzle/20260619120000_study_brain_foundation/migration.sql @@ -74,8 +74,8 @@ CREATE TABLE IF NOT EXISTS "CanonicalTopic" ( CREATE INDEX IF NOT EXISTS "CanonicalTopic_courseId_idx" ON "CanonicalTopic" ("courseId"); --> statement-breakpoint -CREATE INDEX IF NOT EXISTS "CanonicalTopic_title_trgm_idx" - ON "CanonicalTopic" USING gin ("title"); +CREATE INDEX IF NOT EXISTS "CanonicalTopic_title_idx" + ON "CanonicalTopic" ("title"); --> statement-breakpoint CREATE TABLE IF NOT EXISTS "ExamSignal" ( "id" STRING PRIMARY KEY, diff --git a/worker/src/command-agent.ts b/worker/src/command-agent.ts index a46af815..d0bc7b18 100644 --- a/worker/src/command-agent.ts +++ b/worker/src/command-agent.ts @@ -170,7 +170,7 @@ function getTrustedUserContext( }; } -async function getTrustedUserFromCommandToken(input: { +export async function getTrustedUserFromCommandToken(input: { userKey: string; userToken: string; secret: string; diff --git a/worker/src/http.ts b/worker/src/http.ts index 21eebc91..b2d60b7a 100644 --- a/worker/src/http.ts +++ b/worker/src/http.ts @@ -46,7 +46,7 @@ export async function readJsonPayload(request: Request) { return (await request.json().catch(() => null)) as Record | null; } -function readBearerToken(value: string | null) { +export function readBearerToken(value: string | null) { const match = value?.match(/^\s*Bearer\s+(.+?)\s*$/i); return match?.[1]?.trim() ?? ""; } diff --git a/worker/src/study-brain-agent.ts b/worker/src/study-brain-agent.ts index 680e1283..ca9a11b0 100644 --- a/worker/src/study-brain-agent.ts +++ b/worker/src/study-brain-agent.ts @@ -1,5 +1,11 @@ import { Agent, callable } from "agents"; -import { emptyResponse, jsonResponse, readJsonPayload } from "./http"; +import { getTrustedUserFromCommandToken } from "./command-agent"; +import { + emptyResponse, + jsonResponse, + readBearerToken, + readJsonPayload, +} from "./http"; import type { Env, StudyBrainAgentState, @@ -18,6 +24,13 @@ type StudyBrainRunRow = { const STUDY_BRAIN_RUN_LIMIT = 2000; +class UnauthorizedStudyBrainAgentRequestError extends Error { + constructor() { + super("Valid signed study brain user token required."); + this.name = "UnauthorizedStudyBrainAgentRequestError"; + } +} + function ensureStudyBrainSchema(agent: Agent) { agent.sql` CREATE TABLE IF NOT EXISTS study_brain_runs ( @@ -109,9 +122,7 @@ export class ExamCookerStudyBrainAgent extends Agent async onStart() { ensureStudyBrainSchema(this); - await this.scheduleEvery(86_400, "pruneRuns", undefined, { - _idempotent: true, - }); + await this.scheduleEvery(86_400, "pruneRuns"); } async onRequest(request: Request) { @@ -138,10 +149,32 @@ export class ExamCookerStudyBrainAgent extends Agent pruneStudyBrainRuns(this); } + private async getTrustedUser(input: { + userKey?: unknown; + userToken?: unknown; + }) { + const userKey = typeof input.userKey === "string" ? input.userKey.trim() : ""; + const userToken = + typeof input.userToken === "string" ? input.userToken.trim() : ""; + const secret = this.env.CLOUDFLARE_COMMAND_AGENT_ADMIN_TOKEN?.trim(); + + if (!userKey || !userToken || !secret) return null; + + return getTrustedUserFromCommandToken({ userKey, userToken, secret }); + } + @callable() async createPlanRun(input: StudyBrainPlanRunInput) { ensureStudyBrainSchema(this); - const normalized = normalizePlanInput(input); + const trustedUser = await this.getTrustedUser(input); + if (!trustedUser) { + throw new UnauthorizedStudyBrainAgentRequestError(); + } + + const normalized = normalizePlanInput({ + ...input, + userKey: trustedUser.userKey, + }); const timestamp = Date.now(); const id = crypto.randomUUID(); @@ -197,8 +230,17 @@ export class ExamCookerStudyBrainAgent extends Agent } @callable() - async getPlanRun(input: { id?: string }) { + async getPlanRun(input: { + id?: string; + userKey?: string; + userToken?: string | null; + }) { ensureStudyBrainSchema(this); + const trustedUser = await this.getTrustedUser(input); + if (!trustedUser) { + throw new UnauthorizedStudyBrainAgentRequestError(); + } + const id = normalizeString(input.id); if (!id) return null; @@ -212,7 +254,7 @@ export class ExamCookerStudyBrainAgent extends Agent created_at AS createdAt, updated_at AS updatedAt FROM study_brain_runs - WHERE id = ${id} + WHERE id = ${id} AND user_key = ${trustedUser.userKey} LIMIT 1 `; @@ -232,10 +274,21 @@ export class ExamCookerStudyBrainAgent extends Agent private async handleCreateRun(request: Request) { const payload = (await readJsonPayload(request)) as StudyBrainPlanRunInput | null; + const bearerToken = readBearerToken(request.headers.get("Authorization")); + const payloadToken = + typeof payload?.userToken === "string" ? payload.userToken.trim() : ""; + const input = { + ...(payload ?? {}), + userToken: payloadToken || bearerToken, + }; try { - return jsonResponse(await this.createPlanRun(payload ?? {})); + return jsonResponse(await this.createPlanRun(input)); } catch (error) { + if (error instanceof UnauthorizedStudyBrainAgentRequestError) { + return jsonResponse({ error: error.message }, { status: 401 }); + } + return jsonResponse( { error: error instanceof Error ? error.message : "Invalid study brain run." }, { status: 400 }, @@ -245,7 +298,22 @@ export class ExamCookerStudyBrainAgent extends Agent private async handleListRuns(request: Request) { ensureStudyBrainSchema(this); - const userKey = new URL(request.url).searchParams.get("userKey")?.trim() || null; + const userKey = new URL(request.url).searchParams.get("userKey")?.trim() || ""; + if (!userKey) { + return jsonResponse({ error: "userKey is required." }, { status: 400 }); + } + + const trustedUser = await this.getTrustedUser({ + userKey, + userToken: readBearerToken(request.headers.get("Authorization")), + }); + if (!trustedUser) { + return jsonResponse( + { error: "Valid signed study brain user token required." }, + { status: 401 }, + ); + } + const rows = this.sql` SELECT id, @@ -256,7 +324,7 @@ export class ExamCookerStudyBrainAgent extends Agent created_at AS createdAt, updated_at AS updatedAt FROM study_brain_runs - WHERE ${userKey} IS NULL OR user_key = ${userKey} + WHERE user_key = ${trustedUser.userKey} ORDER BY updated_at DESC LIMIT 20 `;