From 823df42f8ee899162adc9cc9cbf4f826b3f4589f Mon Sep 17 00:00:00 2001 From: Mira Date: Fri, 17 Oct 2025 14:49:36 +1100 Subject: [PATCH 1/2] restruture the doc outline and add ai project --- app/api/comments/route.ts | 76 +++++++++ app/components/SiteComments.tsx | 153 ++++++++++++++++++ .../CommunityShare/Geek/raspberry-guide.md | 2 +- .../Language/pte-intro.md | 0 app/docs/ai-projects/index.mdx | 21 +++ app/docs/ai-projects/multimodal-rl/index.mdx | 81 ++++++++++ app/docs/ai/MoE/moe-update.md | 15 +- .../cpp_backend/easy_compile/2_base_gcc.md | 2 +- .../frontend/frontend-learning/index.mdx | 0 .../{ => computer-science}/frontend/index.mdx | 2 +- lib/db.ts | 31 ++++ 11 files changed, 373 insertions(+), 10 deletions(-) create mode 100644 app/api/comments/route.ts create mode 100644 app/components/SiteComments.tsx rename app/docs/{ => CommunityShare}/Language/pte-intro.md (100%) create mode 100644 app/docs/ai-projects/index.mdx create mode 100644 app/docs/ai-projects/multimodal-rl/index.mdx rename app/docs/{ => computer-science}/frontend/frontend-learning/index.mdx (100%) rename app/docs/{ => computer-science}/frontend/index.mdx (97%) create mode 100644 lib/db.ts diff --git a/app/api/comments/route.ts b/app/api/comments/route.ts new file mode 100644 index 0000000..0f715be --- /dev/null +++ b/app/api/comments/route.ts @@ -0,0 +1,76 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/auth"; +import { getDb, ensureCommentsTable } from "@/lib/db"; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const docId = searchParams.get("docId"); + if (!docId) { + return NextResponse.json( + { ok: false, error: "missing_docId" }, + { status: 400 }, + ); + } + const db = getDb(); + if (!db) { + return NextResponse.json( + { ok: false, error: "db_missing" }, + { status: 503 }, + ); + } + await ensureCommentsTable(); + const { rows } = await db.query( + `SELECT id, doc_id, content, user_id, user_name, user_image, parent_id, created_at + FROM comments WHERE doc_id = $1 ORDER BY created_at DESC LIMIT 200`, + [docId], + ); + return NextResponse.json({ ok: true, data: rows }); +} + +export async function POST(req: Request) { + const session = await auth(); + if (!session?.user) { + return NextResponse.json( + { ok: false, error: "unauthorized" }, + { status: 401 }, + ); + } + const db = getDb(); + if (!db) { + return NextResponse.json( + { ok: false, error: "db_missing" }, + { status: 503 }, + ); + } + const body = (await req.json().catch(() => null)) as { + docId?: string; + content?: string; + parentId?: number | null; + } | null; + const docId = body?.docId?.trim(); + const content = body?.content?.trim(); + const parentId = body?.parentId ?? null; + if (!docId || !content) { + return NextResponse.json( + { ok: false, error: "missing_fields" }, + { status: 400 }, + ); + } + if (content.length > 4000) { + return NextResponse.json( + { ok: false, error: "content_too_long" }, + { status: 400 }, + ); + } + await ensureCommentsTable(); + const userId = (session.user as any).id ?? "unknown"; + const userName = session.user.name ?? null; + const userImage = session.user.image ?? null; + const { rows } = await db.query( + `INSERT INTO comments (doc_id, content, user_id, user_name, user_image, parent_id) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id, doc_id, content, user_id, user_name, user_image, parent_id, created_at`, + [docId, content, String(userId), userName, userImage, parentId], + ); + return NextResponse.json({ ok: true, data: rows[0] }, { status: 201 }); +} diff --git a/app/components/SiteComments.tsx b/app/components/SiteComments.tsx new file mode 100644 index 0000000..d996037 --- /dev/null +++ b/app/components/SiteComments.tsx @@ -0,0 +1,153 @@ +"use client"; + +import * as React from "react"; + +interface SiteCommentsProps { + docId: string; + user?: { name?: string | null; image?: string | null } | null; +} + +type Comment = { + id: number; + doc_id: string; + content: string; + user_id: string; + user_name: string | null; + user_image: string | null; + parent_id: number | null; + created_at: string; +}; + +export default function SiteComments({ docId, user }: SiteCommentsProps) { + const [comments, setComments] = React.useState([]); + const [loading, setLoading] = React.useState(true); + const [content, setContent] = React.useState(""); + const [submitting, setSubmitting] = React.useState(false); + const [error, setError] = React.useState(null); + + const fetchComments = React.useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await fetch( + `/api/comments?docId=${encodeURIComponent(docId)}`, + { + cache: "no-store", + }, + ); + const data = await res.json(); + if (!data.ok) throw new Error(data.error || "加载失败"); + setComments(data.data as Comment[]); + } catch (e: any) { + setError(e?.message || "加载失败"); + } finally { + setLoading(false); + } + }, [docId]); + + React.useEffect(() => { + fetchComments(); + }, [fetchComments]); + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + const text = content.trim(); + if (!text) return; + setSubmitting(true); + setError(null); + try { + const res = await fetch(`/api/comments`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ docId, content: text }), + }); + const data = await res.json(); + if (!res.ok || !data.ok) throw new Error(data.error || "提交失败"); + setContent(""); + // 直接插入到顶部 + setComments((prev) => [data.data as Comment, ...prev]); + } catch (e: any) { + setError(e?.message || "提交失败"); + } finally { + setSubmitting(false); + } + } + + return ( +
+
+
+ {user?.image ? ( + // eslint-disable-next-line @next/next/no-img-element + avatar + ) : ( +
+ )} + + {user?.name || "已登录用户"} + +
+