demo(vercel-ai-sdk): add app logic (AI provider, tools, db, utils)

This commit is contained in:
Musa 2026-01-08 15:21:22 -08:00
parent 719a5bf0af
commit f106f8804e
46 changed files with 6344 additions and 0 deletions

View file

@ -0,0 +1,89 @@
"use client";
import { useCallback, useMemo } from "react";
import useSWR from "swr";
import type { UIArtifact } from "@/components/artifact";
export const initialArtifactData: UIArtifact = {
documentId: "init",
content: "",
kind: "text",
title: "",
status: "idle",
isVisible: false,
boundingBox: {
top: 0,
left: 0,
width: 0,
height: 0,
},
};
type Selector<T> = (state: UIArtifact) => T;
export function useArtifactSelector<Selected>(selector: Selector<Selected>) {
const { data: localArtifact } = useSWR<UIArtifact>("artifact", null, {
fallbackData: initialArtifactData,
});
const selectedValue = useMemo(() => {
if (!localArtifact) {
return selector(initialArtifactData);
}
return selector(localArtifact);
}, [localArtifact, selector]);
return selectedValue;
}
export function useArtifact() {
const { data: localArtifact, mutate: setLocalArtifact } = useSWR<UIArtifact>(
"artifact",
null,
{
fallbackData: initialArtifactData,
}
);
const artifact = useMemo(() => {
if (!localArtifact) {
return initialArtifactData;
}
return localArtifact;
}, [localArtifact]);
const setArtifact = useCallback(
(updaterFn: UIArtifact | ((currentArtifact: UIArtifact) => UIArtifact)) => {
setLocalArtifact((currentArtifact) => {
const artifactToUpdate = currentArtifact || initialArtifactData;
if (typeof updaterFn === "function") {
return updaterFn(artifactToUpdate);
}
return updaterFn;
});
},
[setLocalArtifact]
);
const { data: localArtifactMetadata, mutate: setLocalArtifactMetadata } =
useSWR<any>(
() =>
artifact.documentId ? `artifact-metadata-${artifact.documentId}` : null,
null,
{
fallbackData: null,
}
);
return useMemo(
() => ({
artifact,
setArtifact,
metadata: localArtifactMetadata,
setMetadata: setLocalArtifactMetadata,
}),
[artifact, setArtifact, localArtifactMetadata, setLocalArtifactMetadata]
);
}

View file

@ -0,0 +1,53 @@
"use client";
import type { UseChatHelpers } from "@ai-sdk/react";
import { useEffect } from "react";
import { useDataStream } from "@/components/data-stream-provider";
import type { ChatMessage } from "@/lib/types";
export type UseAutoResumeParams = {
autoResume: boolean;
initialMessages: ChatMessage[];
resumeStream: UseChatHelpers<ChatMessage>["resumeStream"];
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
};
export function useAutoResume({
autoResume,
initialMessages,
resumeStream,
setMessages,
}: UseAutoResumeParams) {
const { dataStream } = useDataStream();
useEffect(() => {
if (!autoResume) {
return;
}
const mostRecentMessage = initialMessages.at(-1);
if (mostRecentMessage?.role === "user") {
resumeStream();
}
// we intentionally run this once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoResume, initialMessages.at, resumeStream]);
useEffect(() => {
if (!dataStream) {
return;
}
if (dataStream.length === 0) {
return;
}
const dataPart = dataStream[0];
if (dataPart.type === "data-appendMessage") {
const message = JSON.parse(dataPart.data);
setMessages([...initialMessages, message]);
}
}, [dataStream, initialMessages, setMessages]);
}

View file

@ -0,0 +1,53 @@
"use client";
import { useMemo } from "react";
import useSWR, { useSWRConfig } from "swr";
import { unstable_serialize } from "swr/infinite";
import { updateChatVisibility } from "@/app/(chat)/actions";
import {
type ChatHistory,
getChatHistoryPaginationKey,
} from "@/components/sidebar-history";
import type { VisibilityType } from "@/components/visibility-selector";
export function useChatVisibility({
chatId,
initialVisibilityType,
}: {
chatId: string;
initialVisibilityType: VisibilityType;
}) {
const { mutate, cache } = useSWRConfig();
const history: ChatHistory = cache.get("/api/history")?.data;
const { data: localVisibility, mutate: setLocalVisibility } = useSWR(
`${chatId}-visibility`,
null,
{
fallbackData: initialVisibilityType,
}
);
const visibilityType = useMemo(() => {
if (!history) {
return localVisibility;
}
const chat = history.chats.find((currentChat) => currentChat.id === chatId);
if (!chat) {
return "private";
}
return chat.visibility;
}, [history, chatId, localVisibility]);
const setVisibilityType = (updatedVisibilityType: VisibilityType) => {
setLocalVisibility(updatedVisibilityType);
mutate(unstable_serialize(getChatHistoryPaginationKey));
updateChatVisibility({
chatId,
visibility: updatedVisibilityType,
});
};
return { visibilityType, setVisibilityType };
}

View file

@ -0,0 +1,37 @@
import type { UseChatHelpers } from "@ai-sdk/react";
import { useEffect, useState } from "react";
import type { ChatMessage } from "@/lib/types";
import { useScrollToBottom } from "./use-scroll-to-bottom";
export function useMessages({
status,
}: {
status: UseChatHelpers<ChatMessage>["status"];
}) {
const {
containerRef,
endRef,
isAtBottom,
scrollToBottom,
onViewportEnter,
onViewportLeave,
} = useScrollToBottom();
const [hasSentMessage, setHasSentMessage] = useState(false);
useEffect(() => {
if (status === "submitted") {
setHasSentMessage(true);
}
}, [status]);
return {
containerRef,
endRef,
isAtBottom,
scrollToBottom,
onViewportEnter,
onViewportLeave,
hasSentMessage,
};
}

View file

@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

View file

@ -0,0 +1,127 @@
import { useCallback, useEffect, useRef, useState } from "react";
export function useScrollToBottom() {
const containerRef = useRef<HTMLDivElement>(null);
const endRef = useRef<HTMLDivElement>(null);
const [isAtBottom, setIsAtBottom] = useState(true);
const isAtBottomRef = useRef(true);
const isUserScrollingRef = useRef(false);
// Keep ref in sync with state
useEffect(() => {
isAtBottomRef.current = isAtBottom;
}, [isAtBottom]);
const checkIfAtBottom = useCallback(() => {
if (!containerRef.current) {
return true;
}
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
return scrollTop + clientHeight >= scrollHeight - 100;
}, []);
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
if (!containerRef.current) {
return;
}
containerRef.current.scrollTo({
top: containerRef.current.scrollHeight,
behavior,
});
}, []);
// Handle user scroll events
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
let scrollTimeout: ReturnType<typeof setTimeout>;
const handleScroll = () => {
// Mark as user scrolling
isUserScrollingRef.current = true;
clearTimeout(scrollTimeout);
// Update isAtBottom state
const atBottom = checkIfAtBottom();
setIsAtBottom(atBottom);
isAtBottomRef.current = atBottom;
// Reset user scrolling flag after scroll ends
scrollTimeout = setTimeout(() => {
isUserScrollingRef.current = false;
}, 150);
};
container.addEventListener("scroll", handleScroll, { passive: true });
return () => {
container.removeEventListener("scroll", handleScroll);
clearTimeout(scrollTimeout);
};
}, [checkIfAtBottom]);
// Auto-scroll when content changes
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
const scrollIfNeeded = () => {
// Only auto-scroll if user was at bottom and isn't actively scrolling
if (isAtBottomRef.current && !isUserScrollingRef.current) {
requestAnimationFrame(() => {
container.scrollTo({
top: container.scrollHeight,
behavior: "instant",
});
setIsAtBottom(true);
isAtBottomRef.current = true;
});
}
};
// Watch for DOM changes
const mutationObserver = new MutationObserver(scrollIfNeeded);
mutationObserver.observe(container, {
childList: true,
subtree: true,
characterData: true,
});
// Watch for size changes
const resizeObserver = new ResizeObserver(scrollIfNeeded);
resizeObserver.observe(container);
// Also observe children for size changes
for (const child of container.children) {
resizeObserver.observe(child);
}
return () => {
mutationObserver.disconnect();
resizeObserver.disconnect();
};
}, []);
function onViewportEnter() {
setIsAtBottom(true);
isAtBottomRef.current = true;
}
function onViewportLeave() {
setIsAtBottom(false);
isAtBottomRef.current = false;
}
return {
containerRef,
endRef,
isAtBottom,
scrollToBottom,
onViewportEnter,
onViewportLeave,
};
}

View file

@ -0,0 +1,25 @@
import type { UserType } from "@/app/(auth)/auth";
type Entitlements = {
maxMessagesPerDay: number;
};
export const entitlementsByUserType: Record<UserType, Entitlements> = {
/*
* For users without an account
*/
guest: {
maxMessagesPerDay: 20,
},
/*
* For users with an account
*/
regular: {
maxMessagesPerDay: 50,
},
/*
* TODO: For users with an account and a paid membership
*/
};

View file

@ -0,0 +1,173 @@
import type { LanguageModel } from "ai";
const mockResponses: Record<string, string> = {
default: "This is a mock response for testing.",
weather: "The weather in San Francisco is sunny and 72°F.",
greeting: "Hello! How can I help you today?",
};
const mockUsage = {
inputTokens: { total: 10, noCache: 10, cacheRead: 0, cacheWrite: 0 },
outputTokens: { total: 20, text: 20, reasoning: 0 },
};
function getResponseForPrompt(prompt: unknown): string {
const promptStr = JSON.stringify(prompt).toLowerCase();
if (promptStr.includes("weather") || promptStr.includes("temperature")) {
return mockResponses.weather;
}
if (
promptStr.includes("hello") ||
promptStr.includes("hi") ||
promptStr.includes("hey")
) {
return mockResponses.greeting;
}
return mockResponses.default;
}
const createMockModel = (): LanguageModel => {
return {
specificationVersion: "v3",
provider: "mock",
modelId: "mock-model",
defaultObjectGenerationMode: "tool",
supportedUrls: {},
doGenerate: async ({ prompt }: { prompt: unknown }) => ({
finishReason: "stop",
usage: mockUsage,
content: [{ type: "text", text: getResponseForPrompt(prompt) }],
warnings: [],
}),
doStream: ({ prompt }: { prompt: unknown }) => {
const response = getResponseForPrompt(prompt);
const words = response.split(" ");
return {
stream: new ReadableStream({
async start(controller) {
controller.enqueue({ type: "text-start", id: "t1" });
for (const word of words) {
controller.enqueue({
type: "text-delta",
id: "t1",
delta: `${word} `,
});
await new Promise((resolve) => {
setTimeout(resolve, 10);
});
}
controller.enqueue({ type: "text-end", id: "t1" });
controller.enqueue({
type: "finish",
finishReason: "stop",
usage: mockUsage,
});
controller.close();
},
}),
};
},
} as unknown as LanguageModel;
};
const createMockReasoningModel = (): LanguageModel => {
return {
specificationVersion: "v3",
provider: "mock",
modelId: "mock-reasoning-model",
defaultObjectGenerationMode: "tool",
supportedUrls: {},
doGenerate: async () => ({
finishReason: "stop",
usage: mockUsage,
content: [{ type: "text", text: "This is a reasoned response." }],
reasoning: [
{ type: "text", text: "Let me think through this step by step..." },
],
warnings: [],
}),
doStream: () => ({
stream: new ReadableStream({
async start(controller) {
controller.enqueue({ type: "reasoning-start", id: "r1" });
controller.enqueue({
type: "reasoning-delta",
id: "r1",
delta: "Let me think through this step by step... ",
});
controller.enqueue({ type: "reasoning-end", id: "r1" });
await new Promise((resolve) => {
setTimeout(resolve, 10);
});
controller.enqueue({ type: "text-start", id: "t1" });
controller.enqueue({
type: "text-delta",
id: "t1",
delta: "This is a reasoned response.",
});
controller.enqueue({ type: "text-end", id: "t1" });
controller.enqueue({
type: "finish",
finishReason: "stop",
usage: mockUsage,
});
controller.close();
},
}),
}),
} as unknown as LanguageModel;
};
const createMockTitleModel = (): LanguageModel => {
return {
specificationVersion: "v3",
provider: "mock",
modelId: "mock-title-model",
defaultObjectGenerationMode: "tool",
supportedUrls: {},
doGenerate: async () => ({
finishReason: "stop",
usage: {
inputTokens: { total: 5, noCache: 5, cacheRead: 0, cacheWrite: 0 },
outputTokens: { total: 5, text: 5, reasoning: 0 },
},
content: [{ type: "text", text: "Test Conversation" }],
warnings: [],
}),
doStream: () => ({
stream: new ReadableStream({
start(controller) {
controller.enqueue({ type: "text-start", id: "t1" });
controller.enqueue({
type: "text-delta",
id: "t1",
delta: "Test Conversation",
});
controller.enqueue({ type: "text-end", id: "t1" });
controller.enqueue({
type: "finish",
finishReason: "stop",
usage: {
inputTokens: {
total: 5,
noCache: 5,
cacheRead: 0,
cacheWrite: 0,
},
outputTokens: { total: 5, text: 5, reasoning: 0 },
},
});
controller.close();
},
}),
}),
} as unknown as LanguageModel;
};
export const chatModel = createMockModel();
export const reasoningModel = createMockReasoningModel();
export const titleModel = createMockTitleModel();
export const artifactModel = createMockModel();

