From dce56321611cf2dc0902c8d4450fdcd2175cce19 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Jan 2026 15:16:49 +0000 Subject: [PATCH] feat: add Answer agent for automatic post answer generation - Add answer jsonb column to posts table with migration - Create answer-agent.ts with SetAnswer/SetNoAnswer tools using Haiku 4.5 - Trigger answer agent on post creation and new comments - Skip re-running if post marked as "not-a-question" - Add PostAnswerBox UI component shown after root comment - Pass answer through CommentThreadClient to CommentThread --- agent/answer-agent.ts | 119 ++ agent/types.ts | 6 + .../[postNumber]/comment-thread-client.tsx | 4 + .../[repo]/[postNumber]/comment-thread.tsx | 101 +- app/[owner]/[repo]/[postNumber]/page.tsx | 2 + .../[repo]/[postNumber]/post-answer.tsx | 26 + components/copy-markdown-button.tsx | 6 +- lib/actions/posts.ts | 29 +- lib/db/migrations/0013_supreme_warlock.sql | 1 + lib/db/migrations/meta/0013_snapshot.json | 1038 +++++++++++++++++ lib/db/migrations/meta/_journal.json | 9 +- lib/db/schema.ts | 4 +- 12 files changed, 1291 insertions(+), 54 deletions(-) create mode 100644 agent/answer-agent.ts create mode 100644 app/[owner]/[repo]/[postNumber]/post-answer.tsx create mode 100644 lib/db/migrations/0013_supreme_warlock.sql create mode 100644 lib/db/migrations/meta/0013_snapshot.json diff --git a/agent/answer-agent.ts b/agent/answer-agent.ts new file mode 100644 index 0000000..4d3e1e9 --- /dev/null +++ b/agent/answer-agent.ts @@ -0,0 +1,119 @@ +import { stepCountIs, streamText, tool } from "ai" +import { eq } from "drizzle-orm" +import { updateTag } from "next/cache" +import { z } from "zod" +import { db } from "@/lib/db/client" +import { posts } from "@/lib/db/schema" +import { getSiteOrigin } from "@/lib/utils" +import type { NoAnswerReason, PostAnswer } from "./types" + +export async function runAnswerAgent({ + postId, + owner, + repo, + postNumber, + currentAnswer, +}: { + postId: string + owner: string + repo: string + postNumber: number + currentAnswer?: PostAnswer | null +}) { + const result: { answer?: PostAnswer } = {} + + const llmsTxtUrl = `${getSiteOrigin()}/${owner}/${repo}/${postNumber}/llms.txt` + + let transcript: string + try { + const res = await fetch(llmsTxtUrl) + if (!res.ok) { + console.error("Failed to fetch llms.txt:", res.status) + return + } + transcript = await res.text() + } catch (err) { + console.error("Failed to fetch llms.txt:", err) + return + } + + const currentAnswerContext = currentAnswer + ? currentAnswer.type === "answer" + ? `\n\nYour previous answer was:\n${currentAnswer.text}\n\nReview if this is still accurate given any new comments.` + : `\n\nYou previously determined this was not a question (reason: ${currentAnswer.reason}).` + : "" + + const systemPrompt = `You are a forum answer assistant. Your job is to analyze a forum post transcript and determine the best answer to the original question (the first comment in the post). + +Rules: +- Focus on answering the ORIGINAL question (first comment) - the post may have evolved but we want to answer what was initially asked +- If the post contains a clear answer to the original question (from any participant), use SetAnswer with a concise summary +- If the post is NOT a question (it's an announcement, discussion, or purely informational), use SetNoAnswer with reason "not-a-question" +- If the question is unclear or cannot be answered from the available context, use SetNoAnswer with reason "unclear" or "needs-more-context" +- Be extremely concise in your answer - summarize the key points without unnecessary verbosity +- Only call ONE tool: either SetAnswer OR SetNoAnswer${currentAnswerContext}` + + const tools: Parameters[0]["tools"] = { + SetAnswer: tool({ + description: + "Set the answer to the original question. Use when a clear answer exists in the transcript.", + inputSchema: z.object({ + text: z + .string() + .describe( + "Concise answer to the original question, summarizing the key points" + ), + }), + // biome-ignore lint/suspicious/useAwait: . + execute: async (params) => { + result.answer = { + type: "answer", + text: params.text, + updatedAt: Date.now(), + } + return { ok: true } + }, + }), + SetNoAnswer: tool({ + description: + "Indicate that no answer can be provided. Use when the post is not a question or lacks enough context.", + inputSchema: z.object({ + reason: z + .enum(["not-a-question", "unclear", "needs-more-context"]) + .describe("Why no answer can be provided"), + }), + // biome-ignore lint/suspicious/useAwait: . + execute: async (params) => { + result.answer = { + type: "no-answer", + reason: params.reason as NoAnswerReason, + updatedAt: Date.now(), + } + return { ok: true } + }, + }), + } + + const stream = streamText({ + model: "anthropic/claude-haiku-4.5", + system: systemPrompt, + prompt: `Here's the full post transcript:\n\n${transcript}`, + tools, + stopWhen: stepCountIs(3), + }) + + await stream.finishReason + + if (!result.answer) { + console.log("Answer agent did not produce an answer") + return + } + + await db + .update(posts) + .set({ answer: result.answer, updatedAt: Date.now() }) + .where(eq(posts.id, postId)) + + updateTag(`repo:${owner}:${repo}`) + updateTag(`post:${postId}`) +} diff --git a/agent/types.ts b/agent/types.ts index 95c4aaa..1c1f3a9 100644 --- a/agent/types.ts +++ b/agent/types.ts @@ -9,3 +9,9 @@ export type GitContextData = { message: string date: string } + +export type NoAnswerReason = "not-a-question" | "unclear" | "needs-more-context" + +export type PostAnswer = + | { type: "answer"; text: string; updatedAt: number } + | { type: "no-answer"; reason: NoAnswerReason; updatedAt: number } diff --git a/app/[owner]/[repo]/[postNumber]/comment-thread-client.tsx b/app/[owner]/[repo]/[postNumber]/comment-thread-client.tsx index ff202b2..26166ef 100644 --- a/app/[owner]/[repo]/[postNumber]/comment-thread-client.tsx +++ b/app/[owner]/[repo]/[postNumber]/comment-thread-client.tsx @@ -2,6 +2,7 @@ import type { InferSelectModel } from "drizzle-orm" import { useEffect, useMemo, useState } from "react" +import type { PostAnswer } from "@/agent/types" import { authClient } from "@/lib/auth-client" import type { comments as commentsSchema, @@ -39,6 +40,7 @@ export function CommentThreadClient({ rootCommentId, commentNumbers, askingOptions, + answer, }: { owner: string repo: string @@ -49,6 +51,7 @@ export function CommentThreadClient({ rootCommentId: string | null commentNumbers: Map askingOptions: AskingOption[] + answer?: PostAnswer | null }) { const [replyingToId, setReplyingToId] = useState(null) const isSignedIn = !!authClient.useSession().data?.session @@ -85,6 +88,7 @@ export function CommentThreadClient({ return ( {comment.streamStatus !== "streaming" && ( - + )} void onCancelReply?: () => void askingOptions: ComposerProps["options"]["asking"] + answer?: PostAnswer | null }) { const reactionsByComment: Record = {} for (const reaction of reactions) { @@ -283,51 +284,59 @@ export function CommentThread({ const replies = repliesByThread.get(comment.id) ?? [] const hasReplies = replies.length > 0 + const showAnswer = isRootComment && answer?.type === "answer" + return ( - - {hasReplies && ( -
- {replies.map((reply) => { - const replyAuthor = authorsById[reply.authorId] - if (!replyAuthor) { - return null - } - const replyNumber = commentNumbers.get(reply.id) ?? "?" - return ( - - ) - })} +
+ + {hasReplies && ( +
+ {replies.map((reply) => { + const replyAuthor = authorsById[reply.authorId] + if (!replyAuthor) { + return null + } + const replyNumber = commentNumbers.get(reply.id) ?? "?" + return ( + + ) + })} +
+ )} +
+ {showAnswer && ( +
+
)} - +
) })}
diff --git a/app/[owner]/[repo]/[postNumber]/page.tsx b/app/[owner]/[repo]/[postNumber]/page.tsx index df08646..0859059 100644 --- a/app/[owner]/[repo]/[postNumber]/page.tsx +++ b/app/[owner]/[repo]/[postNumber]/page.tsx @@ -157,6 +157,7 @@ export default async function PostPage({ createdAt: posts.createdAt, updatedAt: posts.updatedAt, gitContexts: posts.gitContexts, + answer: posts.answer, category: { id: categories.id, title: categories.title, @@ -343,6 +344,7 @@ export default async function PostPage({
+
+ + Answer + +
+
+ {answer.text} +
+
+ ) +} diff --git a/components/copy-markdown-button.tsx b/components/copy-markdown-button.tsx index 1fd712f..00516bb 100644 --- a/components/copy-markdown-button.tsx +++ b/components/copy-markdown-button.tsx @@ -28,11 +28,7 @@ function convertMessagesToMarkdown(messages: AgentUIMessage[]): string { .join("\n\n") } -export function CopyMarkdownButton({ - content, -}: { - content: AgentUIMessage[] -}) { +export function CopyMarkdownButton({ content }: { content: AgentUIMessage[] }) { const [isCopied, setIsCopied] = useState(false) const Icon = isCopied ? CheckIcon : FileTextIcon diff --git a/lib/actions/posts.ts b/lib/actions/posts.ts index f96ddae..97a3a04 100644 --- a/lib/actions/posts.ts +++ b/lib/actions/posts.ts @@ -5,9 +5,10 @@ import { updateTag } from "next/cache" import { headers } from "next/headers" import slugify from "slugify" import { getRun, start } from "workflow/api" +import { runAnswerAgent } from "@/agent/answer-agent" import { runCategoryAgent } from "@/agent/category-agent" import { responseAgent } from "@/agent/response-agent" -import type { AgentUIMessage } from "@/agent/types" +import type { AgentUIMessage, PostAnswer } from "@/agent/types" import { auth, extractGitHubUserId, @@ -337,6 +338,15 @@ export async function createPost(data: { waitUntil(indexRepo(data.owner, data.repo)) + waitUntil( + runAnswerAgent({ + postId, + owner: data.owner, + repo: data.repo, + postNumber: newPost.number, + }) + ) + updateTag(`repo:${data.owner}:${data.repo}`) updateTag(`post:${postId}`) if (authorUsername) { @@ -510,6 +520,23 @@ export async function createComment(data: { }) ) + const currentAnswer = post.answer as PostAnswer | null | undefined + const skipAnswerAgent = + currentAnswer?.type === "no-answer" && + currentAnswer.reason === "not-a-question" + + if (!skipAnswerAgent) { + waitUntil( + runAnswerAgent({ + postId: data.postId, + owner: post.owner, + repo: post.repo, + postNumber: post.number, + currentAnswer, + }) + ) + } + updateTag(`repo:${post.owner}:${post.repo}`) updateTag(`post:${post.id}`) if (authorUsername) { diff --git a/lib/db/migrations/0013_supreme_warlock.sql b/lib/db/migrations/0013_supreme_warlock.sql new file mode 100644 index 0000000..8011406 --- /dev/null +++ b/lib/db/migrations/0013_supreme_warlock.sql @@ -0,0 +1 @@ +ALTER TABLE "posts" ADD COLUMN "answer" jsonb; \ No newline at end of file diff --git a/lib/db/migrations/meta/0013_snapshot.json b/lib/db/migrations/meta/0013_snapshot.json new file mode 100644 index 0000000..22af24f --- /dev/null +++ b/lib/db/migrations/meta/0013_snapshot.json @@ -0,0 +1,1038 @@ +{ + "id": "c860db46-cd20-494d-83b7-ce7345c7fa2f", + "prevId": "cc74da2e-1b4d-4bbf-b2df-523257986d63", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.categories": { + "name": "categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(32)", + "primaryKey": true, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_categories_owner_repo_title": { + "name": "idx_categories_owner_repo_title", + "columns": [ + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(32)", + "primaryKey": true, + "notNull": true + }, + "post_id": { + "name": "post_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "thread_comment_id": { + "name": "thread_comment_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "author_username": { + "name": "author_username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "seeking_answer_from": { + "name": "seeking_answer_from", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "stream_status": { + "name": "stream_status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false, + "default": "'idle'" + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "git_ref": { + "name": "git_ref", + "type": "varchar(40)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_comments_post_created": { + "name": "idx_comments_post_created", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_comments_thread": { + "name": "idx_comments_thread", + "columns": [ + { + "expression": "thread_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_comments_author": { + "name": "idx_comments_author", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_comments_stream": { + "name": "idx_comments_stream", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.llm_users": { + "name": "llm_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(32)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "billing_category": { + "name": "billing_category", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "provider": { + "name": "provider", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "is_in_model_picker": { + "name": "is_in_model_picker", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "deprecated_at": { + "name": "deprecated_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mentions": { + "name": "mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(32)", + "primaryKey": true, + "notNull": true + }, + "target_post_id": { + "name": "target_post_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "source_post_id": { + "name": "source_post_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "source_comment_id": { + "name": "source_comment_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "source_post_number": { + "name": "source_post_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_post_title": { + "name": "source_post_title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "source_post_owner": { + "name": "source_post_owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_post_repo": { + "name": "source_post_repo", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "author_username": { + "name": "author_username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_mentions_target": { + "name": "idx_mentions_target", + "columns": [ + { + "expression": "target_post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_mentions_unique": { + "name": "idx_mentions_unique", + "columns": [ + { + "expression": "target_post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_counters": { + "name": "post_counters", + "schema": "", + "columns": { + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "last_number": { + "name": "last_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "post_counters_owner_repo_pk": { + "name": "post_counters_owner_repo_pk", + "columns": [ + "owner", + "repo" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.posts": { + "name": "posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(32)", + "primaryKey": true, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "repo": { + "name": "repo", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "git_contexts": { + "name": "git_contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "root_comment_id": { + "name": "root_comment_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "answer": { + "name": "answer", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_posts_owner_repo_number": { + "name": "idx_posts_owner_repo_number", + "columns": [ + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_posts_owner_repo": { + "name": "idx_posts_owner_repo", + "columns": [ + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repo", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_posts_author": { + "name": "idx_posts_author", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reactions": { + "name": "reactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(32)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "comment_id": { + "name": "comment_id", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_reactions_unique": { + "name": "idx_reactions_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_reactions_comment": { + "name": "idx_reactions_comment", + "columns": [ + { + "expression": "comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index 85e0203..1933191 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1736358000000, "tag": "0012_rename_billing_category_premium_to_pro", "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1768317365712, + "tag": "0013_supreme_warlock", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/lib/db/schema.ts b/lib/db/schema.ts index b17d68a..dc00108 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -1,5 +1,5 @@ import * as p from "drizzle-orm/pg-core" -import type { AgentUIMessage, GitContextData } from "@/agent/types" +import type { AgentUIMessage, GitContextData, PostAnswer } from "@/agent/types" export const posts = p.pgTable( "posts", @@ -16,6 +16,8 @@ export const posts = p.pgTable( authorId: p.varchar("author_id", { length: 255 }).notNull(), + answer: p.jsonb().$type(), + createdAt: p.bigint("created_at", { mode: "number" }).notNull(), updatedAt: p.bigint("updated_at", { mode: "number" }).notNull(), },