diff --git a/.husky/pre-commit b/.husky/pre-commit index 6cf9623d..9e6e5ef9 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,9 +1,6 @@ # 1) 将 /images/* 文章图片就近复制并更新引用 pnpm migrate:images || exit 1 -# 将迁移后的变更加入暂存,确保本次提交包含更新 -git add -A - # 2) 校验图片路径与命名(不合规则阻止提交) pnpm lint:images || exit 1 diff --git a/app/api/analytics/route.ts b/app/api/analytics/route.ts new file mode 100644 index 00000000..3cc785bb --- /dev/null +++ b/app/api/analytics/route.ts @@ -0,0 +1,27 @@ +import { prisma } from "@/lib/db"; + +export async function POST(req: Request) { + try { + const { eventType, eventData, userId } = await req.json(); + + if (!eventType) { + return Response.json( + { error: "Event type is required" }, + { status: 400 }, + ); + } + + await prisma.analyticsEvent.create({ + data: { + eventType, + eventData: eventData ?? {}, + userId: userId ? parseInt(String(userId)) : null, + }, + }); + + return Response.json({ success: true }); + } catch (error) { + console.error("Analytics API error:", error); + return Response.json({ error: "Failed to log event" }, { status: 500 }); + } +} diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index ed2ba00d..7a6ecb45 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,6 +1,10 @@ +import { prisma } from "@/lib/db"; import { streamText, UIMessage, convertToModelMessages } from "ai"; import { getModel, requiresApiKey, type AIProvider } from "@/lib/ai/models"; import { buildSystemMessage } from "@/lib/ai/prompt"; +import { source } from "@/lib/source"; +import fs from "fs/promises"; +import path from "path"; // 流式响应最长30秒 export const maxDuration = 30; @@ -16,6 +20,7 @@ interface ChatRequest { }; provider?: AIProvider; apiKey?: string; + chatId?: string; } export async function POST(req: Request) { @@ -26,6 +31,7 @@ export async function POST(req: Request) { pageContext, provider = "intern", // 默认使用书生模型 apiKey, + chatId, }: ChatRequest = await req.json(); // 对指定Provider验证key是否存在 @@ -39,17 +45,82 @@ export async function POST(req: Request) { ); } + // 如果有 slug 但没有 content,尝试在服务端读取内容 + if (pageContext?.slug && !pageContext.content) { + try { + const slugArray = pageContext.slug.split("/"); + const page = source.getPage(slugArray); + + if (page) { + const fullFilePath = path.join(process.cwd(), "app/docs", page.path); + const rawContent = await fs.readFile(fullFilePath, "utf-8"); + pageContext.content = extractTextFromMDX(rawContent); + } + } catch (error) { + console.warn( + "Failed to fetch content for slug:", + pageContext.slug, + error, + ); + // 出错时不中断,只是缺少上下文 + } + } + // 构建系统消息,包含页面上下文 const systemMessage = buildSystemMessage(system, pageContext); // 根据Provider获取 AI 模型实例 const model = getModel(provider, apiKey); + // 确保有 chatId (如果前端没传,就生成一个临时的,虽然这会导致每次请求都是新会话) + // 理想情况是前端应该维护 chatId + const effectiveChatId = chatId || crypto.randomUUID(); + // 生成流式响应 const result = streamText({ model: model, system: systemMessage, messages: convertToModelMessages(messages), + onFinish: async ({ text }) => { + try { + // 1. 保存/更新会话 + await prisma.chat.upsert({ + where: { id: effectiveChatId }, + update: { updatedAt: new Date() }, + create: { id: effectiveChatId }, + }); + + // 2. 保存用户消息 (取最后一条) + // AI SDK v5 中,UIMessage 不再有 content 字段,内容在 parts 数组中 + const lastUserMessage = messages[messages.length - 1]; + if (lastUserMessage && lastUserMessage.role === "user") { + // 从 parts 数组中提取所有文本内容并拼接 + const userContent = lastUserMessage.parts + .filter((part) => part.type === "text") + .map((part) => (part as { type: "text"; text: string }).text) + .join("\n"); + + await prisma.message.create({ + data: { + chatId: effectiveChatId, + role: "user", + content: userContent, + }, + }); + } + + // 3. 保存 AI 回复 + await prisma.message.create({ + data: { + chatId: effectiveChatId, + role: "assistant", + content: text, + }, + }); + } catch (error) { + console.error("Failed to save chat history:", error); + } + }, }); return result.toUIMessageStreamResponse(); @@ -67,3 +138,28 @@ export async function POST(req: Request) { ); } } + +// 提取纯文本内容,过滤掉 MDX 语法 +function extractTextFromMDX(content: string): string { + let text = content + .replace(/^---[\s\S]*?---/m, "") // 移除头部元数据 (frontmatter) + .replace(/```[\s\S]*?```/g, "") // 移除代码块 + .replace(/`([^`]+)`/g, "$1"); // 移除内联代码符号,保留内容 + + // 递归移除 HTML/MDX 标签,防止嵌套标签清理不干净 + let prevText; + do { + prevText = text; + text = text.replace(/<[^>]+>/g, ""); + } while (text !== prevText); + + return text + .replace(/\*\*([^*]+)\*\*/g, "$1") // 移除粗体符号,保留文字 + .replace(/\*([^*]+)\*/g, "$1") // 移除斜体符号,保留文字 + .replace(/#{1,6}\s+/g, "") // 移除标题符号 (#) + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // 移除链接语法,仅保留链接文本 + .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1") // 移除图片语法,保留 alt 文本 + .replace(/[#*`()[!\]!]/g, "") // 移除剩余的常用 Markdown 符号 + .replace(/\n{2,}/g, "\n") // 规范化换行,将多余的空行合并 + .trim(); +} diff --git a/app/api/suggestions/route.ts b/app/api/suggestions/route.ts new file mode 100644 index 00000000..c2eae766 --- /dev/null +++ b/app/api/suggestions/route.ts @@ -0,0 +1,165 @@ +import { generateText } from "ai"; +import { getModel, requiresApiKey, type AIProvider } from "@/lib/ai/models"; +import { createGlmFlashModel } from "@/lib/ai/providers/glm"; + +// 允许流式响应最长30秒 +export const maxDuration = 30; + +import type { UIMessage, TextUIPart } from "ai"; + +interface SuggestionsRequest { + messages: UIMessage[]; + pageContext?: { + title?: string; + description?: string; + slug?: string; + }; + provider?: AIProvider; + apiKey?: string; +} + +export async function POST(req: Request) { + try { + const { + messages, + pageContext, + provider = "intern", + apiKey, + }: SuggestionsRequest = await req.json(); + + // 如果需要,验证 API 密钥 + if (requiresApiKey(provider) && (!apiKey || apiKey.trim() === "")) { + return Response.json( + { error: "需要 API 密钥 (API key is required)" }, + { status: 400 }, + ); + } + + // 模型选择策略: + // - 若用户选了自己的 Provider(openai/gemini),用用户的模型 + // - 否则(默认 intern)优先用 GLM-4-Flash(免费且快速),若 ZHIPU_API_KEY 未配置则回退到 intern + let model; + if (provider !== "intern") { + // 用户自选模型(openai / gemini) + model = getModel(provider, apiKey); + } else if (process.env.ZHIPU_API_KEY) { + // 默认使用智谱 GLM-4-Flash(免费轻量) + model = createGlmFlashModel(); + } else { + // 兜底:仍使用 intern + model = getModel("intern"); + } + + const isWelcomeRequest = messages.length === 0; + + let prompt = ""; + if (isWelcomeRequest) { + // 欢迎页面的初始动态建议 + const contextInfo = [ + pageContext?.title ? `标题: ${pageContext.title}` : "", + pageContext?.description ? `描述: ${pageContext.description}` : "", + ] + .filter(Boolean) + .join("\n"); + + prompt = `请根据以下当前页面的上下文信息,生成4个引导新手用户提问的建议框内容。\n\n上下文:\n${contextInfo || "未知页面"}\n\n只返回纯JSON数组,包含4个对象,格式如:\n[{"title":"总结本文","label":"内容要点","action":"请帮我总结一下文章主要内容"}]\n其中title简短明确,label为右上角浅色标签,action为点击后自动发送的提问语句。`; + } else { + // 普通的跟进提问建议 + // 只取最后一条用户消息,减少 token 消耗 + const lastUserMsg = messages + .filter((m) => m.role === "user") + .slice(-1)[0]; + const lastText = + (Array.isArray(lastUserMsg?.parts) + ? lastUserMsg.parts + .filter((p): p is TextUIPart => p.type === "text") + .map((p) => p.text) + .join(" ") + : (lastUserMsg as unknown as { content?: string })?.content) ?? ""; + + // 语言检测:简单判断是否包含中文字符 + const isChinese = /[\u4e00-\u9fa5]/.test(lastText); + + prompt = isChinese + ? `用户问:"${lastText}"。给出3个简短中文追问(每个不超过15字),直接返回JSON数组,例如:["问题1","问题2","问题3"]` + : `User asked: "${lastText}". Suggest 3 short follow-up questions (max 10 words each). Return a JSON array only, e.g. ["Q1","Q2","Q3"]`; + } + + const { text } = await generateText({ + model, + prompt, + }); + + let questions: unknown[] = []; + try { + // 尝试解析 JSON + // 清理可能存在的 Markdown 代码块标记 + let cleanedText = text + .replace(/```json/gi, "") + .replace(/```/g, "") + .trim(); + + // 修复大模型可能生成的中文引号 + cleanedText = cleanedText.replace(/“/g, '"').replace(/”/g, '"'); + + // 尝试仅提取数组部分,防止 AI 返回了前缀描述文本 + const arrayMatch = cleanedText.match(/\[[\s\S]*\]/); + if (arrayMatch) { + cleanedText = arrayMatch[0]; + } + + questions = JSON.parse(cleanedText); + } catch (e) { + console.error("解析建议 JSON 失败:", e, "原始文本:", text); + + if (isWelcomeRequest) { + // 把报错原因和原始文本暴露出来方便我调试 + return Response.json({ + questions: [], + debugError: String(e), + debugText: text, + }); + } else { + // 如果解析失败,尝试通过正则提取引号中的内容(兼容中英文引号) + const fallbackMatches = text.match(/(?:["“])([^"”]+)(?:["”])/g); + if (fallbackMatches && fallbackMatches.length > 0) { + questions = fallbackMatches + .map((m) => m.replace(/["“”]/g, "").trim()) + .filter((line) => line.length > 0); + } else { + // 如果连引号都没有,尝试按行分割兜底 + questions = text + .split("\n") + .map((line) => + line + .replace(/^\d+\.\s*/, "") + .replace(/[`"“”]/g, "") + .trim(), + ) + .filter( + (line) => + line.length > 0 && + !line.startsWith("json") && + !line.startsWith("[") && + !line.startsWith("]"), + ); + } + } + } + + // 确保返回的是数组 + if (!Array.isArray(questions)) { + questions = []; + } + + // 对于跟进建议最多返回 3 个,对于欢迎建议最多返回 4 个 + const maxCount = isWelcomeRequest ? 4 : 3; + return Response.json({ questions: questions.slice(0, maxCount) }); + } catch (error) { + console.error("建议 API 错误 (Suggestions API error):", error); + return Response.json( + { error: "无法生成建议 (Failed to generate suggestions)" }, + { status: 500 }, + ); + } +} diff --git a/app/components/CopyTracking.tsx b/app/components/CopyTracking.tsx new file mode 100644 index 00000000..42a5f1d1 --- /dev/null +++ b/app/components/CopyTracking.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useEffect } from "react"; + +export function CopyTracking() { + useEffect(() => { + // 监听全局 Copy 事件 + const handleCopy = () => { + const selection = window.getSelection(); + if (!selection || selection.isCollapsed) return; + + const text = selection.toString(); + if (!text) return; + + // Determine if it's code or text + let type = "text"; + const anchorNode = selection.anchorNode; + const focusNode = selection.focusNode; + + const isCode = (node: Node | null) => { + let current = node; + while (current) { + if ( + current.nodeName === "PRE" || + current.nodeName === "CODE" || + (current instanceof Element && + (current.classList.contains("code-block") || + current.tagName === "PRE" || + current.tagName === "CODE")) + ) { + return true; + } + current = current.parentNode; + } + return false; + }; + + if (isCode(anchorNode) || isCode(focusNode)) { + // 判断选中节点是否包含在
 标签内,区分代码块复制