View file

@ -0,0 +1,81 @@
import { simulateReadableStream } from "ai";
import { MockLanguageModelV3 } from "ai/test";
import { getResponseChunksByPrompt } from "@/tests/prompts/utils";
const mockUsage = {
inputTokens: { total: 10, noCache: 10, cacheRead: 0, cacheWrite: 0 },
outputTokens: { total: 20, text: 20, reasoning: 0 },
};
export const chatModel = new MockLanguageModelV3({
doGenerate: async () => ({
finishReason: "stop",
usage: mockUsage,
content: [{ type: "text", text: "Hello, world!" }],
warnings: [],
}),
doStream: async ({ prompt }) => ({
stream: simulateReadableStream({
chunkDelayInMs: 500,
initialDelayInMs: 1000,
chunks: getResponseChunksByPrompt(prompt),
}),
}),
});
export const reasoningModel = new MockLanguageModelV3({
doGenerate: async () => ({
finishReason: "stop",
usage: mockUsage,
content: [{ type: "text", text: "Hello, world!" }],
warnings: [],
}),
doStream: async ({ prompt }) => ({
stream: simulateReadableStream({
chunkDelayInMs: 500,
initialDelayInMs: 1000,
chunks: getResponseChunksByPrompt(prompt, true),
}),
}),
});
export const titleModel = new MockLanguageModelV3({
doGenerate: async () => ({
finishReason: "stop",
usage: mockUsage,
content: [{ type: "text", text: "This is a test title" }],
warnings: [],
}),
doStream: async () => ({
stream: simulateReadableStream({
chunkDelayInMs: 500,
initialDelayInMs: 1000,
chunks: [
{ id: "1", type: "text-start" },
{ id: "1", type: "text-delta", delta: "This is a test title" },
{ id: "1", type: "text-end" },
{
type: "finish",
finishReason: "stop",
usage: mockUsage,
},
],
}),
}),
});
export const artifactModel = new MockLanguageModelV3({
doGenerate: async () => ({
finishReason: "stop",
usage: mockUsage,
content: [{ type: "text", text: "Hello, world!" }],
warnings: [],
}),
doStream: async ({ prompt }) => ({
stream: simulateReadableStream({
chunkDelayInMs: 50,
initialDelayInMs: 100,
chunks: getResponseChunksByPrompt(prompt),
}),
}),
});

View file

@ -0,0 +1,43 @@
// Curated list of top models from Vercel AI Gateway
export const DEFAULT_CHAT_MODEL = "openai/gpt-4.1-mini";
export type ChatModel = {
id: string;
name: string;
provider: string;
description: string;
};
export const chatModels: ChatModel[] = [
// OpenAI
{
id: "openai/gpt-4.1-mini",
name: "GPT-4.1 Mini",
provider: "openai",
description: "Fast and cost-effective for simple tasks",
},
{
id: "openai/gpt-5.2",
name: "GPT-5.2",
provider: "openai",
description: "Most capable OpenAI model",
},
{
id: "openai/gpt-4o",
name: "GPT-4o",
provider: "openai",
description: "High intelligence and speed",
},
];
// Group models by provider for UI
export const modelsByProvider = chatModels.reduce(
(acc, model) => {
if (!acc[model.provider]) {
acc[model.provider] = [];
}
acc[model.provider].push(model);
return acc;
},
{} as Record<string, ChatModel[]>
);

View file

@ -0,0 +1,132 @@
import type { Geo } from "@vercel/functions";
import type { ArtifactKind } from "@/components/artifact";
export const artifactsPrompt = `
Artifacts is a special user interface mode that helps users with writing, editing, and other content creation tasks. When artifact is open, it is on the right side of the screen, while the conversation is on the left side. When creating or updating documents, changes are reflected in real-time on the artifacts and visible to the user.
When asked to write code, always use artifacts. When writing code, specify the language in the backticks, e.g. \`\`\`python\`code here\`\`\`. The default language is Python. Other languages are not yet supported, so let the user know if they request a different language.
DO NOT UPDATE DOCUMENTS IMMEDIATELY AFTER CREATING THEM. WAIT FOR USER FEEDBACK OR REQUEST TO UPDATE IT.
This is a guide for using artifacts tools: \`createDocument\` and \`updateDocument\`, which render content on a artifacts beside the conversation.
**When to use \`createDocument\`:**
- For substantial content (>10 lines) or code
- For content users will likely save/reuse (emails, code, essays, etc.)
- When explicitly requested to create a document
- For when content contains a single code snippet
**When NOT to use \`createDocument\`:**
- For informational/explanatory content
- For conversational responses
- When asked to keep it in chat
**Using \`updateDocument\`:**
- Default to full document rewrites for major changes
- Use targeted updates only for specific, isolated changes
- Follow user instructions for which parts to modify
**When NOT to use \`updateDocument\`:**
- Immediately after creating a document
Do not update document right after creating it. Wait for user feedback or request to update it.
**Using \`requestSuggestions\`:**
- ONLY use when the user explicitly asks for suggestions on an existing document
- Requires a valid document ID from a previously created document
- Never use for general questions or information requests
`;
export const regularPrompt = `You are a friendly assistant! Keep your responses concise and helpful.
When asked to write, create, or help with something, just do it directly. Don't ask clarifying questions unless absolutely necessary - make reasonable assumptions and proceed with the task.`;
export type RequestHints = {
latitude: Geo["latitude"];
longitude: Geo["longitude"];
city: Geo["city"];
country: Geo["country"];
};
export const getRequestPromptFromHints = (requestHints: RequestHints) => `\
About the origin of user's request:
- lat: ${requestHints.latitude}
- lon: ${requestHints.longitude}
- city: ${requestHints.city}
- country: ${requestHints.country}
`;
export const systemPrompt = ({
selectedChatModel,
requestHints,
}: {
selectedChatModel: string;
requestHints: RequestHints;
}) => {
const requestPrompt = getRequestPromptFromHints(requestHints);
// reasoning models don't need artifacts prompt (they can't use tools)
if (
selectedChatModel.includes("reasoning") ||
selectedChatModel.includes("thinking")
) {
return `${regularPrompt}\n\n${requestPrompt}`;
}
return `${regularPrompt}\n\n${requestPrompt}\n\n${artifactsPrompt}`;
};
export const codePrompt = `
You are a Python code generator that creates self-contained, executable code snippets. When writing code:
1. Each snippet should be complete and runnable on its own
2. Prefer using print() statements to display outputs
3. Include helpful comments explaining the code
4. Keep snippets concise (generally under 15 lines)
5. Avoid external dependencies - use Python standard library
6. Handle potential errors gracefully
7. Return meaningful output that demonstrates the code's functionality
8. Don't use input() or other interactive functions
9. Don't access files or network resources
10. Don't use infinite loops
Examples of good snippets:
# Calculate factorial iteratively
def factorial(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
print(f"Factorial of 5 is: {factorial(5)}")
`;
export const sheetPrompt = `
You are a spreadsheet creation assistant. Create a spreadsheet in csv format based on the given prompt. The spreadsheet should contain meaningful column headers and data.
`;
export const updateDocumentPrompt = (
currentContent: string | null,
type: ArtifactKind
) => {
let mediaType = "document";
if (type === "code") {
mediaType = "code snippet";
} else if (type === "sheet") {
mediaType = "spreadsheet";
}
return `Improve the following contents of the ${mediaType} based on the given prompt.
${currentContent}`;
};
export const titlePrompt = `Generate a very short chat title (2-5 words max) based on the user's message.
Rules:
- Maximum 30 characters
- No quotes, colons, hashtags, or markdown
- Just the topic/intent, not a full sentence
- If the message is a greeting like "hi" or "hello", respond with just "New conversation"
- Be concise: "Weather in NYC" not "User asking about the weather in New York City"`;

View file

@ -0,0 +1,69 @@
import { createOpenAI } from "@ai-sdk/openai";
import {
customProvider,
extractReasoningMiddleware,
wrapLanguageModel,
} from "ai";
import { isTestEnvironment } from "../constants";
const plano = createOpenAI({
baseURL: process.env.PLANO_BASE_URL || "http://localhost:12000/v1",
apiKey: process.env.AI_GATEWAY_API_KEY || "plano",
});
const THINKING_SUFFIX_REGEX = /-thinking$/;
export const myProvider = isTestEnvironment
? (() => {
const {
artifactModel,
chatModel,
reasoningModel,
titleModel,
} = require("./models.mock");
return customProvider({
languageModels: {
"chat-model": chatModel,
"chat-model-reasoning": reasoningModel,
"title-model": titleModel,
"artifact-model": artifactModel,
},
});
})()
: null;
export function getLanguageModel(modelId: string) {
if (isTestEnvironment && myProvider) {
return myProvider.languageModel(modelId);
}
const isReasoningModel =
modelId.includes("reasoning") || modelId.endsWith("-thinking");
if (isReasoningModel) {
const gatewayModelId = modelId.replace(THINKING_SUFFIX_REGEX, "");
return wrapLanguageModel({
model: plano(gatewayModelId),
middleware: extractReasoningMiddleware({ tagName: "thinking" }),
});
}
return plano(modelId);
}
export function getTitleModel() {
if (isTestEnvironment && myProvider) {
return myProvider.languageModel("title-model");
}
// Keep demo dependency-light: default to an OpenAI model so only OPENAI_API_KEY is required.
return plano("openai/gpt-4.1-mini");
}
export function getArtifactModel() {
if (isTestEnvironment && myProvider) {
return myProvider.languageModel("artifact-model");
}
// Keep demo dependency-light: default to an OpenAI model so only OPENAI_API_KEY is required.
return plano("openai/gpt-4o");
}

View file

@ -0,0 +1,74 @@
import { tool } from "ai";
import { z } from "zod";
export const getCurrencyExchange = tool({
description:
"Get current exchange rates between currencies using the Frankfurter API. You can convert between different currencies and get the latest exchange rates.",
inputSchema: z.object({
from: z
.string()
.describe(
"The base currency code (e.g., 'USD', 'EUR', 'GBP'). Defaults to USD if not provided."
)
.optional(),
to: z
.string()
.describe(
"The target currency code to convert to (e.g., 'USD', 'EUR', 'GBP'). If not provided, returns rates for all available currencies."
)
.optional(),
amount: z
.number()
.describe("The amount to convert. Defaults to 1 if not provided.")
.optional(),
}),
needsApproval: true,
execute: async (input) => {
const from = input.from?.toUpperCase() || "USD";
const amount = input.amount || 1;
const to = input.to?.toUpperCase();
try {
// Build the API URL
let url = `https://api.frankfurter.dev/v1/latest?base=${from}`;
if (to) {
url += `&symbols=${to}`;
}
const response = await fetch(url);
if (!response.ok) {
return {
error: `Failed to fetch exchange rates. Please check the currency codes and try again.`,
};
}
const data = await response.json();
// Calculate converted amounts if amount is provided
if (data.rates) {
const convertedRates: Record<string, number> = {};
for (const [currency, rate] of Object.entries(data.rates)) {
convertedRates[currency] = Number(
(amount * (rate as number)).toFixed(2)
);
}
return {
base: data.base,
date: data.date,
amount,
rates: data.rates,
convertedRates,
};
}
return data;
} catch {
return {
error:
"An error occurred while fetching exchange rates. Please try again.",
};
}
},
});

View file

