From c84791fb4206dfc69becc2853e1554e6f324db37 Mon Sep 17 00:00:00 2001 From: 0xinhua Date: Tue, 18 Jun 2024 13:19:05 +0800 Subject: [PATCH] Support storage data in browser --- .env.example | 4 + README.md | 116 ++--------------- app/(chat)/chat/[id]/page.tsx | 22 +++- app/(chat)/page.tsx | 10 +- app/actions.ts | 23 ---- app/api/chat/route.ts | 7 +- app/api/chats/route.ts | 35 +++++- components/chat-history.tsx | 11 +- components/chat.tsx | 41 ++++-- components/clear-history.tsx | 18 +-- components/sidebar-actions.tsx | 11 +- components/sidebar-item.tsx | 2 +- components/sidebar-items.tsx | 2 +- components/sidebar-list.tsx | 9 +- lib/const.ts | 3 +- lib/localforage.ts | 47 +++++++ lib/types.ts | 8 +- store/useChatStore.ts | 110 +++++++++++------ store/useSettingStore.ts | 121 +++++++++++------- supabase/README.md | 220 +++++++++++++++++++++++++++++++++ 20 files changed, 541 insertions(+), 279 deletions(-) create mode 100644 lib/localforage.ts create mode 100644 supabase/README.md diff --git a/.env.example b/.env.example index a6522f9..9c0ebe1 100644 --- a/.env.example +++ b/.env.example @@ -26,3 +26,7 @@ GOOGLE_CLIENT_ID="xxxxxxxx" GOOGLE_CLIENT_SECRET="*******" GROQ_API_KEY="*******" + +# Data storage mode: Set to "local" to save chat data in your browser +# Set to "cloud" to sync data to Supabase. +NEXT_PUBLIC_STORAGE_MODE="local" diff --git a/README.md b/README.md index 998b0d7..6068c4c 100644 --- a/README.md +++ b/README.md @@ -57,119 +57,27 @@ pnpm dev Your app template should now be running on [localhost:3000](http://localhost:3000/). -## Creating database instance on Supabase +## Data Storage -This [docs](https://supabase.com/docs/guides/getting-started) will assist you in creating and configuring your PostgreSQL database instance on Supabase, enabling your application to interact with it. +You can configure how your chat data is stored by setting the `NEXT_PUBLIC_STORAGE_MODE` environment variable in your `.env` file: -Remember to update your environment variables (`SUPABASE_CONNECTION_STRING`, `NEXT_PUBLIC_SUPABASE_URL`, `SUPABASE_PUBLIC_ANON_KEY`,) in the `.env` file with the appropriate credentials provided during the supabase database setup. +- **local**: This mode saves chat data directly in your browser's local storage. +- **cloud**: This mode syncs chat data to Supabase, a cloud-based PostgreSQL database. -Table definition and functions use for this project: - -```sql --- 创建 chat_dataset schema -CREATE SCHEMA chat_dataset; - -GRANT USAGE ON SCHEMA chat_dataset TO anon, authenticated, service_role; -GRANT ALL ON ALL TABLES IN SCHEMA chat_dataset TO anon, authenticated, service_role; -GRANT ALL ON ALL ROUTINES IN SCHEMA chat_dataset TO anon, authenticated, service_role; -GRANT ALL ON ALL SEQUENCES IN SCHEMA chat_dataset TO anon, authenticated, service_role; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA chat_dataset GRANT ALL ON TABLES TO anon, authenticated, service_role; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA chat_dataset GRANT ALL ON ROUTINES TO anon, authenticated, service_role; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA chat_dataset GRANT ALL ON SEQUENCES TO anon, authenticated, service_role; +Example: +```env +# Data storage mode: "local" for browser storage, "cloud" for Supabase storage +NEXT_PUBLIC_STORAGE_MODE="local" ``` -```sql -CREATE TABLE IF NOT EXISTS - chat_dataset.chats ( - id bigint generated always as identity, - chat_id text not null, - user_id uuid not null, - title text null, - path text null, - created_at bigint null default ( - extract( - epoch - from - current_timestamp - ) * (1000)::numeric - ), - messages jsonb not null, - share_path text null, - updated_at bigint null default ( - extract( - epoch - from - current_timestamp - ) * (1000)::numeric - ), - constraint chats_pkey primary key (id), - constraint chats_chat_id_key unique (chat_id), - constraint chats_user_id_fkey foreign key (user_id) references next_auth.users (id) - ) tablespace pg_default; -``` - -```sql -CREATE OR REPLACE FUNCTION get_chat_data(p_user_id uuid, p_chat_id text) -RETURNS TABLE ( - id bigint, - chat_id text, - user_id uuid, - title text, - path text, - created_at bigint, - messages jsonb, - share_path text, - updated_at bigint -) -LANGUAGE plpgsql -AS $$ -BEGIN - RETURN QUERY - SELECT - c.id, - c.chat_id, - c.user_id, - c.title, - c.path, - c.created_at, - c.messages, - c.share_path, - c.updated_at - FROM chat_dataset.chats c - WHERE c.user_id = p_user_id AND c.chat_id = p_chat_id; -END; -$$; +To use Supabase for cloud storage, change the mode to `"cloud"`: +```env +NEXT_PUBLIC_STORAGE_MODE="cloud" ``` -```sql -CREATE OR REPLACE FUNCTION upsert_chat( - p_chat_id text, - p_title text, - p_user_id uuid, - p_created_at bigint, - p_path text, - p_messages jsonb, - p_share_path text -) -RETURNS void -LANGUAGE plpgsql -AS $$ -BEGIN - INSERT INTO chat_dataset.chats (chat_id, title, user_id, created_at, path, messages, share_path) - VALUES (p_chat_id, p_title, p_user_id, p_created_at, p_path, p_messages, p_share_path) - ON CONFLICT (chat_id) DO UPDATE - SET - title = EXCLUDED.title, - user_id = EXCLUDED.user_id, - created_at = EXCLUDED.created_at, - path = EXCLUDED.path, - messages = EXCLUDED.messages, - share_path = EXCLUDED.share_path; -END; -$$; -``` +If you choose the cloud mode, you need to configure the corresponding database table structure on Supabase. For detailed instructions, refer to this [documentation](./supabase/README.md). ## Thanks diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx index 8c08911..b8e28b8 100644 --- a/app/(chat)/chat/[id]/page.tsx +++ b/app/(chat)/chat/[id]/page.tsx @@ -49,12 +49,22 @@ export default function ChatPage({ params }: ChatPageProps) { return
- {loading ?
- Loading - - - -
: } + { + loading + ?
+ Loading + + + +
+ : + }
} diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx index c00d7d9..89c628f 100644 --- a/app/(chat)/page.tsx +++ b/app/(chat)/page.tsx @@ -3,22 +3,18 @@ import { nanoid } from '@/lib/utils' import { Chat } from '@/components/chat' import useUserSettingStore from '@/store/useSettingStore' -import { useEffect } from 'react' export default function IndexPage() { const { systemPrompt, - fetchSystemPrompt } = useUserSettingStore() - useEffect(() => { - fetchSystemPrompt() - }, []) - const id = nanoid() - return ', userId) + + if (!userId) { + return new Response('Unauthorized', { + status: 401 + }) + } + + try { + + const { data, error } = await supabase.rpc('delete_user_chats', { p_user_id: userId }); + + console.log('delete rows data', error) + + return NextResponse.json({ + data: [], + code: 0, + message: 'success', + }) + } catch (error) { console.log('error', error) return NextResponse.json({ diff --git a/components/chat-history.tsx b/components/chat-history.tsx index b69844c..2b069de 100644 --- a/components/chat-history.tsx +++ b/components/chat-history.tsx @@ -21,16 +21,7 @@ interface ChatHistoryProps { export function ChatHistory({ userId, session }: ChatHistoryProps) { const { isSidebarOpen, isLoading, toggleSidebar } = useSidebar() - const { chats, fetchHistory } = useChatStore(state => ({ - chats: state.chats, - fetchHistory: state.fetchHistory - })) - - useEffect(() => { - if (chats.length === 0) { - fetchHistory() - } - }, [fetchHistory, chats.length]) + const { chats } = useChatStore() return (
diff --git a/components/chat.tsx b/components/chat.tsx index 98c8d01..9bc3775 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -2,7 +2,7 @@ import { useChat, type Message } from 'ai/react' -import { cn } from '@/lib/utils' +import { cn, nanoid } from '@/lib/utils' import { ChatList } from '@/components/chat-list' import { ChatPanel } from '@/components/chat-panel' import { EmptyScreen } from '@/components/empty-screen' @@ -22,16 +22,19 @@ import { Input } from './ui/input' import { toast } from 'react-hot-toast' import { usePathname, useRouter } from 'next/navigation' import useChatStore from '@/store/useChatStore' +import { isLocalMode } from '@/lib/const' +import { Chat as IChat } from '@/lib/types' const IS_PREVIEW = process.env.VERCEL_ENV === 'preview' export interface ChatProps extends React.ComponentProps<'div'> { - initialMessages?: Message[] + initialMessages: Message[] id?: string title?: string loading?: boolean + userId?: string | null } -export function Chat({ id, initialMessages, className, title, loading }: ChatProps) { +export function Chat({ id, initialMessages, className, title, loading, userId }: ChatProps) { const router = useRouter() const path = usePathname() const [previewToken, setPreviewToken] = useLocalStorage( @@ -41,9 +44,7 @@ export function Chat({ id, initialMessages, className, title, loading }: ChatPro const [previewTokenDialog, setPreviewTokenDialog] = useState(IS_PREVIEW) const [previewTokenInput, setPreviewTokenInput] = useState(previewToken ?? '') - const { fetchHistory } = useChatStore(state => ({ - fetchHistory: state.fetchHistory - })) + const { fetchHistory, chats, setChats } = useChatStore() const { messages, append, reload, stop, isLoading, input, setInput } = useChat({ @@ -59,11 +60,35 @@ export function Chat({ id, initialMessages, className, title, loading }: ChatPro toast.error(response.statusText) } }, - onFinish() { + onFinish(message: Message) { + + if (isLocalMode) { + + const existingChatIndex = chats.findIndex(chat => chat.chat_id === id) + + if (existingChatIndex !== -1) { + const existingChat = chats[existingChatIndex] + const newChat = { ...existingChat, messages: [message, ...existingChat.messages] } + setChats([...chats.slice(0, existingChatIndex), newChat, ...chats.slice(existingChatIndex + 1)]) + } else { + + const newChat: IChat = { + chat_id: id as string, + title: input.substring(0, 100), + created_at: new Date(), + user_id: userId || undefined, + path: `/chat/${id}`, + messages: [...initialMessages, { role: 'user', content: input, id: nanoid() }, message], + } + + setChats([newChat, ...chats]) + } + } + if (!path.includes('chat')) { router.replace(`/chat/${id}`) // router.refresh() - fetchHistory() + !isLocalMode && fetchHistory() } } }) diff --git a/components/clear-history.tsx b/components/clear-history.tsx index 6e2e1ae..3c33fe4 100644 --- a/components/clear-history.tsx +++ b/components/clear-history.tsx @@ -2,9 +2,7 @@ import * as React from 'react' import { useRouter } from 'next/navigation' -import { toast } from 'react-hot-toast' -import { ServerActionResult } from '@/lib/types' import { Button } from '@/components/ui/button' import { AlertDialog, @@ -22,12 +20,10 @@ import useChatStore from '@/store/useChatStore' interface ClearHistoryProps { isEnabled: boolean - clearChats: () => ServerActionResult } export function ClearHistory({ isEnabled = false, - clearChats }: ClearHistoryProps) { const [open, setOpen] = React.useState(false) const [isPending, startTransition] = React.useTransition() @@ -58,16 +54,10 @@ export function ClearHistory({ disabled={isPending} onClick={event => { event.preventDefault() - startTransition(() => { - clearChats().then(result => { - if (result && 'error' in result) { - toast.error(result.error) - return - } - setOpen(false) - removeChats() - router.push('/') - }) + startTransition(async () => { + setOpen(false) + await removeChats() + router.push('/') }) }} > diff --git a/components/sidebar-actions.tsx b/components/sidebar-actions.tsx index c107f5d..90a8fd4 100644 --- a/components/sidebar-actions.tsx +++ b/components/sidebar-actions.tsx @@ -25,6 +25,7 @@ import { } from '@/components/ui/tooltip' import useChatStore from '@/store/useChatStore' import { revalidatePath } from 'next/cache' +import { isLocalMode } from '@/lib/const' interface SidebarActionsProps { chat: Chat @@ -39,9 +40,6 @@ export function SidebarActions({ const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) const [shareDialogOpen, setShareDialogOpen] = React.useState(false) const [isRemovePending, startRemoveTransition] = React.useTransition() - const { deleteChat } = useChatStore(state => ({ - deleteChat: state.deleteChat - })) const { removeChat } = useChatStore(state => ({ removeChat: state.removeChat @@ -50,7 +48,7 @@ export function SidebarActions({ return ( <>
- + { isLocalMode ? null :
) diff --git a/lib/const.ts b/lib/const.ts index 58a8fb3..7c4b0ac 100644 --- a/lib/const.ts +++ b/lib/const.ts @@ -1 +1,2 @@ -export const defaultSystemPrompt = "You are a helpful assistant, you always give concise answers." \ No newline at end of file +export const defaultSystemPrompt = "You are a helpful assistant, you always give concise answers." +export const isLocalMode = process.env.NEXT_PUBLIC_STORAGE_MODE === 'local' diff --git a/lib/localforage.ts b/lib/localforage.ts new file mode 100644 index 0000000..b42077b --- /dev/null +++ b/lib/localforage.ts @@ -0,0 +1,47 @@ +import localforage from "localforage" + +class LocalForage { + + async get(key: string): Promise { + try { + const val = await localforage.getItem(key) as string + return JSON.parse(val) + } catch (error) { + console.error(error) + return null + } + } + + async set(key: string, val: T) { + try { + return await localforage.setItem(key, JSON.stringify(val)) + } catch (error) { + console.error(error) + return null + } + } + + async remove(key: string, isPrefixMode = true) { + try { + if (isPrefixMode) { + const toRemove: string[] = [] + await localforage.iterate((value, _key) => { + if (_key.startsWith(key)) { + toRemove.push(_key) + } + }) + await Promise.all(toRemove.map(item => localforage.removeItem(item))) + } else { + await localforage.removeItem(key) + } + } catch (error) { + console.error(error) + } + } + + clear() { + localforage.clear() + } +} + +export const localForage = new LocalForage() \ No newline at end of file diff --git a/lib/types.ts b/lib/types.ts index 5a91ea6..c799349 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1,11 +1,11 @@ -import { type Message } from 'ai' +import { Message } from "ai" export interface Chat extends Record { - id: string + id?: string chat_id: string title: string - createdAt: Date - userId: string + created_at: Date + user_id?: string path: string messages: Message[] sharePath?: string diff --git a/store/useChatStore.ts b/store/useChatStore.ts index be03af8..1295e35 100644 --- a/store/useChatStore.ts +++ b/store/useChatStore.ts @@ -2,13 +2,14 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' import localforage from 'localforage' import { Chat } from '@/lib/types' +import { isLocalMode } from '@/lib/const' +import { localForage } from '@/lib/localforage' // 定义状态类型 export interface ChatState { chats: Array setChats: (chats: Array) => void fetchHistory: () => Promise - deleteChat: (id: string) => void removeChats: () => void chat: Chat | null fetchChatById: (id: string) => Promise @@ -20,44 +21,77 @@ export interface ChatState { const useChatStore = create()( persist( - //@ts-ignore - (set) => ({ - chats: [], - chat: null, - chatLoading: false, - setChats: (chats: Chat[]) => set({ chats }), - fetchHistory: async () => { - const response = await fetch('/api/chats') - const { data } = await response.json() - set({ chats: data }) - }, - setChat: (chat: Chat) => set({chat}), - fetchChatById: async (chatId: string) => { - set({chatLoading: true}) - const response = await fetch(`/api/chats/${chatId}`) - const { data } = await response.json() - set({ chatLoading: false }) - set({ chat: data }) - }, - deleteChat: (id: string) => set((state) => ({ - chats: state.chats.filter(chat => chat.id !== id) - })), - removeChats: () => { - set({ chats: [] }) - localforage.removeItem('chat-history') - }, - removeChat: async (chatId: string) => { - const response = await fetch(`/api/chats/${chatId}`, { - method: 'delete' - }) - const { data } = await response.json() - set({ chat: null }) - }, - reset: () => { - set({ chats: [] }) - localforage.removeItem('chat-history') + (set) => { + // init history data + (async () => { + if (isLocalMode) { + const localChatState = await localForage.get('chat-history') as { state: ChatState } || null + set({ chats: localChatState?.state?.chats || [] }) + } else { + const response = await fetch('/api/chats') + const { data } = await response.json() + console.log('init history data', data) + set({ chats: data }) + } + })() + + return { + chats: [], + chat: null, + chatLoading: false, + setChats: (chats: Chat[]) => set({ chats }), + fetchHistory: async () => { + if (!isLocalMode) { + const response = await fetch('/api/chats') + const { data } = await response.json() + console.log('fetch history data', data) + set({ chats: data }) + } + }, + setChat: (chat: Chat) => set({chat}), + fetchChatById: async (chatId: string) => { + set({ chatLoading: true }) + if (isLocalMode) { + const { state } = await localForage.get('chat-history') as { state: ChatState } + const chat = state.chats.find(chat => chat.chat_id === chatId) + if (chat) { + set({ chat }) + } else { + console.error(`Chat with id ${chatId} not found in local storage`) + } + } else { + const response = await fetch(`/api/chats/${chatId}`) + const { data } = await response.json() + set({ chatLoading: false }) + set({ chat: data }) + } + }, + removeChats: async() => { + if (!isLocalMode) { + await fetch(`/api/chats`, { + method: 'delete' + }) + } + set({ chats: [] }) + localForage.remove('chat-history') + }, + removeChat: async (chatId: string) => { + if (!isLocalMode) { + await fetch(`/api/chats/${chatId}`, { + method: 'delete' + }) + } + set((state) => ({ + chats: state.chats.filter(chat => chat.chat_id !== chatId) + })) + set({ chat: null }) + }, + reset: () => { + set({ chats: [] }) + localForage.remove('chat-history') + } } - }), + }, { name: 'chat-history', getStorage: () => localforage, diff --git a/store/useSettingStore.ts b/store/useSettingStore.ts index 3d54c44..16dac00 100644 --- a/store/useSettingStore.ts +++ b/store/useSettingStore.ts @@ -1,7 +1,8 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' import localforage from 'localforage' -import { defaultSystemPrompt } from '@/lib/const'; +import { defaultSystemPrompt, isLocalMode } from '@/lib/const'; +import { localForage } from '@/lib/localforage'; // 定义错误类型 interface CustomError extends Error { @@ -21,57 +22,85 @@ interface UserSettingState { const useUserSettingStore = create()( persist( - (set, get) => ({ - isSettingsDialogOpen: false, - systemPrompt: "You are a helpful assistant.", - loading: false, - error: null, - setSettingsDialogOpen: (isOpen: boolean) => set({ isSettingsDialogOpen: isOpen }), - fetchSystemPrompt: async () => { - set({ loading: true, error: null }) - try { + (set, get) => { + + (async () => { + if (isLocalMode) { + const localChatSetting = await localForage.get('user-setting') as { state: UserSettingState } || null + set({ systemPrompt: localChatSetting?.state?.systemPrompt || defaultSystemPrompt }) + } else { const response = await fetch('/api/user/settings/systemPrompt') + const { data } = await response.json() + set({ systemPrompt: data.prompt || defaultSystemPrompt }) + } + })() - if (!response.ok) { - throw new Error('Failed to fetch system prompt') - } + return { + isSettingsDialogOpen: false, + systemPrompt: defaultSystemPrompt, + loading: false, + error: null, + setSettingsDialogOpen: (isOpen: boolean) => set({ isSettingsDialogOpen: isOpen }), - const json = await response.json() - set({ - systemPrompt: json.data?.prompt ? json.data.prompt : defaultSystemPrompt, - loading: false - }) - } catch (error) { - const customError = error as CustomError - set({ error: customError.message, loading: false }) - } - }, - - updateSystemPrompt: async (prompt: string) => { - set({ loading: true, error: null }) - try { - const response = await fetch('/api/user/settings/systemPrompt', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ prompt }), - }) - if (!response.ok) { - throw new Error('Failed to update system prompt') + fetchSystemPrompt: async () => { + + if (!isLocalMode) { + + set({ loading: true, error: null }) + + try { + const response = await fetch('/api/user/settings/systemPrompt') + + if (!response.ok) { + throw new Error('Failed to fetch system prompt') + } + + const json = await response.json() + + set({ + systemPrompt: json.data?.prompt ? json.data.prompt : defaultSystemPrompt, + loading: false + }) + } catch (error) { + const customError = error as CustomError + set({ error: customError.message, loading: false }) + } } - const data = await response.json() - if (data.code === 0) { + }, + + updateSystemPrompt: async (prompt: string) => { + + if (isLocalMode) { set({ systemPrompt: prompt, loading: false }) - } else { - throw new Error(data.message || 'Unknown error') + return } - } catch (error) { - const customError = error as CustomError - set({ error: customError.message, loading: false }) - } - }, - }), + + set({ loading: true, error: null }) + + try { + const response = await fetch('/api/user/settings/systemPrompt', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ prompt }), + }) + if (!response.ok) { + throw new Error('Failed to update system prompt') + } + const data = await response.json() + if (data.code === 0) { + set({ systemPrompt: prompt, loading: false }) + } else { + throw new Error(data.message || 'Unknown error') + } + } catch (error) { + const customError = error as CustomError + set({ error: customError.message, loading: false }) + } + }, + } + }, { name: 'user-setting', // 存储名称 getStorage: () => localforage, diff --git a/supabase/README.md b/supabase/README.md new file mode 100644 index 0000000..1cb7c25 --- /dev/null +++ b/supabase/README.md @@ -0,0 +1,220 @@ +# Supabase Data Storage Configuration + +This section will guide you through configuring the `NEXT_PUBLIC_STORAGE_MODE` variable and setting up the necessary SQL configurations for storing chat data either locally or in the cloud using Supabase. + +### Storage Mode + +You can configure how your chat data is stored by setting the `NEXT_PUBLIC_STORAGE_MODE` environment variable in your `.env` file: + +- **local**: This mode saves chat data directly in your browser's local storage. +- **cloud**: This mode syncs chat data to Supabase, a cloud-based PostgreSQL database. + +Example: + +To use Supabase for cloud storage, change the mode to `"cloud"`: + +```env +NEXT_PUBLIC_STORAGE_MODE="cloud" +``` + +### Setting Up Supabase + +To store chat data in Supabase, follow these steps: + +1. **Create a Supabase account** and set up a new project by following [this guide](https://supabase.com/docs/guides/getting-started). + +2. **Update environment variables** in your `.env` file with the credentials provided by Supabase: + +```env +SUPABASE_CONNECTION_STRING="your_supabase_connection_string" +NEXT_PUBLIC_SUPABASE_URL="your_supabase_url" +SUPABASE_PUBLIC_ANON_KEY="your_supabase_public_anon_key" +``` + +### SQL Configuration + +Execute the following SQL commands in the Supabase SQL editor to set up the necessary database schema, tables, and functions: + +#### 1. Create Schema and Set Permissions + +```sql +-- Create chat_dataset schema +CREATE SCHEMA chat_dataset; + +GRANT USAGE ON SCHEMA chat_dataset TO anon, authenticated, service_role; +GRANT ALL ON ALL TABLES IN SCHEMA chat_dataset TO anon, authenticated, service_role; +GRANT ALL ON ALL ROUTINES IN SCHEMA chat_dataset TO anon, authenticated, service_role; +GRANT ALL ON ALL SEQUENCES IN SCHEMA chat_dataset TO anon, authenticated, service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA chat_dataset GRANT ALL ON TABLES TO anon, authenticated, service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA chat_dataset GRANT ALL ON ROUTINES TO anon, authenticated, service_role; +ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA chat_dataset GRANT ALL ON SEQUENCES TO anon, authenticated, service_role; +``` + +#### 2. Create Chats Table + +```sql +CREATE TABLE IF NOT EXISTS + chat_dataset.chats ( + id bigint generated always as identity, + chat_id text not null, + user_id uuid not null, + title text null, + path text null, + created_at bigint null default ( + extract( + epoch + from + current_timestamp + ) * (1000)::numeric + ), + messages jsonb not null, + share_path text null, + updated_at bigint null default ( + extract( + epoch + from + current_timestamp + ) * (1000)::numeric + ), + constraint chats_pkey primary key (id), + constraint chats_chat_id_key unique (chat_id), + constraint chats_user_id_fkey foreign key (user_id) references next_auth.users (id) + ) tablespace pg_default; +``` + +#### 3. Create Functions + +- **Function to Get Chat Data** + +```sql +CREATE OR REPLACE FUNCTION get_chat_data(p_user_id uuid, p_chat_id text) +RETURNS TABLE ( + id bigint, + chat_id text, + user_id uuid, + title text, + path text, + created_at bigint, + messages jsonb, + share_path text, + updated_at bigint +) +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN QUERY + SELECT + c.id, + c.chat_id, + c.user_id, + c.title, + c.path, + c.created_at, + c.messages, + c.share_path, + c.updated_at + FROM chat_dataset.chats c + WHERE c.user_id = p_user_id AND c.chat_id = p_chat_id; +END; +$$; +``` + +- **Function to Upsert Chat Data** + +```sql +CREATE OR REPLACE FUNCTION upsert_chat( + p_chat_id text, + p_title text, + p_user_id uuid, + p_created_at bigint, + p_path text, + p_messages jsonb, + p_share_path text +) +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + INSERT INTO chat_dataset.chats (chat_id, title, user_id, created_at, path, messages, share_path) + VALUES (p_chat_id, p_title, p_user_id, p_created_at, p_path, p_messages, p_share_path) + ON CONFLICT (chat_id) DO UPDATE + SET + title = EXCLUDED.title, + user_id = EXCLUDED.user_id, + created_at = EXCLUDED.created_at, + path = EXCLUDED.path, + messages = EXCLUDED.messages, + share_path = EXCLUDED.share_path; +END; +$$; +``` + +- **Function to Delete all Chat data** + +```sql +CREATE OR REPLACE FUNCTION chat_dataset.delete_user_chats(p_user_id UUID) +RETURNS VOID AS $$ +BEGIN + DELETE FROM chat_dataset.chats + WHERE user_id = p_user_id; +END; +$$ LANGUAGE plpgsql; + +``` + +- **Function to get system prompt data** + +```sql +CREATE OR REPLACE FUNCTION chat_dataset.get_system_prompt_by_user_id(_user_id UUID) +RETURNS TABLE ( + id UUID, + user_id UUID, + prompt TEXT, + created_at BIGINT, + updated_at BIGINT +) AS $$ +BEGIN + RETURN QUERY + SELECT + sp.id, + sp.user_id, + sp.prompt, + sp.created_at, + sp.updated_at + FROM + chat_dataset.system_prompts sp + WHERE + sp.user_id = _user_id; +END; +$$ LANGUAGE plpgsql; + +``` + +- **Function to upsert system prompt data** + +```sql +create or replace function chat_dataset.upsert_system_prompt( + _user_id uuid, + _prompt text +) +returns void language plpgsql as $$ +begin + -- Try to update the existing record + update chat_dataset.system_prompts + set + prompt = _prompt, + updated_at = extract(epoch from current_timestamp) * 1000 + where + user_id = _user_id; + + -- If no rows were updated, insert a new record + if not found then + insert into chat_dataset.system_prompts (user_id, prompt, created_at, updated_at) + values (_user_id, _prompt, extract(epoch from current_timestamp) * 1000, extract(epoch from current_timestamp) * 1000); + end if; +end; +$$; + +``` + +By following these steps, you can configure your AI chatbot to store chat data either locally in the browser or in the cloud using Supabase. \ No newline at end of file