Skip to content

Commit

Permalink
Merge branch 'ai-prompt-logging' of github.com:quadratichq/quadratic …
Browse files Browse the repository at this point in the history
…into ai-prompt-logging
  • Loading branch information
AyushAgrawal-A2 committed Jan 10, 2025
2 parents 0f7334b + 096d9e2 commit 92a0a98
Show file tree
Hide file tree
Showing 11 changed files with 91 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 2 additions & 2 deletions quadratic-api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,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])
}
Expand Down
3 changes: 2 additions & 1 deletion quadratic-api/src/routes/v0/teams.$uuid.GET.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions quadratic-api/src/routes/v0/teams.$uuid.GET.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ async function handler(req: Request, res: Response<ApiTypes['/v0/teams/:uuid.GET
id: team.id,
uuid,
name: team.name,
settings: {
analyticsAi: dbTeam.settingAnalyticsAi,
},
},
billing: {
status: dbTeam.stripeSubscriptionStatus || undefined,
Expand Down Expand Up @@ -193,9 +196,6 @@ async function handler(req: Request, res: Response<ApiTypes['/v0/teams/:uuid.GET
type: connection.type,
})),
clientDataKv: isObject(dbTeam.clientDataKv) ? dbTeam.clientDataKv : {},
preferences: {
aiSaveUserPromptsEnabled: dbTeam.preferenceAiSaveUserPromptsEnabled,
},
};

return res.status(200).json(response);
Expand Down
10 changes: 10 additions & 0 deletions quadratic-api/src/routes/v0/teams.$uuid.PATCH.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ describe('PATCH /v0/teams/:uuid', () => {
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)
Expand Down
12 changes: 6 additions & 6 deletions quadratic-api/src/routes/v0/teams.$uuid.PATCH.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const schema = z.object({

async function handler(req: RequestWithUser, res: Response<ApiTypes['/v0/teams/:uuid.PATCH.response']>) {
const {
body: { name, clientDataKv, preferences },
body: { name, clientDataKv, settings },
params: { uuid },
} = parseRequest(req, schema);
const {
Expand All @@ -36,8 +36,8 @@ async function handler(req: RequestWithUser, res: Response<ApiTypes['/v0/teams/:
if (!permissions.includes('TEAM_EDIT')) {
throw new ApiError(403, 'User does not have permission to edit this team.');
}
if (preferences && !permissions.includes('TEAM_MANAGE')) {
throw new ApiError(403, 'User does not have permission to edit this team’s preferences.');
if (settings && !permissions.includes('TEAM_MANAGE')) {
throw new ApiError(403, 'User does not have permission to edit this team’s settings.');
}

// Validate exisiting data in the db
Expand All @@ -51,7 +51,7 @@ async function handler(req: RequestWithUser, res: Response<ApiTypes['/v0/teams/:
data: {
...(name ? { name } : {}),
...(clientDataKv ? { clientDataKv: { ...validatedExisitingClientDataKv, ...clientDataKv } } : {}),
...(preferences ? { preferenceAiSaveUserPromptsEnabled: preferences.aiSaveUserPromptsEnabled } : {}),
...(settings ? { settingAnalyticsAi: settings.analyticsAi } : {}),
},
});

Expand All @@ -61,8 +61,8 @@ async function handler(req: RequestWithUser, res: Response<ApiTypes['/v0/teams/:
return res.status(200).json({
name: newTeam.name,
clientDataKv: newClientDataKv,
preferences: {
aiSaveUserPromptsEnabled: newTeam.preferenceAiSaveUserPromptsEnabled,
settings: {
analyticsAi: newTeam.settingAnalyticsAi,
},
});
}
Expand Down
55 changes: 38 additions & 17 deletions quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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';
Expand All @@ -27,7 +28,6 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) {
const messagesCount = useRecoilValue(aiAnalystCurrentChatMessagesCountAtom);
const loading = useRecoilValue(aiAnalystLoadingAtom);
const [model] = useAIModel();
const [showFeedback, setShowFeedback] = useState(false);

const [div, setDiv] = useState<HTMLDivElement | null>(null);
const ref = useCallback((div: HTMLDivElement | null) => {
Expand Down Expand Up @@ -68,7 +68,6 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) {
if (loading) {
shouldAutoScroll.current = true;
scrollToBottom(true);
setShowFeedback(true);
}
}, [loading, scrollToBottom]);

Expand All @@ -85,7 +84,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;
Expand Down Expand Up @@ -167,25 +165,13 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) {
);
})}

{messages.length > 0 && !loading && <FeedbackButtons handleFeedback={handleFeedback} />}

<div className={cn('flex flex-row gap-1 px-2 transition-opacity', !loading && 'opacity-0')}>
<span className="h-2 w-2 animate-bounce bg-primary" />
<span className="h-2 w-2 animate-bounce bg-primary/60 delay-100" />
<span className="h-2 w-2 animate-bounce bg-primary/20 delay-200" />
</div>

{messages.length > 0 && !loading && showFeedback && (
<div className="flex flex-row justify-end gap-1 px-2">
<div className="flex flex-row gap-1">
<Button onClick={() => handleFeedback(false)} variant="destructive" size="sm">
<ThumbDownIcon className="mr-1" />
</Button>

<Button onClick={() => handleFeedback(true)} variant="success" size="sm">
<ThumbUpIcon className="mr-1" />
</Button>
</div>
</div>
)}
</div>
);
}
Expand All @@ -194,3 +180,38 @@ function MarkdownContent({ children }: { children: string }) {
// Classes applied in Markdown.scss
return <Markdown>{children}</Markdown>;
}

function FeedbackButtons({ handleFeedback }: { handleFeedback: (like: boolean) => void }) {
const [liked, setLiked] = useState<boolean | null>(null);

return (
<div className="relative flex flex-row items-center px-2">
<TooltipPopover label="Good response">
<Button
onClick={() => {
setLiked((val) => (val === true ? null : true));
}}
variant="ghost"
size="icon-sm"
className={cn('hover:text-success', liked === true ? 'text-success' : 'text-muted-foreground')}
disabled={liked === false}
>
<ThumbUpIcon className="scale-75" />
</Button>
</TooltipPopover>
<TooltipPopover label="Bad response">
<Button
onClick={() => {
setLiked((val) => (val === false ? null : false));
}}
variant="ghost"
size="icon-sm"
className={cn('hover:text-destructive', liked === false ? 'text-destructive' : 'text-muted-foreground')}
disabled={liked === true}
>
<ThumbDownIcon className="scale-75" />
</Button>
</TooltipPopover>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 13 additions & 12 deletions quadratic-client/src/routes/teams.$teamUuid.settings.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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';
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';
import { isJsonObject } from '@/shared/utils/isJsonObject';
import type { TeamSettings } from 'quadratic-shared/typesAndSchemas';
import { ReactNode, useEffect, useState } from 'react';
import { Navigate, useFetcher, useSubmit } from 'react-router-dom';

Expand All @@ -17,7 +19,6 @@ export const Component = () => {
activeTeam: {
team,
userMakingRequest: { teamPermissions },
preferences,
},
} = useDashboardRouteLoaderData();

Expand All @@ -28,12 +29,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 };
}
}

Expand All @@ -54,8 +55,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),
Expand Down Expand Up @@ -114,21 +115,21 @@ export const Component = () => {
Privacy
</Type>

<PreferenceControl
<SettingControl
label="Improve AI results"
description={
<>
Help improve AI results by allowing Quadratic to store and analyze anonymized user prompts.{' '}
<a href="TODO:value-here" target="_blank" className="underline hover:text-primary">
Help improve AI results by allowing Quadratic to store and analyze user prompts.{' '}
<a href={DOCUMENTATION_ANALYTICS_AI} target="_blank" className="underline hover:text-primary">
Learn more
</a>
.
</>
}
onCheckedChange={(checked) => {
handleUpdatePreference('aiSaveUserPromptsEnabled', checked);
handleUpdatePreference('analyticsAi', checked);
}}
checked={optimisticPreferences.aiSaveUserPromptsEnabled}
checked={optimisticSettings.analyticsAi}
className="rounded border border-border p-3 shadow-sm"
/>
</Row>
Expand Down
1 change: 1 addition & 0 deletions quadratic-client/src/shared/constants/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
14 changes: 6 additions & 8 deletions quadratic-shared/typesAndSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof TeamPreferencesSchema>;
export type TeamSettings = z.infer<typeof TeamSettingsSchema>;

export const LicenseSchema = z.object({
limits: z.object({
Expand Down Expand Up @@ -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),
Expand All @@ -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) => {
Expand All @@ -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
Expand Down

0 comments on commit 92a0a98

Please sign in to comment.