@ -0,0 +1,79 @@
import { tool } from "ai";
import { z } from "zod";
async function geocodeCity(
city: string
): Promise<{ latitude: number; longitude: number } | null> {
try {
const response = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=en&format=json`
);
if (!response.ok) {
return null;
}
const data = await response.json();
if (!data.results || data.results.length === 0) {
return null;
}
const result = data.results[0];
return {
latitude: result.latitude,
longitude: result.longitude,
};
} catch {
return null;
}
}
export const getWeather = tool({
description:
"Get the current weather at a location. You can provide either coordinates or a city name.",
inputSchema: z.object({
latitude: z.number().optional(),
longitude: z.number().optional(),
city: z
.string()
.describe("City name (e.g., 'San Francisco', 'New York', 'London')")
.optional(),
}),
needsApproval: true,
execute: async (input) => {
let latitude: number;
let longitude: number;
if (input.city) {
const coords = await geocodeCity(input.city);
if (!coords) {
return {
error: `Could not find coordinates for "${input.city}". Please check the city name.`,
};
}
latitude = coords.latitude;
longitude = coords.longitude;
} else if (input.latitude !== undefined && input.longitude !== undefined) {
latitude = input.latitude;
longitude = input.longitude;
} else {
return {
error:
"Please provide either a city name or both latitude and longitude coordinates.",
};
}
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto`
);
const weatherData = await response.json();
if ("city" in input) {
weatherData.cityName = input.city;
}
return weatherData;
},
});

View file

@ -0,0 +1,98 @@
import type { UIMessageStreamWriter } from "ai";
import type { Session } from "next-auth";
import { codeDocumentHandler } from "@/artifacts/code/server";
import { sheetDocumentHandler } from "@/artifacts/sheet/server";
import { textDocumentHandler } from "@/artifacts/text/server";
import type { ArtifactKind } from "@/components/artifact";
import { saveDocument } from "../db/queries";
import type { Document } from "../db/schema";
import type { ChatMessage } from "../types";
export type SaveDocumentProps = {
id: string;
title: string;
kind: ArtifactKind;
content: string;
userId: string;
};
export type CreateDocumentCallbackProps = {
id: string;
title: string;
dataStream: UIMessageStreamWriter<ChatMessage>;
session: Session;
};
export type UpdateDocumentCallbackProps = {
document: Document;
description: string;
dataStream: UIMessageStreamWriter<ChatMessage>;
session: Session;
};
export type DocumentHandler<T = ArtifactKind> = {
kind: T;
onCreateDocument: (args: CreateDocumentCallbackProps) => Promise<void>;
onUpdateDocument: (args: UpdateDocumentCallbackProps) => Promise<void>;
};
export function createDocumentHandler<T extends ArtifactKind>(config: {
kind: T;
onCreateDocument: (params: CreateDocumentCallbackProps) => Promise<string>;
onUpdateDocument: (params: UpdateDocumentCallbackProps) => Promise<string>;
}): DocumentHandler<T> {
return {
kind: config.kind,
onCreateDocument: async (args: CreateDocumentCallbackProps) => {
const draftContent = await config.onCreateDocument({
id: args.id,
title: args.title,
dataStream: args.dataStream,
session: args.session,
});
if (args.session?.user?.id) {
await saveDocument({
id: args.id,
title: args.title,
content: draftContent,
kind: config.kind,
userId: args.session.user.id,
});
}
return;
},
onUpdateDocument: async (args: UpdateDocumentCallbackProps) => {
const draftContent = await config.onUpdateDocument({
document: args.document,
description: args.description,
dataStream: args.dataStream,
session: args.session,
});
if (args.session?.user?.id) {
await saveDocument({
id: args.document.id,
title: args.document.title,
content: draftContent,
kind: config.kind,
userId: args.session.user.id,
});
}
return;
},
};
}
/*
* Use this array to define the document handlers for each artifact kind.
*/
export const documentHandlersByArtifactKind: DocumentHandler[] = [
textDocumentHandler,
codeDocumentHandler,
sheetDocumentHandler,
];
export const artifactKinds = ["text", "code", "sheet"] as const;

View file

@ -0,0 +1,13 @@
import { generateDummyPassword } from "./db/utils";
export const isProductionEnvironment = process.env.NODE_ENV === "production";
export const isDevelopmentEnvironment = process.env.NODE_ENV === "development";
export const isTestEnvironment = Boolean(
process.env.PLAYWRIGHT_TEST_BASE_URL ||
process.env.PLAYWRIGHT ||
process.env.CI_PLAYWRIGHT
);
export const guestRegex = /^guest-\d+$/;
export const DUMMY_PASSWORD = generateDummyPassword();

View file

@ -0,0 +1,253 @@
// This is a helper for an older version of ai, v4.3.13
// import { config } from 'dotenv';
// import postgres from 'postgres';
// import {
// chat,
// message,
// type MessageDeprecated,
// messageDeprecated,
// vote,
// voteDeprecated,
// } from '../schema';
// import { drizzle } from 'drizzle-orm/postgres-js';
// import { inArray } from 'drizzle-orm';
// import { appendResponseMessages, type UIMessage } from 'ai';
// config({
// path: '.env.local',
// });
// if (!process.env.POSTGRES_URL) {
// throw new Error('POSTGRES_URL environment variable is not set');
// }
// const client = postgres(process.env.POSTGRES_URL);
// const db = drizzle(client);
// const BATCH_SIZE = 100; // Process 100 chats at a time
// const INSERT_BATCH_SIZE = 1000; // Insert 1000 messages at a time
// type NewMessageInsert = {
// id: string;
// chatId: string;
// parts: any[];
// role: string;
// attachments: any[];
// createdAt: Date;
// };
// type NewVoteInsert = {
// messageId: string;
// chatId: string;
// isUpvoted: boolean;
// };
// interface MessageDeprecatedContentPart {
// type: string;
// content: unknown;
// }
// function getMessageRank(message: MessageDeprecated): number {
// if (
// message.role === 'assistant' &&
// (message.content as MessageDeprecatedContentPart[]).some(
// (contentPart) => contentPart.type === 'tool-call',
// )
// ) {
// return 0;
// }
// if (
// message.role === 'tool' &&
// (message.content as MessageDeprecatedContentPart[]).some(
// (contentPart) => contentPart.type === 'tool-result',
// )
// ) {
// return 1;
// }
// if (message.role === 'assistant') {
// return 2;
// }
// return 3;
// }
// function dedupeParts<T extends { type: string; [k: string]: any }>(
// parts: T[],
// ): T[] {
// const seen = new Set<string>();
// return parts.filter((p) => {
// const key = `${p.type}|${JSON.stringify(p.content ?? p)}`;
// if (seen.has(key)) return false;
// seen.add(key);
// return true;
// });
// }
// function sanitizeParts<T extends { type: string; [k: string]: any }>(
// parts: T[],
// ): T[] {
// return parts.filter(
// (part) => !(part.type === 'reasoning' && part.reasoning === 'undefined'),
// );
// }
// async function migrateMessages() {
// const chats = await db.select().from(chat);
// let processedCount = 0;
// for (let i = 0; i < chats.length; i += BATCH_SIZE) {
// const chatBatch = chats.slice(i, i + BATCH_SIZE);
// const chatIds = chatBatch.map((chat) => chat.id);
// const allMessages = await db
// .select()
// .from(messageDeprecated)
// .where(inArray(messageDeprecated.chatId, chatIds));
// const allVotes = await db
// .select()
// .from(voteDeprecated)
// .where(inArray(voteDeprecated.chatId, chatIds));
// const newMessagesToInsert: NewMessageInsert[] = [];
// const newVotesToInsert: NewVoteInsert[] = [];
// for (const chat of chatBatch) {
// processedCount++;
// console.info(`Processed ${processedCount}/${chats.length} chats`);
// const messages = allMessages
// .filter((message) => message.chatId === chat.id)
// .sort((a, b) => {
// const differenceInTime =
// new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
// if (differenceInTime !== 0) return differenceInTime;
// return getMessageRank(a) - getMessageRank(b);
// });
// const votes = allVotes.filter((v) => v.chatId === chat.id);
// const messageSection: Array<UIMessage> = [];
// const messageSections: Array<Array<UIMessage>> = [];
// for (const message of messages) {
// const { role } = message;
// if (role === 'user' && messageSection.length > 0) {
// messageSections.push([...messageSection]);
// messageSection.length = 0;
// }
// // @ts-expect-error message.content has different type
// messageSection.push(message);
// }
// if (messageSection.length > 0) {
// messageSections.push([...messageSection]);
// }
// for (const section of messageSections) {
// const [userMessage, ...assistantMessages] = section;
// const [firstAssistantMessage] = assistantMessages;
// try {
// const uiSection = appendResponseMessages({
// messages: [userMessage],
// // @ts-expect-error: message.content has different type
// responseMessages: assistantMessages,
// _internal: {
// currentDate: () => firstAssistantMessage.createdAt ?? new Date(),
// },
// });
// const projectedUISection = uiSection
// .map((message) => {
// if (message.role === 'user') {
// return {
// id: message.id,
// chatId: chat.id,
// parts: [{ type: 'text', text: message.content }],
// role: message.role,
// createdAt: message.createdAt,
// attachments: [],
// } as NewMessageInsert;
// } else if (message.role === 'assistant') {
// const cleanParts = sanitizeParts(
// dedupeParts(message.parts || []),
// );
// return {
// id: message.id,
// chatId: chat.id,
// parts: cleanParts,
// role: message.role,
// createdAt: message.createdAt,
// attachments: [],
// } as NewMessageInsert;
// }
// return null;
// })
// .filter((msg): msg is NewMessageInsert => msg !== null);
// for (const msg of projectedUISection) {
// newMessagesToInsert.push(msg);
// if (msg.role === 'assistant') {
// const voteByMessage = votes.find((v) => v.messageId === msg.id);
// if (voteByMessage) {
// newVotesToInsert.push({
// messageId: msg.id,
// chatId: msg.chatId,
// isUpvoted: voteByMessage.isUpvoted,
// });
// }
// }
// }
// } catch (error) {
// console.error(`Error processing chat ${chat.id}: ${error}`);
// }
// }
// }
// for (let j = 0; j < newMessagesToInsert.length; j += INSERT_BATCH_SIZE) {
// const messageBatch = newMessagesToInsert.slice(j, j + INSERT_BATCH_SIZE);
// if (messageBatch.length > 0) {
// const validMessageBatch = messageBatch.map((msg) => ({
// id: msg.id,
// chatId: msg.chatId,
// parts: msg.parts,
// role: msg.role,
// attachments: msg.attachments,
// createdAt: msg.createdAt,
// }));
// await db.insert(message).values(validMessageBatch);
// }
// }
// for (let j = 0; j < newVotesToInsert.length; j += INSERT_BATCH_SIZE) {
// const voteBatch = newVotesToInsert.slice(j, j + INSERT_BATCH_SIZE);
// if (voteBatch.length > 0) {
// await db.insert(vote).values(voteBatch);
// }
// }
// }
// console.info(`Migration completed: ${processedCount} chats processed`);
// }
// migrateMessages()
// .then(() => {
// console.info('Script completed successfully');
// process.exit(0);
// })
// .catch((error) => {
// console.error('Script failed:', error);
// process.exit(1);
// });

View file

@ -0,0 +1,33 @@
import { config } from "dotenv";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
config({
path: ".env.local",
});
const runMigrate = async () => {
if (!process.env.POSTGRES_URL) {
console.log("⏭️ POSTGRES_URL not defined, skipping migrations");
process.exit(0);
}
const connection = postgres(process.env.POSTGRES_URL, { max: 1 });
const db = drizzle(connection);
console.log("⏳ Running migrations...");
const start = Date.now();
await migrate(db, { migrationsFolder: "./lib/db/migrations" });
const end = Date.now();
console.log("✅ Migrations completed in", end - start, "ms");
process.exit(0);
};
runMigrate().catch((err) => {
console.error("❌ Migration failed");
console.error(err);
process.exit(1);
});

View file

@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS "Chat" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"createdAt" timestamp NOT NULL,
"messages" json NOT NULL,
"userId" uuid NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "User" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"email" varchar(64) NOT NULL,
"password" varchar(64)
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Chat" ADD CONSTRAINT "Chat_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS "Suggestion" (
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
"documentId" uuid NOT NULL,
"documentCreatedAt" timestamp NOT NULL,
"originalText" text NOT NULL,
"suggestedText" text NOT NULL,
"description" text,
"isResolved" boolean DEFAULT false NOT NULL,
"userId" uuid NOT NULL,
"createdAt" timestamp NOT NULL,
CONSTRAINT "Suggestion_id_pk" PRIMARY KEY("id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "Document" (
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
"createdAt" timestamp NOT NULL,
"title" text NOT NULL,
"content" text,
"userId" uuid NOT NULL,
CONSTRAINT "Document_id_createdAt_pk" PRIMARY KEY("id","createdAt")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Suggestion" ADD CONSTRAINT "Suggestion_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Suggestion" ADD CONSTRAINT "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk" FOREIGN KEY ("documentId","documentCreatedAt") REFERENCES "public"."Document"("id","createdAt") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Document" ADD CONSTRAINT "Document_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,35 @@
CREATE TABLE IF NOT EXISTS "Message" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"chatId" uuid NOT NULL,
"role" varchar NOT NULL,
"content" json NOT NULL,
"createdAt" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "Vote" (
"chatId" uuid NOT NULL,
"messageId" uuid NOT NULL,
"isUpvoted" boolean NOT NULL,
CONSTRAINT "Vote_chatId_messageId_pk" PRIMARY KEY("chatId","messageId")
);
--> statement-breakpoint
ALTER TABLE "Chat" ADD COLUMN "title" text NOT NULL;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Message" ADD CONSTRAINT "Message_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Vote" ADD CONSTRAINT "Vote_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Vote" ADD CONSTRAINT "Vote_messageId_Message_id_fk" FOREIGN KEY ("messageId") REFERENCES "public"."Message"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "Chat" DROP COLUMN IF EXISTS "messages";

View file

@ -0,0 +1 @@
ALTER TABLE "Chat" ADD COLUMN "visibility" varchar DEFAULT 'private' NOT NULL;

View file

@ -0,0 +1 @@
ALTER TABLE "Document" ADD COLUMN "text" varchar DEFAULT 'text' NOT NULL;

View file

@ -0,0 +1,33 @@
CREATE TABLE IF NOT EXISTS "Message_v2" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"chatId" uuid NOT NULL,
"role" varchar NOT NULL,
"parts" json NOT NULL,
"attachments" json NOT NULL,
"createdAt" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "Vote_v2" (
"chatId" uuid NOT NULL,
"messageId" uuid NOT NULL,
"isUpvoted" boolean NOT NULL,
CONSTRAINT "Vote_v2_chatId_messageId_pk" PRIMARY KEY("chatId","messageId")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Message_v2" ADD CONSTRAINT "Message_v2_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Vote_v2" ADD CONSTRAINT "Vote_v2_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Vote_v2" ADD CONSTRAINT "Vote_v2_messageId_Message_v2_id_fk" FOREIGN KEY ("messageId") REFERENCES "public"."Message_v2"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS "Stream" (
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
"chatId" uuid NOT NULL,
"createdAt" timestamp NOT NULL,
CONSTRAINT "Stream_id_pk" PRIMARY KEY("id")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Stream" ADD CONSTRAINT "Stream_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1 @@
ALTER TABLE "Chat" ADD COLUMN "lastContext" jsonb;

View file

@ -0,0 +1,90 @@
{
"id": "715ec9ec-6715-4d0f-9f6c-9b5c7f09827c",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.Chat": {
"name": "Chat",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"messages": {
"name": "messages",
"type": "json",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Chat_userId_User_id_fk": {
"name": "Chat_userId_User_id_fk",
"tableFrom": "Chat",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,236 @@
{
"id": "f3d3437c-4735-4c91-80af-1014048a904e",
"prevId": "715ec9ec-6715-4d0f-9f6c-9b5c7f09827c",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.Suggestion": {
"name": "Suggestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"documentId": {
"name": "documentId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"documentCreatedAt": {
"name": "documentCreatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"originalText": {
"name": "originalText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"suggestedText": {
"name": "suggestedText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isResolved": {
"name": "isResolved",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Suggestion_userId_User_id_fk": {
"name": "Suggestion_userId_User_id_fk",
"tableFrom": "Suggestion",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
"tableFrom": "Suggestion",
"tableTo": "Document",
"columnsFrom": ["documentId", "documentCreatedAt"],
"columnsTo": ["id", "createdAt"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Suggestion_id_pk": {
"name": "Suggestion_id_pk",
"columns": ["id"]
}
},
"uniqueConstraints": {}
},
"public.Chat": {
"name": "Chat",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"messages": {
"name": "messages",
"type": "json",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Chat_userId_User_id_fk": {
"name": "Chat_userId_User_id_fk",
"tableFrom": "Chat",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Document": {
"name": "Document",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Document_userId_User_id_fk": {
"name": "Document_userId_User_id_fk",
"tableFrom": "Document",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Document_id_createdAt_pk": {
"name": "Document_id_createdAt_pk",
"columns": ["id", "createdAt"]
}
},
"uniqueConstraints": {}
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,339 @@
{
"id": "b5d8e862-936f-4419-a50f-97be3e7fe665",
"prevId": "f3d3437c-4735-4c91-80af-1014048a904e",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.Suggestion": {
"name": "Suggestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"documentId": {
"name": "documentId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"documentCreatedAt": {
"name": "documentCreatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"originalText": {
"name": "originalText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"suggestedText": {
"name": "suggestedText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isResolved": {
"name": "isResolved",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Suggestion_userId_User_id_fk": {
"name": "Suggestion_userId_User_id_fk",
"tableFrom": "Suggestion",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
"tableFrom": "Suggestion",
"tableTo": "Document",
"columnsFrom": ["documentId", "documentCreatedAt"],
"columnsTo": ["id", "createdAt"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Suggestion_id_pk": {
"name": "Suggestion_id_pk",
"columns": ["id"]
}
},
"uniqueConstraints": {}
},
"public.Chat": {
"name": "Chat",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Chat_userId_User_id_fk": {
"name": "Chat_userId_User_id_fk",
"tableFrom": "Chat",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Document": {
"name": "Document",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Document_userId_User_id_fk": {
"name": "Document_userId_User_id_fk",
"tableFrom": "Document",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Document_id_createdAt_pk": {
"name": "Document_id_createdAt_pk",
"columns": ["id", "createdAt"]
}
},
"uniqueConstraints": {}
},
"public.Message": {
"name": "Message",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_chatId_Chat_id_fk": {
"name": "Message_chatId_Chat_id_fk",
"tableFrom": "Message",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Vote": {
"name": "Vote",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_chatId_Chat_id_fk": {
"name": "Vote_chatId_Chat_id_fk",
"tableFrom": "Vote",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_messageId_Message_id_fk": {
"name": "Vote_messageId_Message_id_fk",
"tableFrom": "Vote",
"tableTo": "Message",
"columnsFrom": ["messageId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_chatId_messageId_pk": {
"name": "Vote_chatId_messageId_pk",
"columns": ["chatId", "messageId"]
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,346 @@
{
"id": "011efa9e-42c7-4ff6-830a-02106f6638c9",
"prevId": "b5d8e862-936f-4419-a50f-97be3e7fe665",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.Chat": {
"name": "Chat",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"visibility": {
"name": "visibility",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'private'"
}
},
"indexes": {},
"foreignKeys": {
"Chat_userId_User_id_fk": {
"name": "Chat_userId_User_id_fk",
"tableFrom": "Chat",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Document": {
"name": "Document",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Document_userId_User_id_fk": {
"name": "Document_userId_User_id_fk",
"tableFrom": "Document",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Document_id_createdAt_pk": {
"name": "Document_id_createdAt_pk",
"columns": ["id", "createdAt"]
}
},
"uniqueConstraints": {}
},
"public.Message": {
"name": "Message",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_chatId_Chat_id_fk": {
"name": "Message_chatId_Chat_id_fk",
"tableFrom": "Message",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Suggestion": {
"name": "Suggestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"documentId": {
"name": "documentId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"documentCreatedAt": {
"name": "documentCreatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"originalText": {
"name": "originalText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"suggestedText": {
"name": "suggestedText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isResolved": {
"name": "isResolved",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Suggestion_userId_User_id_fk": {
"name": "Suggestion_userId_User_id_fk",
"tableFrom": "Suggestion",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
"tableFrom": "Suggestion",
"tableTo": "Document",
"columnsFrom": ["documentId", "documentCreatedAt"],
"columnsTo": ["id", "createdAt"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Suggestion_id_pk": {
"name": "Suggestion_id_pk",
"columns": ["id"]
}
},
"uniqueConstraints": {}
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Vote": {
"name": "Vote",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_chatId_Chat_id_fk": {
"name": "Vote_chatId_Chat_id_fk",
"tableFrom": "Vote",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_messageId_Message_id_fk": {
"name": "Vote_messageId_Message_id_fk",
"tableFrom": "Vote",
"tableTo": "Message",
"columnsFrom": ["messageId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_chatId_messageId_pk": {
"name": "Vote_chatId_messageId_pk",
"columns": ["chatId", "messageId"]
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,353 @@
{
"id": "30ad8233-1432-428b-93fc-2bb1ba867ff1",
"prevId": "011efa9e-42c7-4ff6-830a-02106f6638c9",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.Chat": {
"name": "Chat",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"visibility": {
"name": "visibility",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'private'"
}
},
"indexes": {},
"foreignKeys": {
"Chat_userId_User_id_fk": {
"name": "Chat_userId_User_id_fk",
"tableFrom": "Chat",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Document": {
"name": "Document",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"text": {
"name": "text",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'text'"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Document_userId_User_id_fk": {
"name": "Document_userId_User_id_fk",
"tableFrom": "Document",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Document_id_createdAt_pk": {
"name": "Document_id_createdAt_pk",
"columns": ["id", "createdAt"]
}
},
"uniqueConstraints": {}
},
"public.Message": {
"name": "Message",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_chatId_Chat_id_fk": {
"name": "Message_chatId_Chat_id_fk",
"tableFrom": "Message",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Suggestion": {
"name": "Suggestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"documentId": {
"name": "documentId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"documentCreatedAt": {
"name": "documentCreatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"originalText": {
"name": "originalText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"suggestedText": {
"name": "suggestedText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isResolved": {
"name": "isResolved",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Suggestion_userId_User_id_fk": {
"name": "Suggestion_userId_User_id_fk",
"tableFrom": "Suggestion",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
"tableFrom": "Suggestion",
"tableTo": "Document",
"columnsFrom": ["documentId", "documentCreatedAt"],
"columnsTo": ["id", "createdAt"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Suggestion_id_pk": {
"name": "Suggestion_id_pk",
"columns": ["id"]
}
},
"uniqueConstraints": {}
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Vote": {
"name": "Vote",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_chatId_Chat_id_fk": {
"name": "Vote_chatId_Chat_id_fk",
"tableFrom": "Vote",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_messageId_Message_id_fk": {
"name": "Vote_messageId_Message_id_fk",
"tableFrom": "Vote",
"tableTo": "Message",
"columnsFrom": ["messageId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_chatId_messageId_pk": {
"name": "Vote_chatId_messageId_pk",
"columns": ["chatId", "messageId"]
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,462 @@
{
"id": "c6c102e6-b64e-4f0c-a7a6-32df758de437",
"prevId": "30ad8233-1432-428b-93fc-2bb1ba867ff1",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.Chat": {
"name": "Chat",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"visibility": {
"name": "visibility",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'private'"
}
},
"indexes": {},
"foreignKeys": {
"Chat_userId_User_id_fk": {
"name": "Chat_userId_User_id_fk",
"tableFrom": "Chat",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Document": {
"name": "Document",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"text": {
"name": "text",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'text'"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Document_userId_User_id_fk": {
"name": "Document_userId_User_id_fk",
"tableFrom": "Document",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Document_id_createdAt_pk": {
"name": "Document_id_createdAt_pk",
"columns": ["id", "createdAt"]
}
},
"uniqueConstraints": {}
},
"public.Message_v2": {
"name": "Message_v2",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"parts": {
"name": "parts",
"type": "json",
"primaryKey": false,
"notNull": true
},
"attachments": {
"name": "attachments",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_v2_chatId_Chat_id_fk": {
"name": "Message_v2_chatId_Chat_id_fk",
"tableFrom": "Message_v2",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Message": {
"name": "Message",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_chatId_Chat_id_fk": {
"name": "Message_chatId_Chat_id_fk",
"tableFrom": "Message",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Suggestion": {
"name": "Suggestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"documentId": {
"name": "documentId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"documentCreatedAt": {
"name": "documentCreatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"originalText": {
"name": "originalText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"suggestedText": {
"name": "suggestedText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isResolved": {
"name": "isResolved",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Suggestion_userId_User_id_fk": {
"name": "Suggestion_userId_User_id_fk",
"tableFrom": "Suggestion",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
"tableFrom": "Suggestion",
"tableTo": "Document",
"columnsFrom": ["documentId", "documentCreatedAt"],
"columnsTo": ["id", "createdAt"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Suggestion_id_pk": {
"name": "Suggestion_id_pk",
"columns": ["id"]
}
},
"uniqueConstraints": {}
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Vote_v2": {
"name": "Vote_v2",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_v2_chatId_Chat_id_fk": {
"name": "Vote_v2_chatId_Chat_id_fk",
"tableFrom": "Vote_v2",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_v2_messageId_Message_v2_id_fk": {
"name": "Vote_v2_messageId_Message_v2_id_fk",
"tableFrom": "Vote_v2",
"tableTo": "Message_v2",
"columnsFrom": ["messageId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_v2_chatId_messageId_pk": {
"name": "Vote_v2_chatId_messageId_pk",
"columns": ["chatId", "messageId"]
}
},
"uniqueConstraints": {}
},
"public.Vote": {
"name": "Vote",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_chatId_Chat_id_fk": {
"name": "Vote_chatId_Chat_id_fk",
"tableFrom": "Vote",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_messageId_Message_id_fk": {
"name": "Vote_messageId_Message_id_fk",
"tableFrom": "Vote",
"tableTo": "Message",
"columnsFrom": ["messageId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_chatId_messageId_pk": {
"name": "Vote_chatId_messageId_pk",
"columns": ["chatId", "messageId"]
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,506 @@
{
"id": "443de550-b7e8-4bfb-b229-c12dc6c132f0",
"prevId": "c6c102e6-b64e-4f0c-a7a6-32df758de437",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.Chat": {
"name": "Chat",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"visibility": {
"name": "visibility",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'private'"
}
},
"indexes": {},
"foreignKeys": {
"Chat_userId_User_id_fk": {
"name": "Chat_userId_User_id_fk",
"tableFrom": "Chat",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Document": {
"name": "Document",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"text": {
"name": "text",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'text'"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Document_userId_User_id_fk": {
"name": "Document_userId_User_id_fk",
"tableFrom": "Document",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Document_id_createdAt_pk": {
"name": "Document_id_createdAt_pk",
"columns": ["id", "createdAt"]
}
},
"uniqueConstraints": {}
},
"public.Message_v2": {
"name": "Message_v2",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"parts": {
"name": "parts",
"type": "json",
"primaryKey": false,
"notNull": true
},
"attachments": {
"name": "attachments",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_v2_chatId_Chat_id_fk": {
"name": "Message_v2_chatId_Chat_id_fk",
"tableFrom": "Message_v2",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Message": {
"name": "Message",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_chatId_Chat_id_fk": {
"name": "Message_chatId_Chat_id_fk",
"tableFrom": "Message",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Stream": {
"name": "Stream",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Stream_chatId_Chat_id_fk": {
"name": "Stream_chatId_Chat_id_fk",
"tableFrom": "Stream",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Stream_id_pk": {
"name": "Stream_id_pk",
"columns": ["id"]
}
},
"uniqueConstraints": {}
},
"public.Suggestion": {
"name": "Suggestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"documentId": {
"name": "documentId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"documentCreatedAt": {
"name": "documentCreatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"originalText": {
"name": "originalText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"suggestedText": {
"name": "suggestedText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isResolved": {
"name": "isResolved",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Suggestion_userId_User_id_fk": {
"name": "Suggestion_userId_User_id_fk",
"tableFrom": "Suggestion",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
"tableFrom": "Suggestion",
"tableTo": "Document",
"columnsFrom": ["documentId", "documentCreatedAt"],
"columnsTo": ["id", "createdAt"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Suggestion_id_pk": {
"name": "Suggestion_id_pk",
"columns": ["id"]
}
},
"uniqueConstraints": {}
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Vote_v2": {
"name": "Vote_v2",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_v2_chatId_Chat_id_fk": {
"name": "Vote_v2_chatId_Chat_id_fk",
"tableFrom": "Vote_v2",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_v2_messageId_Message_v2_id_fk": {
"name": "Vote_v2_messageId_Message_v2_id_fk",
"tableFrom": "Vote_v2",
"tableTo": "Message_v2",
"columnsFrom": ["messageId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_v2_chatId_messageId_pk": {
"name": "Vote_v2_chatId_messageId_pk",
"columns": ["chatId", "messageId"]
}
},
"uniqueConstraints": {}
},
"public.Vote": {
"name": "Vote",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_chatId_Chat_id_fk": {
"name": "Vote_chatId_Chat_id_fk",
"tableFrom": "Vote",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_messageId_Message_id_fk": {
"name": "Vote_messageId_Message_id_fk",
"tableFrom": "Vote",
"tableTo": "Message",
"columnsFrom": ["messageId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_chatId_messageId_pk": {
"name": "Vote_chatId_messageId_pk",
"columns": ["chatId", "messageId"]
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,512 @@
{
"id": "097660a7-976a-4b3e-8ebb-79312e3ece6f",
"prevId": "443de550-b7e8-4bfb-b229-c12dc6c132f0",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.Chat": {
"name": "Chat",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"visibility": {
"name": "visibility",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'private'"
},
"lastContext": {
"name": "lastContext",
"type": "jsonb",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"Chat_userId_User_id_fk": {
"name": "Chat_userId_User_id_fk",
"tableFrom": "Chat",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Document": {
"name": "Document",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"text": {
"name": "text",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'text'"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Document_userId_User_id_fk": {
"name": "Document_userId_User_id_fk",
"tableFrom": "Document",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Document_id_createdAt_pk": {
"name": "Document_id_createdAt_pk",
"columns": ["id", "createdAt"]
}
},
"uniqueConstraints": {}
},
"public.Message_v2": {
"name": "Message_v2",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"parts": {
"name": "parts",
"type": "json",
"primaryKey": false,
"notNull": true
},
"attachments": {
"name": "attachments",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_v2_chatId_Chat_id_fk": {
"name": "Message_v2_chatId_Chat_id_fk",
"tableFrom": "Message_v2",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Message": {
"name": "Message",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_chatId_Chat_id_fk": {
"name": "Message_chatId_Chat_id_fk",
"tableFrom": "Message",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Stream": {
"name": "Stream",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Stream_chatId_Chat_id_fk": {
"name": "Stream_chatId_Chat_id_fk",
"tableFrom": "Stream",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Stream_id_pk": {
"name": "Stream_id_pk",
"columns": ["id"]
}
},
"uniqueConstraints": {}
},
"public.Suggestion": {
"name": "Suggestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"documentId": {
"name": "documentId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"documentCreatedAt": {
"name": "documentCreatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"originalText": {
"name": "originalText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"suggestedText": {
"name": "suggestedText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isResolved": {
"name": "isResolved",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Suggestion_userId_User_id_fk": {
"name": "Suggestion_userId_User_id_fk",
"tableFrom": "Suggestion",
"tableTo": "User",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
"tableFrom": "Suggestion",
"tableTo": "Document",
"columnsFrom": ["documentId", "documentCreatedAt"],
"columnsTo": ["id", "createdAt"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Suggestion_id_pk": {
"name": "Suggestion_id_pk",
"columns": ["id"]
}
},
"uniqueConstraints": {}
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Vote_v2": {
"name": "Vote_v2",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_v2_chatId_Chat_id_fk": {
"name": "Vote_v2_chatId_Chat_id_fk",
"tableFrom": "Vote_v2",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_v2_messageId_Message_v2_id_fk": {
"name": "Vote_v2_messageId_Message_v2_id_fk",
"tableFrom": "Vote_v2",
"tableTo": "Message_v2",
"columnsFrom": ["messageId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_v2_chatId_messageId_pk": {
"name": "Vote_v2_chatId_messageId_pk",
"columns": ["chatId", "messageId"]
}
},
"uniqueConstraints": {}
},
"public.Vote": {
"name": "Vote",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_chatId_Chat_id_fk": {
"name": "Vote_chatId_Chat_id_fk",
"tableFrom": "Vote",
"tableTo": "Chat",
"columnsFrom": ["chatId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_messageId_Message_id_fk": {
"name": "Vote_messageId_Message_id_fk",
"tableFrom": "Vote",
"tableTo": "Message",
"columnsFrom": ["messageId"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_chatId_messageId_pk": {
"name": "Vote_chatId_messageId_pk",
"columns": ["chatId", "messageId"]
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,62 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1728598022383,
"tag": "0000_keen_devos",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1730207363999,
"tag": "0001_sparkling_blue_marvel",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1730725226313,
"tag": "0002_wandering_riptide",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1733403031014,
"tag": "0003_cloudy_glorian",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1733945232355,
"tag": "0004_odd_slayback",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1741934630596,
"tag": "0005_wooden_whistler",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1746118166211,
"tag": "0006_marvelous_frog_thor",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1757362773211,
"tag": "0007_flowery_ben_parker",
"breakpoints": true
}
]
}

View file

@ -0,0 +1,602 @@
import "server-only";
import {
and,
asc,
count,
desc,
eq,
gt,
gte,
inArray,
lt,
type SQL,
} from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import type { ArtifactKind } from "@/components/artifact";
import type { VisibilityType } from "@/components/visibility-selector";
import { ChatSDKError } from "../errors";
import { generateUUID } from "../utils";
import {
type Chat,
chat,
type DBMessage,
document,
message,
type Suggestion,
stream,
suggestion,
type User,
user,
vote,
} from "./schema";
import { generateHashedPassword } from "./utils";
// Optionally, if not using email/pass login, you can
// use the Drizzle adapter for Auth.js / NextAuth
// https://authjs.dev/reference/adapter/drizzle
// biome-ignore lint: Forbidden non-null assertion.
const client = postgres(process.env.POSTGRES_URL!);
const db = drizzle(client);
export async function getUser(email: string): Promise<User[]> {
try {
return await db.select().from(user).where(eq(user.email, email));
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to get user by email"
);
}
}
export async function createUser(email: string, password: string) {
const hashedPassword = generateHashedPassword(password);
try {
return await db.insert(user).values({ email, password: hashedPassword });
} catch (_error) {
throw new ChatSDKError("bad_request:database", "Failed to create user");
}
}
export async function createGuestUser() {
const email = `guest-${Date.now()}`;
const password = generateHashedPassword(generateUUID());
try {
return await db.insert(user).values({ email, password }).returning({
id: user.id,
email: user.email,
});
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to create guest user"
);
}
}
export async function saveChat({
id,
userId,
title,
visibility,
}: {
id: string;
userId: string;
title: string;
visibility: VisibilityType;
}) {
try {
return await db.insert(chat).values({
id,
createdAt: new Date(),
userId,
title,
visibility,
});
} catch (_error) {
throw new ChatSDKError("bad_request:database", "Failed to save chat");
}
}
export async function deleteChatById({ id }: { id: string }) {
try {
await db.delete(vote).where(eq(vote.chatId, id));
await db.delete(message).where(eq(message.chatId, id));
await db.delete(stream).where(eq(stream.chatId, id));
const [chatsDeleted] = await db
.delete(chat)
.where(eq(chat.id, id))
.returning();
return chatsDeleted;
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to delete chat by id"
);
}
}
export async function deleteAllChatsByUserId({ userId }: { userId: string }) {
try {
const userChats = await db
.select({ id: chat.id })
.from(chat)
.where(eq(chat.userId, userId));
if (userChats.length === 0) {
return { deletedCount: 0 };
}
const chatIds = userChats.map((c) => c.id);
await db.delete(vote).where(inArray(vote.chatId, chatIds));
await db.delete(message).where(inArray(message.chatId, chatIds));
await db.delete(stream).where(inArray(stream.chatId, chatIds));
const deletedChats = await db
.delete(chat)
.where(eq(chat.userId, userId))
.returning();
return { deletedCount: deletedChats.length };
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to delete all chats by user id"
);
}
}
export async function getChatsByUserId({
id,
limit,
startingAfter,
endingBefore,
}: {
id: string;
limit: number;
startingAfter: string | null;
endingBefore: string | null;
}) {
try {
const extendedLimit = limit + 1;
const query = (whereCondition?: SQL<any>) =>
db
.select()
.from(chat)
.where(
whereCondition
? and(whereCondition, eq(chat.userId, id))
: eq(chat.userId, id)
)
.orderBy(desc(chat.createdAt))
.limit(extendedLimit);
let filteredChats: Chat[] = [];
if (startingAfter) {
const [selectedChat] = await db
.select()
.from(chat)
.where(eq(chat.id, startingAfter))
.limit(1);
if (!selectedChat) {
throw new ChatSDKError(
"not_found:database",
`Chat with id ${startingAfter} not found`
);
}
filteredChats = await query(gt(chat.createdAt, selectedChat.createdAt));
} else if (endingBefore) {
const [selectedChat] = await db
.select()
.from(chat)
.where(eq(chat.id, endingBefore))
.limit(1);
if (!selectedChat) {
throw new ChatSDKError(
"not_found:database",
`Chat with id ${endingBefore} not found`
);
}
filteredChats = await query(lt(chat.createdAt, selectedChat.createdAt));
} else {
filteredChats = await query();
}
const hasMore = filteredChats.length > limit;
return {
chats: hasMore ? filteredChats.slice(0, limit) : filteredChats,
hasMore,
};
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to get chats by user id"
);
}
}
export async function getChatById({ id }: { id: string }) {
try {
const [selectedChat] = await db.select().from(chat).where(eq(chat.id, id));
if (!selectedChat) {
return null;
}
return selectedChat;
} catch (_error) {
throw new ChatSDKError("bad_request:database", "Failed to get chat by id");
}
}
export async function saveMessages({ messages }: { messages: DBMessage[] }) {
try {
return await db.insert(message).values(messages);
} catch (_error) {
throw new ChatSDKError("bad_request:database", "Failed to save messages");
}
}
export async function updateMessage({
id,
parts,
}: {
id: string;
parts: DBMessage["parts"];
}) {
try {
return await db.update(message).set({ parts }).where(eq(message.id, id));
} catch (_error) {
throw new ChatSDKError("bad_request:database", "Failed to update message");
}
}
export async function getMessagesByChatId({ id }: { id: string }) {
try {
return await db
.select()
.from(message)
.where(eq(message.chatId, id))
.orderBy(asc(message.createdAt));
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to get messages by chat id"
);
}
}
export async function voteMessage({
chatId,
messageId,
type,
}: {
chatId: string;
messageId: string;
type: "up" | "down";
}) {
try {
const [existingVote] = await db
.select()
.from(vote)
.where(and(eq(vote.messageId, messageId)));
if (existingVote) {
return await db
.update(vote)
.set({ isUpvoted: type === "up" })
.where(and(eq(vote.messageId, messageId), eq(vote.chatId, chatId)));
}
return await db.insert(vote).values({
chatId,
messageId,
isUpvoted: type === "up",
});
} catch (_error) {
throw new ChatSDKError("bad_request:database", "Failed to vote message");
}
}
export async function getVotesByChatId({ id }: { id: string }) {
try {
return await db.select().from(vote).where(eq(vote.chatId, id));
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to get votes by chat id"
);
}
}
export async function saveDocument({
id,
title,
kind,
content,
userId,
}: {
id: string;
title: string;
kind: ArtifactKind;
content: string;
userId: string;
}) {
try {
return await db
.insert(document)
.values({
id,
title,
kind,
content,
userId,
createdAt: new Date(),
})
.returning();
} catch (_error) {
throw new ChatSDKError("bad_request:database", "Failed to save document");
}
}
export async function getDocumentsById({ id }: { id: string }) {
try {
const documents = await db
.select()
.from(document)
.where(eq(document.id, id))
.orderBy(asc(document.createdAt));
return documents;
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to get documents by id"
);
}
}
export async function getDocumentById({ id }: { id: string }) {
try {
const [selectedDocument] = await db
.select()
.from(document)
.where(eq(document.id, id))
.orderBy(desc(document.createdAt));
return selectedDocument;
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to get document by id"
);
}
}
export async function deleteDocumentsByIdAfterTimestamp({
id,
timestamp,
}: {
id: string;
timestamp: Date;
}) {
try {
await db
.delete(suggestion)
.where(
and(
eq(suggestion.documentId, id),
gt(suggestion.documentCreatedAt, timestamp)
)
);
return await db
.delete(document)
.where(and(eq(document.id, id), gt(document.createdAt, timestamp)))
.returning();
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to delete documents by id after timestamp"
);
}
}
export async function saveSuggestions({
suggestions,
}: {
suggestions: Suggestion[];
}) {
try {
return await db.insert(suggestion).values(suggestions);
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to save suggestions"
);
}
}
export async function getSuggestionsByDocumentId({
documentId,
}: {
documentId: string;
}) {
try {
return await db
.select()
.from(suggestion)
.where(eq(suggestion.documentId, documentId));
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to get suggestions by document id"
);
}
}
export async function getMessageById({ id }: { id: string }) {
try {
return await db.select().from(message).where(eq(message.id, id));
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to get message by id"
);
}
}
export async function deleteMessagesByChatIdAfterTimestamp({
chatId,
timestamp,
}: {
chatId: string;
timestamp: Date;
}) {
try {
const messagesToDelete = await db
.select({ id: message.id })
.from(message)
.where(
and(eq(message.chatId, chatId), gte(message.createdAt, timestamp))
);
const messageIds = messagesToDelete.map(
(currentMessage) => currentMessage.id
);
if (messageIds.length > 0) {
await db
.delete(vote)
.where(
and(eq(vote.chatId, chatId), inArray(vote.messageId, messageIds))
);
return await db
.delete(message)
.where(
and(eq(message.chatId, chatId), inArray(message.id, messageIds))
);
}
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to delete messages by chat id after timestamp"
);
}
}
export async function updateChatVisibilityById({
chatId,
visibility,
}: {
chatId: string;
visibility: "private" | "public";
}) {
try {
return await db.update(chat).set({ visibility }).where(eq(chat.id, chatId));
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to update chat visibility by id"
);
}
}
export async function updateChatTitleById({
chatId,
title,
}: {
chatId: string;
title: string;
}) {
try {
return await db.update(chat).set({ title }).where(eq(chat.id, chatId));
} catch (error) {
console.warn("Failed to update title for chat", chatId, error);
return;
}
}
export async function getMessageCountByUserId({
id,
differenceInHours,
}: {
id: string;
differenceInHours: number;
}) {
try {
const twentyFourHoursAgo = new Date(
Date.now() - differenceInHours * 60 * 60 * 1000
);
const [stats] = await db
.select({ count: count(message.id) })
.from(message)
.innerJoin(chat, eq(message.chatId, chat.id))
.where(
and(
eq(chat.userId, id),
gte(message.createdAt, twentyFourHoursAgo),
eq(message.role, "user")
)
)
.execute();
return stats?.count ?? 0;
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to get message count by user id"
);
}
}
export async function createStreamId({
streamId,
chatId,
}: {
streamId: string;
chatId: string;
}) {
try {
await db
.insert(stream)
.values({ id: streamId, chatId, createdAt: new Date() });
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to create stream id"
);
}
}
export async function getStreamIdsByChatId({ chatId }: { chatId: string }) {
try {
const streamIds = await db
.select({ id: stream.id })
.from(stream)
.where(eq(stream.chatId, chatId))
.orderBy(asc(stream.createdAt))
.execute();
return streamIds.map(({ id }) => id);
} catch (_error) {
throw new ChatSDKError(
"bad_request:database",
"Failed to get stream ids by chat id"
);
}
}

View file

@ -0,0 +1,170 @@
import type { InferSelectModel } from "drizzle-orm";
import {
boolean,
foreignKey,
json,
pgTable,
primaryKey,
text,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
export const user = pgTable("User", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
email: varchar("email", { length: 64 }).notNull(),
password: varchar("password", { length: 64 }),
});
export type User = InferSelectModel<typeof user>;
export const chat = pgTable("Chat", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
createdAt: timestamp("createdAt").notNull(),
title: text("title").notNull(),
userId: uuid("userId")
.notNull()
.references(() => user.id),
visibility: varchar("visibility", { enum: ["public", "private"] })
.notNull()
.default("private"),
});
export type Chat = InferSelectModel<typeof chat>;
// DEPRECATED: The following schema is deprecated and will be removed in the future.
// Read the migration guide at https://chat-sdk.dev/docs/migration-guides/message-parts
export const messageDeprecated = pgTable("Message", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
chatId: uuid("chatId")
.notNull()
.references(() => chat.id),
role: varchar("role").notNull(),
content: json("content").notNull(),
createdAt: timestamp("createdAt").notNull(),
});
export type MessageDeprecated = InferSelectModel<typeof messageDeprecated>;
export const message = pgTable("Message_v2", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
chatId: uuid("chatId")
.notNull()
.references(() => chat.id),
role: varchar("role").notNull(),
parts: json("parts").notNull(),
attachments: json("attachments").notNull(),
createdAt: timestamp("createdAt").notNull(),
});
export type DBMessage = InferSelectModel<typeof message>;
// DEPRECATED: The following schema is deprecated and will be removed in the future.
// Read the migration guide at https://chat-sdk.dev/docs/migration-guides/message-parts
export const voteDeprecated = pgTable(
"Vote",
{
chatId: uuid("chatId")
.notNull()
.references(() => chat.id),
messageId: uuid("messageId")
.notNull()
.references(() => messageDeprecated.id),
isUpvoted: boolean("isUpvoted").notNull(),
},
(table) => {
return {
pk: primaryKey({ columns: [table.chatId, table.messageId] }),
};
}
);
export type VoteDeprecated = InferSelectModel<typeof voteDeprecated>;
export const vote = pgTable(
"Vote_v2",
{
chatId: uuid("chatId")
.notNull()
.references(() => chat.id),
messageId: uuid("messageId")
.notNull()
.references(() => message.id),
isUpvoted: boolean("isUpvoted").notNull(),
},
(table) => {
return {
pk: primaryKey({ columns: [table.chatId, table.messageId] }),
};
}
);
export type Vote = InferSelectModel<typeof vote>;
export const document = pgTable(
"Document",
{
id: uuid("id").notNull().defaultRandom(),
createdAt: timestamp("createdAt").notNull(),
title: text("title").notNull(),
content: text("content"),
kind: varchar("text", { enum: ["text", "code", "image", "sheet"] })
.notNull()
.default("text"),
userId: uuid("userId")
.notNull()
.references(() => user.id),
},
(table) => {
return {
pk: primaryKey({ columns: [table.id, table.createdAt] }),
};
}
);
export type Document = InferSelectModel<typeof document>;
export const suggestion = pgTable(
"Suggestion",
{
id: uuid("id").notNull().defaultRandom(),
documentId: uuid("documentId").notNull(),
documentCreatedAt: timestamp("documentCreatedAt").notNull(),
originalText: text("originalText").notNull(),
suggestedText: text("suggestedText").notNull(),
description: text("description"),
isResolved: boolean("isResolved").notNull().default(false),
userId: uuid("userId")
.notNull()
.references(() => user.id),
createdAt: timestamp("createdAt").notNull(),
},
(table) => ({
pk: primaryKey({ columns: [table.id] }),
documentRef: foreignKey({
columns: [table.documentId, table.documentCreatedAt],
foreignColumns: [document.id, document.createdAt],
}),
})
);
export type Suggestion = InferSelectModel<typeof suggestion>;
export const stream = pgTable(
"Stream",
{
id: uuid("id").notNull().defaultRandom(),
chatId: uuid("chatId").notNull(),
createdAt: timestamp("createdAt").notNull(),
},
(table) => ({
pk: primaryKey({ columns: [table.id] }),
chatRef: foreignKey({
columns: [table.chatId],
foreignColumns: [chat.id],
}),
})
);
export type Stream = InferSelectModel<typeof stream>;

View file

@ -0,0 +1,16 @@
import { generateId } from "ai";
import { genSaltSync, hashSync } from "bcrypt-ts";
export function generateHashedPassword(password: string) {
const salt = genSaltSync(10);
const hash = hashSync(password, salt);
return hash;
}
export function generateDummyPassword() {
const password = generateId();
const hashedPassword = generateHashedPassword(password);
return hashedPassword;
}

View file

@ -0,0 +1,49 @@
import { textblockTypeInputRule } from "prosemirror-inputrules";
import { Schema } from "prosemirror-model";
import { schema } from "prosemirror-schema-basic";
import { addListNodes } from "prosemirror-schema-list";
import type { Transaction } from "prosemirror-state";
import type { EditorView } from "prosemirror-view";
import type { MutableRefObject } from "react";
import { buildContentFromDocument } from "./functions";
export const documentSchema = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks,
});
export function headingRule(level: number) {
return textblockTypeInputRule(
new RegExp(`^(#{1,${level}})\\s$`),
documentSchema.nodes.heading,
() => ({ level })
);
}
export const handleTransaction = ({
transaction,
editorRef,
onSaveContent,
}: {
transaction: Transaction;
editorRef: MutableRefObject<EditorView | null>;
onSaveContent: (updatedContent: string, debounce: boolean) => void;
}) => {
if (!editorRef || !editorRef.current) {
return;
}
const newState = editorRef.current.state.apply(transaction);
editorRef.current.updateState(newState);
if (transaction.docChanged && !transaction.getMeta("no-save")) {
const updatedContent = buildContentFromDocument(newState.doc);
if (transaction.getMeta("no-debounce")) {
onSaveContent(updatedContent, false);
} else {
onSaveContent(updatedContent, true);
}
}
};

