From 5bfc29f27be2a5929239df7dbc003c5fbd5e96c6 Mon Sep 17 00:00:00 2001 From: Joel M Date: Mon, 13 May 2024 09:19:20 +0000 Subject: [PATCH 01/21] wip on settings --- .../backend/store-backend-capabilities.ts | 1 + .../supabasesync/SupabaseSyncSettings.tsx | 69 +++++++++++++++++++ .../store-module-supabase-sync.ts | 33 +++++++++ .../supabasesync/supabaseSync.client.ts | 4 ++ 4 files changed, 107 insertions(+) create mode 100644 src/modules/supabasesync/SupabaseSyncSettings.tsx create mode 100644 src/modules/supabasesync/store-module-supabase-sync.ts create mode 100644 src/modules/supabasesync/supabaseSync.client.ts diff --git a/src/modules/backend/store-backend-capabilities.ts b/src/modules/backend/store-backend-capabilities.ts index 0f6ee1f3a4..15adb79fc4 100644 --- a/src/modules/backend/store-backend-capabilities.ts +++ b/src/modules/backend/store-backend-capabilities.ts @@ -24,6 +24,7 @@ export interface BackendCapabilities { hasLlmPerplexity: boolean; hasLlmTogetherAI: boolean; hasVoiceElevenLabs: boolean; + hasSupabaseSync: boolean; llmConfigHash: string; } diff --git a/src/modules/supabasesync/SupabaseSyncSettings.tsx b/src/modules/supabasesync/SupabaseSyncSettings.tsx new file mode 100644 index 0000000000..0077c86ed7 --- /dev/null +++ b/src/modules/supabasesync/SupabaseSyncSettings.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { shallow } from 'zustand/shallow'; + +import { FormControl, FormHelperText, Input } from '@mui/joy'; +import KeyIcon from '@mui/icons-material/Key'; +import SearchIcon from '@mui/icons-material/Search'; + +import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities'; + +import { FormLabelStart } from '~/common/components/forms/FormLabelStart'; +import { Link } from '~/common/components/Link'; + +import { isValidSupabaseConnection } from './supabaseSync.client'; +import { useSupabaseSyncStore } from './store-module-supabase-sync'; + + +export function SupabaseSyncSettings() { + + // external state + const backendHasSupabaseSync = getBackendCapabilities().hasSupabaseSync; + const { supabaseUrl, setSupabaseUrl, supabaseAnonKey, setSupabaseAnonKey } = useSupabaseSyncStore(state => ({ + supabaseUrl: state.supabaseUrl, setSupabaseUrl: state.setSupabaseUrl, + supabaseAnonKey: state.supabaseAnonKey, setSupabaseAnonKey: state.setSupabaseAnonKey, + }), shallow); + + + // derived state + const isValueUrl = supabaseUrl ? isValidSupabaseConnection(supabaseUrl, supabaseAnonKey) : backendHasSupabaseSync; + const isValidAnonKey = isValueUrl; + + const handleSupabaseSyncChange = (e: React.ChangeEvent) => setSupabaseUrl(e.target.value); + + const handleSupabaseAnonKeyChane = (e: React.ChangeEvent) => setSupabaseAnonKey(e.target.value); + + + return <> + + + Configure the Supabase Chat Sync, if you don't have a Supabase account you will need to create one. + + + + Create one here} + tooltip='Create your Supabase Database and enter the url here' /> + } + slotProps={{ input: { sx: { width: '100%' } } }} + sx={{ width: '100%' }} + /> + + + + Get it here} + tooltip='Your database connections Anon Key' /> + } + slotProps={{ input: { sx: { width: '100%' } } }} + sx={{ width: '100%' }} + /> + + + ; +} \ No newline at end of file diff --git a/src/modules/supabasesync/store-module-supabase-sync.ts b/src/modules/supabasesync/store-module-supabase-sync.ts new file mode 100644 index 0000000000..85d9659890 --- /dev/null +++ b/src/modules/supabasesync/store-module-supabase-sync.ts @@ -0,0 +1,33 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + + +interface ModuleSupabaseSyncStore { + + // Supabase Sync Settings + + supabaseUrl: string; + setSupabaseUrl: (supaUrl: string) => void; + + supabaseAnonKey: string; + setSupabaseAnonKey: (anonKey: string) => void; + +} + +export const useSupabaseSyncStore = create()( + persist( + (set) => ({ + + // Supabase Sync Settings + + supabaseUrl: '', + setSupabaseUrl: (supaUrl: string) => set({ supabaseUrl: supaUrl }), + + supabaseAnonKey: '', + setSupabaseAnonKey: (anonKey: string) => set({ supabaseAnonKey: anonKey }), + + }), + { + name: 'app-module-supabase-sync', + }), +); \ No newline at end of file diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts new file mode 100644 index 0000000000..10dbf4d14a --- /dev/null +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -0,0 +1,4 @@ + + +export const isValidSupabaseConnection = (supabaseUrl?: string, supabaseAnonKey?: string) =>!!supabaseUrl && !!supabaseAnonKey; + From f719acef7eee0675a28c3f90c79ef2bebcff801d Mon Sep 17 00:00:00 2001 From: Joel M Date: Mon, 13 May 2024 09:31:57 +0000 Subject: [PATCH 02/21] settings saving --- src/apps/settings-modal/SettingsModal.tsx | 4 ++++ src/modules/supabasesync/SupabaseSyncSettings.tsx | 14 +++++++------- .../supabasesync/store-module-supabase-sync.ts | 8 ++++---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/apps/settings-modal/SettingsModal.tsx b/src/apps/settings-modal/SettingsModal.tsx index a4202dffc9..9a7d969c84 100644 --- a/src/apps/settings-modal/SettingsModal.tsx +++ b/src/apps/settings-modal/SettingsModal.tsx @@ -10,6 +10,7 @@ import { BrowseSettings } from '~/modules/browse/BrowseSettings'; import { DallESettings } from '~/modules/t2i/dalle/DallESettings'; import { ElevenlabsSettings } from '~/modules/elevenlabs/ElevenlabsSettings'; import { GoogleSearchSettings } from '~/modules/google/GoogleSearchSettings'; +import { SupabaseSyncSettings } from '~/modules/supabasesync/SupabaseSyncSettings'; import { ProdiaSettings } from '~/modules/t2i/prodia/ProdiaSettings'; import { T2ISettings } from '~/modules/t2i/T2ISettings'; @@ -206,6 +207,9 @@ export function SettingsModal(props: { } title='Google Search API' startCollapsed> + } title='Supabase Sync' startCollapsed> + + {/**/} diff --git a/src/modules/supabasesync/SupabaseSyncSettings.tsx b/src/modules/supabasesync/SupabaseSyncSettings.tsx index 0077c86ed7..27b0bbb94b 100644 --- a/src/modules/supabasesync/SupabaseSyncSettings.tsx +++ b/src/modules/supabasesync/SupabaseSyncSettings.tsx @@ -18,14 +18,14 @@ export function SupabaseSyncSettings() { // external state const backendHasSupabaseSync = getBackendCapabilities().hasSupabaseSync; - const { supabaseUrl, setSupabaseUrl, supabaseAnonKey, setSupabaseAnonKey } = useSupabaseSyncStore(state => ({ + const { supabaseUrl, setSupabaseUrl, supabaseAnonKey: supabaseKey, setSupabaseAnonKey } = useSupabaseSyncStore(state => ({ supabaseUrl: state.supabaseUrl, setSupabaseUrl: state.setSupabaseUrl, - supabaseAnonKey: state.supabaseAnonKey, setSupabaseAnonKey: state.setSupabaseAnonKey, + supabaseAnonKey: state.supabaseKey, setSupabaseAnonKey: state.setSupabaseKey, }), shallow); // derived state - const isValueUrl = supabaseUrl ? isValidSupabaseConnection(supabaseUrl, supabaseAnonKey) : backendHasSupabaseSync; + const isValueUrl = supabaseUrl ? isValidSupabaseConnection(supabaseUrl, supabaseKey) : backendHasSupabaseSync; const isValidAnonKey = isValueUrl; const handleSupabaseSyncChange = (e: React.ChangeEvent) => setSupabaseUrl(e.target.value); @@ -53,12 +53,12 @@ export function SupabaseSyncSettings() { - Get it here} - tooltip='Your database connections Anon Key' /> + } slotProps={{ input: { sx: { width: '100%' } } }} sx={{ width: '100%' }} diff --git a/src/modules/supabasesync/store-module-supabase-sync.ts b/src/modules/supabasesync/store-module-supabase-sync.ts index 85d9659890..fdea554519 100644 --- a/src/modules/supabasesync/store-module-supabase-sync.ts +++ b/src/modules/supabasesync/store-module-supabase-sync.ts @@ -9,8 +9,8 @@ interface ModuleSupabaseSyncStore { supabaseUrl: string; setSupabaseUrl: (supaUrl: string) => void; - supabaseAnonKey: string; - setSupabaseAnonKey: (anonKey: string) => void; + supabaseKey: string; + setSupabaseKey: (key: string) => void; } @@ -23,8 +23,8 @@ export const useSupabaseSyncStore = create()( supabaseUrl: '', setSupabaseUrl: (supaUrl: string) => set({ supabaseUrl: supaUrl }), - supabaseAnonKey: '', - setSupabaseAnonKey: (anonKey: string) => set({ supabaseAnonKey: anonKey }), + supabaseKey: '', + setSupabaseKey: (key: string) => set({ supabaseKey: key }), }), { From 3ec54824705cc205702379da283001a1638710ba Mon Sep 17 00:00:00 2001 From: Joel M Date: Mon, 13 May 2024 09:45:12 +0000 Subject: [PATCH 03/21] wip on supabase connection --- package-lock.json | 105 +++++++++++++++++- package.json | 1 + .../supabasesync/supabaseSync.client.ts | 27 ++++- 3 files changed, 129 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 734a60c913..5c8c186584 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@next/third-parties": "^14.2.3", "@prisma/client": "^5.13.0", "@sanity/diff-match-patch": "^3.1.1", + "@supabase/supabase-js": "^2.43.1", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/react-query": "~4.36.1", "@trpc/client": "10.44.1", @@ -1776,6 +1777,93 @@ "node": ">=14.18" } }, + "node_modules/@supabase/auth-js": { + "version": "2.64.2", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.64.2.tgz", + "integrity": "sha512-s+lkHEdGiczDrzXJ1YWt2y3bxRi+qIUnXcgkpLSrId7yjBeaXBFygNjTaoZLG02KNcYwbuZ9qkEIqmj2hF7svw==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.3.1.tgz", + "integrity": "sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz", + "integrity": "sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.5.tgz", + "integrity": "sha512-TEHlGwNGGmKPdeMtca1lFTYCedrhTAv3nZVoSjrKQ+wkMmaERuCe57zkC5KSWFzLYkb5FVHW8Hrr+PX1DDwplQ==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.14.2" + } + }, + "node_modules/@supabase/realtime-js/node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz", + "integrity": "sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.43.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.43.1.tgz", + "integrity": "sha512-A+RV50mWNtyKo6M0u4G6AOqEifQD+MoOjZcpRkPMPpEAFgMsc2dt3kBlBlR/MgZizWQgUKhsvrwKk0efc8g6Ug==", + "dependencies": { + "@supabase/auth-js": "2.64.2", + "@supabase/functions-js": "2.3.1", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.15.2", + "@supabase/realtime-js": "2.9.5", + "@supabase/storage-js": "2.5.5" + } + }, "node_modules/@swc/helpers": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", @@ -1973,7 +2061,6 @@ "version": "20.12.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1989,6 +2076,11 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, + "node_modules/@types/phoenix": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.4.tgz", + "integrity": "sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA==" + }, "node_modules/@types/plantuml-encoder": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@types/plantuml-encoder/-/plantuml-encoder-1.4.2.tgz", @@ -2093,6 +2185,14 @@ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/parser": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", @@ -8397,8 +8497,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unified": { "version": "11.0.4", diff --git a/package.json b/package.json index d02516f6d5..2ee2dfebff 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@next/third-parties": "^14.2.3", "@prisma/client": "^5.13.0", "@sanity/diff-match-patch": "^3.1.1", + "@supabase/supabase-js": "^2.43.1", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/react-query": "~4.36.1", "@trpc/client": "10.44.1", diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index 10dbf4d14a..e2a2110b28 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -1,4 +1,29 @@ +import { apiAsync } from '~/common/util/trpc.client'; +import { createClient } from '@supabase/supabase-js' +import { Database } from './types/supabase' // Import the generated Supabase types -export const isValidSupabaseConnection = (supabaseUrl?: string, supabaseAnonKey?: string) =>!!supabaseUrl && !!supabaseAnonKey; +import { useSupabaseSyncStore } from "./store-module-supabase-sync"; +export const isValidSupabaseConnection = (url?: string, key?: string) => !!url && !!key; + +/** + * This function tests the Supabase connection + * @param url + * @param key + * @returns true if the connection is valid, false otherwise + */ +export async function testSupabaseConnection(url: string, key: string): Promise { + + // get the keys (empty if they're on server) + const { supabaseUrl, supabaseKey } = useSupabaseSyncStore.getState(); + + try { + const supabase = createClient(supabaseUrl, supabaseKey) + await supabase.auth.api.getUser(); + return true; + } catch (error: any) { + console.error(`testSupabaseConnection: ${error}`); + return false; + } +} \ No newline at end of file From 9a0c165cfdc67f9bd19c379d9937762472389f5b Mon Sep 17 00:00:00 2001 From: Joel M Date: Tue, 14 May 2024 08:15:45 +0000 Subject: [PATCH 04/21] wip on sync --- src/modules/google/GoogleSearchSettings.tsx | 2 -- src/modules/supabasesync/supabaseSync.client.ts | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/modules/google/GoogleSearchSettings.tsx b/src/modules/google/GoogleSearchSettings.tsx index 00f3bc563d..8566fbf846 100644 --- a/src/modules/google/GoogleSearchSettings.tsx +++ b/src/modules/google/GoogleSearchSettings.tsx @@ -28,12 +28,10 @@ export function GoogleSearchSettings() { const isValidKey = googleCloudApiKey ? isValidGoogleCloudApiKey(googleCloudApiKey) : backendHasGoogle; const isValidId = googleCSEId ? isValidGoogleCseId(googleCSEId) : backendHasGoogle; - const handleGoogleApiKeyChange = (e: React.ChangeEvent) => setGoogleCloudApiKey(e.target.value); const handleCseIdChange = (e: React.ChangeEvent) => setGoogleCSEId(e.target.value); - return <> diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index e2a2110b28..78cf6a8362 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -1,7 +1,7 @@ import { apiAsync } from '~/common/util/trpc.client'; -import { createClient } from '@supabase/supabase-js' -import { Database } from './types/supabase' // Import the generated Supabase types +import { createClient } from "@supabase/supabase-js"; +//import { Database } from './types/supabase' // Import the generated Supabase types import { useSupabaseSyncStore } from "./store-module-supabase-sync"; @@ -19,8 +19,11 @@ export async function testSupabaseConnection(url: string, key: string): Promise< const { supabaseUrl, supabaseKey } = useSupabaseSyncStore.getState(); try { - const supabase = createClient(supabaseUrl, supabaseKey) - await supabase.auth.api.getUser(); + console.log('test Connection'); + //const supabase = createClient(supabaseUrl, supabaseKey); + //supabase. + //await supabase.auth.api.getUser(); + //const { data: todos } = await supabase.from('todos').select(); return true; } catch (error: any) { console.error(`testSupabaseConnection: ${error}`); From 7aafb73c64ffb8ad9ccb8e6505b9c4ecb9a88ee2 Mon Sep 17 00:00:00 2001 From: Joel M Date: Tue, 14 May 2024 08:53:24 +0000 Subject: [PATCH 05/21] . --- src/modules/google/GoogleSearchSettings.tsx | 2 -- .../supabasesync/supabaseSync.client.ts | 5 +---- src/modules/trade/ExportChats.tsx | 18 ++++++++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/modules/google/GoogleSearchSettings.tsx b/src/modules/google/GoogleSearchSettings.tsx index 8566fbf846..149b458438 100644 --- a/src/modules/google/GoogleSearchSettings.tsx +++ b/src/modules/google/GoogleSearchSettings.tsx @@ -13,7 +13,6 @@ import { Link } from '~/common/components/Link'; import { isValidGoogleCloudApiKey, isValidGoogleCseId } from './search.client'; import { useGoogleSearchStore } from './store-module-google'; - export function GoogleSearchSettings() { // external state @@ -23,7 +22,6 @@ export function GoogleSearchSettings() { googleCSEId: state.googleCSEId, setGoogleCSEId: state.setGoogleCSEId, }), shallow); - // derived state const isValidKey = googleCloudApiKey ? isValidGoogleCloudApiKey(googleCloudApiKey) : backendHasGoogle; const isValidId = googleCSEId ? isValidGoogleCseId(googleCSEId) : backendHasGoogle; diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index 78cf6a8362..4f176bac1f 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -1,8 +1,5 @@ import { apiAsync } from '~/common/util/trpc.client'; - import { createClient } from "@supabase/supabase-js"; -//import { Database } from './types/supabase' // Import the generated Supabase types - import { useSupabaseSyncStore } from "./store-module-supabase-sync"; export const isValidSupabaseConnection = (url?: string, key?: string) => !!url && !!key; @@ -23,7 +20,7 @@ export async function testSupabaseConnection(url: string, key: string): Promise< //const supabase = createClient(supabaseUrl, supabaseKey); //supabase. //await supabase.auth.api.getUser(); - //const { data: todos } = await supabase.from('todos').select(); + //const { data: Conversations } = await supabase.from('Conversation').select(); return true; } catch (error: any) { console.error(`testSupabaseConnection: ${error}`); diff --git a/src/modules/trade/ExportChats.tsx b/src/modules/trade/ExportChats.tsx index 0388fc3b8e..878c18a148 100644 --- a/src/modules/trade/ExportChats.tsx +++ b/src/modules/trade/ExportChats.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { Box, Button, Grid, Typography } from '@mui/joy'; import DoneIcon from '@mui/icons-material/Done'; import FileDownloadIcon from '@mui/icons-material/FileDownload'; +import SyncIcon from '@mui/icons-material/Sync'; import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities'; @@ -13,6 +14,7 @@ import { KeyStroke } from '~/common/components/KeyStroke'; import { ChatLinkExport } from './link/ChatLinkExport'; import { PublishExport } from './publish/PublishExport'; import { downloadAllConversationsJson, downloadConversation } from './trade.client'; +//import { syncAllConversations } from './' //TODO: implement somewhere export type ExportConfig = { @@ -65,6 +67,12 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void } .catch(() => setDownloadedAllState('fail')); }; + const handleSyncAllConversations = () => { + syncAllConversations() + .then(() => setDownloadedAllState('ok')) + .catch(() => setDownloadedAllState('fail')); + } + const hasConversation = !!props.config.conversationId; @@ -147,6 +155,16 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void } Download All · JSON + + )} From 0f623bb14f2b8bc372c5f7fe1698576dcc34573a Mon Sep 17 00:00:00 2001 From: Joel M Date: Tue, 14 May 2024 18:59:26 +0000 Subject: [PATCH 06/21] sync wip --- .../supabasesync/supabaseSync.client.ts | 65 ++++++++++++++++++- src/modules/trade/ExportChats.tsx | 3 +- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index 4f176bac1f..60ebf52a70 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -1,6 +1,10 @@ import { apiAsync } from '~/common/util/trpc.client'; -import { createClient } from "@supabase/supabase-js"; +import { createClient, SupabaseClient } from "@supabase/supabase-js"; import { useSupabaseSyncStore } from "./store-module-supabase-sync"; +import { DModelSource, useModelsStore } from '~/modules/llms/store-llms'; +import { conversationToJsonV1 } from '~/modules/trade/trade.client'; +import { conversationTitle, DConversation, type DConversationId, DMessage, useChatStore } from '~/common/state/store-chats'; +import { DFolder, useFolderStore } from '~/common/state/store-folders'; export const isValidSupabaseConnection = (url?: string, key?: string) => !!url && !!key; @@ -26,4 +30,63 @@ export async function testSupabaseConnection(url: string, key: string): Promise< console.error(`testSupabaseConnection: ${error}`); return false; } +} + +function createSupabase(): SupabaseClient { + const { supabaseUrl, supabaseKey } = useSupabaseSyncStore.getState(); + const supabase = createClient(supabaseUrl, supabaseKey); + return supabase; +} + +async function getLastSyncTime(supabase: SupabaseClient): Promise { + const { data, error } = await supabase + .from('Conversation') + .select('updated') + .order('updated', { ascending: false }) + .limit(1); + + if (error) { + console.error('Error fetching lastSyncTime:', error); + return 0; + } + + if (data && data.length > 0) { + return data[0].updated; + } else { + return 0; + } +} + +export async function syncAllConversations() { + console.log('syncAllConversations'); + + const { folders, enableFolders } = useFolderStore.getState(); + const conversations = useChatStore.getState().conversations; //.map(conversationToJsonV1); + + const supabase = createSupabase(); + const lastSyncTime = await getLastSyncTime(supabase); + + // find all conversations that have been updated since the last sync + const updatedConversations = conversations + .filter(conversation => conversation.updated && conversation.updated > lastSyncTime) + .map(conversationToJsonV1); + + if (updatedConversations.length === 0) { + console.log('No conversations to sync'); + return; + } + + console.log(`Syncing ${updatedConversations.length} conversations`); + + const { data, error } = await supabase + .from('Conversation') + .upsert(updatedConversations, { returning:'minimal' }); + + if (error) { + console.error('Error syncing conversations:', error); + return; + } + + console.log(`Synced ${updatedConversations.length} conversations`); + } \ No newline at end of file diff --git a/src/modules/trade/ExportChats.tsx b/src/modules/trade/ExportChats.tsx index 878c18a148..b166f642d2 100644 --- a/src/modules/trade/ExportChats.tsx +++ b/src/modules/trade/ExportChats.tsx @@ -14,8 +14,7 @@ import { KeyStroke } from '~/common/components/KeyStroke'; import { ChatLinkExport } from './link/ChatLinkExport'; import { PublishExport } from './publish/PublishExport'; import { downloadAllConversationsJson, downloadConversation } from './trade.client'; -//import { syncAllConversations } from './' //TODO: implement somewhere - +import { syncAllConversations } from '~/modules/supabasesync/supabaseSync.client'; export type ExportConfig = { dir: 'export', From a1c41b07db46f12d327471e874eb3ef7f7287c8f Mon Sep 17 00:00:00 2001 From: Joel M Date: Wed, 15 May 2024 05:49:50 +0000 Subject: [PATCH 07/21] save to db working --- src/common/state/store-chats.ts | 4 +- .../supabasesync/supabaseSync.client.ts | 79 ++++++++++++++++--- src/modules/trade/ExportChats.tsx | 9 ++- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/src/common/state/store-chats.ts b/src/common/state/store-chats.ts index a1d3f226e7..2cb7fc185f 100644 --- a/src/common/state/store-chats.ts +++ b/src/common/state/store-chats.ts @@ -382,16 +382,18 @@ export const useChatStore = create()(devtools( systemPurposeId, }), - setAutoTitle: (conversationId: string, autoTitle: string) => + setAutoTitle: (conversationId: string, autoTitle: string, touchUpdated: boolean = true) => _get()._editConversation(conversationId, { autoTitle, + updated: Date.now(), }), setUserTitle: (conversationId: string, userTitle: string) => _get()._editConversation(conversationId, { userTitle, + updated: Date.now(), }), }), diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index 60ebf52a70..742510a594 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -38,9 +38,9 @@ function createSupabase(): SupabaseClient { return supabase; } -async function getLastSyncTime(supabase: SupabaseClient): Promise { +async function getServersLastSyncTime(supabase: SupabaseClient): Promise { const { data, error } = await supabase - .from('Conversation') + .from('conversation') .select('updated') .order('updated', { ascending: false }) .limit(1); @@ -57,19 +57,14 @@ async function getLastSyncTime(supabase: SupabaseClient): Promise { } } -export async function syncAllConversations() { - console.log('syncAllConversations'); +async function syncToServer(supabase: SupabaseClient, conversations: DConversation[]): Promise { + // find all conversations that have been updated since the last sync - const { folders, enableFolders } = useFolderStore.getState(); - const conversations = useChatStore.getState().conversations; //.map(conversationToJsonV1); + const lastSyncTime = await getServersLastSyncTime(supabase); - const supabase = createSupabase(); - const lastSyncTime = await getLastSyncTime(supabase); - - // find all conversations that have been updated since the last sync const updatedConversations = conversations .filter(conversation => conversation.updated && conversation.updated > lastSyncTime) - .map(conversationToJsonV1); + .map(conversationToJsonV1); // this removes some of the fields we want to sync if (updatedConversations.length === 0) { console.log('No conversations to sync'); @@ -79,8 +74,8 @@ export async function syncAllConversations() { console.log(`Syncing ${updatedConversations.length} conversations`); const { data, error } = await supabase - .from('Conversation') - .upsert(updatedConversations, { returning:'minimal' }); + .from('conversation') + .upsert(updatedConversations); if (error) { console.error('Error syncing conversations:', error); @@ -89,4 +84,62 @@ export async function syncAllConversations() { console.log(`Synced ${updatedConversations.length} conversations`); +} + +async function syncFromServerToClient(supabase: SupabaseClient, conversations: DConversation[], maxConversationTime: number): Promise { + + // Find all conversations from the server where the updated field is greater than maxConversationTime + console.log(`Fetching conversations from server > ${maxConversationTime}`); + + const { data, error } = await supabase + .from('conversation') + .select("*") + .gt('updated', maxConversationTime); + + if (error) { + console.error('Error fetching conversations from Server:', error); + return; + } + + // map server data into conversations, this will need to be saved back into state + + // if the conversation.id exists then replace it with the value from the data + // if the conversation does not exist then we need to add + + if (data && data.length > 0) { + console.log(`Found ${data.length} conversations from server`); + const conversationsFromServer = data.map(conversationFromServer => { + const conversation = conversations.find(conversation => conversation.id === conversationFromServer.id); + if (conversation) { + return { + ...conversation, + updated: conversationFromServer.updated, + }; + } else { + return conversationFromServer; + } + }); + console.log(`Found ${conversationsFromServer.length} conversations from server`); + console.warn("update ui still to do..."); + //useChatStore.getState().setConversations(conversationsFromServer); + //cOutcome.importedConversationId = useChatStore.getState().importConversation(cOutcome.conversation, preventClash); + } else { + console.log('No conversations from server'); + } + + +} + +export async function syncAllConversations() { + console.log('syncAllConversations'); + + //const { folders, enableFolders } = useFolderStore.getState(); + const conversations = useChatStore.getState().conversations; //.map(conversationToJsonV1); + const supabase = createSupabase(); + // find the max `updated` value from all conversations (must do this before we sync with server) + const maxConversationTime = Math.max(...conversations.map(conversation => conversation.updated || 0)); + await syncToServer(supabase, conversations); + + //await syncFromServerToClient(supabase, conversations); + } \ No newline at end of file diff --git a/src/modules/trade/ExportChats.tsx b/src/modules/trade/ExportChats.tsx index b166f642d2..6bfc021bcc 100644 --- a/src/modules/trade/ExportChats.tsx +++ b/src/modules/trade/ExportChats.tsx @@ -32,6 +32,7 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void } const [downloadedJSONState, setDownloadedJSONState] = React.useState<'ok' | 'fail' | null>(null); const [downloadedMarkdownState, setDownloadedMarkdownState] = React.useState<'ok' | 'fail' | null>(null); const [downloadedAllState, setDownloadedAllState] = React.useState<'ok' | 'fail' | null>(null); + const [syncAllState, setSyncAllState] = React.useState<'ok' | 'fail' | null>(null); // external state const enableSharing = getBackendCapabilities().hasDB; @@ -68,8 +69,8 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void } const handleSyncAllConversations = () => { syncAllConversations() - .then(() => setDownloadedAllState('ok')) - .catch(() => setDownloadedAllState('fail')); + .then(() => setSyncAllState('ok')) + .catch(() => setSyncAllState('fail')); } @@ -156,8 +157,8 @@ export function ExportChats(props: { config: ExportConfig, onClose: () => void } + {syncMessage && ( + + {syncMessage} + + )} + )} From da8c26e6441855037a5786364b095d5b96af3bf0 Mon Sep 17 00:00:00 2001 From: Joel M Date: Wed, 15 May 2024 11:03:12 +0000 Subject: [PATCH 11/21] publish fixes --- src/modules/backend/backend.router.ts | 1 + src/modules/backend/store-backend-capabilities.ts | 1 + src/modules/supabasesync/SupabaseSyncSettings.tsx | 2 +- src/server/env.mjs | 4 ++++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/modules/backend/backend.router.ts b/src/modules/backend/backend.router.ts index 2d39c10304..1b2a028a31 100644 --- a/src/modules/backend/backend.router.ts +++ b/src/modules/backend/backend.router.ts @@ -60,6 +60,7 @@ export const backendRouter = createTRPCRouter({ hasLlmPerplexity: !!env.PERPLEXITY_API_KEY, hasLlmTogetherAI: !!env.TOGETHERAI_API_KEY, hasVoiceElevenLabs: !!env.ELEVENLABS_API_KEY, + hasSupabaseSync: !!env.SUPABASE_SYNC_URL && !!env.SUPABASE_SYNC_KEY, llmConfigHash: generateLlmEnvConfigHash(env), }; }), diff --git a/src/modules/backend/store-backend-capabilities.ts b/src/modules/backend/store-backend-capabilities.ts index 15adb79fc4..f4ab583384 100644 --- a/src/modules/backend/store-backend-capabilities.ts +++ b/src/modules/backend/store-backend-capabilities.ts @@ -54,6 +54,7 @@ const useBackendCapabilitiesStore = create()( hasLlmPerplexity: false, hasLlmTogetherAI: false, hasVoiceElevenLabs: false, + hasSupabaseSync: false, llmConfigHash: '', loadedCapabilities: false, diff --git a/src/modules/supabasesync/SupabaseSyncSettings.tsx b/src/modules/supabasesync/SupabaseSyncSettings.tsx index dd08048c59..a2fb73f4d1 100644 --- a/src/modules/supabasesync/SupabaseSyncSettings.tsx +++ b/src/modules/supabasesync/SupabaseSyncSettings.tsx @@ -45,7 +45,7 @@ export function SupabaseSyncSettings() { return <> - Configure the Supabase Chat Sync, if you don't have a Supabase account you will need to create one. + Configure the Supabase Chat Sync, if you do not have a Supabase account you will need to create one. diff --git a/src/server/env.mjs b/src/server/env.mjs index 8c75b7ec2b..562ef09d73 100644 --- a/src/server/env.mjs +++ b/src/server/env.mjs @@ -68,6 +68,10 @@ export const env = createEnv({ GOOGLE_CLOUD_API_KEY: z.string().optional(), GOOGLE_CSE_ID: z.string().optional(), + // Supabase Sync + SUPABASE_SYNC_URL: z.string().url().optional(), + SUPABASE_SYNC_KEY: z.string().optional(), + // Browsing Service PUPPETEER_WSS_ENDPOINT: z.string().url().optional(), From f4be5dc69d38129cf5a22caaef65822fdc30516f Mon Sep 17 00:00:00 2001 From: Joel M Date: Wed, 15 May 2024 11:12:58 +0000 Subject: [PATCH 12/21] PR tidy up --- src/common/state/store-chats.ts | 2 +- src/modules/google/GoogleSearchSettings.tsx | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/common/state/store-chats.ts b/src/common/state/store-chats.ts index 2cb7fc185f..752a2067d0 100644 --- a/src/common/state/store-chats.ts +++ b/src/common/state/store-chats.ts @@ -382,7 +382,7 @@ export const useChatStore = create()(devtools( systemPurposeId, }), - setAutoTitle: (conversationId: string, autoTitle: string, touchUpdated: boolean = true) => + setAutoTitle: (conversationId: string, autoTitle: string) => _get()._editConversation(conversationId, { autoTitle, diff --git a/src/modules/google/GoogleSearchSettings.tsx b/src/modules/google/GoogleSearchSettings.tsx index 149b458438..00f3bc563d 100644 --- a/src/modules/google/GoogleSearchSettings.tsx +++ b/src/modules/google/GoogleSearchSettings.tsx @@ -13,6 +13,7 @@ import { Link } from '~/common/components/Link'; import { isValidGoogleCloudApiKey, isValidGoogleCseId } from './search.client'; import { useGoogleSearchStore } from './store-module-google'; + export function GoogleSearchSettings() { // external state @@ -22,14 +23,17 @@ export function GoogleSearchSettings() { googleCSEId: state.googleCSEId, setGoogleCSEId: state.setGoogleCSEId, }), shallow); + // derived state const isValidKey = googleCloudApiKey ? isValidGoogleCloudApiKey(googleCloudApiKey) : backendHasGoogle; const isValidId = googleCSEId ? isValidGoogleCseId(googleCSEId) : backendHasGoogle; + const handleGoogleApiKeyChange = (e: React.ChangeEvent) => setGoogleCloudApiKey(e.target.value); const handleCseIdChange = (e: React.ChangeEvent) => setGoogleCSEId(e.target.value); + return <> From 460068ba158c1bc921aaaefa6aa40c6d29b21105 Mon Sep 17 00:00:00 2001 From: Joel M Date: Fri, 17 May 2024 05:54:44 +0000 Subject: [PATCH 13/21] improve sync + support sync from Module settings (e.g. first sync) --- .../supabasesync/SupabaseSyncSettings.tsx | 36 +++++++++++++++++-- .../supabasesync/supabaseSync.client.ts | 23 +++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/modules/supabasesync/SupabaseSyncSettings.tsx b/src/modules/supabasesync/SupabaseSyncSettings.tsx index a2fb73f4d1..214814360d 100644 --- a/src/modules/supabasesync/SupabaseSyncSettings.tsx +++ b/src/modules/supabasesync/SupabaseSyncSettings.tsx @@ -1,7 +1,9 @@ import * as React from 'react'; import { shallow } from 'zustand/shallow'; -import { FormControl, FormHelperText, Input } from '@mui/joy'; +import { FormControl, FormHelperText, Input, Button, Typography } from '@mui/joy'; +import DoneIcon from '@mui/icons-material/Done'; +import SyncIcon from '@mui/icons-material/Sync'; import KeyIcon from '@mui/icons-material/Key'; import SearchIcon from '@mui/icons-material/Search'; @@ -12,7 +14,7 @@ import { Link } from '~/common/components/Link'; import { isValidSupabaseConnection } from './supabaseSync.client'; import { useSupabaseSyncStore } from './store-module-supabase-sync'; - +import { syncAllConversations } from '~/modules/supabasesync/supabaseSync.client'; export function SupabaseSyncSettings() { @@ -28,6 +30,8 @@ export function SupabaseSyncSettings() { // derived state const isValueUrl = supabaseUrl ? isValidSupabaseConnection(supabaseUrl, supabaseKey) : backendHasSupabaseSync; const isValidAnonKey = isValueUrl; + const [syncAllState, setSyncAllState] = React.useState<'ok' | 'fail' | null>(null); + const [syncMessage, setSyncMessage] = React.useState(null); const handleSupabaseSyncChange = (e: React.ChangeEvent) => setSupabaseUrl(e.target.value); @@ -42,6 +46,15 @@ export function SupabaseSyncSettings() { setLastSyncTime(value); } + const handleSyncAllConversations = async () => { + try { + const syncedCount = await syncAllConversations(setSyncMessage); + setSyncAllState('ok'); + } catch { + setSyncAllState('fail'); + } + } + return <> @@ -86,6 +99,25 @@ export function SupabaseSyncSettings() { sx={{ width: '100%' }} /> + + WARNING: Resetting Last Synced to 0 will force push all exiting chats to the server and will overwrite them. + + {(lastSyncTime === 0) && ( + + )} + {syncMessage && ( + + {syncMessage} + + )} ; } \ No newline at end of file diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index 555b5cb74e..e820334b0f 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -77,13 +77,14 @@ async function getServersLastSyncTime(supabase: SupabaseClient): Promise } } -async function syncToServer(supabase: SupabaseClient, conversations: DConversation[]): Promise { +async function syncToServer(supabase: SupabaseClient, conversations: DConversation[], lastSyncTime: number): Promise { // find all conversations that have been updated since the last sync - const lastSyncTime = await getServersLastSyncTime(supabase); + // not the last time the server was synced as we may have changes that were before another client synced and those would get missed + // sync time needs to be the last time this instance synced with the server const updatedConversations = conversations - .filter(conversation => conversation.updated && conversation.updated > lastSyncTime) + .filter(conversation => conversation.updated && conversation.updated > lastSyncTime && conversation.messages.length > 0) .map(conversationToJsonV1); // this removes some of the fields we want to sync if (updatedConversations.length === 0) { @@ -123,18 +124,22 @@ async function syncFromServerToClient(supabase: SupabaseClient, conversations: D const conversationsFromServer: SupabaseConversation[] = data.map(record => ({ ...record })); const importConversation = useChatStore.getState().importConversation; - + let importCount = 0; conversationsFromServer.forEach(conversationFromServer => { let conversation = conversations.find(conversation => conversation.id === conversationFromServer.id); + if (conversation) { + // we may have just sent this to the server, in which case we don't need to update it // is it already updated (e.g. did we just push that to the server?) - if (conversation.updated && conversation.updated > (conversationFromServer.updated ?? 0)) { + if (conversation.updated && conversation.updated >= (conversationFromServer.updated ?? 0)) { return; // the same, don't touch } } else { conversation = createDConversation(); conversation.id = conversationFromServer.id; conversation.created = conversationFromServer.created; + + //conversations.push(conversation); // insert the new conversation into the current working array } conversation.updated = conversationFromServer.updated; @@ -143,9 +148,10 @@ async function syncFromServerToClient(supabase: SupabaseClient, conversations: D conversation.messages = conversationFromServer.messages; importConversation(conversation, false); + importCount++; }); - return conversationsFromServer.length; + return importCount; } else { console.debug('No conversations from server'); } @@ -160,15 +166,16 @@ export async function syncAllConversations(setMessage?: (message: string | null) //const { folders, enableFolders } = useFolderStore.getState(); // ToDo: folder Sync ? try { + logInfo('Starting sync to server...'); - const pushedCount = await syncToServer(supabase, conversations); + const pushedCount = await syncToServer(supabase, conversations, lastSyncTime); logInfo('Sync to server completed.'); const updatedSyncTime = Date.now(); logInfo('Starting sync from server to client...'); const pulledCount = await syncFromServerToClient(supabase, conversations, lastSyncTime); logInfo('Sync from server to client completed.'); - + setLastSyncTime(updatedSyncTime); logInfo(`Sync completed. Last sync time updated to ${updatedSyncTime}.`); setMessage?.(`Sync Successful, ${pushedCount} pushed, ${pulledCount} pulled`); From 002c6502e78323d10a42003bc7919124c054a4f6 Mon Sep 17 00:00:00 2001 From: Joel Meikle Date: Sat, 25 May 2024 15:29:42 +1200 Subject: [PATCH 14/21] wip not working, but might be close. need to put session info in app top level and wire up Suapbase Auth component --- package-lock.json | 38 ++- package.json | 4 +- .../supabasesync/SupabaseSyncSettings.tsx | 289 +++++++++++------- .../store-module-supabase-sync.ts | 51 ++-- .../supabasesync/supabaseSync.client.ts | 108 ++++--- src/modules/supabasesync/use-session.ts | 81 +++++ 6 files changed, 382 insertions(+), 189 deletions(-) create mode 100644 src/modules/supabasesync/use-session.ts diff --git a/package-lock.json b/package-lock.json index ebb119d397..4df962d6e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,9 @@ "@next/third-parties": "^14.2.3", "@prisma/client": "^5.13.0", "@sanity/diff-match-patch": "^3.1.1", - "@supabase/supabase-js": "^2.43.1", + "@supabase/auth-ui-react": "^0.4.7", + "@supabase/auth-ui-shared": "^0.1.8", + "@supabase/supabase-js": "^2.43.4", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/react-query": "~4.36.1", "@trpc/client": "10.44.1", @@ -1781,6 +1783,11 @@ "node": ">=14.18" } }, + "node_modules/@stitches/core": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@stitches/core/-/core-1.2.8.tgz", + "integrity": "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg==" + }, "node_modules/@supabase/auth-js": { "version": "2.64.2", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.64.2.tgz", @@ -1789,6 +1796,29 @@ "@supabase/node-fetch": "^2.6.14" } }, + "node_modules/@supabase/auth-ui-react": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@supabase/auth-ui-react/-/auth-ui-react-0.4.7.tgz", + "integrity": "sha512-Lp4FQGFh7BMX1Y/BFaUKidbryL7eskj1fl6Lby7BeHrTctbdvDbCMjVKS8wZ2rxuI8FtPS2iU900fSb70FHknQ==", + "dependencies": { + "@stitches/core": "^1.2.8", + "@supabase/auth-ui-shared": "0.1.8", + "prop-types": "^15.7.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.21.0" + } + }, + "node_modules/@supabase/auth-ui-shared": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@supabase/auth-ui-shared/-/auth-ui-shared-0.1.8.tgz", + "integrity": "sha512-ouQ0DjKcEFg+0gZigFIEgu01V3e6riGZPzgVD0MJsCBNsMsiDT74+GgCEIElMUpTGkwSja3xLwdFRFgMNFKcjg==", + "peerDependencies": { + "@supabase/supabase-js": "^2.21.0" + } + }, "node_modules/@supabase/functions-js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.3.1.tgz", @@ -1856,9 +1886,9 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.43.1", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.43.1.tgz", - "integrity": "sha512-A+RV50mWNtyKo6M0u4G6AOqEifQD+MoOjZcpRkPMPpEAFgMsc2dt3kBlBlR/MgZizWQgUKhsvrwKk0efc8g6Ug==", + "version": "2.43.4", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.43.4.tgz", + "integrity": "sha512-/pLPaxiIsn5Vaz3s32HC6O/VNwfeddnzS0bZRpOW0AKcPuXroD8pT9G8mpiBlZfpKsMmq6k7tlhW7Sr1PAQ1lw==", "dependencies": { "@supabase/auth-js": "2.64.2", "@supabase/functions-js": "2.3.1", diff --git a/package.json b/package.json index 9385d354c9..4605137afa 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,9 @@ "@next/third-parties": "^14.2.3", "@prisma/client": "^5.13.0", "@sanity/diff-match-patch": "^3.1.1", - "@supabase/supabase-js": "^2.43.1", + "@supabase/auth-ui-react": "^0.4.7", + "@supabase/auth-ui-shared": "^0.1.8", + "@supabase/supabase-js": "^2.43.4", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/react-query": "~4.36.1", "@trpc/client": "10.44.1", diff --git a/src/modules/supabasesync/SupabaseSyncSettings.tsx b/src/modules/supabasesync/SupabaseSyncSettings.tsx index 214814360d..f1e429ff78 100644 --- a/src/modules/supabasesync/SupabaseSyncSettings.tsx +++ b/src/modules/supabasesync/SupabaseSyncSettings.tsx @@ -1,123 +1,188 @@ -import * as React from 'react'; -import { shallow } from 'zustand/shallow'; +import {useContext, useEffect, useState, ChangeEvent} from "react"; +import {shallow} from 'zustand/shallow'; -import { FormControl, FormHelperText, Input, Button, Typography } from '@mui/joy'; +import {FormControl, FormHelperText, Input, Button, Typography, Box} from '@mui/joy'; +import {GoodModal} from '~/common/components/GoodModal'; import DoneIcon from '@mui/icons-material/Done'; import SyncIcon from '@mui/icons-material/Sync'; -import KeyIcon from '@mui/icons-material/Key'; import SearchIcon from '@mui/icons-material/Search'; -import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities'; +import {getBackendCapabilities} from '~/modules/backend/store-backend-capabilities'; +import {AlreadySet} from '~/common/components/AlreadySet'; +import {FormInputKey} from '~/common/components/forms/FormInputKey'; +import {FormLabelStart} from '~/common/components/forms/FormLabelStart'; +import {Link} from '~/common/components/Link'; -import { FormLabelStart } from '~/common/components/forms/FormLabelStart'; -import { Link } from '~/common/components/Link'; - -import { isValidSupabaseConnection } from './supabaseSync.client'; -import { useSupabaseSyncStore } from './store-module-supabase-sync'; -import { syncAllConversations } from '~/modules/supabasesync/supabaseSync.client'; +import {isValidSupabaseConnection} from './supabaseSync.client'; +import {useSupabaseSyncStore} from './store-module-supabase-sync'; +import {syncAllConversations, getSupabaseClient} from '~/modules/supabasesync/supabaseSync.client'; +import {useSession, UserContext} from '~/modules/supabasesync/use-session'; +import {Auth} from '@supabase/auth-ui-react' +import {ThemeSupa} from '@supabase/auth-ui-shared' export function SupabaseSyncSettings() { + //const [session, setSession] = React.useState(null) + const [loginDialogIsOpen, setLoginDialogIsOpen] = useState(false) + const [authMode, setAuthMode] = useState<"sign_in" | "sign_up">("sign_in"); + + const {session, profile} = useContext(UserContext); + const userInfo = useSession(); + + // external state + const backendHasSupabaseSync = getBackendCapabilities().hasSupabaseSync; + const { + supabaseUrl, + setSupabaseUrl, + supabaseAnonKey: supabaseKey, + setSupabaseAnonKey, + lastSyncTime, + setLastSyncTime + } = useSupabaseSyncStore(state => ({ + supabaseUrl: state.supabaseUrl, setSupabaseUrl: state.setSupabaseUrl, + supabaseAnonKey: state.supabaseKey, setSupabaseAnonKey: state.setSupabaseKey, + lastSyncTime: state.lastSyncTime, setLastSyncTime: state.setLastSyncTime, + }), shallow); + + + // derived state + const isValidUrl = supabaseUrl ? isValidSupabaseConnection(supabaseUrl, supabaseKey) : backendHasSupabaseSync; + const isValidAnonKey = isValidUrl; + const [syncAllState, setSyncAllState] = useState<'ok' | 'fail' | null>(null); + const [syncMessage, setSyncMessage] = useState(null); + + // set sync time from input + const handleLastSyncTimeChange = (e: ChangeEvent) => { + // need to convert e.target.value to a number or 0 if empty or nan + let value = e.target.value ? Number(e.target.value) : 0; + if (isNaN(value)) + value = 0; + setLastSyncTime(value); + } + + const handleSyncAllConversations = async () => { + try { + const syncedCount = await syncAllConversations(setSyncMessage); + setSyncAllState('ok'); + } catch { + setSyncAllState('fail'); + } + } - // external state - const backendHasSupabaseSync = getBackendCapabilities().hasSupabaseSync; - const { supabaseUrl, setSupabaseUrl, supabaseAnonKey: supabaseKey, setSupabaseAnonKey, lastSyncTime, setLastSyncTime } = useSupabaseSyncStore(state => ({ - supabaseUrl: state.supabaseUrl, setSupabaseUrl: state.setSupabaseUrl, - supabaseAnonKey: state.supabaseKey, setSupabaseAnonKey: state.setSupabaseKey, - lastSyncTime: state.lastSyncTime, setLastSyncTime: state.setLastSyncTime, - }), shallow); - - - // derived state - const isValueUrl = supabaseUrl ? isValidSupabaseConnection(supabaseUrl, supabaseKey) : backendHasSupabaseSync; - const isValidAnonKey = isValueUrl; - const [syncAllState, setSyncAllState] = React.useState<'ok' | 'fail' | null>(null); - const [syncMessage, setSyncMessage] = React.useState(null); - - const handleSupabaseSyncChange = (e: React.ChangeEvent) => setSupabaseUrl(e.target.value); - - const handleSupabaseAnonKeyChange = (e: React.ChangeEvent) => setSupabaseAnonKey(e.target.value); - - // set sync time from input - const handleLastSyncTimeChange = (e: React.ChangeEvent) => { - // need to convert e.target.value to a number or 0 if empty or nan - let value = e.target.value? Number(e.target.value) : 0; - if (isNaN(value)) - value = 0; - setLastSyncTime(value); - } - - const handleSyncAllConversations = async () => { - try { - const syncedCount = await syncAllConversations(setSyncMessage); - setSyncAllState('ok'); - } catch { - setSyncAllState('fail'); + const handleLogin = () => { + // can only call if its valid + if (isValidAnonKey) { + setAuthMode("sign_in"); + setLoginDialogIsOpen(true); + } } - } - - return <> - - - Configure the Supabase Chat Sync, if you do not have a Supabase account you will need to create one. - - - - Create one here} - tooltip='Create your Supabase Database and enter the url here' /> - } - slotProps={{ input: { sx: { width: '100%' } } }} - sx={{ width: '100%' }} - /> - - - - - } - slotProps={{ input: { sx: { width: '100%' } } }} - sx={{ width: '100%' }} - /> - - - - - } - slotProps={{ input: { sx: { width: '100%' } } }} - sx={{ width: '100%' }} - /> - - - WARNING: Resetting Last Synced to 0 will force push all exiting chats to the server and will overwrite them. - - {(lastSyncTime === 0) && ( - - )} - {syncMessage && ( - - {syncMessage} - - )} - - ; + + const handleSignUp = () => { + if (isValidAnonKey) { + setAuthMode("sign_up"); + setLoginDialogIsOpen(true); + } + } + + return <> + + + Configure the Supabase Chat Sync, if you do not have a Supabase account you will need to create + one here, or you can use the Sign Up button + below. + + + } + required={!backendHasSupabaseSync} + isError={!isValidUrl && !backendHasSupabaseSync} + placeholder={backendHasSupabaseSync ? '...' : 'https://...supabase.co (or your self hosted url)'} + /> + + } + required={!backendHasSupabaseSync} + isError={!isValidAnonKey && !backendHasSupabaseSync} + placeholder={backendHasSupabaseSync ? '...' : 'SUPABASE_ANON_KEY'} + /> + + + + } + slotProps={{input: {sx: {width: '100%'}}}} + sx={{width: '100%'}} + /> + + + WARNING: Resetting Last Synced to 0 will force push all exiting chats to the server and will overwrite + them. + + + + + + + + {syncMessage && ( + + {syncMessage} + + )} + + {session && profile && ( + + Logged in as {profile?.username} + + )} + + setLoginDialogIsOpen(false)} + > + + + + ; } \ No newline at end of file diff --git a/src/modules/supabasesync/store-module-supabase-sync.ts b/src/modules/supabasesync/store-module-supabase-sync.ts index ca978926ec..a2a059b5e1 100644 --- a/src/modules/supabasesync/store-module-supabase-sync.ts +++ b/src/modules/supabasesync/store-module-supabase-sync.ts @@ -1,39 +1,44 @@ -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; - +import {create} from 'zustand'; +import {persist} from 'zustand/middleware'; interface ModuleSupabaseSyncStore { - // Supabase Sync Settings + // Supabase Sync Settings + + supabaseUrl: string; + setSupabaseUrl: (supabaseUrl: string) => void; - supabaseUrl: string; - setSupabaseUrl: (supaUrl: string) => void; + supabaseKey: string; + setSupabaseKey: (key: string) => void; - supabaseKey: string; - setSupabaseKey: (key: string) => void; + lastSyncTime: number; + setLastSyncTime: (lastSyncTime: number) => void; - lastSyncTime: number; - setLastSyncTime: (lastSyncTime: number) => void; + userEmail: string; + setUserEmail: (userEmail: string) => void; } export const useSupabaseSyncStore = create()( - persist( - (set) => ({ + persist( + (set) => ({ + + // Supabase Sync Settings - // Supabase Sync Settings + supabaseUrl: '', + setSupabaseUrl: (supabaseUrl: string) => set({supabaseUrl: supabaseUrl}), - supabaseUrl: '', - setSupabaseUrl: (supaUrl: string) => set({ supabaseUrl: supaUrl }), + supabaseKey: '', + setSupabaseKey: (key: string) => set({supabaseKey: key}), - supabaseKey: '', - setSupabaseKey: (key: string) => set({ supabaseKey: key }), + lastSyncTime: 0, + setLastSyncTime: (lastSyncTime: number) => set({lastSyncTime: lastSyncTime}), - lastSyncTime: 0, - setLastSyncTime: (lastSyncTime: number) => set({ lastSyncTime: lastSyncTime }), + userEmail: '', + setUserEmail: (userEmail: string) => set({userEmail: userEmail}), - }), - { - name: 'app-module-supabase-sync', - }), + }), + { + name: 'app-module-supabase-sync', + }), ); \ No newline at end of file diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index e820334b0f..a91f4c1c47 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -1,12 +1,8 @@ -//import { apiAsync } from '~/common/util/trpc.client'; import { createClient, SupabaseClient } from "@supabase/supabase-js"; import { useSupabaseSyncStore } from "./store-module-supabase-sync"; -import { DModelSource, useModelsStore } from '~/modules/llms/store-llms'; import { conversationToJsonV1 } from '~/modules/trade/trade.client'; -import { conversationTitle, DConversation, type DConversationId, DMessage, useChatStore, createDConversation } from '~/common/state/store-chats'; -//import { DFolder, useFolderStore } from '~/common/state/store-folders'; -import { shallow } from 'zustand/shallow'; -import { defaultSystemPurposeId, SystemPurposeId, SystemPurposes } from '../../data'; +import { DConversation, DMessage, useChatStore, createDConversation } from '~/common/state/store-chats'; +import { SystemPurposeId } from '../../data'; type SupabaseConversation = { id: string; @@ -28,55 +24,69 @@ function logError(message: string, error: any) { console.error(`[ERROR]: ${message}`, error); } -/** - * This function tests the Supabase connection - * @param url - * @param key - * @returns true if the connection is valid, false otherwise - */ -export async function testSupabaseConnection(url: string, key: string): Promise { +// Singleton instance of Supabase Client, recreate when api key changes +let supabaseClientInstance: SupabaseClient | null = null; +let lastSupabaseClientKey: string | null = null; - // get the keys (empty if they're on server) +// Singleton instance of Supabase Realtime, recreate when api key changes +export function getSupabaseClient(): SupabaseClient { const { supabaseUrl, supabaseKey } = useSupabaseSyncStore.getState(); - try { - console.log('test Connection'); - //const supabase = createClient(supabaseUrl, supabaseKey); - //supabase. - //await supabase.auth.api.getUser(); - //const { data: Conversations } = await supabase.from('Conversation').select(); - return true; - } catch (error: any) { - console.error(`testSupabaseConnection: ${error}`); - return false; - } -} - -function createSupabase(): SupabaseClient { - const { supabaseUrl, supabaseKey } = useSupabaseSyncStore.getState(); - const supabase = createClient(supabaseUrl, supabaseKey); - return supabase; -} - -async function getServersLastSyncTime(supabase: SupabaseClient): Promise { - const { data, error } = await supabase - .from('conversation') - .select('updated') - .order('updated', { ascending: false }) - .limit(1); - - if (error) { - console.error('Error fetching lastSyncTime:', error); - return 0; - } - - if (data && data.length > 0) { - return data[0].updated; + // if the url or key is not set the return null + if (supabaseClientInstance && lastSupabaseClientKey === supabaseKey) { + return supabaseClientInstance; } else { - return 0; + + // dispose of the previous instance if it exists + // if (supabaseClientInstance) { + // await supabaseClientInstance.auth.signOut(); + // supabaseClientInstance = null; + // } + + supabaseClientInstance = createClient(supabaseUrl, supabaseKey); + lastSupabaseClientKey = supabaseKey; + return supabaseClientInstance; } } +// async function signIn(): Promise { +// const { supabaseUrl, supabaseKey } = useSupabaseSyncStore.getState(); +// if (!isValidSupabaseConnection(supabaseUrl, supabaseKey)) { +// throw new Error('Invalid Supabase Connection'); +// } +// +// const supabase = createSupabaseClient(); +// const { data, error } = await supabase +// .auth.signInWithPassword(); +// +// if (error) { +// throw error; +// } +// +// if (!data) { +// throw new Error('Invalid Supabase Connection'); +// } +// } + +// async function getServersLastSyncTime(supabase: SupabaseClient): Promise { +// const { data, error } = await supabase +// .from('conversation') +// .select('updated') +// .order('updated', { ascending: false }) +// .limit(1); +// +// if (error) { +// console.error('Error fetching lastSyncTime:', error); +// return 0; +// } +// +// if (data && data.length > 0) { +// return data[0].updated; +// } else { +// return 0; +// } +// } + async function syncToServer(supabase: SupabaseClient, conversations: DConversation[], lastSyncTime: number): Promise { // find all conversations that have been updated since the last sync @@ -162,7 +172,7 @@ async function syncFromServerToClient(supabase: SupabaseClient, conversations: D export async function syncAllConversations(setMessage?: (message: string | null) => void): Promise { const { lastSyncTime, setLastSyncTime } = useSupabaseSyncStore.getState(); const conversations = useChatStore.getState().conversations; - const supabase = createSupabase(); + const supabase = await getSupabaseClient(); //const { folders, enableFolders } = useFolderStore.getState(); // ToDo: folder Sync ? try { diff --git a/src/modules/supabasesync/use-session.ts b/src/modules/supabasesync/use-session.ts new file mode 100644 index 0000000000..3d7874e42d --- /dev/null +++ b/src/modules/supabasesync/use-session.ts @@ -0,0 +1,81 @@ +import { RealtimeChannel, Session } from "@supabase/supabase-js"; +import { useEffect, useState, createContext } from "react"; +import { getSupabaseClient } from '~/modules/supabasesync/supabaseSync.client'; +import * as React from "react"; + +export interface UserProfile { + username: string; + avatarUrl?: string; +} + +export interface BigAgiUserInfo { + session: Session | null; + profile: UserProfile | null; +} + +export const UserContext = React.createContext({ + session: null, + profile: null, +}); + + +export function useSession(): BigAgiUserInfo { + const [userInfo, setUserInfo] = useState({ + profile: null, + session: null, + }); + const [channel, setChannel] = useState(null); + useEffect(() => { + const supaClient = getSupabaseClient(); + supaClient.auth.getSession().then(({ data: { session } }) => { + setUserInfo({ ...userInfo, session }); + supaClient.auth.onAuthStateChange((_event, session) => { + setUserInfo({ session, profile: null }); + }); + }); + }, []); + + useEffect(() => { + if (userInfo.session?.user && !userInfo.profile) { + listenToUserProfileChanges(userInfo.session.user.id).then( + (newChannel) => { + if (channel) { + channel.unsubscribe(); + } + setChannel(newChannel); + } + ); + } else if (!userInfo.session?.user) { + channel?.unsubscribe(); + setChannel(null); + } + }, [userInfo.session]); + + async function listenToUserProfileChanges(userId: string) { + const supaClient = getSupabaseClient(); + const { data } = await supaClient + .from("user_profiles") + .select("*") + .filter("user_id", "eq", userId); + if (data?.[0]) { + setUserInfo({ ...userInfo, profile: data?.[0] }); + } + return supaClient + .channel(`public:user_profiles`) + .on( + "postgres_changes", + { + event: "*", + schema: "public", + table: "user_profiles", + filter: `user_id=eq.${userId}`, + }, + (payload) => { + setUserInfo({ ...userInfo, profile: payload.new as UserProfile }); + } + ) + .subscribe(); + } + + return userInfo; +} \ No newline at end of file From 23afb2563cf0eb4bb7a6c4c336ccb0108667cc2b Mon Sep 17 00:00:00 2001 From: Joel M Date: Mon, 3 Jun 2024 04:11:33 +0000 Subject: [PATCH 15/21] doc: supabase db schema --- src/modules/supabasesync/supabase-schema.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/modules/supabasesync/supabase-schema.sql diff --git a/src/modules/supabasesync/supabase-schema.sql b/src/modules/supabasesync/supabase-schema.sql new file mode 100644 index 0000000000..40ce03ae31 --- /dev/null +++ b/src/modules/supabasesync/supabase-schema.sql @@ -0,0 +1,13 @@ +-- Table to store our conversations + +create table user_conversation ( + id uuid not null, + systemPurposeId character varying(255) null, + folderId uuid null, + created bigint not null, + updated bigint not null, + userTitle character varying(255) null, + autoTitle character varying(255) null, + messages json null, + constraint conversation_pkey primary key (id) + ); \ No newline at end of file From ade4299ce9dd34a19ff8c58298f7c19424c2ef9b Mon Sep 17 00:00:00 2001 From: Joel M Date: Mon, 3 Jun 2024 05:58:31 +0000 Subject: [PATCH 16/21] wip: so close yet so far --- .../supabasesync/SupabaseSyncSettings.tsx | 124 ++++++++++----- .../supabasesync/supabaseSync.client.ts | 63 +++++++- src/modules/supabasesync/use-session.ts | 149 +++++++++--------- 3 files changed, 222 insertions(+), 114 deletions(-) diff --git a/src/modules/supabasesync/SupabaseSyncSettings.tsx b/src/modules/supabasesync/SupabaseSyncSettings.tsx index f1e429ff78..613e11b498 100644 --- a/src/modules/supabasesync/SupabaseSyncSettings.tsx +++ b/src/modules/supabasesync/SupabaseSyncSettings.tsx @@ -1,7 +1,7 @@ import {useContext, useEffect, useState, ChangeEvent} from "react"; import {shallow} from 'zustand/shallow'; -import {FormControl, FormHelperText, Input, Button, Typography, Box} from '@mui/joy'; +import {FormControl, FormHelperText, Input, Button, Typography, Box, Divider} from '@mui/joy'; import {GoodModal} from '~/common/components/GoodModal'; import DoneIcon from '@mui/icons-material/Done'; import SyncIcon from '@mui/icons-material/Sync'; @@ -13,20 +13,15 @@ import {FormInputKey} from '~/common/components/forms/FormInputKey'; import {FormLabelStart} from '~/common/components/forms/FormLabelStart'; import {Link} from '~/common/components/Link'; -import {isValidSupabaseConnection} from './supabaseSync.client'; import {useSupabaseSyncStore} from './store-module-supabase-sync'; -import {syncAllConversations, getSupabaseClient} from '~/modules/supabasesync/supabaseSync.client'; -import {useSession, UserContext} from '~/modules/supabasesync/use-session'; -import {Auth} from '@supabase/auth-ui-react' -import {ThemeSupa} from '@supabase/auth-ui-shared' +import {isValidSupabaseConnection, syncAllConversations, getSupabaseClient, getSupabaseUserName, supabaseSignOut} from '~/modules/supabasesync/supabaseSync.client'; export function SupabaseSyncSettings() { - //const [session, setSession] = React.useState(null) - const [loginDialogIsOpen, setLoginDialogIsOpen] = useState(false) - const [authMode, setAuthMode] = useState<"sign_in" | "sign_up">("sign_in"); + const [supabaseUserName, setSupabaseUserName] = useState(null); + const [loginDialogIsOpen, setLoginDialogIsOpen] = useState(false); - const {session, profile} = useContext(UserContext); - const userInfo = useSession(); + // const {session, profile} = useContext(UserContext); + // const userInfo = useSession(); // external state const backendHasSupabaseSync = getBackendCapabilities().hasSupabaseSync; @@ -61,6 +56,12 @@ export function SupabaseSyncSettings() { const handleSyncAllConversations = async () => { try { + if (!supabaseUserName) { + // need to sign in first, just a catch incase UI disable doesn't work + setSyncAllState('fail'); + setSyncMessage('Please Sign in first.'); + return; + } const syncedCount = await syncAllConversations(setSyncMessage); setSyncAllState('ok'); } catch { @@ -71,20 +72,35 @@ export function SupabaseSyncSettings() { const handleLogin = () => { // can only call if its valid if (isValidAnonKey) { - setAuthMode("sign_in"); setLoginDialogIsOpen(true); } } + const handleSupaUserSignIn = () => { + // must have user and pwd + + // if success close + setLoginDialogIsOpen(false); + } + const handleSignUp = () => { if (isValidAnonKey) { - setAuthMode("sign_up"); setLoginDialogIsOpen(true); } } + const supaClient = getSupabaseClient(); + if (supaClient && isValidUrl && !supabaseUserName) { + getSupabaseUserName().then( (name) => { + setSupabaseUserName(name); + }); + } + + const handleSignOut = () => { + supabaseSignOut(); + } + return <> - Configure the Supabase Chat Sync, if you do not have a Supabase account you will need to create one + {supabaseUserName && ( + + Logged in as {supabaseUserName} + + )} + {supabaseUserName && ( + + )} + {!supabaseUserName && ( + <> + + )} + + + {/* */} + )} - - setLoginDialogIsOpen(false)} - > - - - ; } \ No newline at end of file diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index a91f4c1c47..ff1c0e1b03 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -1,4 +1,4 @@ -import { createClient, SupabaseClient } from "@supabase/supabase-js"; +import { createClient, SupabaseClient, Session } from "@supabase/supabase-js"; import { useSupabaseSyncStore } from "./store-module-supabase-sync"; import { conversationToJsonV1 } from '~/modules/trade/trade.client'; import { DConversation, DMessage, useChatStore, createDConversation } from '~/common/state/store-chats'; @@ -14,6 +14,8 @@ type SupabaseConversation = { updated: number | null; } + + export const isValidSupabaseConnection = (url?: string, key?: string) => !!url && !!key; function logInfo(message: string) { @@ -27,10 +29,16 @@ function logError(message: string, error: any) { // Singleton instance of Supabase Client, recreate when api key changes let supabaseClientInstance: SupabaseClient | null = null; let lastSupabaseClientKey: string | null = null; +let supabaseSession: Session | null = null; + // Singleton instance of Supabase Realtime, recreate when api key changes -export function getSupabaseClient(): SupabaseClient { +export function getSupabaseClient(): SupabaseClient | null { const { supabaseUrl, supabaseKey } = useSupabaseSyncStore.getState(); + + if (!isValidSupabaseConnection(supabaseUrl, supabaseKey)) { + return null; + } // if the url or key is not set the return null if (supabaseClientInstance && lastSupabaseClientKey === supabaseKey) { @@ -43,12 +51,49 @@ export function getSupabaseClient(): SupabaseClient { // supabaseClientInstance = null; // } - supabaseClientInstance = createClient(supabaseUrl, supabaseKey); + supabaseClientInstance = createClient(supabaseUrl, supabaseKey, { + auth: { + persistSession: true + } + }); lastSupabaseClientKey = supabaseKey; + //supabaseClientInstance.auth.getSession(); // are we logged in? + //supabaseClientInstance.auth.getUser return supabaseClientInstance; } } +export async function getSupabaseSession(): Promise { + + if (!supabaseSession) { + return supabaseSession; + } + + const supaClient = getSupabaseClient(); + if (!supaClient) { + return null; + } + + // auto sign-in if we can + const { data: { session } } = await supaClient.auth.getSession(); + supabaseSession = session; + return supabaseSession; +} + +export function supabaseSignOut(): void +{ + supabaseSession = null; + if (supabaseClientInstance) { + supabaseClientInstance.auth.signOut(); + } +} + +export async function getSupabaseUserName(): Promise { + // auto sign-in if we can + const session = await getSupabaseSession(); + return session?.user.email ?? null; +} + // async function signIn(): Promise { // const { supabaseUrl, supabaseKey } = useSupabaseSyncStore.getState(); // if (!isValidSupabaseConnection(supabaseUrl, supabaseKey)) { @@ -172,11 +217,19 @@ async function syncFromServerToClient(supabase: SupabaseClient, conversations: D export async function syncAllConversations(setMessage?: (message: string | null) => void): Promise { const { lastSyncTime, setLastSyncTime } = useSupabaseSyncStore.getState(); const conversations = useChatStore.getState().conversations; - const supabase = await getSupabaseClient(); + const supabase = getSupabaseClient(); //const { folders, enableFolders } = useFolderStore.getState(); // ToDo: folder Sync ? try { - + if (!supabase) { + setMessage?.('Please configure Supabase and log in before Syncing'); + return 0; + } + const { data: { session } } = await supabase.auth.getSession(); + if (!session) { + setMessage?.('Please log in before Syncing'); + return 0; + } logInfo('Starting sync to server...'); const pushedCount = await syncToServer(supabase, conversations, lastSyncTime); logInfo('Sync to server completed.'); diff --git a/src/modules/supabasesync/use-session.ts b/src/modules/supabasesync/use-session.ts index 3d7874e42d..5439948462 100644 --- a/src/modules/supabasesync/use-session.ts +++ b/src/modules/supabasesync/use-session.ts @@ -1,81 +1,84 @@ -import { RealtimeChannel, Session } from "@supabase/supabase-js"; -import { useEffect, useState, createContext } from "react"; -import { getSupabaseClient } from '~/modules/supabasesync/supabaseSync.client'; -import * as React from "react"; +// import { RealtimeChannel, Session } from "@supabase/supabase-js"; +// import { useEffect, useState, createContext } from "react"; +// import { getSupabaseClient } from '~/modules/supabasesync/supabaseSync.client'; +// import * as React from "react"; -export interface UserProfile { - username: string; - avatarUrl?: string; -} +// export interface UserProfile { +// username: string; +// avatarUrl?: string; +// } -export interface BigAgiUserInfo { - session: Session | null; - profile: UserProfile | null; -} +// export interface BigAgiUserInfo { +// session: Session | null; +// profile: UserProfile | null; +// } -export const UserContext = React.createContext({ - session: null, - profile: null, -}); +// export const UserContext = React.createContext({ +// session: null, +// profile: null, +// }); -export function useSession(): BigAgiUserInfo { - const [userInfo, setUserInfo] = useState({ - profile: null, - session: null, - }); - const [channel, setChannel] = useState(null); - useEffect(() => { - const supaClient = getSupabaseClient(); - supaClient.auth.getSession().then(({ data: { session } }) => { - setUserInfo({ ...userInfo, session }); - supaClient.auth.onAuthStateChange((_event, session) => { - setUserInfo({ session, profile: null }); - }); - }); - }, []); +// export function useSession(): BigAgiUserInfo { +// const [userInfo, setUserInfo] = useState({ +// profile: null, +// session: null, +// }); +// const [channel, setChannel] = useState(null); +// useEffect(() => { +// const supaClient = getSupabaseClient(); +// if (!supaClient) { +// return; +// } +// supaClient.auth.getSession().then(({ data: { session } }) => { +// setUserInfo({ ...userInfo, session }); +// supaClient.auth.onAuthStateChange((_event, session) => { +// setUserInfo({ session, profile: null }); +// }); +// }); +// }, []); - useEffect(() => { - if (userInfo.session?.user && !userInfo.profile) { - listenToUserProfileChanges(userInfo.session.user.id).then( - (newChannel) => { - if (channel) { - channel.unsubscribe(); - } - setChannel(newChannel); - } - ); - } else if (!userInfo.session?.user) { - channel?.unsubscribe(); - setChannel(null); - } - }, [userInfo.session]); +// useEffect(() => { +// if (userInfo.session?.user && !userInfo.profile) { +// listenToUserProfileChanges(userInfo.session.user.id).then( +// (newChannel) => { +// if (channel) { +// channel.unsubscribe(); +// } +// setChannel(newChannel); +// } +// ); +// } else if (!userInfo.session?.user) { +// channel?.unsubscribe(); +// setChannel(null); +// } +// }, [userInfo.session]); - async function listenToUserProfileChanges(userId: string) { - const supaClient = getSupabaseClient(); - const { data } = await supaClient - .from("user_profiles") - .select("*") - .filter("user_id", "eq", userId); - if (data?.[0]) { - setUserInfo({ ...userInfo, profile: data?.[0] }); - } - return supaClient - .channel(`public:user_profiles`) - .on( - "postgres_changes", - { - event: "*", - schema: "public", - table: "user_profiles", - filter: `user_id=eq.${userId}`, - }, - (payload) => { - setUserInfo({ ...userInfo, profile: payload.new as UserProfile }); - } - ) - .subscribe(); - } +// async function listenToUserProfileChanges(userId: string) { +// const supaClient = getSupabaseClient(); +// const { data } = await supaClient +// .from("user_profiles") +// .select("*") +// .filter("user_id", "eq", userId); +// if (data?.[0]) { +// setUserInfo({ ...userInfo, profile: data?.[0] }); +// } +// return supaClient +// .channel(`public:user_profiles`) +// .on( +// "postgres_changes", +// { +// event: "*", +// schema: "public", +// table: "user_profiles", +// filter: `user_id=eq.${userId}`, +// }, +// (payload) => { +// setUserInfo({ ...userInfo, profile: payload.new as UserProfile }); +// } +// ) +// .subscribe(); +// } - return userInfo; -} \ No newline at end of file +// return userInfo; +// } \ No newline at end of file From 161b10444a55309c1e58d0ca4cbd27c05b9eefd2 Mon Sep 17 00:00:00 2001 From: Joel M Date: Wed, 26 Jun 2024 10:26:23 +0000 Subject: [PATCH 17/21] enh: use Supabase user auth --- .../supabasesync/SupabaseSyncSettings.tsx | 312 ++++++++++-------- src/modules/supabasesync/supabase-schema.sql | 20 +- .../supabasesync/supabaseSync.client.ts | 93 +++--- 3 files changed, 234 insertions(+), 191 deletions(-) diff --git a/src/modules/supabasesync/SupabaseSyncSettings.tsx b/src/modules/supabasesync/SupabaseSyncSettings.tsx index 613e11b498..87fd3eda2b 100644 --- a/src/modules/supabasesync/SupabaseSyncSettings.tsx +++ b/src/modules/supabasesync/SupabaseSyncSettings.tsx @@ -1,27 +1,27 @@ -import {useContext, useEffect, useState, ChangeEvent} from "react"; -import {shallow} from 'zustand/shallow'; +import { useContext, useEffect, useState, ChangeEvent } from "react"; +import { shallow } from 'zustand/shallow'; -import {FormControl, FormHelperText, Input, Button, Typography, Box, Divider} from '@mui/joy'; -import {GoodModal} from '~/common/components/GoodModal'; +import { FormControl, FormHelperText, Input, Button, Typography, Box, Divider } from '@mui/joy'; +import { GoodModal } from '~/common/components/GoodModal'; import DoneIcon from '@mui/icons-material/Done'; import SyncIcon from '@mui/icons-material/Sync'; import SearchIcon from '@mui/icons-material/Search'; -import {getBackendCapabilities} from '~/modules/backend/store-backend-capabilities'; -import {AlreadySet} from '~/common/components/AlreadySet'; -import {FormInputKey} from '~/common/components/forms/FormInputKey'; -import {FormLabelStart} from '~/common/components/forms/FormLabelStart'; -import {Link} from '~/common/components/Link'; +import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities'; +import { AlreadySet } from '~/common/components/AlreadySet'; +import { FormInputKey } from '~/common/components/forms/FormInputKey'; +import { FormLabelStart } from '~/common/components/forms/FormLabelStart'; +import { Link } from '~/common/components/Link'; -import {useSupabaseSyncStore} from './store-module-supabase-sync'; -import {isValidSupabaseConnection, syncAllConversations, getSupabaseClient, getSupabaseUserName, supabaseSignOut} from '~/modules/supabasesync/supabaseSync.client'; +import { useSupabaseSyncStore } from './store-module-supabase-sync'; +import { isValidSupabaseConnection, syncAllConversations, getSupabaseClient, getSupabaseUserName, supabaseSignOut } from '~/modules/supabasesync/supabaseSync.client'; export function SupabaseSyncSettings() { const [supabaseUserName, setSupabaseUserName] = useState(null); const [loginDialogIsOpen, setLoginDialogIsOpen] = useState(false); - - // const {session, profile} = useContext(UserContext); - // const userInfo = useSession(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loginError, setLoginError] = useState(null); // external state const backendHasSupabaseSync = getBackendCapabilities().hasSupabaseSync; @@ -45,6 +45,15 @@ export function SupabaseSyncSettings() { const [syncAllState, setSyncAllState] = useState<'ok' | 'fail' | null>(null); const [syncMessage, setSyncMessage] = useState(null); + useEffect(() => { + const supaClient = getSupabaseClient(); + if (supaClient && isValidUrl && !supabaseUserName) { + getSupabaseUserName().then((name) => { + setSupabaseUserName(name); + }); + } + }, [isValidUrl, supabaseUserName]); + // set sync time from input const handleLastSyncTimeChange = (e: ChangeEvent) => { // need to convert e.target.value to a number or 0 if empty or nan @@ -76,11 +85,27 @@ export function SupabaseSyncSettings() { } } - const handleSupaUserSignIn = () => { - // must have user and pwd + const handleSupaUserSignIn = async () => { + if (!email || !password) { + setLoginError('Please enter both email and password.'); + return; + } + + const supaClient = getSupabaseClient(); + if (supaClient) { + const { error } = await supaClient.auth.signInWithPassword({ + email, + password, + }); - // if success close - setLoginDialogIsOpen(false); + if (error) { + setLoginError(error.message); + } else { + setLoginDialogIsOpen(false); + const name = await getSupabaseUserName(); + setSupabaseUserName(name); + } + } } const handleSignUp = () => { @@ -91,150 +116,165 @@ export function SupabaseSyncSettings() { const supaClient = getSupabaseClient(); if (supaClient && isValidUrl && !supabaseUserName) { - getSupabaseUserName().then( (name) => { + getSupabaseUserName().then((name) => { setSupabaseUserName(name); }); } - const handleSignOut = () => { - supabaseSignOut(); + const handleSignOut = async () => { + await supabaseSignOut(); + setSupabaseUserName(null); } return <> - - Configure the Supabase Chat Sync, if you do not have a Supabase account you will need to create - one + Configure the Supabase Chat Sync, if you do not have a Supabase account you will need to create + one here, or you can use the Sign Up button - below. - - - } - required={!backendHasSupabaseSync} - isError={!isValidUrl && !backendHasSupabaseSync} - placeholder={backendHasSupabaseSync ? '...' : 'https://...supabase.co (or your self hosted url)'} - /> + below. + - } - required={!backendHasSupabaseSync} - isError={!isValidAnonKey && !backendHasSupabaseSync} - placeholder={backendHasSupabaseSync ? '...' : 'SUPABASE_ANON_KEY'} - /> + } + required={!backendHasSupabaseSync} + isError={!isValidUrl && !backendHasSupabaseSync} + placeholder={backendHasSupabaseSync ? '...' : 'https://...supabase.co (or your self hosted url)'} + /> + + } + required={!backendHasSupabaseSync} + isError={!isValidAnonKey && !backendHasSupabaseSync} + placeholder={backendHasSupabaseSync ? '...' : 'SUPABASE_ANON_KEY'} + /> - - - } - slotProps={{input: {sx: {width: '100%'}}}} - sx={{width: '100%'}} - /> - - - WARNING: Resetting Last Synced to 0 will force push all exiting chats to the server and will overwrite - them. - + + + } + slotProps={{ input: { sx: { width: '100%' } } }} + sx={{ width: '100%' }} + /> + + + WARNING: Resetting Last Synced to 0 will force push all exiting chats to the server and will overwrite + them. + + {supabaseUserName && ( + + Logged in as {supabaseUserName} + + )} + {supabaseUserName && ( - - Logged in as {supabaseUserName} - - )} - - {supabaseUserName && ( - )} - {!supabaseUserName && ( + )} + {!supabaseUserName && ( <> - - + Login + - )} - - - - {syncMessage && ( - - {syncMessage} - )} + + - {supaClient && ( - setLoginDialogIsOpen(false)} - > - - + {syncMessage && ( + + {syncMessage} + + )} + + {supaClient && ( + setLoginDialogIsOpen(false)} + hideBottomClose={true} + > + + + + setEmail(e.target.value)} + placeholder="Enter your email" + /> + + + + setPassword(e.target.value)} + placeholder="Enter your password" + /> + + {loginError && ( + + {loginError} + + )} + + + - + - - {/* */} - - )} + onClick={handleSupaUserSignIn} + > + Login + + + + )} ; } \ No newline at end of file diff --git a/src/modules/supabasesync/supabase-schema.sql b/src/modules/supabasesync/supabase-schema.sql index 40ce03ae31..0e2c4c785d 100644 --- a/src/modules/supabasesync/supabase-schema.sql +++ b/src/modules/supabasesync/supabase-schema.sql @@ -2,12 +2,20 @@ create table user_conversation ( id uuid not null, - systemPurposeId character varying(255) null, - folderId uuid null, + "systemPurposeId" character varying(255) null, + "folderId" uuid null, created bigint not null, updated bigint not null, - userTitle character varying(255) null, - autoTitle character varying(255) null, + "userTitle" character varying(255) null, + "autoTitle" character varying(255) null, messages json null, - constraint conversation_pkey primary key (id) - ); \ No newline at end of file + user_id uuid null default auth.uid (), + constraint user_conversation_pkey primary key (id) + ); + +create policy "Users can mange their own data" +on "public"."user_conversation" +to public +using ( + (auth.uid() = user_id) +); \ No newline at end of file diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index ff1c0e1b03..4d50d72c69 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -10,14 +10,15 @@ type SupabaseConversation = { systemPurposeId: SystemPurposeId; userTitle?: string; autoTitle?: string; + tokenCount: number; created: number; updated: number | null; } - - export const isValidSupabaseConnection = (url?: string, key?: string) => !!url && !!key; +const superbase_conversation_tablename = 'user_conversation'; // old sync it was conversation + function logInfo(message: string) { console.log(`[INFO]: ${message}`); } @@ -53,7 +54,9 @@ export function getSupabaseClient(): SupabaseClient | null { supabaseClientInstance = createClient(supabaseUrl, supabaseKey, { auth: { - persistSession: true + persistSession: true, + autoRefreshToken: true, + detectSessionInUrl: true } }); lastSupabaseClientKey = supabaseKey; @@ -65,7 +68,7 @@ export function getSupabaseClient(): SupabaseClient | null { export async function getSupabaseSession(): Promise { - if (!supabaseSession) { + if (supabaseSession) { return supabaseSession; } @@ -80,12 +83,12 @@ export async function getSupabaseSession(): Promise { return supabaseSession; } -export function supabaseSignOut(): void -{ - supabaseSession = null; - if (supabaseClientInstance) { - supabaseClientInstance.auth.signOut(); +export async function supabaseSignOut(): Promise { + const supaClient = getSupabaseClient(); + if (supaClient) { + await supaClient.auth.signOut(); } + supabaseSession = null; } export async function getSupabaseUserName(): Promise { @@ -94,43 +97,35 @@ export async function getSupabaseUserName(): Promise { return session?.user.email ?? null; } -// async function signIn(): Promise { -// const { supabaseUrl, supabaseKey } = useSupabaseSyncStore.getState(); -// if (!isValidSupabaseConnection(supabaseUrl, supabaseKey)) { -// throw new Error('Invalid Supabase Connection'); -// } -// -// const supabase = createSupabaseClient(); -// const { data, error } = await supabase -// .auth.signInWithPassword(); -// -// if (error) { -// throw error; -// } -// -// if (!data) { -// throw new Error('Invalid Supabase Connection'); -// } -// } - -// async function getServersLastSyncTime(supabase: SupabaseClient): Promise { -// const { data, error } = await supabase -// .from('conversation') -// .select('updated') -// .order('updated', { ascending: false }) -// .limit(1); -// -// if (error) { -// console.error('Error fetching lastSyncTime:', error); -// return 0; -// } -// -// if (data && data.length > 0) { -// return data[0].updated; -// } else { -// return 0; -// } -// } +export async function supabaseSignIn(email: string, password: string): Promise { + const supaClient = getSupabaseClient(); + if (!supaClient) { + throw new Error('Invalid Supabase Connection'); + } + + try { + const response = await supaClient.auth.signInWithPassword({ + email, + password, + }); + + if (response.error) { + throw response.error; + } + + supabaseSession = response.data.session; + return supabaseSession.user.email ?? ""; + } catch (error) { + if (error instanceof Error) { + if (error.message.includes('CORS')) { + throw new Error('CORS error: Ensure your application domain is allowed in Supabase settings and you\'re using HTTPS.'); + } else if (error.message.includes('NetworkError')) { + throw new Error('Network error: Check your internet connection and ensure Supabase URL is correct.'); + } + } + throw error; + } +} async function syncToServer(supabase: SupabaseClient, conversations: DConversation[], lastSyncTime: number): Promise { // find all conversations that have been updated since the last sync @@ -149,7 +144,7 @@ async function syncToServer(supabase: SupabaseClient, conversations: DConversati console.log(`Syncing ${updatedConversations.length} conversations`); const { data, error } = await supabase - .from('conversation') + .from(superbase_conversation_tablename) .upsert(updatedConversations); if (error) { @@ -165,7 +160,7 @@ async function syncFromServerToClient(supabase: SupabaseClient, conversations: D console.log(`Fetching conversations from server > ${lastSyncTime}`); const { data, error } = await supabase - .from('conversation') + .from(superbase_conversation_tablename) .select("*") .gt('updated', lastSyncTime); @@ -225,7 +220,7 @@ export async function syncAllConversations(setMessage?: (message: string | null) setMessage?.('Please configure Supabase and log in before Syncing'); return 0; } - const { data: { session } } = await supabase.auth.getSession(); + const session = await getSupabaseSession(); if (!session) { setMessage?.('Please log in before Syncing'); return 0; From bf97301c3ebac96039aaa5bea5984cc8897d9b34 Mon Sep 17 00:00:00 2001 From: Joel M Date: Mon, 1 Jul 2024 19:09:52 +0000 Subject: [PATCH 18/21] tidy: fix up code for PR, login consistency + db table check --- .../supabasesync/SupabaseSyncSettings.tsx | 67 +++++---------- .../store-module-supabase-sync.ts | 6 -- .../supabasesync/supabaseSync.client.ts | 54 ++++++++---- src/modules/supabasesync/use-session.ts | 84 ------------------- 4 files changed, 59 insertions(+), 152 deletions(-) delete mode 100644 src/modules/supabasesync/use-session.ts diff --git a/src/modules/supabasesync/SupabaseSyncSettings.tsx b/src/modules/supabasesync/SupabaseSyncSettings.tsx index 87fd3eda2b..9514bac524 100644 --- a/src/modules/supabasesync/SupabaseSyncSettings.tsx +++ b/src/modules/supabasesync/SupabaseSyncSettings.tsx @@ -42,6 +42,7 @@ export function SupabaseSyncSettings() { // derived state const isValidUrl = supabaseUrl ? isValidSupabaseConnection(supabaseUrl, supabaseKey) : backendHasSupabaseSync; const isValidAnonKey = isValidUrl; + const canSync = isValidAnonKey && supabaseUserName; const [syncAllState, setSyncAllState] = useState<'ok' | 'fail' | null>(null); const [syncMessage, setSyncMessage] = useState(null); @@ -85,6 +86,11 @@ export function SupabaseSyncSettings() { } } + const handleCancelLogin = () => { + setLoginError(null); + setLoginDialogIsOpen(false); + } + const handleSupaUserSignIn = async () => { if (!email || !password) { setLoginError('Please enter both email and password.'); @@ -101,19 +107,13 @@ export function SupabaseSyncSettings() { if (error) { setLoginError(error.message); } else { - setLoginDialogIsOpen(false); const name = await getSupabaseUserName(); setSupabaseUserName(name); + handleCancelLogin(); } } } - const handleSignUp = () => { - if (isValidAnonKey) { - setLoginDialogIsOpen(true); - } - } - const supaClient = getSupabaseClient(); if (supaClient && isValidUrl && !supabaseUserName) { getSupabaseUserName().then((name) => { @@ -168,51 +168,26 @@ export function SupabaseSyncSettings() { WARNING: Resetting Last Synced to 0 will force push all exiting chats to the server and will overwrite them. - {supabaseUserName && ( - - Logged in as {supabaseUserName} - - )} + + {supabaseUserName ? `Logged in as ${supabaseUserName}` : 'Please Sign In'} + - {supabaseUserName && ( - - )} - {!supabaseUserName && ( - <> - - - - )} + @@ -262,7 +237,7 @@ export function SupabaseSyncSettings() { diff --git a/src/modules/supabasesync/store-module-supabase-sync.ts b/src/modules/supabasesync/store-module-supabase-sync.ts index a2a059b5e1..8fb042bef9 100644 --- a/src/modules/supabasesync/store-module-supabase-sync.ts +++ b/src/modules/supabasesync/store-module-supabase-sync.ts @@ -14,9 +14,6 @@ interface ModuleSupabaseSyncStore { lastSyncTime: number; setLastSyncTime: (lastSyncTime: number) => void; - userEmail: string; - setUserEmail: (userEmail: string) => void; - } export const useSupabaseSyncStore = create()( @@ -34,9 +31,6 @@ export const useSupabaseSyncStore = create()( lastSyncTime: 0, setLastSyncTime: (lastSyncTime: number) => set({lastSyncTime: lastSyncTime}), - userEmail: '', - setUserEmail: (userEmail: string) => set({userEmail: userEmail}), - }), { name: 'app-module-supabase-sync', diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index 4d50d72c69..a22accf6d8 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -10,10 +10,10 @@ type SupabaseConversation = { systemPurposeId: SystemPurposeId; userTitle?: string; autoTitle?: string; - tokenCount: number; + tokenCount: number; created: number; updated: number | null; - } +} export const isValidSupabaseConnection = (url?: string, key?: string) => !!url && !!key; @@ -40,7 +40,7 @@ export function getSupabaseClient(): SupabaseClient | null { if (!isValidSupabaseConnection(supabaseUrl, supabaseKey)) { return null; } - + // if the url or key is not set the return null if (supabaseClientInstance && lastSupabaseClientKey === supabaseKey) { return supabaseClientInstance; @@ -51,7 +51,7 @@ export function getSupabaseClient(): SupabaseClient | null { // await supabaseClientInstance.auth.signOut(); // supabaseClientInstance = null; // } - + supabaseClientInstance = createClient(supabaseUrl, supabaseKey, { auth: { persistSession: true, @@ -86,7 +86,7 @@ export async function getSupabaseSession(): Promise { export async function supabaseSignOut(): Promise { const supaClient = getSupabaseClient(); if (supaClient) { - await supaClient.auth.signOut(); + await supaClient.auth.signOut(); } supabaseSession = null; } @@ -134,8 +134,8 @@ async function syncToServer(supabase: SupabaseClient, conversations: DConversati // sync time needs to be the last time this instance synced with the server const updatedConversations = conversations - .filter(conversation => conversation.updated && conversation.updated > lastSyncTime && conversation.messages.length > 0) - .map(conversationToJsonV1); // this removes some of the fields we want to sync + .filter(conversation => conversation.updated && conversation.updated > lastSyncTime && conversation.messages.length > 0) + .map(conversationToJsonV1); // this removes some of the fields we want to sync if (updatedConversations.length === 0) { return 0; @@ -144,8 +144,8 @@ async function syncToServer(supabase: SupabaseClient, conversations: DConversati console.log(`Syncing ${updatedConversations.length} conversations`); const { data, error } = await supabase - .from(superbase_conversation_tablename) - .upsert(updatedConversations); + .from(superbase_conversation_tablename) + .upsert(updatedConversations); if (error) { console.error('Error syncing conversations:', error); @@ -163,7 +163,7 @@ async function syncFromServerToClient(supabase: SupabaseClient, conversations: D .from(superbase_conversation_tablename) .select("*") .gt('updated', lastSyncTime); - + if (error) { console.error('Error fetching conversations from Server:', error); return 0; @@ -172,12 +172,12 @@ async function syncFromServerToClient(supabase: SupabaseClient, conversations: D if (data && data.length > 0) { console.debug(`Found ${data.length} conversations from server`); const conversationsFromServer: SupabaseConversation[] = data.map(record => ({ ...record })); - + const importConversation = useChatStore.getState().importConversation; let importCount = 0; conversationsFromServer.forEach(conversationFromServer => { let conversation = conversations.find(conversation => conversation.id === conversationFromServer.id); - + if (conversation) { // we may have just sent this to the server, in which case we don't need to update it // is it already updated (e.g. did we just push that to the server?) @@ -188,7 +188,7 @@ async function syncFromServerToClient(supabase: SupabaseClient, conversations: D conversation = createDConversation(); conversation.id = conversationFromServer.id; conversation.created = conversationFromServer.created; - + //conversations.push(conversation); // insert the new conversation into the current working array } @@ -209,6 +209,27 @@ async function syncFromServerToClient(supabase: SupabaseClient, conversations: D return 0; } +async function isValidSupabaseDatabase(supabase: SupabaseClient | null): Promise { + if (!supabase) { + return 'Please configure Supabase and log in before Syncing'; + } + + try { + const { data, error } = await supabase + .from(superbase_conversation_tablename) + .select('*') + .limit(1); + + // could validate schema as well? + if (error) + return `Database Setup Error: ${error.message}`; + + return ''; + } catch (error) { + return `Database Setup Exception: ${error}`; + } +} + export async function syncAllConversations(setMessage?: (message: string | null) => void): Promise { const { lastSyncTime, setLastSyncTime } = useSupabaseSyncStore.getState(); const conversations = useChatStore.getState().conversations; @@ -216,8 +237,9 @@ export async function syncAllConversations(setMessage?: (message: string | null) //const { folders, enableFolders } = useFolderStore.getState(); // ToDo: folder Sync ? try { - if (!supabase) { - setMessage?.('Please configure Supabase and log in before Syncing'); + const invalidDbMessage = await isValidSupabaseDatabase(supabase); + if (invalidDbMessage !== '') { + setMessage?.(invalidDbMessage); return 0; } const session = await getSupabaseSession(); @@ -233,7 +255,7 @@ export async function syncAllConversations(setMessage?: (message: string | null) logInfo('Starting sync from server to client...'); const pulledCount = await syncFromServerToClient(supabase, conversations, lastSyncTime); logInfo('Sync from server to client completed.'); - + setLastSyncTime(updatedSyncTime); logInfo(`Sync completed. Last sync time updated to ${updatedSyncTime}.`); setMessage?.(`Sync Successful, ${pushedCount} pushed, ${pulledCount} pulled`); diff --git a/src/modules/supabasesync/use-session.ts b/src/modules/supabasesync/use-session.ts deleted file mode 100644 index 5439948462..0000000000 --- a/src/modules/supabasesync/use-session.ts +++ /dev/null @@ -1,84 +0,0 @@ -// import { RealtimeChannel, Session } from "@supabase/supabase-js"; -// import { useEffect, useState, createContext } from "react"; -// import { getSupabaseClient } from '~/modules/supabasesync/supabaseSync.client'; -// import * as React from "react"; - -// export interface UserProfile { -// username: string; -// avatarUrl?: string; -// } - -// export interface BigAgiUserInfo { -// session: Session | null; -// profile: UserProfile | null; -// } - -// export const UserContext = React.createContext({ -// session: null, -// profile: null, -// }); - - -// export function useSession(): BigAgiUserInfo { -// const [userInfo, setUserInfo] = useState({ -// profile: null, -// session: null, -// }); -// const [channel, setChannel] = useState(null); -// useEffect(() => { -// const supaClient = getSupabaseClient(); -// if (!supaClient) { -// return; -// } -// supaClient.auth.getSession().then(({ data: { session } }) => { -// setUserInfo({ ...userInfo, session }); -// supaClient.auth.onAuthStateChange((_event, session) => { -// setUserInfo({ session, profile: null }); -// }); -// }); -// }, []); - -// useEffect(() => { -// if (userInfo.session?.user && !userInfo.profile) { -// listenToUserProfileChanges(userInfo.session.user.id).then( -// (newChannel) => { -// if (channel) { -// channel.unsubscribe(); -// } -// setChannel(newChannel); -// } -// ); -// } else if (!userInfo.session?.user) { -// channel?.unsubscribe(); -// setChannel(null); -// } -// }, [userInfo.session]); - -// async function listenToUserProfileChanges(userId: string) { -// const supaClient = getSupabaseClient(); -// const { data } = await supaClient -// .from("user_profiles") -// .select("*") -// .filter("user_id", "eq", userId); -// if (data?.[0]) { -// setUserInfo({ ...userInfo, profile: data?.[0] }); -// } -// return supaClient -// .channel(`public:user_profiles`) -// .on( -// "postgres_changes", -// { -// event: "*", -// schema: "public", -// table: "user_profiles", -// filter: `user_id=eq.${userId}`, -// }, -// (payload) => { -// setUserInfo({ ...userInfo, profile: payload.new as UserProfile }); -// } -// ) -// .subscribe(); -// } - -// return userInfo; -// } \ No newline at end of file From 96dd36f9ea5e9baee0a6f0a9a51c80e5ae6faf86 Mon Sep 17 00:00:00 2001 From: Joel M Date: Mon, 1 Jul 2024 19:44:46 +0000 Subject: [PATCH 19/21] doc: update documentation --- src/modules/supabasesync/README.md | 49 ++++++++++++++------ src/modules/supabasesync/supabase-schema.sql | 21 --------- 2 files changed, 36 insertions(+), 34 deletions(-) delete mode 100644 src/modules/supabasesync/supabase-schema.sql diff --git a/src/modules/supabasesync/README.md b/src/modules/supabasesync/README.md index 1d88057b35..b28d5c5fb1 100644 --- a/src/modules/supabasesync/README.md +++ b/src/modules/supabasesync/README.md @@ -4,29 +4,52 @@ > To sync all conversations from big-agi's localDb to a server and back allowing use on multiple devices while preserving big-agi's private and local approach. -The assumtion here is 1 user will use 1 database, you can share all chats or none. -e.g. this is not intended to keep some chats shared/synced and others private. +Supabase supports multi user authentication so this module assumes you will have users setup and users can save their own data/chats to this database and they will not be accessable by other users (e.g. not used for team conversation sharing, if that is desired then get all team members to use same user account) -## Requirements +## Module Status -- Supabase project setup (free account is fine), you will need your ulr & key -- a table called `conversation` with the following schema +**Whats working:** + +- Sync "Chat Conversations" to Supabase + +**Planned:** + +- Sync Conversation folders +- Sync other shared user settings like theme, what the "Enter" key does etc + +## Supabase Setup + +- Supabase project setup (free account is fine), you will need your url & anon-key +- Table called `user_conversation` with the following schema +- Row Level Security (RLS) turned on for this table and policies setup +- One or more supabase user accounts with access to the `user_conversation` table ```sql -create table conversation ( + +create table user_conversation ( id uuid not null, - systemPurposeId character varying(255) null, + "systemPurposeId" character varying(255) null, + "folderId" uuid null, created bigint not null, updated bigint not null, - userTitle character varying(255) null, - autoTitle character varying(255) null, + "userTitle" character varying(255) null, + "autoTitle" character varying(255) null, messages json null, - constraint conversation_pkey primary key (id) + user_id uuid null default auth.uid (), + constraint user_conversation_pkey primary key (id) ); - ``` - ## Setup +create policy "Users can mange their own data" +on "public"."user_conversation" +to public +using ( + (auth.uid() = user_id) +); + +``` + + ## Big-Agi Setup - Navigate to your hosted instance and set your Supabase URL & KEY under the `Preferences -> Tools -> Supabase Sync` + Navigate to your hosted instance and set your Supabase URL & KEY under the `Preferences -> Tools -> Supabase Sync` then login with your supabase user NOTE: the `Last Synced` is a way of tracking what chnages you need to get. To do a full sync (possibly loosing any un-synced data) reset this value to 0. diff --git a/src/modules/supabasesync/supabase-schema.sql b/src/modules/supabasesync/supabase-schema.sql deleted file mode 100644 index 0e2c4c785d..0000000000 --- a/src/modules/supabasesync/supabase-schema.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Table to store our conversations - -create table user_conversation ( - id uuid not null, - "systemPurposeId" character varying(255) null, - "folderId" uuid null, - created bigint not null, - updated bigint not null, - "userTitle" character varying(255) null, - "autoTitle" character varying(255) null, - messages json null, - user_id uuid null default auth.uid (), - constraint user_conversation_pkey primary key (id) - ); - -create policy "Users can mange their own data" -on "public"."user_conversation" -to public -using ( - (auth.uid() = user_id) -); \ No newline at end of file From dd7a3441c0fcfd55aeafdb5307acf29c8fc2209c Mon Sep 17 00:00:00 2001 From: Joel M Date: Mon, 1 Jul 2024 19:45:19 +0000 Subject: [PATCH 20/21] fix: " vs ' for import --- src/modules/supabasesync/SupabaseSyncSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/supabasesync/SupabaseSyncSettings.tsx b/src/modules/supabasesync/SupabaseSyncSettings.tsx index 9514bac524..46b7c105cd 100644 --- a/src/modules/supabasesync/SupabaseSyncSettings.tsx +++ b/src/modules/supabasesync/SupabaseSyncSettings.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState, ChangeEvent } from "react"; +import { useContext, useEffect, useState, ChangeEvent } from 'react'; import { shallow } from 'zustand/shallow'; import { FormControl, FormHelperText, Input, Button, Typography, Box, Divider } from '@mui/joy'; From 58eff5e038e4edd0db203a1424b344f8f130ab1f Mon Sep 17 00:00:00 2001 From: Joel M Date: Mon, 1 Jul 2024 19:46:45 +0000 Subject: [PATCH 21/21] fix: " vs ' in imports --- src/modules/supabasesync/supabaseSync.client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/supabasesync/supabaseSync.client.ts b/src/modules/supabasesync/supabaseSync.client.ts index a22accf6d8..d6c1047442 100644 --- a/src/modules/supabasesync/supabaseSync.client.ts +++ b/src/modules/supabasesync/supabaseSync.client.ts @@ -1,5 +1,5 @@ -import { createClient, SupabaseClient, Session } from "@supabase/supabase-js"; -import { useSupabaseSyncStore } from "./store-module-supabase-sync"; +import { createClient, SupabaseClient, Session } from '@supabase/supabase-js'; +import { useSupabaseSyncStore } from './store-module-supabase-sync'; import { conversationToJsonV1 } from '~/modules/trade/trade.client'; import { DConversation, DMessage, useChatStore, createDConversation } from '~/common/state/store-chats'; import { SystemPurposeId } from '../../data';