From 089120f0f1d04e6e4c3709157bd89c96a1cb5d3a Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 9 Jan 2025 14:50:14 -0700 Subject: [PATCH 1/7] update preference -> setting; add tests --- .../migration.sql | 9 +++++++++ quadratic-api/prisma/schema.prisma | 4 ++-- .../src/routes/v0/teams.$uuid.GET.test.ts | 3 ++- .../src/routes/v0/teams.$uuid.GET.ts | 6 +++--- .../src/routes/v0/teams.$uuid.PATCH.test.ts | 10 ++++++++++ .../src/routes/v0/teams.$uuid.PATCH.ts | 10 +++++----- .../src/routes/teams.$teamUuid.settings.tsx | 20 +++++++++---------- quadratic-shared/typesAndSchemas.ts | 14 ++++++------- 8 files changed, 47 insertions(+), 29 deletions(-) create mode 100644 quadratic-api/prisma/migrations/20250109213645_rename_save_prompt/migration.sql diff --git a/quadratic-api/prisma/migrations/20250109213645_rename_save_prompt/migration.sql b/quadratic-api/prisma/migrations/20250109213645_rename_save_prompt/migration.sql new file mode 100644 index 0000000000..a00867a0cd --- /dev/null +++ b/quadratic-api/prisma/migrations/20250109213645_rename_save_prompt/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `preference_ai_save_user_prompts_enabled` on the `Team` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Team" DROP COLUMN "preference_ai_save_user_prompts_enabled", +ADD COLUMN "setting_analytics_ai" BOOLEAN NOT NULL DEFAULT true; diff --git a/quadratic-api/prisma/schema.prisma b/quadratic-api/prisma/schema.prisma index 60b22929f1..a2840f8460 100644 --- a/quadratic-api/prisma/schema.prisma +++ b/quadratic-api/prisma/schema.prisma @@ -123,8 +123,8 @@ model Team { // Use for client-specific data that is not useful to the server or other services clientDataKv Json @default("{}") @map("client_data_kv") - // Preferences - preferenceAiSaveUserPromptsEnabled Boolean @default(true) @map("preference_ai_save_user_prompts_enabled") + // Settings + settingAnalyticsAi Boolean @default(true) @map("setting_analytics_ai") @@index([uuid]) } diff --git a/quadratic-api/src/routes/v0/teams.$uuid.GET.test.ts b/quadratic-api/src/routes/v0/teams.$uuid.GET.test.ts index d9c2db663d..c116362a19 100644 --- a/quadratic-api/src/routes/v0/teams.$uuid.GET.test.ts +++ b/quadratic-api/src/routes/v0/teams.$uuid.GET.test.ts @@ -1,4 +1,4 @@ -import { User } from 'auth0'; +import type { User } from 'auth0'; import request from 'supertest'; import { app } from '../../app'; import dbClient from '../../dbClient'; @@ -97,6 +97,7 @@ describe('GET /v0/teams/:uuid', () => { expect(res.body).toHaveProperty('team'); expect(res.body.team.uuid).toBe('00000000-0000-4000-8000-000000000001'); + expect(res.body.team.settings.analyticsAi).toBe(true); expect(res.body.clientDataKv).toStrictEqual({}); expect(res.body.connections).toHaveLength(1); expect(res.body.files).toHaveLength(1); diff --git a/quadratic-api/src/routes/v0/teams.$uuid.GET.ts b/quadratic-api/src/routes/v0/teams.$uuid.GET.ts index 8a7f591264..9567eb284e 100644 --- a/quadratic-api/src/routes/v0/teams.$uuid.GET.ts +++ b/quadratic-api/src/routes/v0/teams.$uuid.GET.ts @@ -129,6 +129,9 @@ async function handler(req: Request, res: Response { expect(res.body.name).toBe('Foobar'); }); }); + it('accepts setting change', async () => { + await request(app) + .patch(`/v0/teams/00000000-0000-4000-8000-000000000001`) + .send({ settings: { analyticsAi: false } }) + .set('Authorization', `Bearer ValidToken team_1_owner`) + .expect(200) + .expect((res) => { + expect(res.body.settings.analyticsAi).toBe(false); + }); + }); it('accepst key/value pair updates', async () => { // Create value await request(app) diff --git a/quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts b/quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts index 651bab4707..624643de5d 100644 --- a/quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts +++ b/quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts @@ -21,7 +21,7 @@ const schema = z.object({ async function handler(req: RequestWithUser, res: Response) { const { - body: { name, clientDataKv, preferences }, + body: { name, clientDataKv, settings }, params: { uuid }, } = parseRequest(req, schema); const { @@ -36,7 +36,7 @@ async function handler(req: RequestWithUser, res: Response { activeTeam: { team, userMakingRequest: { teamPermissions }, - preferences, }, } = useDashboardRouteLoaderData(); @@ -28,12 +28,12 @@ export const Component = () => { const disabled = value === '' || value === team.name || fetcher.state !== 'idle'; // Optimistic UI - let optimisticPreferences = preferences; + let optimisticSettings = team.settings; if (fetcher.state !== 'idle' && isJsonObject(fetcher.json)) { const optimisticData = fetcher.json as TeamAction['request.update-team']; - if (optimisticData.preferences) { - optimisticPreferences = { ...preferences, ...optimisticData.preferences }; + if (optimisticData.settings) { + optimisticSettings = { ...optimisticSettings, ...optimisticData.settings }; } } @@ -54,8 +54,8 @@ export const Component = () => { }); }; - const handleUpdatePreference = (key: string, checked: boolean) => { - const data = getActionUpdateTeam({ preferences: { [key]: checked } }); + const handleUpdatePreference = (key: keyof TeamSettings, checked: boolean) => { + const data = getActionUpdateTeam({ settings: { [key]: checked } }); submit(data, { method: 'POST', action: ROUTES.TEAM(team.uuid), @@ -108,7 +108,7 @@ export const Component = () => { - {teamPermissions.includes('TEAM_MANAGE') && ( + {teamPermissions.includes('TEAM_MANAGE') && optimisticSettings && ( Privacy @@ -118,7 +118,7 @@ export const Component = () => { label="Improve AI results" description={ <> - Help improve AI results by allowing Quadratic to store and analyze anonymized user prompts.{' '} + Help improve AI results by allowing Quadratic to store and analyze user prompts.{' '} Learn more @@ -126,9 +126,9 @@ export const Component = () => { } onCheckedChange={(checked) => { - handleUpdatePreference('aiSaveUserPromptsEnabled', checked); + handleUpdatePreference('analyticsAi', checked); }} - checked={optimisticPreferences.aiSaveUserPromptsEnabled} + checked={optimisticSettings.analyticsAi} className="rounded border border-border p-3 shadow-sm" /> diff --git a/quadratic-shared/typesAndSchemas.ts b/quadratic-shared/typesAndSchemas.ts index 8645f13731..6610517ed3 100644 --- a/quadratic-shared/typesAndSchemas.ts +++ b/quadratic-shared/typesAndSchemas.ts @@ -103,11 +103,10 @@ const TeamUserMakingRequestSchema = z.object({ export const TeamClientDataKvSchema = z.record(z.any()); -const TeamPreferencesSchema = z.object({ - aiSaveUserPromptsEnabled: z.boolean(), - // aiProcessors: z.array(z.enum(['OPENAI', 'ANTRHOPIC', 'EXA', 'AWS_BEDROCK'])), +const TeamSettingsSchema = z.object({ + analyticsAi: z.boolean(), }); -export type TeamPreferences = z.infer; +export type TeamSettings = z.infer; export const LicenseSchema = z.object({ limits: z.object({ @@ -314,7 +313,7 @@ export const ApiSchemas = { }), '/v0/teams.POST.response': TeamSchema.pick({ uuid: true, name: true }), '/v0/teams/:uuid.GET.response': z.object({ - team: TeamSchema.pick({ id: true, uuid: true, name: true }), + team: TeamSchema.pick({ id: true, uuid: true, name: true }).merge(z.object({ settings: TeamSettingsSchema })), userMakingRequest: z.object({ id: TeamUserSchema.shape.id, teamPermissions: z.array(TeamPermissionSchema), @@ -341,13 +340,12 @@ export const ApiSchemas = { license: LicenseSchema, connections: ConnectionListSchema, clientDataKv: TeamClientDataKvSchema, - preferences: TeamPreferencesSchema, }), '/v0/teams/:uuid.PATCH.request': z .object({ name: TeamSchema.shape.name.optional(), clientDataKv: TeamClientDataKvSchema.optional(), - preferences: TeamPreferencesSchema.partial().optional(), + settings: TeamSettingsSchema.partial().optional(), }) .refine( (data) => { @@ -362,7 +360,7 @@ export const ApiSchemas = { '/v0/teams/:uuid.PATCH.response': z.object({ name: TeamSchema.shape.name, clientDataKv: TeamClientDataKvSchema, - preferences: TeamPreferencesSchema, + settings: TeamSettingsSchema, }), '/v0/teams/:uuid/invites.POST.request': TeamUserSchema.pick({ email: true, role: true }), '/v0/teams/:uuid/invites.POST.response': z From 254f59394f8a136d91c98a135b77ad1902cd6d24 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 9 Jan 2025 16:03:27 -0700 Subject: [PATCH 2/7] update feedback UI buttons --- .../ui/menus/AIAnalyst/AIAnalystMessages.tsx | 55 +++++++++++++------ 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystMessages.tsx b/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystMessages.tsx index bf876f81f0..61ad4c447d 100644 --- a/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystMessages.tsx +++ b/quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystMessages.tsx @@ -12,6 +12,7 @@ import { AIAnalystUserMessageForm } from '@/app/ui/menus/AIAnalyst/AIAnalystUser import { apiClient } from '@/shared/api/apiClient'; import { ThumbDownIcon, ThumbUpIcon } from '@/shared/components/Icons'; import { Button } from '@/shared/shadcn/ui/button'; +import { TooltipPopover } from '@/shared/shadcn/ui/tooltip'; import { cn } from '@/shared/shadcn/utils'; import { getPromptMessages } from 'quadratic-shared/ai/helpers/message.helper'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -25,7 +26,6 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) { const messages = useRecoilValue(aiAnalystCurrentChatMessagesAtom); const messagesCount = useRecoilValue(aiAnalystCurrentChatMessagesCountAtom); const loading = useRecoilValue(aiAnalystLoadingAtom); - const [showFeedback, setShowFeedback] = useState(false); const [div, setDiv] = useState(null); const ref = useCallback((div: HTMLDivElement | null) => { @@ -66,7 +66,6 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) { if (loading) { shouldAutoScroll.current = true; scrollToBottom(true); - setShowFeedback(true); } }, [loading, scrollToBottom]); @@ -83,7 +82,6 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) { const handleFeedback = useRecoilCallback( ({ snapshot }) => (like: boolean) => { - setShowFeedback(false); const messages = snapshot.getLoadable(aiAnalystCurrentChatMessagesAtom).getValue(); const promptMessageLength = getPromptMessages(messages).length; @@ -164,25 +162,13 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) { ); })} + {messages.length > 0 && !loading && } +
- - {messages.length > 0 && !loading && showFeedback && ( -
-
- - - -
-
- )} ); } @@ -191,3 +177,38 @@ function MarkdownContent({ children }: { children: string }) { // Classes applied in Markdown.scss return {children}; } + +function FeedbackButtons({ handleFeedback }: { handleFeedback: (like: boolean) => void }) { + const [liked, setLiked] = useState(null); + + return ( +
+ + + + + + +
+ ); +} From ba4bb9b60c065c8981c9360b0a4eb9839453be67 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 9 Jan 2025 16:16:34 -0700 Subject: [PATCH 3/7] update docs link --- quadratic-client/src/routes/teams.$teamUuid.settings.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quadratic-client/src/routes/teams.$teamUuid.settings.tsx b/quadratic-client/src/routes/teams.$teamUuid.settings.tsx index 8afb1a2ac8..663a59aebe 100644 --- a/quadratic-client/src/routes/teams.$teamUuid.settings.tsx +++ b/quadratic-client/src/routes/teams.$teamUuid.settings.tsx @@ -5,6 +5,7 @@ import { getActionUpdateTeam, type TeamAction } from '@/routes/teams.$teamUuid'; import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; import { Type } from '@/shared/components/Type'; import { ROUTES } from '@/shared/constants/routes'; +import { DOCUMENTATION_ANALYTICS_AI } from '@/shared/constants/urls'; import { Button } from '@/shared/shadcn/ui/button'; import { Input } from '@/shared/shadcn/ui/input'; import { cn } from '@/shared/shadcn/utils'; @@ -119,7 +120,7 @@ export const Component = () => { description={ <> Help improve AI results by allowing Quadratic to store and analyze user prompts.{' '} - + Learn more . From ee991194c1ba8d5e75f5d58aba936f5807dbb20b Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 9 Jan 2025 16:16:37 -0700 Subject: [PATCH 4/7] Update urls.ts --- quadratic-client/src/shared/constants/urls.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/quadratic-client/src/shared/constants/urls.ts b/quadratic-client/src/shared/constants/urls.ts index 538e2ce482..58f5199c04 100644 --- a/quadratic-client/src/shared/constants/urls.ts +++ b/quadratic-client/src/shared/constants/urls.ts @@ -11,6 +11,7 @@ export const DOCUMENTATION_CONNECTIONS_IP_LIST_URL = `${DOCUMENTATION_CONNECTION export const DOCUMENTATION_BROWSER_COMPATIBILITY_URL = `${DOCUMENTATION_URL}/spreadsheet/browser-compatibility`; export const DOCUMENTATION_DATE_TIME_FORMATTING = `${DOCUMENTATION_URL}/spreadsheet/date-time-formatting`; export const DOCUMENTATION_NEGATIVE_OFFSETS = `${DOCUMENTATION_URL}/spreadsheet/negative-offsets`; +export const DOCUMENTATION_ANALYTICS_AI = `${DOCUMENTATION_URL}/security#ai-data-privacy-setting`; export const TRUST_CENTER = 'https://trust.quadratichq.com'; export const BUG_REPORT_URL = 'https://github.com/quadratichq/quadratic/issues'; export const TWITTER = 'https://twitter.com/quadratichq'; From 4b6b397df01008ed0d38e97e4984949f9a4b0575 Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 9 Jan 2025 16:19:52 -0700 Subject: [PATCH 5/7] remove unused check in code --- quadratic-client/src/routes/teams.$teamUuid.settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-client/src/routes/teams.$teamUuid.settings.tsx b/quadratic-client/src/routes/teams.$teamUuid.settings.tsx index 663a59aebe..86bd77ac34 100644 --- a/quadratic-client/src/routes/teams.$teamUuid.settings.tsx +++ b/quadratic-client/src/routes/teams.$teamUuid.settings.tsx @@ -109,7 +109,7 @@ export const Component = () => { - {teamPermissions.includes('TEAM_MANAGE') && optimisticSettings && ( + {teamPermissions.includes('TEAM_MANAGE') && ( Privacy From 310dcdf0b8cb69fcb73e2e98bfe16b455738d22d Mon Sep 17 00:00:00 2001 From: Jim Nielsen Date: Thu, 9 Jan 2025 16:23:57 -0700 Subject: [PATCH 6/7] Update teams.$uuid.PATCH.ts --- quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts b/quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts index 624643de5d..6d7aee1e09 100644 --- a/quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts +++ b/quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts @@ -37,7 +37,7 @@ async function handler(req: RequestWithUser, res: Response Date: Thu, 9 Jan 2025 16:26:28 -0700 Subject: [PATCH 7/7] rename Preference -> Setting --- .../components/{PreferenceControl.tsx => SettingControl.tsx} | 2 +- quadratic-client/src/routes/teams.$teamUuid.settings.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename quadratic-client/src/dashboard/components/{PreferenceControl.tsx => SettingControl.tsx} (97%) diff --git a/quadratic-client/src/dashboard/components/PreferenceControl.tsx b/quadratic-client/src/dashboard/components/SettingControl.tsx similarity index 97% rename from quadratic-client/src/dashboard/components/PreferenceControl.tsx rename to quadratic-client/src/dashboard/components/SettingControl.tsx index 8bd34f1442..f48ca8c539 100644 --- a/quadratic-client/src/dashboard/components/PreferenceControl.tsx +++ b/quadratic-client/src/dashboard/components/SettingControl.tsx @@ -7,7 +7,7 @@ import { ReactNode, useId } from 'react'; * Component that renders a preference control (a label, description, and a switch) */ -export function PreferenceControl({ +export function SettingControl({ label, description, onCheckedChange, diff --git a/quadratic-client/src/routes/teams.$teamUuid.settings.tsx b/quadratic-client/src/routes/teams.$teamUuid.settings.tsx index 86bd77ac34..5d88df23be 100644 --- a/quadratic-client/src/routes/teams.$teamUuid.settings.tsx +++ b/quadratic-client/src/routes/teams.$teamUuid.settings.tsx @@ -1,5 +1,5 @@ import { DashboardHeader } from '@/dashboard/components/DashboardHeader'; -import { PreferenceControl } from '@/dashboard/components/PreferenceControl'; +import { SettingControl } from '@/dashboard/components/SettingControl'; import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; import { getActionUpdateTeam, type TeamAction } from '@/routes/teams.$teamUuid'; import { useGlobalSnackbar } from '@/shared/components/GlobalSnackbarProvider'; @@ -115,7 +115,7 @@ export const Component = () => { Privacy -