View file

@ -0,0 +1,475 @@
// Modified from https://github.com/hamflx/prosemirror-diff/blob/master/src/diff.js
import { diff_match_patch } from "diff-match-patch";
import { Fragment, Node } from "prosemirror-model";
export const DiffType = {
Unchanged: 0,
Deleted: -1,
Inserted: 1,
};
export const patchDocumentNode = (schema, oldNode, newNode) => {
assertNodeTypeEqual(oldNode, newNode);
const finalLeftChildren = [];
const finalRightChildren = [];
const oldChildren = normalizeNodeContent(oldNode);
const newChildren = normalizeNodeContent(newNode);
const oldChildLen = oldChildren.length;
const newChildLen = newChildren.length;
const minChildLen = Math.min(oldChildLen, newChildLen);
let left = 0;
let right = 0;
for (; left < minChildLen; left++) {
const oldChild = oldChildren[left];
const newChild = newChildren[left];
if (!isNodeEqual(oldChild, newChild)) {
break;
}
finalLeftChildren.push(...ensureArray(oldChild));
}
for (; right + left + 1 < minChildLen; right++) {
const oldChild = oldChildren[oldChildLen - right - 1];
const newChild = newChildren[newChildLen - right - 1];
if (!isNodeEqual(oldChild, newChild)) {
break;
}
finalRightChildren.unshift(...ensureArray(oldChild));
}
const diffOldChildren = oldChildren.slice(left, oldChildLen - right);
const diffNewChildren = newChildren.slice(left, newChildLen - right);
if (diffOldChildren.length && diffNewChildren.length) {
const matchedNodes = matchNodes(
schema,
diffOldChildren,
diffNewChildren
).sort((a, b) => b.count - a.count);
const bestMatch = matchedNodes[0];
if (bestMatch) {
const { oldStartIndex, newStartIndex, oldEndIndex, newEndIndex } =
bestMatch;
const oldBeforeMatchChildren = diffOldChildren.slice(0, oldStartIndex);
const newBeforeMatchChildren = diffNewChildren.slice(0, newStartIndex);
finalLeftChildren.push(
...patchRemainNodes(
schema,
oldBeforeMatchChildren,
newBeforeMatchChildren
)
);
finalLeftChildren.push(
...diffOldChildren.slice(oldStartIndex, oldEndIndex)
);
const oldAfterMatchChildren = diffOldChildren.slice(oldEndIndex);
const newAfterMatchChildren = diffNewChildren.slice(newEndIndex);
finalRightChildren.unshift(
...patchRemainNodes(
schema,
oldAfterMatchChildren,
newAfterMatchChildren
)
);
} else {
finalLeftChildren.push(
...patchRemainNodes(schema, diffOldChildren, diffNewChildren)
);
}
} else {
finalLeftChildren.push(
...patchRemainNodes(schema, diffOldChildren, diffNewChildren)
);
}
return createNewNode(oldNode, [...finalLeftChildren, ...finalRightChildren]);
};
const matchNodes = (_schema, oldChildren, newChildren) => {
const matches = [];
for (
let oldStartIndex = 0;
oldStartIndex < oldChildren.length;
oldStartIndex++
) {
const oldStartNode = oldChildren[oldStartIndex];
const newStartIndex = findMatchNode(newChildren, oldStartNode);
if (newStartIndex !== -1) {
let oldEndIndex = oldStartIndex + 1;
let newEndIndex = newStartIndex + 1;
for (
;
oldEndIndex < oldChildren.length && newEndIndex < newChildren.length;
oldEndIndex++, newEndIndex++
) {
const oldEndNode = oldChildren[oldEndIndex];
if (!isNodeEqual(newChildren[newEndIndex], oldEndNode)) {
break;
}
}
matches.push({
oldStartIndex,
newStartIndex,
oldEndIndex,
newEndIndex,
count: newEndIndex - newStartIndex,
});
}
}
return matches;
};
const findMatchNode = (children, node, startIndex = 0) => {
for (let i = startIndex; i < children.length; i++) {
if (isNodeEqual(children[i], node)) {
return i;
}
}
return -1;
};
const patchRemainNodes = (schema, oldChildren, newChildren) => {
const finalLeftChildren = [];
const finalRightChildren = [];
const oldChildLen = oldChildren.length;
const newChildLen = newChildren.length;
let left = 0;
let right = 0;
while (oldChildLen - left - right > 0 && newChildLen - left - right > 0) {
const leftOldNode = oldChildren[left];
const leftNewNode = newChildren[left];
const rightOldNode = oldChildren[oldChildLen - right - 1];
const rightNewNode = newChildren[newChildLen - right - 1];
let updateLeft =
!isTextNode(leftOldNode) && matchNodeType(leftOldNode, leftNewNode);
let updateRight =
!isTextNode(rightOldNode) && matchNodeType(rightOldNode, rightNewNode);
if (Array.isArray(leftOldNode) && Array.isArray(leftNewNode)) {
finalLeftChildren.push(
...patchTextNodes(schema, leftOldNode, leftNewNode)
);
left += 1;
continue;
}
if (updateLeft && updateRight) {
const equalityLeft = computeChildEqualityFactor(leftOldNode, leftNewNode);
const equalityRight = computeChildEqualityFactor(
rightOldNode,
rightNewNode
);
if (equalityLeft < equalityRight) {
updateLeft = false;
} else {
updateRight = false;
}
}
if (updateLeft) {
finalLeftChildren.push(
patchDocumentNode(schema, leftOldNode, leftNewNode)
);
left += 1;
} else if (updateRight) {
finalRightChildren.unshift(
patchDocumentNode(schema, rightOldNode, rightNewNode)
);
right += 1;
} else {
// Delete and insert
finalLeftChildren.push(
createDiffNode(schema, leftOldNode, DiffType.Deleted)
);
finalLeftChildren.push(
createDiffNode(schema, leftNewNode, DiffType.Inserted)
);
left += 1;
}
}
const deleteNodeLen = oldChildLen - left - right;
const insertNodeLen = newChildLen - left - right;
if (deleteNodeLen) {
finalLeftChildren.push(
...oldChildren
.slice(left, left + deleteNodeLen)
.flat()
.map((node) => createDiffNode(schema, node, DiffType.Deleted))
);
}
if (insertNodeLen) {
finalRightChildren.unshift(
...newChildren
.slice(left, left + insertNodeLen)
.flat()
.map((node) => createDiffNode(schema, node, DiffType.Inserted))
);
}
return [...finalLeftChildren, ...finalRightChildren];
};
// Updated function to perform sentence-level diffs
export const patchTextNodes = (schema, oldNode, newNode) => {
const dmp = new diff_match_patch();
// Concatenate the text from the text nodes
const oldText = oldNode.map((n) => getNodeText(n)).join("");
const newText = newNode.map((n) => getNodeText(n)).join("");
// Tokenize the text into sentences
const oldSentences = tokenizeSentences(oldText);
const newSentences = tokenizeSentences(newText);
// Map sentences to unique characters
const { chars1, chars2, lineArray } = sentencesToChars(
oldSentences,
newSentences
);
// Perform the diff
let diffs = dmp.diff_main(chars1, chars2, false);
// Convert back to sentences
diffs = diffs.map(([type, text]) => {
const sentences = text
.split("")
.map((char) => lineArray[char.charCodeAt(0)]);
return [type, sentences];
});
// Map diffs to nodes
const res = diffs.flatMap(([type, sentences]) => {
return sentences.map((sentence) => {
const node = createTextNode(
schema,
sentence,
type !== DiffType.Unchanged ? [createDiffMark(schema, type)] : []
);
return node;
});
});
return res;
};
// Function to tokenize text into sentences
const tokenizeSentences = (text) => {
return text.match(/[^.!?]+[.!?]*\s*/g) || [];
};
// Function to map sentences to unique characters
const sentencesToChars = (oldSentences, newSentences) => {
const lineArray = [];
const lineHash = {};
let lineStart = 0;
const chars1 = oldSentences
.map((sentence) => {
const line = sentence;
if (line in lineHash) {
return String.fromCharCode(lineHash[line]);
}
lineHash[line] = lineStart;
lineArray[lineStart] = line;
lineStart++;
return String.fromCharCode(lineHash[line]);
})
.join("");
const chars2 = newSentences
.map((sentence) => {
const line = sentence;
if (line in lineHash) {
return String.fromCharCode(lineHash[line]);
}
lineHash[line] = lineStart;
lineArray[lineStart] = line;
lineStart++;
return String.fromCharCode(lineHash[line]);
})
.join("");
return { chars1, chars2, lineArray };
};
export const computeChildEqualityFactor = (_node1, _node2) => {
return 0;
};
export const assertNodeTypeEqual = (node1, node2) => {
if (getNodeProperty(node1, "type") !== getNodeProperty(node2, "type")) {
throw new Error(`node type not equal: ${node1.type} !== ${node2.type}`);
}
};
export const ensureArray = (value) => {
return Array.isArray(value) ? value : [value];
};
export const isNodeEqual = (node1, node2) => {
const isNode1Array = Array.isArray(node1);
const isNode2Array = Array.isArray(node2);
if (isNode1Array !== isNode2Array) {
return false;
}
if (isNode1Array) {
return (
node1.length === node2.length &&
node1.every((node, index) => isNodeEqual(node, node2[index]))
);
}
const type1 = getNodeProperty(node1, "type");
const type2 = getNodeProperty(node2, "type");
if (type1 !== type2) {
return false;
}
if (isTextNode(node1)) {
const text1 = getNodeProperty(node1, "text");
const text2 = getNodeProperty(node2, "text");
if (text1 !== text2) {
return false;
}
}
const attrs1 = getNodeAttributes(node1);
const attrs2 = getNodeAttributes(node2);
const attrs = [...new Set([...Object.keys(attrs1), ...Object.keys(attrs2)])];
for (const attr of attrs) {
if (attrs1[attr] !== attrs2[attr]) {
return false;
}
}
const marks1 = getNodeMarks(node1);
const marks2 = getNodeMarks(node2);
if (marks1.length !== marks2.length) {
return false;
}
for (let i = 0; i < marks1.length; i++) {
if (!isNodeEqual(marks1[i], marks2[i])) {
return false;
}
}
const children1 = getNodeChildren(node1);
const children2 = getNodeChildren(node2);
if (children1.length !== children2.length) {
return false;
}
for (let i = 0; i < children1.length; i++) {
if (!isNodeEqual(children1[i], children2[i])) {
return false;
}
}
return true;
};
export const normalizeNodeContent = (node) => {
const content = getNodeChildren(node) ?? [];
const res = [];
for (let i = 0; i < content.length; i++) {
const child = content[i];
if (isTextNode(child)) {
const textNodes = [];
for (
let textNode = content[i];
i < content.length && isTextNode(textNode);
textNode = content[++i]
) {
textNodes.push(textNode);
}
i--;
res.push(textNodes);
} else {
res.push(child);
}
}
return res;
};
export const getNodeProperty = (node, property) => {
if (property === "type") {
return node.type?.name;
}
return node[property];
};
export const getNodeAttribute = (node, attribute) =>
node.attrs ? node.attrs[attribute] : undefined;
export const getNodeAttributes = (node) => (node.attrs ? node.attrs : {});
export const getNodeMarks = (node) => node.marks ?? [];
export const getNodeChildren = (node) => node.content?.content ?? [];
export const getNodeText = (node) => node.text;
export const isTextNode = (node) => node.type?.name === "text";
export const matchNodeType = (node1, node2) =>
node1.type?.name === node2.type?.name ||
(Array.isArray(node1) && Array.isArray(node2));
export const createNewNode = (oldNode, children) => {
if (!oldNode.type) {
throw new Error("oldNode.type is undefined");
}
return new Node(
oldNode.type,
oldNode.attrs,
Fragment.fromArray(children),
oldNode.marks
);
};
export const createDiffNode = (schema, node, type) => {
return mapDocumentNode(node, (currentNode) => {
if (isTextNode(currentNode)) {
return createTextNode(schema, getNodeText(currentNode), [
...(currentNode.marks || []),
createDiffMark(schema, type),
]);
}
return currentNode;
});
};
function mapDocumentNode(node, mapper) {
const copy = node.copy(
Fragment.from(
node.content.content
.map((currentNode) => mapDocumentNode(currentNode, mapper))
.filter((n) => n)
)
);
return mapper(copy) || copy;
}
export const createDiffMark = (schema, type) => {
if (type === DiffType.Inserted) {
return schema.mark("diffMark", { type });
}
if (type === DiffType.Deleted) {
return schema.mark("diffMark", { type });
}
throw new Error("type is not valid");
};
export const createTextNode = (schema, content, marks = []) => {
return schema.text(content, marks);
};
export const diffEditor = (schema, oldDoc, newDoc) => {
const oldNode = Node.fromJSON(schema, oldDoc);
const newNode = Node.fromJSON(schema, newDoc);
return patchDocumentNode(schema, oldNode, newNode);
};

