-
Notifications
You must be signed in to change notification settings - Fork 3
Add AI pre-review for devlogs and projects #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request adds AI-powered pre-review functionality for devlogs and projects submitted to Hack Club Construct. When projects are shipped or viewed by moderators, an AI model analyzes devlogs and project details to flag potential over-reporting of time spent, providing moderators with preliminary context before manual review. The feature gracefully degrades when the AI_API_KEY is not configured.
Key changes:
- New database tables (
ai_devlog_reviewandai_project_review) to store AI-generated review results - Integration with HackClub AI API using the qwen/qwen3-vl-235b-a22b-instruct model
- UI updates in admin review panel and devlog components to display AI review status and rationale
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
src/lib/server/db/schema.ts |
Adds two new database tables for storing AI devlog reviews and project reviews with foreign keys to devlog and project tables |
src/lib/server/ai/reviewer.ts |
Implements AI review logic by calling HackClub AI API to analyze devlogs and projects for time over-reporting |
src/lib/server/ai/ensure.ts |
Provides caching and persistence layer for AI reviews, only re-reviewing when content is updated |
src/routes/dashboard/projects/[id]/ship/+page.server.ts |
Triggers fire-and-forget AI pre-review when projects are shipped |
src/routes/dashboard/admin/review/[id]/+page.server.ts |
Fetches and ensures AI reviews are available for moderators viewing projects |
src/routes/dashboard/admin/review/[id]/+page.svelte |
Displays AI review summary showing approval status and flagged devlog counts |
src/lib/components/Devlog.svelte |
Shows AI review status badges and rationale for individual devlogs |
drizzle/meta/_journal.json |
Adds migration entry for AI review tables |
drizzle/meta/0004_snapshot.json |
Complete database schema snapshot including new AI review tables |
drizzle/0004_ai_prechecks.sql |
Migration SQL to create ai_devlog_review and ai_project_review tables with constraints |
README.md |
Documents the new AI pre-review feature and AI_API_KEY environment variable requirement |
.env.example |
Adds AI_API_KEY configuration example |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| CREATE TABLE "ai_devlog_review" ( | ||
| "id" serial PRIMARY KEY NOT NULL, | ||
| "devlogId" integer NOT NULL, | ||
| "projectId" integer NOT NULL, | ||
| "approved" boolean NOT NULL, | ||
| "rationale" text NOT NULL, | ||
| "prompt" text NOT NULL, | ||
| "model" text NOT NULL, | ||
| "createdAt" timestamp DEFAULT now() NOT NULL, | ||
| "updatedAt" timestamp DEFAULT now() NOT NULL, | ||
| CONSTRAINT "ai_devlog_review_devlogId_unique" UNIQUE("devlogId") | ||
| ); | ||
| --> statement-breakpoint | ||
| CREATE TABLE "ai_project_review" ( | ||
| "id" serial PRIMARY KEY NOT NULL, | ||
| "projectId" integer NOT NULL, | ||
| "overallApproved" boolean NOT NULL, | ||
| "summary" text NOT NULL, | ||
| "prompt" text NOT NULL, | ||
| "model" text NOT NULL, | ||
| "createdAt" timestamp DEFAULT now() NOT NULL, | ||
| "updatedAt" timestamp DEFAULT now() NOT NULL, | ||
| CONSTRAINT "ai_project_review_projectId_unique" UNIQUE("projectId") | ||
| ); | ||
| --> statement-breakpoint | ||
| ALTER TABLE "ai_devlog_review" ADD CONSTRAINT "ai_devlog_review_devlogId_devlog_id_fk" FOREIGN KEY ("devlogId") REFERENCES "public"."devlog"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | ||
| ALTER TABLE "ai_devlog_review" ADD CONSTRAINT "ai_devlog_review_projectId_project_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."project"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | ||
| ALTER TABLE "ai_project_review" ADD CONSTRAINT "ai_project_review_projectId_project_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."project"("id") ON DELETE no action ON UPDATE no action; No newline at end of file |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a discrepancy between migration files 0004_milky_iron_patriot.sql and 0010_swift_korg.sql. The former uses ON DELETE cascade for foreign keys while the latter uses ON DELETE no action. Since 0010 appears to be the final/correct migration that should be run, this inconsistency suggests 0004_milky_iron_patriot.sql is an outdated duplicate that should be removed to avoid confusion.
| CREATE TABLE "ai_devlog_review" ( | |
| "id" serial PRIMARY KEY NOT NULL, | |
| "devlogId" integer NOT NULL, | |
| "projectId" integer NOT NULL, | |
| "approved" boolean NOT NULL, | |
| "rationale" text NOT NULL, | |
| "prompt" text NOT NULL, | |
| "model" text NOT NULL, | |
| "createdAt" timestamp DEFAULT now() NOT NULL, | |
| "updatedAt" timestamp DEFAULT now() NOT NULL, | |
| CONSTRAINT "ai_devlog_review_devlogId_unique" UNIQUE("devlogId") | |
| ); | |
| --> statement-breakpoint | |
| CREATE TABLE "ai_project_review" ( | |
| "id" serial PRIMARY KEY NOT NULL, | |
| "projectId" integer NOT NULL, | |
| "overallApproved" boolean NOT NULL, | |
| "summary" text NOT NULL, | |
| "prompt" text NOT NULL, | |
| "model" text NOT NULL, | |
| "createdAt" timestamp DEFAULT now() NOT NULL, | |
| "updatedAt" timestamp DEFAULT now() NOT NULL, | |
| CONSTRAINT "ai_project_review_projectId_unique" UNIQUE("projectId") | |
| ); | |
| --> statement-breakpoint | |
| ALTER TABLE "ai_devlog_review" ADD CONSTRAINT "ai_devlog_review_devlogId_devlog_id_fk" FOREIGN KEY ("devlogId") REFERENCES "public"."devlog"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | |
| ALTER TABLE "ai_devlog_review" ADD CONSTRAINT "ai_devlog_review_projectId_project_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."project"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | |
| ALTER TABLE "ai_project_review" ADD CONSTRAINT "ai_project_review_projectId_project_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."project"("id") ON DELETE no action ON UPDATE no action; |
| if (REVIEWABLE_STATUSES.has(queriedProject.project.status)) { | ||
| try { | ||
| const ensured = await ensureAiReviewsForProject({ | ||
| project: { | ||
| id: queriedProject.project.id, | ||
| name: queriedProject.project.name, | ||
| description: queriedProject.project.description, | ||
| status: queriedProject.project.status, | ||
| updatedAt: queriedProject.project.updatedAt | ||
| }, | ||
| devlogs: devlogs.map((log) => ({ | ||
| ...log, | ||
| s3PublicUrl: env.S3_PUBLIC_URL ?? '' | ||
| })) | ||
| }); | ||
|
|
||
| latestDevlogReviews = ensured.devlogReviews; | ||
| latestProjectReview = ensured.projectReview ?? latestProjectReview; | ||
| } catch (err) { | ||
| console.error('AI review ensure failed', err); | ||
| } | ||
| } |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The AI review calls happen synchronously during page load for the admin review page, which could significantly slow down page load times if the AI API is slow or if there are many devlogs. Consider moving this to a background job or showing a loading state while reviews are being generated.
| await db.transaction(async (tx) => { | ||
| await Promise.all( | ||
| devlogReviews.map((review) => | ||
| tx | ||
| .insert(aiDevlogReview) | ||
| .values({ | ||
| devlogId: review.devlogId, | ||
| projectId: review.projectId, | ||
| approved: review.approved, | ||
| rationale: review.rationale, | ||
| prompt: review.prompt, | ||
| model: review.model | ||
| }) | ||
| .onConflictDoUpdate({ | ||
| target: aiDevlogReview.devlogId, | ||
| set: { | ||
| projectId: review.projectId, | ||
| approved: review.approved, | ||
| rationale: review.rationale, | ||
| prompt: review.prompt, | ||
| model: review.model, | ||
| updatedAt: sql`now()` | ||
| } | ||
| }) | ||
| ) | ||
| ); |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The closing parenthesis for Promise.all is missing on line 87. This causes the transaction callback to be malformed. The Promise.all should be closed before the transaction continues to insert the project review.
| devlogReviews: Pick<AiDevlogReview, 'devlogId' | 'projectId' | 'approved' | 'rationale' | 'prompt' | 'model'>[]; | ||
| }> { | ||
| const projectReview = await reviewProjectOverall(project, devlogs); | ||
| const devlogReviews = await Promise.all(devlogs.map(log => reviewSingleDevlog(project.id, log))); |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing arrow function syntax for the map callback. The code has 'log =>' but should be '(log) =>' for consistency with the project's coding style, though both are valid JavaScript. However, more importantly, this function processes all devlogs in parallel which could be inefficient or hit rate limits when there are many devlogs. Consider adding batching or sequential processing with delays to avoid overwhelming the AI API.
| const devlogReviews = await Promise.all(devlogs.map(log => reviewSingleDevlog(project.id, log))); | |
| const devlogReviews = []; | |
| for (const log of devlogs) { | |
| devlogReviews.push(await reviewSingleDevlog(project.id, log)); | |
| } |
| latestDevlogReviews = await db | ||
| .select() | ||
| .from(aiDevlogReview) | ||
| .where(and(eq(aiDevlogReview.projectId, queriedProject.project.id), inArray(aiDevlogReview.devlogId, devlogs.map((log) => log.id)))); |
Copilot
AI
Dec 14, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inArray query on line 86 will fail if devlogs is an empty array. Most SQL databases require at least one value in an IN clause. This code should check if devlogs.length is greater than 0 before executing this query, or handle the empty array case separately.
| latestDevlogReviews = await db | |
| .select() | |
| .from(aiDevlogReview) | |
| .where(and(eq(aiDevlogReview.projectId, queriedProject.project.id), inArray(aiDevlogReview.devlogId, devlogs.map((log) => log.id)))); | |
| if (devlogs.length > 0) { | |
| latestDevlogReviews = await db | |
| .select() | |
| .from(aiDevlogReview) | |
| .where(and(eq(aiDevlogReview.projectId, queriedProject.project.id), inArray(aiDevlogReview.devlogId, devlogs.map((log) => log.id)))); | |
| } else { | |
| latestDevlogReviews = []; | |
| } |
It does require some changes on server side: