Skip to content

Commit 377e224

Browse files
SaxonFalaisterivasilov
authored
Feat/save assistant messages (supabase#30823)
* save chat messages * just ai state in subscribe * fix hydration issue * check for url param to open assistant * fix dashboard history * fix ts * safeguard against parsing errors * Fix the type errors. * Another try. --------- Co-authored-by: Alaister Young <[email protected]> Co-authored-by: Ivan Vasilov <[email protected]>
1 parent 0192dc4 commit 377e224

File tree

5 files changed

+119
-43
lines changed

5 files changed

+119
-43
lines changed

apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Head from 'next/head'
22
import { useRouter } from 'next/router'
3-
import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect } from 'react'
3+
import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect, useState } from 'react'
44

55
import { useParams } from 'common'
66
import ProjectAPIDocs from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs'
@@ -110,6 +110,12 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
110110
router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings')
111111
const showPausedState = isPaused && !ignorePausedState
112112

113+
const [isClient, setIsClient] = useState(false)
114+
115+
useEffect(() => {
116+
setIsClient(true)
117+
}, [])
118+
113119
useEffect(() => {
114120
const handler = (e: KeyboardEvent) => {
115121
if (e.metaKey && e.code === 'KeyI' && !e.altKey && !e.shiftKey) {
@@ -195,7 +201,7 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
195201
)}
196202
</main>
197203
</ResizablePanel>
198-
{aiAssistantPanel.open && (
204+
{isClient && aiAssistantPanel.open && (
199205
<>
200206
<ResizableHandle />
201207
<ResizablePanel

apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ export const AIAssistant = ({
135135
id,
136136
api: `${BASE_PATH}/api/ai/sql/generate-v3`,
137137
maxSteps: 5,
138-
// [Joshen] Not currently used atm, but initialMessages will be for...
139138
initialMessages,
140139
body: {
141140
includeSchemaMetadata,
@@ -144,6 +143,11 @@ export const AIAssistant = ({
144143
schema: currentSchema,
145144
table: currentTable?.name,
146145
},
146+
onFinish: (message) => {
147+
setAiAssistantPanel({
148+
messages: [...chatMessages, message],
149+
})
150+
},
147151
})
148152

149153
const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations')
@@ -185,7 +189,7 @@ export const AIAssistant = ({
185189
headers: { Authorization: headerData.get('Authorization') ?? '' },
186190
})
187191

188-
setAiAssistantPanel({ sqlSnippets: undefined })
192+
setAiAssistantPanel({ sqlSnippets: undefined, messages: [...messages, payload] })
189193
setValue('')
190194
setAssistantError(undefined)
191195
setLastSentMessage(payload)
@@ -400,7 +404,7 @@ export const AIAssistant = ({
400404
</h3>
401405
{suggestions.title && <p>{suggestions.title}</p>}
402406
<div className="-mx-3 mt-4 mb-12">
403-
{suggestions?.prompts?.map((prompt, idx) => (
407+
{suggestions?.prompts?.map((prompt: string, idx: number) => (
404408
<Button
405409
key={`suggestion-${idx}`}
406410
size="small"
@@ -496,7 +500,7 @@ export const AIAssistant = ({
496500
<div className="p-5 pt-0 z-20 relative">
497501
{sqlSnippets && sqlSnippets.length > 0 && (
498502
<div className="mb-2">
499-
{sqlSnippets.map((snippet, index) => (
503+
{sqlSnippets.map((snippet: string, index: number) => (
500504
<CollapsibleCodeBlock
501505
key={index}
502506
hideLineNumbers
@@ -552,7 +556,9 @@ export const AIAssistant = ({
552556
event.preventDefault()
553557
if (includeSchemaMetadata) {
554558
const sqlSnippetsString =
555-
sqlSnippets?.map((snippet) => '```sql\n' + snippet + '\n```').join('\n') || ''
559+
sqlSnippets
560+
?.map((snippet: string) => '```sql\n' + snippet + '\n```')
561+
.join('\n') || ''
556562
const valueWithSnippets = [value, sqlSnippetsString].filter(Boolean).join('\n\n')
557563
sendMessageToAssistant(valueWithSnippets)
558564
} else {

apps/studio/components/ui/AIAssistantPanel/AIAssistantPanel.tsx

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
1+
import type { Message as MessageType } from 'ai/react'
12
import { uuidv4 } from 'lib/helpers'
2-
import { useState, useEffect } from 'react'
3+
import { useState } from 'react'
34
import { useAppStateSnapshot } from 'state/app-state'
45
import { cn } from 'ui'
56
import { AIAssistant } from './AIAssistant'
6-
import type { Message as MessageType } from 'ai/react'
77

88
export const AiAssistantPanel = () => {
99
const { aiAssistantPanel, resetAiAssistantPanel } = useAppStateSnapshot()
10-
const [initialMessages, setInitialMessages] = useState<MessageType[] | undefined>(undefined)
11-
12-
useEffect(() => {
13-
// set initial state of local messages to the global state if it exists
14-
if (aiAssistantPanel.messages) {
15-
const messagesCopy = aiAssistantPanel.messages.map((msg) => ({
16-
content: msg.content,
17-
createdAt: msg.createdAt,
18-
role: msg.role,
19-
id: msg.id,
20-
}))
21-
setInitialMessages(messagesCopy)
22-
}
23-
}, [])
10+
const [initialMessages, setInitialMessages] = useState<MessageType[] | undefined>(
11+
aiAssistantPanel.messages?.length > 0 ? (aiAssistantPanel.messages as any) : undefined
12+
)
2413

2514
const { open } = aiAssistantPanel
2615
const [chatId, setChatId] = useState(() => uuidv4())

apps/studio/lib/constants/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export const USAGE_APPROACHING_THRESHOLD = 0.75
3030
export const LOCAL_STORAGE_KEYS = {
3131
RECENTLY_VISITED_ORGANIZATION: 'supabase-organization',
3232

33+
AI_ASSISTANT_STATE: 'supabase-ai-assistant-state',
34+
3335
UI_PREVIEW_NAVIGATION_LAYOUT: 'supabase-ui-preview-nav-layout',
3436
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
3537
UI_PREVIEW_CLS: 'supabase-ui-cls',

apps/studio/state/app-state.ts

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,8 @@
1-
import { proxy, snapshot, useSnapshot } from 'valtio'
2-
1+
import { proxy, subscribe, snapshot, useSnapshot } from 'valtio'
2+
import type { Message as MessageType } from 'ai/react'
33
import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types'
44
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
55
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common'
6-
import type { Message as MessageType } from 'ai/react'
7-
8-
const EMPTY_DASHBOARD_HISTORY: {
9-
sql?: string
10-
editor?: string
11-
} = {
12-
sql: undefined,
13-
editor: undefined,
14-
}
156

167
export type CommonDatabaseEntity = {
178
id: number
@@ -27,7 +18,7 @@ export type SuggestionsType = {
2718

2819
type AiAssistantPanelType = {
2920
open: boolean
30-
messages?: MessageType[] | undefined
21+
messages: MessageType[]
3122
initialInput: string
3223
sqlSnippets?: string[]
3324
suggestions?: SuggestionsType
@@ -39,9 +30,14 @@ type AiAssistantPanelType = {
3930
tables: { schema: string; name: string }[]
4031
}
4132

33+
type DashboardHistoryType = {
34+
sql?: string
35+
editor?: string
36+
}
37+
4238
const INITIAL_AI_ASSISTANT: AiAssistantPanelType = {
4339
open: false,
44-
messages: undefined,
40+
messages: [],
4541
sqlSnippets: undefined,
4642
initialInput: '',
4743
suggestions: undefined,
@@ -51,9 +47,76 @@ const INITIAL_AI_ASSISTANT: AiAssistantPanelType = {
5147
tables: [],
5248
}
5349

50+
const EMPTY_DASHBOARD_HISTORY: DashboardHistoryType = {
51+
sql: undefined,
52+
editor: undefined,
53+
}
54+
55+
const getInitialState = () => {
56+
if (typeof window === 'undefined') {
57+
return {
58+
aiAssistantPanel: INITIAL_AI_ASSISTANT,
59+
dashboardHistory: EMPTY_DASHBOARD_HISTORY,
60+
activeDocsSection: ['introduction'],
61+
docsLanguage: 'js',
62+
showProjectApiDocs: false,
63+
isOptedInTelemetry: false,
64+
showEnableBranchingModal: false,
65+
showFeaturePreviewModal: false,
66+
selectedFeaturePreview: '',
67+
showAiSettingsModal: false,
68+
showGenerateSqlModal: false,
69+
navigationPanelOpen: false,
70+
navigationPanelJustClosed: false,
71+
}
72+
}
73+
74+
const stored = localStorage.getItem(LOCAL_STORAGE_KEYS.AI_ASSISTANT_STATE)
75+
76+
const urlParams = new URLSearchParams(window.location.search)
77+
const aiAssistantPanelOpenParam = urlParams.get('aiAssistantPanelOpen')
78+
79+
let parsedAiAssistant = INITIAL_AI_ASSISTANT
80+
81+
try {
82+
if (stored) {
83+
parsedAiAssistant = JSON.parse(stored, (key, value) => {
84+
if (key === 'createdAt' && value) {
85+
return new Date(value)
86+
}
87+
return value
88+
})
89+
}
90+
} catch {
91+
// Ignore parsing errors
92+
}
93+
94+
return {
95+
aiAssistantPanel: {
96+
...parsedAiAssistant,
97+
open:
98+
aiAssistantPanelOpenParam !== null
99+
? aiAssistantPanelOpenParam === 'true'
100+
: parsedAiAssistant.open,
101+
},
102+
dashboardHistory: EMPTY_DASHBOARD_HISTORY,
103+
activeDocsSection: ['introduction'],
104+
docsLanguage: 'js',
105+
showProjectApiDocs: false,
106+
isOptedInTelemetry: false,
107+
showEnableBranchingModal: false,
108+
showFeaturePreviewModal: false,
109+
selectedFeaturePreview: '',
110+
showAiSettingsModal: false,
111+
showGenerateSqlModal: false,
112+
navigationPanelOpen: false,
113+
navigationPanelJustClosed: false,
114+
}
115+
}
116+
54117
export const appState = proxy({
55-
// [Joshen] Last visited "entity" for any page that we wanna track
56-
dashboardHistory: EMPTY_DASHBOARD_HISTORY,
118+
...getInitialState(),
119+
57120
setDashboardHistory: (ref: string, key: 'sql' | 'editor', id: string) => {
58121
if (appState.dashboardHistory[key] !== id) {
59122
appState.dashboardHistory[key] = id
@@ -84,22 +147,27 @@ export const appState = proxy({
84147
localStorage.setItem(COMMON_LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT, value.toString())
85148
}
86149
},
150+
87151
showEnableBranchingModal: false,
88152
setShowEnableBranchingModal: (value: boolean) => {
89153
appState.showEnableBranchingModal = value
90154
},
155+
91156
showFeaturePreviewModal: false,
92157
setShowFeaturePreviewModal: (value: boolean) => {
93158
appState.showFeaturePreviewModal = value
94159
},
160+
95161
selectedFeaturePreview: '',
96162
setSelectedFeaturePreview: (value: string) => {
97163
appState.selectedFeaturePreview = value
98164
},
165+
99166
showAiSettingsModal: false,
100167
setShowAiSettingsModal: (value: boolean) => {
101168
appState.showAiSettingsModal = value
102169
},
170+
103171
showGenerateSqlModal: false,
104172
setShowGenerateSqlModal: (value: boolean) => {
105173
appState.showGenerateSqlModal = value
@@ -109,18 +177,14 @@ export const appState = proxy({
109177
navigationPanelJustClosed: false,
110178
setNavigationPanelOpen: (value: boolean, trackJustClosed: boolean = false) => {
111179
if (value === false) {
112-
// If closing navigation panel by clicking on icon/button, nav bar should not open again until mouse leaves nav bar
113180
if (trackJustClosed) {
114181
appState.navigationPanelOpen = false
115182
appState.navigationPanelJustClosed = true
116183
} else {
117-
// If closing navigation panel by leaving nav bar, nav bar can open again when mouse re-enter
118184
appState.navigationPanelOpen = false
119185
appState.navigationPanelJustClosed = false
120186
}
121187
} else {
122-
// If opening nav panel, check if it was just closed by a nav icon/button click
123-
// If yes, do not open nav panel, otherwise open as per normal
124188
if (appState.navigationPanelJustClosed === false) {
125189
appState.navigationPanelOpen = true
126190
}
@@ -137,7 +201,6 @@ export const appState = proxy({
137201
}
138202
},
139203

140-
aiAssistantPanel: INITIAL_AI_ASSISTANT as AiAssistantPanelType,
141204
setAiAssistantPanel: (value: Partial<AiAssistantPanelType>) => {
142205
const hasEntityChanged = value.entity?.id !== appState.aiAssistantPanel.entity?.id
143206

@@ -149,6 +212,16 @@ export const appState = proxy({
149212
},
150213
})
151214

215+
// Set up localStorage subscription
216+
if (typeof window !== 'undefined') {
217+
subscribe(appState, () => {
218+
localStorage.setItem(
219+
LOCAL_STORAGE_KEYS.AI_ASSISTANT_STATE,
220+
JSON.stringify(appState.aiAssistantPanel)
221+
)
222+
})
223+
}
224+
152225
export const getAppStateSnapshot = () => snapshot(appState)
153226

154227
export const useAppStateSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>

0 commit comments

Comments
 (0)