View file

@ -0,0 +1,62 @@
"use client";
import { defaultMarkdownSerializer } from "prosemirror-markdown";
import { DOMParser, type Node } from "prosemirror-model";
import { Decoration, DecorationSet, type EditorView } from "prosemirror-view";
import { renderToString } from "react-dom/server";
import { Response } from "@/components/elements/response";
import { documentSchema } from "./config";
import { createSuggestionWidget, type UISuggestion } from "./suggestions";
export const buildDocumentFromContent = (content: string) => {
const parser = DOMParser.fromSchema(documentSchema);
const stringFromMarkdown = renderToString(<Response>{content}</Response>);
const tempContainer = document.createElement("div");
tempContainer.innerHTML = stringFromMarkdown;
return parser.parse(tempContainer);
};
export const buildContentFromDocument = (document: Node) => {
return defaultMarkdownSerializer.serialize(document);
};
export const createDecorations = (
suggestions: UISuggestion[],
view: EditorView
) => {
const decorations: Decoration[] = [];
for (const suggestion of suggestions) {
decorations.push(
Decoration.inline(
suggestion.selectionStart,
suggestion.selectionEnd,
{
class: "suggestion-highlight",
},
{
suggestionId: suggestion.id,
type: "highlight",
}
)
);
decorations.push(
Decoration.widget(
suggestion.selectionStart,
(currentView) => {
const { dom } = createSuggestionWidget(suggestion, currentView);
return dom;
},
{
suggestionId: suggestion.id,
type: "widget",
}
)
);
}
return DecorationSet.create(view.state.doc, decorations);
};