+        type = "code";
+      }
+
+      // Umami 埋点: 记录复制行为,区分文本/代码类型和复制长度
+      if (window.umami) {
+        window.umami.track("content_copy", {
+          type,
+          content_length: text.length,
+        });
+      }
+    };
+
+    document.addEventListener("copy", handleCopy);
+    return () => document.removeEventListener("copy", handleCopy);
+  }, []);
+
+  return null;
+}
diff --git a/app/components/CustomSearchDialog.tsx b/app/components/CustomSearchDialog.tsx
index c7a4bd3e..c155a586 100644
--- a/app/components/CustomSearchDialog.tsx
+++ b/app/components/CustomSearchDialog.tsx
@@ -17,6 +17,7 @@ import {
   TagsListItem,
   type SharedProps,
 } from "fumadocs-ui/components/dialog/search";
+import { useRouter } from "next/navigation";
 
 interface TagItem {
   name: string;
@@ -34,6 +35,12 @@ interface DefaultSearchDialogProps extends SharedProps {
   allowClear?: boolean;
 }
 
+interface SearchItem {
+  url?: string;
+  onSelect?: (value: string) => void;
+  [key: string]: unknown;
+}
+
 export function CustomSearchDialog({
   defaultTag,
   tags = [],
@@ -46,7 +53,12 @@ export function CustomSearchDialog({
   ...props
 }: DefaultSearchDialogProps) {
   const { locale } = useI18n();
+  const router = useRouter();
   const [tag, setTag] = useState(defaultTag);
+
+  // Extract onOpenChange to use in dependency array cleanly
+  const { onOpenChange, ...otherProps } = props;
+
   const { search, setSearch, query } = useDocsSearch(
     type === "fetch"
       ? {
@@ -65,11 +77,12 @@ export function CustomSearchDialog({
         },
   );
 
-  // Tracking logic
+  // Tracking logic for queries
   useEffect(() => {
     if (!search) return;
 
     const timer = setTimeout(() => {
+            // Umami 埋点: 搜索结果点击
       if (window.umami) {
         window.umami.track("search_query", { query: search });
       }
@@ -88,12 +101,48 @@ export function CustomSearchDialog({
     }));
   }, [links]);
 
+  // 使用 useMemo 劫持 search items,注入埋点逻辑
+  const trackedItems = useMemo(() => {
+    const data = query.data !== "empty" && query.data ? query.data : defaultItems;
+    if (!data) return [];
+
+    return data.map((item: unknown, index: number) => {
+        const searchItem = item as SearchItem;
+        return {
+          ...searchItem,
+          onSelect: (value: string) => {
+            // Umami 埋点: 搜索结果点击
+            if (window.umami) {
+              window.umami.track("search_result_click", {
+                query: search,
+                rank: index + 1,
+                url: searchItem.url,
+              });
+            }
+
+            // Call original onSelect if it exists
+            if (searchItem.onSelect) searchItem.onSelect(value);
+
+            // Handle navigation if URL exists
+            if (searchItem.url) {
+                // 显式执行路由跳转和关闭弹窗,确保点击行为能够同时触发埋点和导航
+                router.push(searchItem.url);
+                if (onOpenChange) {
+                    onOpenChange(false);
+                }
+            }
+          },
+        };
+    });
+  }, [query.data, defaultItems, search, router, onOpenChange]);
+
   return (
     
       
       
@@ -102,11 +151,8 @@ export function CustomSearchDialog({
           
           
         
-        
+        {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
+        
       
       
         {tags.length > 0 && (
diff --git a/app/components/DocsAssistant.tsx b/app/components/DocsAssistant.tsx
index 60e2af6e..f9bdfde9 100644
--- a/app/components/DocsAssistant.tsx
+++ b/app/components/DocsAssistant.tsx
@@ -1,6 +1,6 @@
 "use client";
 
-import { useCallback, useEffect, useMemo } from "react";
+import { useCallback, useEffect, useMemo, useState, useRef } from "react";
 
 import { AssistantRuntimeProvider } from "@assistant-ui/react";
 import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk";
@@ -15,10 +15,15 @@ import {
 interface PageContext {
   title?: string;
   description?: string;
-  content?: string;
   slug?: string;
 }
 
+export interface WelcomeSuggestion {
+  title: string;
+  label: string;
+  action: string;
+}
+
 interface DocsAssistantProps {
   pageContext: PageContext;
 }
@@ -34,40 +39,247 @@ export function DocsAssistant({ pageContext }: DocsAssistantProps) {
 function DocsAssistantInner({ pageContext }: DocsAssistantProps) {
   const { provider, openaiApiKey, geminiApiKey } = useAssistantSettings();
 
+  const apiKey =
+    provider === "openai"
+      ? openaiApiKey
+      : provider === "gemini"
+        ? geminiApiKey
+        : "";
+
+  // 生成唯一的会话 ID
+  const [chatId] = useState(
+    () => `chat-${Date.now()}-${Math.random().toString(36).slice(2)}`,
+  );
+
   const transport = useMemo(
     () =>
       new DefaultChatTransport({
         api: "/api/chat",
-        body: () => {
-          const apiKey =
-            provider === "openai"
-              ? openaiApiKey
-              : provider === "gemini"
-                ? geminiApiKey
-                : ""; // intern provider doesn't need API key
-
-          return { pageContext, provider, apiKey };
+        body: {
+          pageContext,
+          provider,
+          apiKey,
+          chatId,
         },
       }),
-    [geminiApiKey, openaiApiKey, pageContext, provider],
+    [pageContext, provider, apiKey, chatId],
   );
 
+  const [suggestions, setSuggestions] = useState([]);
+  // 仅标志后台是否正在获取建议(用于逻辑判断)
+  const [isFetchingSuggestions, setIsFetchingSuggestions] = useState(false);
+  // 控制 UI 上是否显示“正在思考...”加载状态(只有主回答结束后,由于建议还在获取,才显示骨架屏)
+  const [showSuggestionsLoader, setShowSuggestionsLoader] = useState(false);
+  // 缓存获取好的建议,等待主回答结束后才推给 Thread 渲染
+  const [pendingSuggestions, setPendingSuggestions] = useState([]);
+
+  // 欢迎页建议相关的 state
+  const [welcomeSuggestions, setWelcomeSuggestions] = useState<
+    WelcomeSuggestion[]
+  >([]);
+  const [isLoadingWelcome, setIsLoadingWelcome] = useState(false);
+  const fetchedWelcomeRef = useRef(false);
+
+  // 埋点上报函数
+  const logAnalyticsEvent = useCallback(
+    async (eventType: string, eventData?: Record) => {
+      try {
+        await fetch("/api/analytics", {
+          method: "POST",
+          headers: { "Content-Type": "application/json" },
+          body: JSON.stringify({
+            eventType,
+            eventData: {
+              ...eventData,
+              chatId,
+              url: window.location.href,
+              provider,
+            },
+          }),
+        });
+      } catch (e) {
+        console.error("Failed to log analytics event:", e);
+      }
+    },
+    [chatId, provider],
+  );
+
+  // 组件挂载时上报打开事件
+  useEffect(() => {
+    logAnalyticsEvent("assistant_opened");
+  }, [logAnalyticsEvent]);
+
   const chat = useChat({
+    id: chatId,
+    // 当 Provider 或 Key 更改时强制重置聊天 (但保持 chatId 不变会不会有问题?可能需要重新生成)
+    // 这里我们暂时保留 chatId 不变,视为同一会话切换了模型
     transport,
     onFinish: () => {
-      // 当对话结束时(流式传输完成),记录一次查询行为
+      // 聊天流式传输完成后(onFinish),记录一次查询行为
       if (window.umami) {
         window.umami.track("ai_assistant_query");
       }
+      logAnalyticsEvent("message_completed");
     },
   });
 
   const {
     error: chatError,
     status: chatStatus,
+    messages,
     clearError: clearChatError,
+    // 其他需要的属性...
   } = chat;
 
+  // 初次加载欢迎建议的 Effect
+  useEffect(() => {
+    // 只有在没消息、且还没尝试获取过时才去拉取
+    if (messages.length === 0 && !fetchedWelcomeRef.current) {
+      fetchedWelcomeRef.current = true;
+      setIsLoadingWelcome(true);
+
+      (async () => {
+        try {
+          const response = await fetch("/api/suggestions", {
+            method: "POST",
+            headers: { "Content-Type": "application/json" },
+            body: JSON.stringify({
+              messages: [],
+              pageContext,
+              provider,
+              apiKey,
+            }),
+          });
+
+          if (response.ok) {
+            const data = await response.json();
+            if (data && Array.isArray(data.questions)) {
+              // 这里的 questions 实际上在欢迎时是个对象数组
+              setWelcomeSuggestions(data.questions);
+            }
+          }
+        } catch (error) {
+          console.error("获取欢迎建议失败:", error);
+        } finally {
+          setIsLoadingWelcome(false);
+        }
+      })();
+    }
+  }, [messages.length, pageContext, provider, apiKey]);
+
+  // 跟踪上一次的状态,用于检测对话结束
+  const prevStatusRef = useRef(chatStatus);
+
+  useEffect(() => {
+    const prevStatus = prevStatusRef.current;
+
+    // 当用户发送新消息时(状态从非提交/流式跳转为提交/流式状态)
+    const isNewRequest =
+      (chatStatus === "submitted" || chatStatus === "streaming") &&
+      prevStatus !== "submitted" &&
+      prevStatus !== "streaming";
+
+    if (isNewRequest) {
+      // 对话开始,清空旧建议,准备在后台预先获取新建议并行处理
+      setSuggestions([]);
+      setPendingSuggestions([]);
+      setIsFetchingSuggestions(true);
+      setShowSuggestionsLoader(false);
+
+      const currentMessages = messages;
+
+      (async () => {
+        try {
+          const response = await fetch("/api/suggestions", {
+            method: "POST",
+            headers: { "Content-Type": "application/json" },
+            body: JSON.stringify({
+              messages: currentMessages,
+              pageContext,
+              provider,
+              apiKey,
+            }),
+          });
+
+          if (response.ok) {
+            const data = await response.json();
+            if (data && Array.isArray(data.questions)) {
+              setPendingSuggestions(data.questions);
+              logAnalyticsEvent("suggestions_generated", {
+                count: data.questions.length,
+              });
+            }
+          }
+        } catch (error) {
+          console.error("获取建议失败:", error);
+        } finally {
+          setIsFetchingSuggestions(false);
+        }
+      })();
+    }
+
+    // 监控 AI 主流程是否结束:当从 'streaming' 变为 'submitted' / 结束状态时
+    const isChatFinished =
+      prevStatus === "streaming" &&
+      chatStatus !== "streaming" &&
+      chatStatus !== "submitted";
+
+    if (isChatFinished) {
+      // 如果到主回答结束时,后台的预取建议还在进行,就开始在UI显示“正在思考”
+      if (isFetchingSuggestions) {
+        setShowSuggestionsLoader(true);
+      } else {
+        // 如果当时预取就已经完成了,则直接显示收集好的预取建议
+        setSuggestions(pendingSuggestions);
+        setShowSuggestionsLoader(false);
+      }
+    }
+
+    // 最关键:更新过去的聊天状态
+    prevStatusRef.current = chatStatus;
+  }, [
+    chatStatus,
+    messages,
+    pageContext,
+    provider,
+    apiKey,
+    isFetchingSuggestions,
+    pendingSuggestions,
+    logAnalyticsEvent,
+  ]);
+
+  // 当建议获取状态或 pending 数据改变,且主回答已经不是打字状态时,更新 UI
+  useEffect(() => {
+    // 假设非正在打字且非提交中,即为主回复闲置状态
+    const isIdle = chatStatus !== "streaming" && chatStatus !== "submitted";
+
+    // 如果后台刚刚完成了预取,并且主回复已经闲置,而且存在建议可以展示
+    if (!isFetchingSuggestions && isIdle && pendingSuggestions.length > 0) {
+      // 检查当前建议是否为空且 pending 建议非空,来避免多次重复触发渲染
+      setSuggestions((prev) => {
+        if (prev.length === 0) {
+          return pendingSuggestions;
+        }
+        return prev;
+      });
+      setShowSuggestionsLoader(false);
+    }
+  }, [isFetchingSuggestions, pendingSuggestions, chatStatus]);
+
+  // 当对话状态重置或开始时清空建议
+  useEffect(() => {
+    if (chatStatus === "streaming" || chatStatus === "submitted") {
+      setSuggestions([]);
+      setShowSuggestionsLoader(false);
+    }
+  }, [chatStatus]);
+
+  // 当 Provider 更改时清除之前的错误
+  useEffect(() => {
+    clearChatError();
+  }, [provider, clearChatError]);
+
+  // 当对话状态重置时也清除错误
   useEffect(() => {
     if (chatStatus === "submitted" || chatStatus === "streaming") {
       clearChatError();
@@ -91,6 +303,10 @@ function DocsAssistantInner({ pageContext }: DocsAssistantProps) {
         errorMessage={assistantError?.message}
         showSettingsAction={assistantError?.showSettingsCTA ?? false}
         onClearError={assistantError ? handleClearError : undefined}
+        suggestions={suggestions}
+        isLoadingSuggestions={showSuggestionsLoader}
+        welcomeSuggestions={welcomeSuggestions}
+        isLoadingWelcome={isLoadingWelcome}
       />
     
   );
@@ -175,6 +391,7 @@ function deriveAssistantError(
   let showSettingsCTA = false;
 
   // For intern provider, don't show settings CTA for API key related errors
+  // 对于书生,不要显示 API 密钥相关的错误
   if (
     provider !== "intern" &&
     (statusCode === 400 ||
@@ -244,6 +461,7 @@ function extractErrorFromResponseBody(body: string): string | undefined {
     }
   } catch {
     // Ignore JSON parsing issues and fall back to the raw body text.
+    // 忽略 JSON 解析问题,回退到原始正文文本。
   }
 
   return trimmed;
diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx
index 61336dae..14e6ae1d 100644
--- a/app/components/Footer.tsx
+++ b/app/components/Footer.tsx
@@ -63,6 +63,7 @@ export function Footer() {
                   
                   
                     Mission Brief
@@ -136,6 +144,10 @@ export function Footer() {
                 
  • Network Status diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index b2831a85..45f65c9c 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -83,9 +83,10 @@ export function Hero() { + + + + ); +} diff --git a/app/components/assistant-ui/SettingsDialog.tsx b/app/components/assistant-ui/SettingsDialog.tsx index 9825faf8..a52c66b0 100644 --- a/app/components/assistant-ui/SettingsDialog.tsx +++ b/app/components/assistant-ui/SettingsDialog.tsx @@ -44,7 +44,7 @@ export const SettingsDialog = ({ return ( - + AI Assistant Settings diff --git a/app/components/assistant-ui/assistant-modal.tsx b/app/components/assistant-ui/assistant-modal.tsx index a484ad2b..68d96e0e 100644 --- a/app/components/assistant-ui/assistant-modal.tsx +++ b/app/components/assistant-ui/assistant-modal.tsx @@ -8,16 +8,26 @@ import { AssistantModalPrimitive } from "@assistant-ui/react"; import { Thread } from "@/app/components/assistant-ui/thread"; import { TooltipIconButton } from "@/app/components/assistant-ui/tooltip-icon-button"; +import { WelcomeSuggestion } from "@/app/components/DocsAssistant"; + interface AssistantModalProps { errorMessage?: string; showSettingsAction?: boolean; onClearError?: () => void; + suggestions?: string[]; + isLoadingSuggestions?: boolean; + welcomeSuggestions?: WelcomeSuggestion[]; + isLoadingWelcome?: boolean; } export const AssistantModal: FC = ({ errorMessage, showSettingsAction = false, onClearError, + suggestions, + isLoadingSuggestions, + welcomeSuggestions, + isLoadingWelcome, }) => { const [showBubble, setShowBubble] = useState(false); @@ -84,6 +94,10 @@ export const AssistantModal: FC = ({ errorMessage={errorMessage} showSettingsAction={showSettingsAction} onClearError={onClearError} + suggestions={suggestions} + isLoadingSuggestions={isLoadingSuggestions} + welcomeSuggestions={welcomeSuggestions} + isLoadingWelcome={isLoadingWelcome} /> diff --git a/app/components/assistant-ui/thread.tsx b/app/components/assistant-ui/thread.tsx index a164a273..9dac98d9 100644 --- a/app/components/assistant-ui/thread.tsx +++ b/app/components/assistant-ui/thread.tsx @@ -5,6 +5,7 @@ import { ErrorPrimitive, MessagePrimitive, ThreadPrimitive, + useMessage, } from "@assistant-ui/react"; import { AlertCircleIcon, @@ -37,17 +38,39 @@ import { cn } from "@/lib/utils"; import { LazyMotion, MotionConfig, domAnimation } from "motion/react"; import * as m from "motion/react-m"; +export interface WelcomeSuggestion { + title: string; + label: string; + action: string; +} + interface ThreadProps { + // 错误信息 errorMessage?: string; + // 是否显示设置操作按钮 showSettingsAction?: boolean; + // 清除错误的回调函数 onClearError?: () => void; + // AI 生成的后续问题建议 + suggestions?: string[]; + // 是否正在加载建议 + isLoadingSuggestions?: boolean; + // AI 生成的欢迎问题建议 + welcomeSuggestions?: WelcomeSuggestion[]; + // 是否正在加载欢迎建议 + isLoadingWelcome?: boolean; } export const Thread: FC = ({ errorMessage, showSettingsAction = false, onClearError, + suggestions, + isLoadingSuggestions, + welcomeSuggestions, + isLoadingWelcome, }) => { + // 控制设置对话框是否打开的状态 const [isSettingsOpen, setIsSettingsOpen] = useState(false); const handleSettingsChange = useCallback( @@ -60,6 +83,7 @@ export const Thread: FC = ({ [onClearError], ); + // 打开设置对话框的处理函数 const handleOpenSettings = useCallback(() => { handleSettingsChange(true); }, [handleSettingsChange]); @@ -74,8 +98,16 @@ export const Thread: FC = ({ }} > + {/* 欢迎界面,当没有消息时显示 */} + + + + {/* 消息列表,包含用户消息和 AI 消息 */} = ({ } /> ) : null} + + {/* 如果非空(有消息),显示后续建议和底部输入框 */} +
    { ); }; +// 欢迎界面组件 const ThreadWelcome: FC = () => { return ( @@ -180,7 +219,7 @@ const ThreadWelcome: FC = () => { exit={{ opacity: 0, y: 10 }} className="aui-thread-welcome-message-motion-1 text-2xl font-serif font-bold text-[var(--foreground)]" > - Hello there! + 你好! { transition={{ delay: 0.1 }} className="aui-thread-welcome-message-motion-2 text-2xl text-neutral-500 font-serif italic" > - How can I help you today? + 今天有什么可以帮你的吗?
    @@ -198,31 +237,66 @@ const ThreadWelcome: FC = () => { ); }; -const ThreadWelcomeSuggestions: FC = () => { +// 欢迎页面的初始建议组件 +interface ThreadWelcomeSuggestionsProps { + suggestions?: WelcomeSuggestion[]; + isLoading?: boolean; +} + +const ThreadWelcomeSuggestions: FC = ({ + suggestions, + isLoading, +}) => { + if (isLoading) { + return ( +
    + {/* 显示4个骨架屏来代表四条预取建议 */} + {[1, 2, 3, 4].map((i) => ( +
    +
    +
    +
    +
    +
    + ))} +
    + ); + } + + // 如果建议为空也没显示骨架屏(例如出错或加载失败),依然展示默认退避问题 + const defaultSuggestions: WelcomeSuggestion[] = [ + { + title: "总结本文", + label: "内容要点", + action: "请帮我总结一下当前页面的主要内容和要点", + }, + { + title: "什么是基座大模型", + label: "概念解释", + action: "什么是基座大模型?请详细解释一下", + }, + { + title: "解释技术概念", + label: "深入理解", + action: "请解释一下这个页面中提到的核心技术概念", + }, + { + title: "学习建议", + label: "如何入门", + action: "基于当前内容,你能给出一些学习建议和入门路径吗?", + }, + ]; + + const displaySuggestions = suggestions?.length + ? suggestions + : defaultSuggestions; + return (
    - {[ - { - title: "总结本文", - label: "内容要点", - action: "请帮我总结一下当前页面的主要内容和要点", - }, - { - title: "什么是基座大模型", - label: "概念解释", - action: "什么是基座大模型?请详细解释一下", - }, - { - title: "解释技术概念", - label: "深入理解", - action: "请解释一下这个页面中提到的核心技术概念", - }, - { - title: "学习建议", - label: "如何入门", - action: "基于当前内容,你能给出一些学习建议和入门路径吗?", - }, - ].map((suggestedAction, index) => ( + {displaySuggestions.map((suggestedAction, index) => ( { ); }; +// 输入框组件 Props interface ComposerProps { isSettingsOpen: boolean; onOpenChange: (open: boolean) => void; onClearError?: () => void; } +// 输入框组件,负责处理用户输入和发送消息 const Composer: FC = ({ isSettingsOpen, onOpenChange, @@ -290,16 +366,14 @@ const Composer: FC = ({ return (
    - - - + {/* 当没有消息时,显示空状态内容(现已经移到上面的Viewport内以统一滑动,这里可以置空或保留其它用途)*/} {!hasActiveKey && ( -
    +

    Add your {providerLabel} API key in Settings to start chatting.

    @@ -342,6 +416,7 @@ interface ComposerActionProps { onClearError?: () => void; } +// 输入框操作按钮组件(发送、设置、取消) const ComposerAction: FC = ({ canSend, isSettingsOpen, @@ -415,6 +490,57 @@ const MessageError: FC = () => { ); }; +// AI 消息组件 +// 正在思考的加载状态组件 +const ThreadThinking: FC = () => { + return ( + + + + + + ); +}; + +const AssistantMessageContent: FC = () => { + const message = useMessage() as unknown as { + status?: string | { type: string }; + content?: string | unknown[]; + }; + + const isRunning = + message.status === "in_progress" || + (typeof message.status === "object" && message.status?.type === "running"); + + const hasContent = + message.content && + (typeof message.content === "string" + ? message.content.length > 0 + : Array.isArray(message.content) + ? message.content.length > 0 + : !!message.content); + + return ( + <> + + + {/* 当正在生成且内容为空时显示加载动画 */} + {isRunning && !hasContent && ( +
    + +
    + )} + + + + ); +}; + const AssistantMessage: FC = () => { return ( @@ -422,14 +548,8 @@ const AssistantMessage: FC = () => { className="aui-assistant-message-root relative mx-auto w-full max-w-[var(--thread-max-width)] animate-in py-4 duration-200 fade-in slide-in-from-bottom-1 last:mb-24" data-role="assistant" > -
    - - +
    +
    @@ -441,6 +561,7 @@ const AssistantMessage: FC = () => { ); }; +// AI 消息操作栏(复制、刷新) const AssistantActionBar: FC = () => { return ( { ); }; +// 用户消息组件 const UserMessage: FC = () => { return ( @@ -492,6 +614,7 @@ const UserMessage: FC = () => { ); }; +// 用户消息操作栏(编辑) const UserActionBar: FC = () => { return ( { ); }; +// 编辑输入框组件(用于编辑已发送的消息) const EditComposer: FC = () => { return (
    @@ -534,6 +658,7 @@ const EditComposer: FC = () => { ); }; +// 分支切换组件(用于在多次生成的回复之间切换) const BranchPicker: FC = ({ className, ...rest @@ -563,3 +688,53 @@ const BranchPicker: FC = ({ ); }; + +// 后续问题建议组件 Props +interface ThreadFollowupSuggestionsProps { + suggestions?: string[]; + isLoading?: boolean; +} + +// 后续问题建议组件,显示 AI 生成的建议问题 +const ThreadFollowupSuggestions: FC = ({ + suggestions, + isLoading, +}) => { + if (isLoading) { + return ( +
    +
    + 正在思考后续问题... +
    +
    + ); + } + + if (!suggestions || suggestions.length === 0) return null; + + return ( +
    +
    + 建议提问 +
    +
    + {suggestions.map((suggestion, index) => ( + + + + ))} +
    +
    + ); +}; diff --git a/app/components/float-window/FloatWindow.tsx b/app/components/float-window/FloatWindow.tsx index 4b4cc29d..31cda913 100644 --- a/app/components/float-window/FloatWindow.tsx +++ b/app/components/float-window/FloatWindow.tsx @@ -1,8 +1,9 @@ "use client"; -import { useState, useEffect, useCallback, useRef } from "react"; +import { useState, useCallback } from "react"; import { usePathname } from "next/navigation"; import Image from "next/image"; +import { motion, AnimatePresence } from "motion/react"; import { activityEventsConfig } from "@/app/types/event"; import { cn } from "@/lib/utils"; import { X, ChevronUp, ExternalLink, Play } from "lucide-react"; @@ -33,42 +34,6 @@ export function FloatWindow() { // 仅在首页 (/) 可见 const isHomePage = pathname === "/"; - const [position, setPosition] = useState<{ x: number; y: number } | null>( - null, - ); - const [isDragging, setIsDragging] = useState(false); - const dragOffset = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); - - const handlePointerDown = (e: React.PointerEvent) => { - if (e.button !== 0) return; - const el = e.currentTarget as HTMLElement; - const rect = el.getBoundingClientRect(); - setIsDragging(true); - dragOffset.current = { - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }; - if (!position) setPosition({ x: rect.left, y: rect.top }); - el.setPointerCapture(e.pointerId); - }; - - useEffect(() => { - if (!isDragging) return; - const handlePointerMove = (e: PointerEvent) => { - setPosition({ - x: e.clientX - dragOffset.current.x, - y: e.clientY - dragOffset.current.y, - }); - }; - const handlePointerUp = () => setIsDragging(false); - window.addEventListener("pointermove", handlePointerMove); - window.addEventListener("pointerup", handlePointerUp); - return () => { - window.removeEventListener("pointermove", handlePointerMove); - window.removeEventListener("pointerup", handlePointerUp); - }; - }, [isDragging]); - const handleDismiss = useCallback(() => setIsDismissed(true), []); const handleToggle = useCallback(() => setIsCollapsed((prev) => !prev), []); @@ -78,147 +43,156 @@ export function FloatWindow() { const currentEvent = latestEvent; return ( -
    { - if (node) node.style.touchAction = "none"; - }} + e.stopPropagation()} // Prevent conflicts > - {/* 极简折叠状态 */} - {isCollapsed ? ( - - ) : ( - /* 展开状态 - 报纸卡片 */ -
    - {/* Header Bar */} -
    - - The Daily Feed - -
    - - -
    -
    - - {/* Content */} -
    - {/* 图片区域 - 默认为灰度(暗黑模式除外) */} -
    - {currentEvent.name} - {/* 突发新闻徽章 */} - {!currentEvent.deprecated && ( -
    - Breaking -
    - )} -
    - - {/* 标题与描述 */} -
    -

    - {currentEvent.name} -

    -

    - {currentEvent.deprecated - ? "This event has concluded. View the archives for full coverage." - : "Join us for this significant community event. Detailed coverage inside."} -

    - - {/* 操作按钮 */} - {currentEvent.deprecated && currentEvent.playback ? ( - + {isCollapsed ? ( + + + Latest + + + ) : ( + /* 展开状态 - 报纸卡片 */ + + {/* Header Bar */} +
    + + The Daily Feed + +
    + + +
    -
    -
    - )} -
    + + {/* Content */} +
    + {/* 图片区域 - 默认为灰度(暗黑模式除外) */} +
    + {currentEvent.name} + {/* 突发新闻徽章 */} + {!currentEvent.deprecated && ( +
    + Breaking +
    + )} +
    + + {/* 标题与描述 */} +
    +

    + {currentEvent.name} +

    +

    + {currentEvent.deprecated + ? "This event has concluded. View the archives for full coverage." + : "Join us for this significant community event. Detailed coverage inside."} +

    + + {/* 操作按钮 */} + {currentEvent.deprecated && currentEvent.playback ? ( + e.stopPropagation()} + className={cn( + "flex items-center justify-center gap-2 w-full px-4 py-2", + "border border-[#111111] dark:border-[#F9F9F7]", + "bg-transparent hover:bg-[#111111] hover:text-[#F9F9F7]", + "dark:hover:bg-[#F9F9F7] dark:hover:text-[#111111]", + "font-mono text-xs uppercase tracking-widest font-bold", + "transition-colors duration-200", + )} + > + + Watch Replay + + ) : ( + e.stopPropagation()} + className={cn( + "flex items-center justify-center gap-2 w-full px-4 py-2", + "bg-[#111111] text-[#F9F9F7]", + "hover:bg-[#CC0000] hover:border-[#CC0000]", + "font-mono text-xs uppercase tracking-widest font-bold", + "transition-colors duration-200", + )} + > + Read More + + + )} +
    +
    + + )} + + ); } diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx index e982a4c5..64c62e29 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/docs/[...slug]/page.tsx @@ -13,31 +13,9 @@ import { import { Contributors } from "@/app/components/Contributors"; import { DocsAssistant } from "@/app/components/DocsAssistant"; import { LicenseNotice } from "@/app/components/LicenseNotice"; -import fs from "fs/promises"; -import path from "path"; - -// Extract clean text content from MDX -function extractTextFromMDX(content: string): string { - let text = content - .replace(/^---[\s\S]*?---/m, "") // Remove frontmatter - .replace(/```[\s\S]*?```/g, "") // Remove code blocks - .replace(/`([^`]+)`/g, "$1"); // Remove inline code - // Remove HTML/MDX tags recursively to prevent incomplete multi-character sanitization - let prevText; - do { - prevText = text; - text = text.replace(/<[^>]+>/g, ""); - } while (text !== prevText); - return text - .replace(/\*\*([^*]+)\*\*/g, "$1") // Remove bold - .replace(/\*([^*]+)\*/g, "$1") // Remove italic - .replace(/#{1,6}\s+/g, "") // Remove headers - .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // Remove links, keep text - .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1") // Remove images, keep alt text - .replace(/[#*`()[!\]!]/g, "") // Remove common markdown symbols - .replace(/\n{2,}/g, "\n") // Normalize line breaks - .trim(); -} +import { PageFeedback } from "@/app/components/PageFeedback"; +// Extract clean text content from MDX - no longer used on client/page side +// content fetching moved to API route for performance interface Param { params: Promise<{ @@ -66,20 +44,6 @@ export default async function DocPage({ params }: Param) { getDocContributorsByDocId(docIdFromPage); const Mdx = page.data.body; - // Prepare page content for AI assistant - let pageContentForAI = ""; - try { - const fullFilePath = path.join(process.cwd(), "app/docs", page.file.path); - const rawContent = await fs.readFile(fullFilePath, "utf-8"); - const extractedText = extractTextFromMDX(rawContent); - // Use full extracted content without truncation - pageContentForAI = extractedText; - } catch (error) { - console.warn("Failed to read file content for AI assistant:", error); - // Fallback to using page metadata - pageContentForAI = `${page.data.title}\n${page.data.description || ""}`; - } - return ( <> @@ -92,6 +56,7 @@ export default async function DocPage({ params }: Param) {
    +
    @@ -102,7 +67,6 @@ export default async function DocPage({ params }: Param) { pageContext={{ title: page.data.title, description: page.data.description, - content: pageContentForAI, slug: slug?.join("/"), }} /> diff --git a/app/docs/layout.tsx b/app/docs/layout.tsx index 8e0d46ab..404848db 100644 --- a/app/docs/layout.tsx +++ b/app/docs/layout.tsx @@ -4,6 +4,7 @@ import { baseOptions } from "@/lib/layout.shared"; import type { ReactNode } from "react"; import { DocsRouteFlag } from "@/app/components/RouteFlags"; import type { PageTree } from "fumadocs-core/server"; +import { CopyTracking } from "@/app/components/CopyTracking"; function pruneEmptyFolders(root: PageTree.Root): PageTree.Root { const transformNode = (node: PageTree.Node): PageTree.Node | null => { @@ -68,6 +69,7 @@ export default async function Layout({ children }: { children: ReactNode }) { return ( <> {/* Add a class on while in docs to adjust global backgrounds */} + { : parsed.provider === "intern" ? "intern" : "openai", + // Use only stored key if saveToLocalStorage is true // 只有在saveToLocalStorage为true时才使用存储的key openaiApiKey: saveToLocalStorage && typeof parsed.openaiApiKey === "string" @@ -100,6 +101,7 @@ export const AssistantSettingsProvider = ({ } try { + // Decide whether to save API keys based on saveToLocalStorage // 根据saveToLocalStorage决定是否保存API key const dataToSave = settings.saveToLocalStorage ? settings diff --git a/app/not-found.tsx b/app/not-found.tsx index c161da29..0053a3b7 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -9,8 +9,12 @@ export default function NotFound() { const pathname = usePathname(); useEffect(() => { + // Umami 埋点: 记录 404 错误和来源页面 (Referrer) if (window.umami) { - window.umami.track("404", { path: pathname }); + window.umami.track("error_404", { + path: pathname, + referrer: document.referrer || "direct", + }); } }, [pathname]); diff --git a/auth.ts b/auth.ts index 6ce9d9f3..87b78e96 100644 --- a/auth.ts +++ b/auth.ts @@ -1,23 +1,14 @@ import NextAuth from "next-auth"; import { authConfig } from "./auth.config"; import GitHub from "next-auth/providers/github"; -import { Pool } from "@neondatabase/serverless"; -import NeonAdapter from "@auth/neon-adapter"; - -type NeonAdapterPool = Parameters[0]; +import { PrismaAdapter } from "@auth/prisma-adapter"; +import { prisma } from "@/lib/db"; export const { handlers, auth, signIn, signOut } = NextAuth(() => { - // Neon 连接只在有数据库配置时启用;本地协作者若没有 `.env`,将回退为纯 JWT 会话,避免直接抛错阻塞开发。 - const databaseUrl = process.env.DATABASE_URL; - const adapter = databaseUrl - ? NeonAdapter( - new Pool({ - connectionString: databaseUrl, - }) as unknown as NeonAdapterPool, - ) - : undefined; + // 数据库适配器:仅在有 DATABASE_URL 时启用 + const adapter = process.env.DATABASE_URL ? PrismaAdapter(prisma) : undefined; - if (!databaseUrl) { + if (!process.env.DATABASE_URL) { console.warn("[auth] DATABASE_URL missing – running without Neon adapter"); } return { diff --git a/data/event.json b/data/event.json index 6e2ab7d3..279b11c7 100644 --- a/data/event.json +++ b/data/event.json @@ -26,4 +26,4 @@ "deprecated": true } ] -} \ No newline at end of file +} diff --git a/docs/umami_tracking.md b/docs/umami_tracking.md index ffeb9a37..909e0a76 100644 --- a/docs/umami_tracking.md +++ b/docs/umami_tracking.md @@ -6,13 +6,15 @@ **内部导航 (Internal Navigation)**: 仅用于网站内部页面的跳转。 -| 区域 (Region) | 元素说明 | 触发行为 | 埋点事件名 (Event Name) | 埋点传参 (Event Data) | -| :------------- | :---------------------------- | :------- | :---------------------- | :------------------------------------------------------------------------------------ | -| **header** | 导航链接 (特点, 社区等) | 点击 | `navigation_click` | `region`: "header", `label`: 链接名 (e.g., "features"), `path`: 目标路径 | -| **footer** | 归档/资源链接 | 点击 | `navigation_click` | `region`: "footer", `label`: 链接名 (e.g., "AI & Mathematics"), `category`: "archive" | -| **sidebar** | 文档侧边栏链接 | 点击 | `navigation_click` | `region`: "sidebar", `label`: 页面标题, `path`: 目标路径 | -| **toc** | 目录 (Table of Contents) 链接 | 点击 | `navigation_click` | `region`: "toc", `label`: 章节标题 | -| **pagination** | 上一篇/下一篇 | 点击 | `navigation_click` | `region`: "pagination", `label`: "prev" / "next", `path`: 目标路径 | +| 区域 (Region) | 元素说明 | 触发行为 | 埋点事件名 (Event Name) | 埋点传参 (Event Data) | +| :------------------- | :---------------------------- | :------- | :---------------------- | :------------------------------------------------------------------------------------ | +| **header** | 导航链接 (特点, 社区等) | 点击 | `navigation_click` | `region`: "header", `label`: 链接名 (e.g., "features"), `path`: 目标路径 | +| **footer** | 归档/资源链接 | 点击 | `navigation_click` | `region`: "footer", `label`: 链接名 (e.g., "Mission Brief"), `category`: "archive" | +| **sidebar** | 文档侧边栏链接 | 点击 | `navigation_click` | `region`: "sidebar", `label`: 页面标题, `path`: 目标路径 | +| **toc** | 目录 (Table of Contents) 链接 | 点击 | `navigation_click` | `region`: "toc", `label`: 章节标题 | +| **pagination** | 上一篇/下一篇 | 点击 | `navigation_click` | `region`: "pagination", `label`: "prev" / "next", `path`: 目标路径 | +| **hero_cta** | 首页 Hero 按钮 | 点击 | `navigation_click` | `region`: "hero_cta", `label`: "Access Articles" | +| **home_categories** | 首页分类卡片 | 点击 | `navigation_click` | `region`: "home_categories", `label`: 分类标题 (e.g., "AI") | **资源与外部链接 (Resources & External Links)**: 用于追踪外部资源、工具或社交媒体的点击。 @@ -35,18 +37,19 @@ ## 3. 内容互动与反馈 (Content Interaction & Feedback) -| 区域 | 元素说明 | 触发行为 | 埋点事件名 (Event Name) | 埋点传参 (Event Data) | -| :--------------- | :----------------- | :------- | :---------------------- | :------------------------------------------------------------------- | -| **Content** | 复制生词/代码块 | 点击 | `prose_copy` | `type`: "code"/"text", `content_length`: 字符数范围 | -| **Content** | 页面反馈 (Helpful) | 点击 | `feedback_submit` | `page`: 当前页面路径, `vote`: "helpful" / "not_helpful" | -| **Feature** | 投稿 (Contribute) | 点击 | `contribute_trigger` | `location`: "hero" / "docs" | -| **AI Assistant** | 提问 | 发送 | `ai_assistant_query` | `length`: 字符数范围 (e.g., "0-50") _注意:不记录具体内容以保护隐私_ | +| 区域 | 元素说明 | 触发行为 | 埋点事件名 (Event Name) | 埋点传参 (Event Data) | +| :--------------- | :------------------------- | :------- | :--------------------------- | :----------------------------------------------------------- | +| **Content** | 复制生词/代码块 | 复制 | `content_copy` | `type`: "code"/"text", `content_length`: 字符数 (number) | +| **Content** | 页面反馈 (Helpful) | 点击 | `feedback_submit` | `page`: 当前页面路径, `vote`: "helpful" / "not_helpful" | +| **Feature** | 投稿 (Contribute) | 点击 | `contribute_trigger` | `location`: "hero" / "docs" | +| **Feature** | 投稿跳转 (Github Redirect) | 跳转 | `contribute_github_redirect` | `dir`: 目标目录, `filename`: 文件名 | +| **AI Assistant** | 提问 | 完成 | `ai_assistant_query` | _(暂未包含具体参数)_ | ## 4. 异常与错误 (Errors) -| 场景 | 说明 | 触发条件 | 埋点事件名 (Event Name) | 埋点传参 (Event Data) | -| :------ | :--------- | :------------ | :---------------------- | :------------------------------------- | -| **404** | 页面未找到 | 访问 404 页面 | `error_404` | `path`: 访问路径, `referrer`: 来源页面 | +| 场景 | 说明 | 触发条件 | 埋点事件名 (Event Name) | 埋点传参 (Event Data) | +| :------ | :--------- | :------------ | :---------------------- | :----------------------------------------------- | +| **404** | 页面未找到 | 访问 404 页面 | `error_404` | `path`: 访问路径, `referrer`: 来源页面 (document.referrer) | ## 实施指南 @@ -68,15 +71,23 @@ >Artificial Intelligence ``` -### 搜索埋点示例 (配合 cmdk / fumadocs) +### 搜索埋点示例 (CustomSearchDialog) ```tsx -// 在搜索组件中 -const onSelectResult = (result, index) => { - umami.track("search_result_click", { - query: searchQuery, - rank: index + 1, - url: result.url, - }); -}; +// 在搜索结果点击时 +umami.track("search_result_click", { + query: searchQuery, + rank: index + 1, + url: result.url, +}); +``` + +### 复制监听示例 (CopyTracking) + +```tsx +// 自动监听 document copy 事件 +umami.track("content_copy", { + type: isCode ? "code" : "text", + content_length: selection.toString().length, +}); ``` diff --git a/generated/doc-contributors.json b/generated/doc-contributors.json index 1003d7d7..d687b8d8 100644 --- a/generated/doc-contributors.json +++ b/generated/doc-contributors.json @@ -1,6 +1,6 @@ { "repo": "InvolutionHell/involutionhell", - "generatedAt": "2026-02-10T16:55:33.836Z", + "generatedAt": "2026-02-19T14:50:35.107Z", "docsDir": "app/docs", "totalDocs": 146, "results": [ diff --git a/lib/ai/providers/glm.ts b/lib/ai/providers/glm.ts new file mode 100644 index 00000000..7b3a3f80 --- /dev/null +++ b/lib/ai/providers/glm.ts @@ -0,0 +1,16 @@ +import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; + +/** + * 创建智谱 GLM 模型实例 + * 使用 GLM-4-Flash(免费模型),适合轻量任务如建议生成 + * API key 需在环境变量 ZHIPU_API_KEY 中配置 + */ +export function createGlmFlashModel() { + const glm = createOpenAICompatible({ + name: "zhipu", + baseURL: "https://open.bigmodel.cn/api/paas/v4/", + apiKey: process.env.ZHIPU_API_KEY, + }); + + return glm("glm-4-flash"); +} diff --git a/lib/db.ts b/lib/db.ts new file mode 100644 index 00000000..c9c962d4 --- /dev/null +++ b/lib/db.ts @@ -0,0 +1,16 @@ +import { PrismaClient } from "../generated/prisma/client"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { Pool } from "pg"; + +const connectionString = `${process.env.DATABASE_URL}`; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +const pool = new Pool({ connectionString }); + +const adapter = new PrismaPg(pool); +export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter }); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; diff --git a/package.json b/package.json index 768bafbe..92805367 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "start": "next start -p 3000", "test": "vitest run", "test:watch": "vitest", - "postinstall": "fumadocs-mdx", + "postinstall": "fumadocs-mdx && prisma generate", "prepare": "husky", "lint:images": "node scripts/check-images.mjs", "migrate:images": "node scripts/move-doc-images.mjs", @@ -28,16 +28,16 @@ "@assistant-ui/react": "^0.11.14", "@assistant-ui/react-ai-sdk": "^1.1.0", "@assistant-ui/react-markdown": "^0.11.0", - "@auth/neon-adapter": "^1.10.0", + "@auth/prisma-adapter": "^2.11.1", "@aws-sdk/client-s3": "^3.932.0", "@aws-sdk/s3-request-presigner": "^3.932.0", "@giscus/react": "^3.1.0", "@milkdown/crepe": "^7.17.1", "@milkdown/kit": "^7.17.1", - "@neondatabase/serverless": "^1.0.1", "@orama/orama": "^3.1.14", "@orama/tokenizers": "^3.1.14", - "@prisma/client": "^6.16.2", + "@prisma/adapter-pg": "^7.4.1", + "@prisma/client": "7", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -46,6 +46,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@types/mdx": "^2.0.13", + "@types/pg": "^8.16.0", "@vercel/speed-insights": "^1.2.0", "ai": "^5.0.96", "antd": "^5.27.4", @@ -64,7 +65,7 @@ "next": "16.1.5", "next-auth": "5.0.0-beta.30", "next-intl": "^4.3.9", - "prisma": "^6.16.2", + "pg": "^8.18.0", "react": "19.2.3", "react-dom": "19.2.3", "rehype-autolink-headings": "^7.1.0", @@ -95,6 +96,7 @@ "lint-staged": "^16.1.6", "postcss": "^8.5.6", "prettier": "3.6.2", + "prisma": "^7.4.1", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0", "tailwindcss": "^4.1.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4210635c..8297bca8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,9 +33,9 @@ importers: '@assistant-ui/react-markdown': specifier: ^0.11.0 version: 0.11.0(@assistant-ui/react@0.11.14(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(use-sync-external-store@1.5.0(react@19.2.3)))(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@auth/neon-adapter': - specifier: ^1.10.0 - version: 1.10.0 + '@auth/prisma-adapter': + specifier: ^2.11.1 + version: 2.11.1(@prisma/client@7.4.1(prisma@7.4.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2))(typescript@5.9.2)) '@aws-sdk/client-s3': specifier: ^3.932.0 version: 3.948.0 @@ -51,18 +51,18 @@ importers: '@milkdown/kit': specifier: ^7.17.1 version: 7.17.1(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.6)(typescript@5.9.2) - '@neondatabase/serverless': - specifier: ^1.0.1 - version: 1.0.1 '@orama/orama': specifier: ^3.1.14 version: 3.1.14 '@orama/tokenizers': specifier: ^3.1.14 version: 3.1.14 + '@prisma/adapter-pg': + specifier: ^7.4.1 + version: 7.4.1 '@prisma/client': - specifier: ^6.16.2 - version: 6.16.2(prisma@6.16.2(typescript@5.9.2))(typescript@5.9.2) + specifier: '7' + version: 7.4.1(prisma@7.4.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2))(typescript@5.9.2) '@radix-ui/react-avatar': specifier: ^1.1.10 version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -87,6 +87,9 @@ importers: '@types/mdx': specifier: ^2.0.13 version: 2.0.13 + '@types/pg': + specifier: ^8.16.0 + version: 8.16.0 '@vercel/speed-insights': specifier: ^1.2.0 version: 1.2.0(next@16.1.5(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(vue@3.5.24(typescript@5.9.2)) @@ -141,9 +144,9 @@ importers: next-intl: specifier: ^4.3.9 version: 4.3.9(next@16.1.5(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.2) - prisma: - specifier: ^6.16.2 - version: 6.16.2(typescript@5.9.2) + pg: + specifier: ^8.18.0 + version: 8.18.0 react: specifier: 19.2.3 version: 19.2.3 @@ -226,6 +229,9 @@ importers: prettier: specifier: 3.6.2 version: 3.6.2 + prisma: + specifier: ^7.4.1 + version: 7.4.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2) rehype-katex: specifier: ^7.0.1 version: 7.0.1 @@ -400,8 +406,8 @@ packages: react: optional: true - '@auth/core@0.40.0': - resolution: {integrity: sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==} + '@auth/core@0.41.0': + resolution: {integrity: sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 @@ -414,12 +420,12 @@ packages: nodemailer: optional: true - '@auth/core@0.41.0': - resolution: {integrity: sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==} + '@auth/core@0.41.1': + resolution: {integrity: sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 '@simplewebauthn/server': ^9.0.2 - nodemailer: ^6.8.0 + nodemailer: ^7.0.7 peerDependenciesMeta: '@simplewebauthn/browser': optional: true @@ -428,8 +434,10 @@ packages: nodemailer: optional: true - '@auth/neon-adapter@1.10.0': - resolution: {integrity: sha512-KuwialF1LrM5AZOzGurw6OUfeO/sW1ZAQirceYuxzpiVPvUJCeOPHQro0vcvD29JmdpWh5XegnEbRHXtVXROPg==} + '@auth/prisma-adapter@2.11.1': + resolution: {integrity: sha512-Ke7DXP0Fy0Mlmjz/ZJLXwQash2UkA4621xCM0rMtEczr1kppLc/njCbUkHkIQ/PnmILjqSPEKeTjDPsYruvkug==} + peerDependencies: + '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5 || >=6' '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} @@ -669,6 +677,18 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@chevrotain/cst-dts-gen@10.5.0': + resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==} + + '@chevrotain/gast@10.5.0': + resolution: {integrity: sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==} + + '@chevrotain/types@10.5.0': + resolution: {integrity: sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==} + + '@chevrotain/utils@10.5.0': + resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} + '@codemirror/autocomplete@6.19.1': resolution: {integrity: sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==} @@ -786,6 +806,20 @@ packages: resolution: {integrity: sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==} engines: {node: '>=16'} + '@electric-sql/pglite-socket@0.0.20': + resolution: {integrity: sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite-tools@0.2.20': + resolution: {integrity: sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==} + peerDependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite@0.3.15': + resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==} + '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} @@ -1186,6 +1220,12 @@ packages: react: ^16 || ^17 || ^18 || ^19 react-dom: ^16 || ^17 || ^18 || ^19 + '@hono/node-server@1.19.9': + resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1501,19 +1541,16 @@ packages: '@milkdown/utils@7.17.1': resolution: {integrity: sha512-QTjbaxv+ZOB4a1BaQULkeJExyIvMnQw69UKf9QoM/E8iY2q1c8kppnN6i6ZeN9ZkCh4lXu+r7w/LH6zSFXrsdA==} + '@mrleebo/prisma-ast@0.13.1': + resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} + engines: {node: '>=16'} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} '@napi-rs/wasm-runtime@1.0.7': resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} - '@neondatabase/serverless@0.10.4': - resolution: {integrity: sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==} - - '@neondatabase/serverless@1.0.1': - resolution: {integrity: sha512-O6yC5TT0jbw86VZVkmnzCZJB0hfxBl0JJz6f+3KHoZabjb/X08r9eFA+vuY06z1/qaovykvdkrXYq3SPUuvogA==} - engines: {node: '>=19.0.0'} - '@next/env@16.1.5': resolution: {integrity: sha512-CRSCPJiSZoi4Pn69RYBDI9R7YK2g59vLexPQFXY0eyw+ILevIenCywzg+DqmlBik9zszEnw2HLFOUlLAcJbL7g==} @@ -1620,35 +1657,63 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@prisma/client@6.16.2': - resolution: {integrity: sha512-E00PxBcalMfYO/TWnXobBVUai6eW/g5OsifWQsQDzJYm7yaY+IRLo7ZLsaefi0QkTpxfuhFcQ/w180i6kX3iJw==} - engines: {node: '>=18.18'} + '@prisma/adapter-pg@7.4.1': + resolution: {integrity: sha512-AH9XrqvSoBAaStn0Gm/sAnF97pDKz8uLpNmn51j1S9O9dhUva6LIxGdoDiiU9VXRIR89wAJXsvJSy+mK40m2xw==} + + '@prisma/client-runtime-utils@7.4.1': + resolution: {integrity: sha512-8fy74OMYC7mt9cJ2MncIDk1awPRgmtXVvwTN2FlW4JVhbck8Dgt0wTkhPG85myfj4ZeP2stjF9Sdg12n5HrpQg==} + + '@prisma/client@7.4.1': + resolution: {integrity: sha512-pgIll2W1NVdof37xLeyySW+yfQ4rI+ERGCRwnO3BjVOx42GpYq6jhTyuALK8VKirvJJIvImgfGDA2qwhYVvMuA==} + engines: {node: ^20.19 || ^22.12 || >=24.0} peerDependencies: prisma: '*' - typescript: '>=5.1.0' + typescript: '>=5.4.0' peerDependenciesMeta: prisma: optional: true typescript: optional: true - '@prisma/config@6.16.2': - resolution: {integrity: sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==} + '@prisma/config@7.4.1': + resolution: {integrity: sha512-vteSXm8N46bo3FW9MhPGVHAj+KRgrR6TWtlSk6GqToCKjTnOexXdPZyiDyEsfVW38YhqEmVl6w/6iHN8uYVJcw==} + + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + + '@prisma/debug@7.4.1': + resolution: {integrity: sha512-qEtzO8oLouRv18JDQUC3G3Gnv+fGVscHZm/x1DBB/WT+kOvPDQLM2woX6IGgWnSMYYlrxjuALshT7G/blvY0bQ==} + + '@prisma/dev@0.20.0': + resolution: {integrity: sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==} + + '@prisma/driver-adapter-utils@7.4.1': + resolution: {integrity: sha512-gEZOC2tnlHaZNbHUdbK8YvQphq2tKq/Ovu1YixJ/hPSutDAvNzC3R+xUeBuJ4AJp236eELMzwxb7rgo3UbRkTg==} - '@prisma/debug@6.16.2': - resolution: {integrity: sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA==} + '@prisma/engines-version@7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3': + resolution: {integrity: sha512-fUxVd1TjOW8K4XsZ8dAm88sDW5Ry7AxWDfsYEWwScS6Fjo3caKC6hgNumUfsmsy0Il9LjDn5X0PpVXNt3iwayw==} - '@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43': - resolution: {integrity: sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==} + '@prisma/engines@7.4.1': + resolution: {integrity: sha512-BZEBdHvNJx5PzIG37EI/Zi5UUI5hGWjkYsQmKa7OIK6evAvebOTwutjS/VRI6cA6grmA52eLZR+oekGRMqkKxQ==} - '@prisma/engines@6.16.2': - resolution: {integrity: sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA==} + '@prisma/fetch-engine@7.4.1': + resolution: {integrity: sha512-Z9kbuxX2bvEsyeS3LZEiEnxG0lVtZbpYgaAnPj69N+A9f2De8Lta0EoFtld9zhfERVPIQWhSWUc8himky3qYdA==} - '@prisma/fetch-engine@6.16.2': - resolution: {integrity: sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ==} + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} - '@prisma/get-platform@6.16.2': - resolution: {integrity: sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==} + '@prisma/get-platform@7.4.1': + resolution: {integrity: sha512-kN4tmkQzlgm/KtE+jTNSYjsDxxe/5i6GApPI32BN9T0tlgsgSBtDJbjGBICttkAIjsh73dXf8raPKxO/2n2UUg==} + + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + + '@prisma/studio-core@0.13.1': + resolution: {integrity: sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==} + peerDependencies: + '@types/react': 19.2.7 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2743,17 +2808,14 @@ packages: '@types/node@16.18.11': resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==} - '@types/node@22.19.7': - resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} - '@types/node@25.1.0': resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==} - '@types/pg@8.11.6': - resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==} + '@types/node@25.3.0': + resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} - '@types/pg@8.15.5': - resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} + '@types/pg@8.16.0': + resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} @@ -3270,6 +3332,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axe-core@4.10.3: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} @@ -3373,6 +3439,9 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chevrotain@10.5.0: + resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} + chokidar@4.0.0: resolution: {integrity: sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA==} engines: {node: '>= 14.16.0'} @@ -3571,6 +3640,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -3627,8 +3700,8 @@ packages: engines: {node: '>=16'} hasBin: true - effect@3.16.12: - resolution: {integrity: sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==} + effect@3.18.4: + resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} electron-to-chromium@1.5.222: resolution: {integrity: sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==} @@ -4225,6 +4298,9 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + generic-pool@3.4.2: resolution: {integrity: sha512-H7cUpwCQSiJmAHM4c/aFu6fUfrhWXW1ncyh8ftxEPMu6AiYkHw9K8br720TGPZJbk5eOH2bynjZD1yPvdDAmag==} engines: {node: '>= 4'} @@ -4245,6 +4321,9 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -4301,9 +4380,15 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + grammex@3.1.12: + resolution: {integrity: sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphmatch@1.1.1: + resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} @@ -4383,6 +4468,10 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hono@4.11.4: + resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} + engines: {node: '>=16.9.0'} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -4397,6 +4486,9 @@ packages: resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} engines: {node: '>= 0.6'} + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -4414,6 +4506,10 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -4553,6 +4649,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -4758,6 +4857,10 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -4790,10 +4893,16 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -4815,6 +4924,10 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lucide-react@0.544.0: resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} peerDependencies: @@ -5107,6 +5220,14 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + nano-spawn@1.0.3: resolution: {integrity: sha512-jtpsQDetTnvS2Ts1fiRdci5rx0VYws5jGyC+4IYOTnIQ/wwdf6JdomlHBwqC3bJYOvaKu0C2GSZ1A60anrYpaA==} engines: {node: '>=20.17'} @@ -5280,9 +5401,6 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - obuf@1.1.2: - resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -5390,24 +5508,39 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.11.0: + resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-numeric@1.0.2: - resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} - engines: {node: '>=4'} + pg-pool@3.11.0: + resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} + peerDependencies: + pg: '>=8.0' - pg-protocol@1.10.3: - resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + pg-protocol@1.11.0: + resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} - pg-types@4.1.0: - resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==} - engines: {node: '>=10'} + pg@8.18.0: + resolution: {integrity: sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -5462,29 +5595,18 @@ packages: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} - postgres-bytea@3.0.0: - resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} - engines: {node: '>= 6'} - postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} engines: {node: '>=0.10.0'} - postgres-date@2.1.0: - resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} - engines: {node: '>=12'} - postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - postgres-interval@3.0.0: - resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} engines: {node: '>=12'} - postgres-range@1.1.4: - resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - preact-render-to-string@6.5.11: resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} peerDependencies: @@ -5506,13 +5628,16 @@ packages: resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} engines: {node: '>=10'} - prisma@6.16.2: - resolution: {integrity: sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==} - engines: {node: '>=18.18'} + prisma@7.4.1: + resolution: {integrity: sha512-gDKOXwnPiMdB+uYMhMeN8jj4K7Cu3Q2wB/wUsITOoOk446HtVb8T9BZxFJ1Zop6alc89k6PMNdR2FZCpbXp/jw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} hasBin: true peerDependencies: - typescript: '>=5.1.0' + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' peerDependenciesMeta: + better-sqlite3: + optional: true typescript: optional: true @@ -5522,6 +5647,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} @@ -5926,6 +6054,9 @@ packages: regex@6.0.1: resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + regexp-to-ast@0.5.0: + resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -5966,6 +6097,9 @@ packages: remark@15.0.1: resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -5997,6 +6131,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -6070,6 +6208,9 @@ packages: engines: {node: '>=10'} hasBin: true + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6145,9 +6286,17 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + srvx@0.8.9: resolution: {integrity: sha512-wYc3VLZHRzwYrWJhkEqkhLb31TI0SOkfYZDkUhXdp3NoCnNS0FqajiQszZZjfow/VYEuc6Q5sZh9nM6kPy2NBQ==} engines: {node: '>=20.16.0'} @@ -6450,12 +6599,12 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} @@ -6574,6 +6723,14 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vercel@48.8.0: resolution: {integrity: sha512-ZPoq4txk2OkKAZVi+E+UKEFA70n4FkFmv/PSHTv4wAvJl3n2WvCaGCrfT33b9U9iDGQs9ZnwE2eQDylamc8FVA==} engines: {node: '>= 18'} @@ -6776,6 +6933,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -6996,7 +7156,7 @@ snapshots: optionalDependencies: react: 19.2.3 - '@auth/core@0.40.0': + '@auth/core@0.41.0': dependencies: '@panva/hkdf': 1.2.1 jose: 6.1.0 @@ -7004,7 +7164,7 @@ snapshots: preact: 10.24.3 preact-render-to-string: 6.5.11(preact@10.24.3) - '@auth/core@0.41.0': + '@auth/core@0.41.1': dependencies: '@panva/hkdf': 1.2.1 jose: 6.1.0 @@ -7012,10 +7172,10 @@ snapshots: preact: 10.24.3 preact-render-to-string: 6.5.11(preact@10.24.3) - '@auth/neon-adapter@1.10.0': + '@auth/prisma-adapter@2.11.1(@prisma/client@7.4.1(prisma@7.4.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2))(typescript@5.9.2))': dependencies: - '@auth/core': 0.40.0 - '@neondatabase/serverless': 0.10.4 + '@auth/core': 0.41.1 + '@prisma/client': 7.4.1(prisma@7.4.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2))(typescript@5.9.2) transitivePeerDependencies: - '@simplewebauthn/browser' - '@simplewebauthn/server' @@ -7620,6 +7780,21 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@chevrotain/cst-dts-gen@10.5.0': + dependencies: + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + lodash: 4.17.21 + + '@chevrotain/gast@10.5.0': + dependencies: + '@chevrotain/types': 10.5.0 + lodash: 4.17.21 + + '@chevrotain/types@10.5.0': {} + + '@chevrotain/utils@10.5.0': {} + '@codemirror/autocomplete@6.19.1': dependencies: '@codemirror/language': 6.11.3 @@ -7891,6 +8066,16 @@ snapshots: dependencies: '@edge-runtime/primitives': 4.1.0 + '@electric-sql/pglite-socket@0.0.20(@electric-sql/pglite@0.3.15)': + dependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite-tools@0.2.20(@electric-sql/pglite@0.3.15)': + dependencies: + '@electric-sql/pglite': 0.3.15 + + '@electric-sql/pglite@0.3.15': {} + '@emnapi/core@1.5.0': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -8162,6 +8347,10 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + '@hono/node-server@1.19.9(hono@4.11.4)': + dependencies: + hono: 4.11.4 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -8709,6 +8898,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@mrleebo/prisma-ast@0.13.1': + dependencies: + chevrotain: 10.5.0 + lilconfig: 2.1.0 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.5.0 @@ -8723,15 +8917,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@neondatabase/serverless@0.10.4': - dependencies: - '@types/pg': 8.11.6 - - '@neondatabase/serverless@1.0.1': - dependencies: - '@types/node': 22.19.7 - '@types/pg': 8.15.5 - '@next/env@16.1.5': {} '@next/eslint-plugin-next@16.0.7': @@ -8801,40 +8986,92 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@prisma/client@6.16.2(prisma@6.16.2(typescript@5.9.2))(typescript@5.9.2)': + '@prisma/adapter-pg@7.4.1': + dependencies: + '@prisma/driver-adapter-utils': 7.4.1 + pg: 8.18.0 + postgres-array: 3.0.4 + transitivePeerDependencies: + - pg-native + + '@prisma/client-runtime-utils@7.4.1': {} + + '@prisma/client@7.4.1(prisma@7.4.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2))(typescript@5.9.2)': + dependencies: + '@prisma/client-runtime-utils': 7.4.1 optionalDependencies: - prisma: 6.16.2(typescript@5.9.2) + prisma: 7.4.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2) typescript: 5.9.2 - '@prisma/config@6.16.2': + '@prisma/config@7.4.1': dependencies: c12: 3.1.0 deepmerge-ts: 7.1.5 - effect: 3.16.12 + effect: 3.18.4 empathic: 2.0.0 transitivePeerDependencies: - magicast - '@prisma/debug@6.16.2': {} + '@prisma/debug@7.2.0': {} + + '@prisma/debug@7.4.1': {} + + '@prisma/dev@0.20.0(typescript@5.9.2)': + dependencies: + '@electric-sql/pglite': 0.3.15 + '@electric-sql/pglite-socket': 0.0.20(@electric-sql/pglite@0.3.15) + '@electric-sql/pglite-tools': 0.2.20(@electric-sql/pglite@0.3.15) + '@hono/node-server': 1.19.9(hono@4.11.4) + '@mrleebo/prisma-ast': 0.13.1 + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + hono: 4.11.4 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.33.4 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.9.2) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + + '@prisma/driver-adapter-utils@7.4.1': + dependencies: + '@prisma/debug': 7.4.1 - '@prisma/engines-version@6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43': {} + '@prisma/engines-version@7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3': {} - '@prisma/engines@6.16.2': + '@prisma/engines@7.4.1': dependencies: - '@prisma/debug': 6.16.2 - '@prisma/engines-version': 6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43 - '@prisma/fetch-engine': 6.16.2 - '@prisma/get-platform': 6.16.2 + '@prisma/debug': 7.4.1 + '@prisma/engines-version': 7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3 + '@prisma/fetch-engine': 7.4.1 + '@prisma/get-platform': 7.4.1 - '@prisma/fetch-engine@6.16.2': + '@prisma/fetch-engine@7.4.1': dependencies: - '@prisma/debug': 6.16.2 - '@prisma/engines-version': 6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43 - '@prisma/get-platform': 6.16.2 + '@prisma/debug': 7.4.1 + '@prisma/engines-version': 7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3 + '@prisma/get-platform': 7.4.1 - '@prisma/get-platform@6.16.2': + '@prisma/get-platform@7.2.0': dependencies: - '@prisma/debug': 6.16.2 + '@prisma/debug': 7.2.0 + + '@prisma/get-platform@7.4.1': + dependencies: + '@prisma/debug': 7.4.1 + + '@prisma/query-plan-executor@7.2.0': {} + + '@prisma/studio-core@0.13.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@types/react': 19.2.7 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) '@radix-ui/number@1.1.1': {} @@ -9992,24 +10229,18 @@ snapshots: '@types/node@16.18.11': {} - '@types/node@22.19.7': - dependencies: - undici-types: 6.21.0 - '@types/node@25.1.0': dependencies: undici-types: 7.16.0 - '@types/pg@8.11.6': + '@types/node@25.3.0': dependencies: - '@types/node': 25.1.0 - pg-protocol: 1.10.3 - pg-types: 4.1.0 + undici-types: 7.18.2 - '@types/pg@8.15.5': + '@types/pg@8.16.0': dependencies: - '@types/node': 25.1.0 - pg-protocol: 1.10.3 + '@types/node': 25.3.0 + pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/react-dom@19.2.3(@types/react@19.2.7)': @@ -10759,6 +10990,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + aws-ssl-profiles@1.1.2: {} + axe-core@4.10.3: {} axobject-query@4.1.0: {} @@ -10857,6 +11090,15 @@ snapshots: character-reference-invalid@2.0.1: {} + chevrotain@10.5.0: + dependencies: + '@chevrotain/cst-dts-gen': 10.5.0 + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + '@chevrotain/utils': 10.5.0 + lodash: 4.17.21 + regexp-to-ast: 0.5.0 + chokidar@4.0.0: dependencies: readdirp: 4.1.2 @@ -11028,6 +11270,8 @@ snapshots: defu@6.1.4: {} + denque@2.1.0: {} + depd@1.1.2: {} dequal@2.0.3: {} @@ -11078,7 +11322,7 @@ snapshots: signal-exit: 4.0.2 time-span: 4.0.0 - effect@3.16.12: + effect@3.18.4: dependencies: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 @@ -11852,6 +12096,10 @@ snapshots: functions-have-names@1.2.3: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + generic-pool@3.4.2: {} gensync@1.0.0-beta.2: {} @@ -11873,6 +12121,8 @@ snapshots: get-nonce@1.0.1: {} + get-port-please@3.2.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -11937,8 +12187,12 @@ snapshots: graceful-fs@4.2.11: {} + grammex@3.1.12: {} + graphemer@1.4.0: {} + graphmatch@1.1.1: {} + gray-matter@4.0.3: dependencies: js-yaml: 3.14.2 @@ -12097,6 +12351,8 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hono@4.11.4: {} + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} @@ -12114,6 +12370,8 @@ snapshots: statuses: 1.5.0 toidentifier: 1.0.0 + http-status-codes@2.3.0: {} + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -12129,6 +12387,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -12258,6 +12520,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-property@1.0.2: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -12443,6 +12707,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 + lilconfig@2.1.0: {} + lilconfig@3.1.3: {} lint-staged@16.1.6: @@ -12493,6 +12759,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash@4.17.21: {} + log-update@6.1.0: dependencies: ansi-escapes: 7.1.0 @@ -12501,6 +12769,8 @@ snapshots: strip-ansi: 7.1.2 wrap-ansi: 9.0.2 + long@5.3.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -12519,6 +12789,8 @@ snapshots: dependencies: yallist: 4.0.0 + lru.min@1.1.4: {} + lucide-react@0.544.0(react@19.2.3): dependencies: react: 19.2.3 @@ -13072,6 +13344,22 @@ snapshots: ms@2.1.3: {} + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + nano-spawn@1.0.3: {} nanoid@3.3.11: {} @@ -13212,8 +13500,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - obuf@1.1.2: {} - ohash@2.0.11: {} once@1.3.3: @@ -13321,11 +13607,18 @@ snapshots: perfect-debounce@1.0.0: {} + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.11.0: {} + pg-int8@1.0.1: {} - pg-numeric@1.0.2: {} + pg-pool@3.11.0(pg@8.18.0): + dependencies: + pg: 8.18.0 - pg-protocol@1.10.3: {} + pg-protocol@1.11.0: {} pg-types@2.2.0: dependencies: @@ -13335,15 +13628,19 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg-types@4.1.0: + pg@8.18.0: dependencies: - pg-int8: 1.0.1 - pg-numeric: 1.0.2 - postgres-array: 3.0.4 - postgres-bytea: 3.0.0 - postgres-date: 2.1.0 - postgres-interval: 3.0.0 - postgres-range: 1.1.4 + pg-connection-string: 2.11.0 + pg-pool: 3.11.0(pg@8.18.0) + pg-protocol: 1.11.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 picocolors@1.0.0: {} @@ -13388,21 +13685,13 @@ snapshots: postgres-bytea@1.0.0: {} - postgres-bytea@3.0.0: - dependencies: - obuf: 1.1.2 - postgres-date@1.0.7: {} - postgres-date@2.1.0: {} - postgres-interval@1.2.0: dependencies: xtend: 4.0.2 - postgres-interval@3.0.0: {} - - postgres-range@1.1.4: {} + postgres@3.4.7: {} preact-render-to-string@6.5.11(preact@10.24.3): dependencies: @@ -13418,14 +13707,21 @@ snapshots: dependencies: parse-ms: 2.1.0 - prisma@6.16.2(typescript@5.9.2): + prisma@7.4.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2): dependencies: - '@prisma/config': 6.16.2 - '@prisma/engines': 6.16.2 + '@prisma/config': 7.4.1 + '@prisma/dev': 0.20.0(typescript@5.9.2) + '@prisma/engines': 7.4.1 + '@prisma/studio-core': 0.13.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + mysql2: 3.15.3 + postgres: 3.4.7 optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: + - '@types/react' - magicast + - react + - react-dom promisepipe@3.0.0: {} @@ -13435,6 +13731,12 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + property-information@7.1.0: {} prosemirror-changeset@2.3.1: @@ -13993,6 +14295,8 @@ snapshots: dependencies: regex-utilities: 2.3.0 + regexp-to-ast@0.5.0: {} + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -14102,6 +14406,8 @@ snapshots: transitivePeerDependencies: - supports-color + remeda@2.33.4: {} + require-from-string@2.0.2: {} resize-observer-polyfill@1.5.1: {} @@ -14129,6 +14435,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + retry@0.13.1: {} reusify@1.1.0: {} @@ -14235,6 +14543,8 @@ snapshots: semver@7.7.3: {} + seq-queue@0.0.5: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -14355,8 +14665,12 @@ snapshots: space-separated-tokens@2.0.2: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} + sqlstring@2.3.3: {} + srvx@0.8.9: dependencies: cookie-es: 2.0.0 @@ -14686,10 +15000,10 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.21.0: {} - undici-types@7.16.0: {} + undici-types@7.18.2: {} + undici@5.28.4: dependencies: '@fastify/busboy': 2.1.1 @@ -14832,6 +15146,10 @@ snapshots: v8-compile-cache-lib@3.0.1: {} + valibot@1.2.0(typescript@5.9.2): + optionalDependencies: + typescript: 5.9.2 + vercel@48.8.0(rollup@4.52.5)(typescript@5.9.2): dependencies: '@vercel/backends': 0.0.4(rollup@4.52.5)(typescript@5.9.2) @@ -15063,6 +15381,11 @@ snapshots: yocto-queue@0.1.0: {} + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.12 + graphmatch: 1.1.1 + zod-validation-error@4.0.2(zod@4.1.11): dependencies: zod: 4.1.11 diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 00000000..8f05042d --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + datasource: { + url: process.env.DATABASE_URL, + }, +}); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 21145483..8e900cbc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,11 +1,10 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-client" output = "../generated/prisma" } datasource db { provider = "postgresql" - url = env("DATABASE_URL") } model accounts { @@ -61,11 +60,13 @@ model sessions { } model users { - id Int @id @default(autoincrement()) - name String? @db.VarChar(255) - email String? @db.VarChar(255) - emailVerified DateTime? @db.Timestamptz(6) - image String? + id Int @id @default(autoincrement()) + name String? @db.VarChar(255) + email String? @db.VarChar(255) + emailVerified DateTime? @db.Timestamptz(6) + image String? + chats Chat[] + analyticsEvents AnalyticsEvent[] } model verification_token { @@ -87,3 +88,40 @@ model doc_paths { @@id([doc_id, path]) @@index([path]) } + +model Chat { + id String @id @default(cuid()) + userId Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + messages Message[] + + user users? @relation(fields: [userId], references: [id]) + + @@index([userId]) +} + +model Message { + id String @id @default(cuid()) + chatId String + role String + content String + createdAt DateTime @default(now()) + + chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade) + + @@index([chatId]) +} + +model AnalyticsEvent { + id String @id @default(cuid()) + userId Int? + eventType String + eventData Json? + createdAt DateTime @default(now()) + + user users? @relation(fields: [userId], references: [id]) + + @@index([eventType]) + @@index([userId]) +}