View file

@ -0,0 +1,13 @@
import { createRoot } from "react-dom/client";
// biome-ignore lint/complexity/noStaticOnlyClass: "Needs to be static"
export class ReactRenderer {
static render(component: React.ReactElement, dom: HTMLElement) {
const root = createRoot(dom);
root.render(component);
return {
destroy: () => root.unmount(),
};
}
}

View file

@ -0,0 +1,158 @@
import type { Node } from "prosemirror-model";
import { Plugin, PluginKey } from "prosemirror-state";
import {
type Decoration,
DecorationSet,
type EditorView,
} from "prosemirror-view";
import { createRoot } from "react-dom/client";
import type { ArtifactKind } from "@/components/artifact";
import { Suggestion as PreviewSuggestion } from "@/components/suggestion";
import type { Suggestion } from "@/lib/db/schema";
export interface UISuggestion extends Suggestion {
selectionStart: number;
selectionEnd: number;
}
type Position = {
start: number;
end: number;
};
function findPositionsInDoc(doc: Node, searchText: string): Position | null {
let positions: { start: number; end: number } | null = null;
doc.nodesBetween(0, doc.content.size, (node, pos) => {
if (node.isText && node.text) {
const index = node.text.indexOf(searchText);
if (index !== -1) {
positions = {
start: pos + index,
end: pos + index + searchText.length,
};
return false;
}
}
return true;
});
return positions;
}
export function projectWithPositions(
doc: Node,
suggestions: Suggestion[]
): UISuggestion[] {
return suggestions.map((suggestion) => {
const positions = findPositionsInDoc(doc, suggestion.originalText);
if (!positions) {
return {
...suggestion,
selectionStart: 0,
selectionEnd: 0,
};
}
return {
...suggestion,
selectionStart: positions.start,
selectionEnd: positions.end,
};
});
}
export function createSuggestionWidget(
suggestion: UISuggestion,
view: EditorView,
artifactKind: ArtifactKind = "text"
): { dom: HTMLElement; destroy: () => void } {
const dom = document.createElement("span");
const root = createRoot(dom);
dom.addEventListener("mousedown", (event) => {
event.preventDefault();
view.dom.blur();
});
const onApply = () => {
const { state, dispatch } = view;
const decorationTransaction = state.tr;
const currentState = suggestionsPluginKey.getState(state);
const currentDecorations = currentState?.decorations;
if (currentDecorations) {
const newDecorations = DecorationSet.create(
state.doc,
currentDecorations.find().filter((decoration: Decoration) => {
return decoration.spec.suggestionId !== suggestion.id;
})
);
decorationTransaction.setMeta(suggestionsPluginKey, {
decorations: newDecorations,
selected: null,
});
dispatch(decorationTransaction);
}
const textTransaction = view.state.tr.replaceWith(
suggestion.selectionStart,
suggestion.selectionEnd,
state.schema.text(suggestion.suggestedText)
);
textTransaction.setMeta("no-debounce", true);
dispatch(textTransaction);
};
root.render(
<PreviewSuggestion
artifactKind={artifactKind}
onApply={onApply}
suggestion={suggestion}
/>
);
return {
dom,
destroy: () => {
// Wrapping unmount in setTimeout to avoid synchronous unmounting during render
setTimeout(() => {
root.unmount();
}, 0);
},
};
}
export const suggestionsPluginKey = new PluginKey("suggestions");
export const suggestionsPlugin = new Plugin({
key: suggestionsPluginKey,
state: {
init() {
return { decorations: DecorationSet.empty, selected: null };
},
apply(tr, state) {
const newDecorations = tr.getMeta(suggestionsPluginKey);
if (newDecorations) {
return newDecorations;
}
return {
decorations: state.decorations.map(tr.mapping, tr.doc),
selected: state.selected,
};
},
},
props: {
decorations(state) {
return this.getState(state)?.decorations ?? DecorationSet.empty;
},
},
});

View file

@ -0,0 +1,137 @@
export type ErrorType =
| "bad_request"
| "unauthorized"
| "forbidden"
| "not_found"
| "rate_limit"
| "offline";
export type Surface =
| "chat"
| "auth"
| "api"
| "stream"
| "database"
| "history"
| "vote"
| "document"
| "suggestions"
| "activate_gateway";
export type ErrorCode = `${ErrorType}:${Surface}`;
export type ErrorVisibility = "response" | "log" | "none";
export const visibilityBySurface: Record<Surface, ErrorVisibility> = {
database: "log",
chat: "response",
auth: "response",
stream: "response",
api: "response",
history: "response",
vote: "response",
document: "response",
suggestions: "response",
activate_gateway: "response",
};
export class ChatSDKError extends Error {
type: ErrorType;
surface: Surface;
statusCode: number;
constructor(errorCode: ErrorCode, cause?: string) {
super();
const [type, surface] = errorCode.split(":");
this.type = type as ErrorType;
this.cause = cause;
this.surface = surface as Surface;
this.message = getMessageByErrorCode(errorCode);
this.statusCode = getStatusCodeByType(this.type);
}
toResponse() {
const code: ErrorCode = `${this.type}:${this.surface}`;
const visibility = visibilityBySurface[this.surface];
const { message, cause, statusCode } = this;
if (visibility === "log") {
console.error({
code,
message,
cause,
});
return Response.json(
{ code: "", message: "Something went wrong. Please try again later." },
{ status: statusCode }
);
}
return Response.json({ code, message, cause }, { status: statusCode });
}
}
export function getMessageByErrorCode(errorCode: ErrorCode): string {
if (errorCode.includes("database")) {
return "An error occurred while executing a database query.";
}
switch (errorCode) {
case "bad_request:api":
return "The request couldn't be processed. Please check your input and try again.";
case "bad_request:activate_gateway":
return "AI Gateway requires a valid credit card on file to service requests. Please visit https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card to add a card and unlock your free credits.";
case "unauthorized:auth":
return "You need to sign in before continuing.";
case "forbidden:auth":
return "Your account does not have access to this feature.";
case "rate_limit:chat":
return "You have exceeded your maximum number of messages for the day. Please try again later.";
case "not_found:chat":
return "The requested chat was not found. Please check the chat ID and try again.";
case "forbidden:chat":
return "This chat belongs to another user. Please check the chat ID and try again.";
case "unauthorized:chat":
return "You need to sign in to view this chat. Please sign in and try again.";
case "offline:chat":
return "We're having trouble sending your message. Please check your internet connection and try again.";
case "not_found:document":
return "The requested document was not found. Please check the document ID and try again.";
case "forbidden:document":
return "This document belongs to another user. Please check the document ID and try again.";
case "unauthorized:document":
return "You need to sign in to view this document. Please sign in and try again.";
case "bad_request:document":
return "The request to create or update the document was invalid. Please check your input and try again.";
default:
return "Something went wrong. Please try again later.";
}
}
function getStatusCodeByType(type: ErrorType) {
switch (type) {
case "bad_request":
return 400;
case "unauthorized":
return 401;
case "forbidden":
return 403;
case "not_found":
return 404;
case "rate_limit":
return 429;
case "offline":
return 503;
default:
return 500;
}
}

View file

@ -0,0 +1,49 @@
import type { InferUITool, UIMessage } from "ai";
import { z } from "zod";
import type { ArtifactKind } from "@/components/artifact";
import type { getWeather } from "./ai/tools/get-weather";
import type { getCurrencyExchange } from "./ai/tools/get-currency-exchange";
import type { Suggestion } from "./db/schema";
export type DataPart = { type: "append-message"; message: string };
export const messageMetadataSchema = z.object({
createdAt: z.string(),
});
export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
type weatherTool = InferUITool<typeof getWeather>;
type currencyExchangeTool = InferUITool<typeof getCurrencyExchange>;
export type ChatTools = {
getWeather: weatherTool;
getCurrencyExchange: currencyExchangeTool;
};
export type CustomUIDataTypes = {
textDelta: string;
imageDelta: string;
sheetDelta: string;
codeDelta: string;
suggestion: Suggestion;
appendMessage: string;
id: string;
title: string;
kind: ArtifactKind;
clear: null;
finish: null;
"chat-title": string;
};
export type ChatMessage = UIMessage<
MessageMetadata,
CustomUIDataTypes,
ChatTools
>;
export type Attachment = {
name: string;
url: string;
contentType: string;
};

View file

@ -0,0 +1,116 @@
import type {
AssistantModelMessage,
ToolModelMessage,
UIMessage,
UIMessagePart,
} from 'ai';
import { type ClassValue, clsx } from 'clsx';
import { formatISO } from 'date-fns';
import { twMerge } from 'tailwind-merge';
import type { DBMessage, Document } from '@/lib/db/schema';
import { ChatSDKError, type ErrorCode } from './errors';
import type { ChatMessage, ChatTools, CustomUIDataTypes } from './types';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export const fetcher = async (url: string) => {
const response = await fetch(url);
if (!response.ok) {
const { code, cause } = await response.json();
throw new ChatSDKError(code as ErrorCode, cause);
}
return response.json();
};
export async function fetchWithErrorHandlers(
input: RequestInfo | URL,
init?: RequestInit,
) {
try {
const response = await fetch(input, init);
if (!response.ok) {
const { code, cause } = await response.json();
throw new ChatSDKError(code as ErrorCode, cause);
}
return response;
} catch (error: unknown) {
if (typeof navigator !== 'undefined' && !navigator.onLine) {
throw new ChatSDKError('offline:chat');
}
throw error;
}
}
export function getLocalStorage(key: string) {
if (typeof window !== 'undefined') {
return JSON.parse(localStorage.getItem(key) || '[]');
}
return [];
}
export function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
type ResponseMessageWithoutId = ToolModelMessage | AssistantModelMessage;
type ResponseMessage = ResponseMessageWithoutId & { id: string };
export function getMostRecentUserMessage(messages: UIMessage[]) {
const userMessages = messages.filter((message) => message.role === 'user');
return userMessages.at(-1);
}
export function getDocumentTimestampByIndex(
documents: Document[],
index: number,
) {
if (!documents) { return new Date(); }
if (index > documents.length) { return new Date(); }
return documents[index].createdAt;
}
export function getTrailingMessageId({
messages,
}: {
messages: ResponseMessage[];
}): string | null {
const trailingMessage = messages.at(-1);
if (!trailingMessage) { return null; }
return trailingMessage.id;
}
export function sanitizeText(text: string) {
return text.replace('<has_function_call>', '');
}
export function convertToUIMessages(messages: DBMessage[]): ChatMessage[] {
return messages.map((message) => ({
id: message.id,
role: message.role as 'user' | 'assistant' | 'system',
parts: message.parts as UIMessagePart<CustomUIDataTypes, ChatTools>[],
metadata: {
createdAt: formatISO(message.createdAt),
},
}));
}
export function getTextFromMessage(message: ChatMessage | UIMessage): string {
return message.parts
.filter((part) => part.type === 'text')
.map((part) => (part as { type: 'text'; text: string}).text)
.join('');
}