Skip to content

Commit

Permalink
feedback buttons working
Browse files Browse the repository at this point in the history
  • Loading branch information
AyushAgrawal-A2 committed Jan 10, 2025
1 parent 92a0a98 commit 9311270
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 44 deletions.
2 changes: 1 addition & 1 deletion quadratic-api/src/routes/v0/ai.chat.POST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async function handler(req: RequestWithUser, res: Response<ApiTypes['/v0/ai/chat

console.log('ownerTeam', ownerTeam);

if (ownerTeam.preferenceAiSaveUserPromptsEnabled) {
if (ownerTeam.settingAnalyticsAi) {
// todo(ayush): upload to s3 / file-storage

await dbClient.analyticsAIChat.upsert({
Expand Down
8 changes: 7 additions & 1 deletion quadratic-api/src/routes/v0/files.$uuid.GET.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,13 @@ async function handler(
thumbnail: thumbnailSignedUrl,
ownerUserId: ownerUserId ? ownerUserId : undefined,
},
team: { uuid: (ownerTeam as any).uuid, name: (ownerTeam as any).name },
team: {
uuid: ownerTeam.uuid,
name: ownerTeam.name,
settings: {
analyticsAi: ownerTeam.settingAnalyticsAi,
},
},
userMakingRequest: {
id: userId,
filePermissions,
Expand Down
6 changes: 3 additions & 3 deletions quadratic-api/src/routes/v0/files.$uuid.sharing.GET.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Request, Response } from 'express';
import { ApiTypes } from 'quadratic-shared/typesAndSchemas';
import type { Request, Response } from 'express';
import type { ApiTypes } from 'quadratic-shared/typesAndSchemas';
import { z } from 'zod';
import { getUsers } from '../../auth/auth';
import dbClient from '../../dbClient';
import { getFile } from '../../middleware/getFile';
import { userMiddleware } from '../../middleware/user';
import { validateAccessToken } from '../../middleware/validateAccessToken';
import { validateRequestSchema } from '../../middleware/validateRequestSchema';
import { RequestWithUser } from '../../types/Request';
import type { RequestWithUser } from '../../types/Request';
import { ApiError } from '../../utils/ApiError';

export default [
Expand Down
7 changes: 6 additions & 1 deletion quadratic-client/src/app/atoms/editorInteractionStateAtom.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { focusGrid } from '@/app/helpers/focusGrid.js';
import { SearchOptions } from '@/app/quadratic-core-types';
import { User } from '@/auth/auth';
import { FilePermission } from 'quadratic-shared/typesAndSchemas';
import { FilePermission, type TeamSettings } from 'quadratic-shared/typesAndSchemas';
import { atom, DefaultValue, selector } from 'recoil';

export interface EditorInteractionState {
Expand All @@ -18,6 +18,7 @@ export interface EditorInteractionState {
showValidation: boolean | string;
annotationState?: 'dropdown' | 'date-format' | 'calendar' | 'calendar-time';
permissions: FilePermission[];
settings: TeamSettings;
user?: User;
uuid: string;
follow?: string;
Expand All @@ -39,6 +40,9 @@ export const defaultEditorInteractionState: EditorInteractionState = {
showValidation: false,
annotationState: undefined,
permissions: ['FILE_VIEW'], // FYI: when we call <RecoilRoot> we initialize this with the value from the server
settings: {
analyticsAi: false,
},
user: undefined,
uuid: '', // when we call <RecoilRoot> we initialize this with the value from the server
follow: undefined,
Expand Down Expand Up @@ -107,6 +111,7 @@ export const editorInteractionStateShowValidationAtom = createSelector('showVali

export const editorInteractionStateAnnotationStateAtom = createSelector('annotationState');
export const editorInteractionStatePermissionsAtom = createSelector('permissions');
export const editorInteractionStateSettingsAtom = createSelector('settings');
export const editorInteractionStateUserAtom = createSelector('user');
export const editorInteractionStateUuidAtom = createSelector('uuid');
export const editorInteractionStateFollowAtom = createSelector('follow');
Expand Down
85 changes: 52 additions & 33 deletions quadratic-client/src/app/ui/menus/AIAnalyst/AIAnalystMessages.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useAIModel } from '@/app/ai/hooks/useAIModel';
import {
aiAnalystCurrentChatAtom,
aiAnalystCurrentChatMessagesAtom,
aiAnalystCurrentChatMessagesCountAtom,
aiAnalystLoadingAtom,
} from '@/app/atoms/aiAnalystAtom';
import { editorInteractionStateSettingsAtom } from '@/app/atoms/editorInteractionStateAtom';
import { debugShowAIInternalContext } from '@/app/debugFlags';
import { Markdown } from '@/app/ui/components/Markdown';
import { AIAnalystExamplePrompts } from '@/app/ui/menus/AIAnalyst/AIAnalystExamplePrompts';
Expand All @@ -16,6 +16,7 @@ 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 type { AIMessagePrompt } from 'quadratic-shared/typesAndSchemasAI';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useRecoilCallback, useRecoilValue } from 'recoil';

Expand All @@ -27,7 +28,7 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) {
const messages = useRecoilValue(aiAnalystCurrentChatMessagesAtom);
const messagesCount = useRecoilValue(aiAnalystCurrentChatMessagesCountAtom);
const loading = useRecoilValue(aiAnalystLoadingAtom);
const [model] = useAIModel();
const settings = useRecoilValue(editorInteractionStateSettingsAtom);

const [div, setDiv] = useState<HTMLDivElement | null>(null);
const ref = useCallback((div: HTMLDivElement | null) => {
Expand Down Expand Up @@ -81,21 +82,6 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) {
scrollToBottom();
}, [messages, scrollToBottom]);

const handleFeedback = useRecoilCallback(
({ snapshot }) =>
(like: boolean) => {
const messages = snapshot.getLoadable(aiAnalystCurrentChatMessagesAtom).getValue();

const promptMessageLength = getPromptMessages(messages).length;
if (promptMessageLength === 0) return;

const chatId = snapshot.getLoadable(aiAnalystCurrentChatAtom).getValue().id;
console.log('chatId', chatId, model);
apiClient.ai.feedback({ chatId, model, messageIndex: promptMessageLength, like });
},
[apiClient, model]
);

if (messagesCount === 0) {
return <AIAnalystExamplePrompts />;
}
Expand Down Expand Up @@ -142,13 +128,13 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) {
textareaRef={textareaRef}
/>
) : Array.isArray(message.content) ? (
message.content.map(({ content }) => <MarkdownContent key={content}>{content}</MarkdownContent>)
message.content.map(({ content }) => <Markdown key={content}>{content}</Markdown>)
) : (
<MarkdownContent key={message.content}>{message.content}</MarkdownContent>
<Markdown key={message.content}>{message.content}</Markdown>
)
) : (
<>
{message.content && <MarkdownContent key={message.content}>{message.content}</MarkdownContent>}
{message.content && <Markdown key={message.content}>{message.content}</Markdown>}

{message.contextType === 'userPrompt' &&
message.toolCalls.map((toolCall) => (
Expand All @@ -165,7 +151,7 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) {
);
})}

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

<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" />
Expand All @@ -176,38 +162,71 @@ export function AIAnalystMessages({ textareaRef }: AIAnalystMessagesProps) {
);
}

function MarkdownContent({ children }: { children: string }) {
// Classes applied in Markdown.scss
return <Markdown>{children}</Markdown>;
}
function FeedbackButtons() {
const [like, setLike] = useState<boolean | null>(null);

const handleFeedback = useRecoilCallback(
({ snapshot }) =>
(like: boolean | null) => {
const messages = snapshot.getLoadable(aiAnalystCurrentChatMessagesAtom).getValue();

const promptMessage = getPromptMessages(messages);
const promptMessageLength = promptMessage.length;
if (promptMessageLength === 0) return;

const lastAIPromptMessage = promptMessage
.reverse()
.find(
(message): message is AIMessagePrompt =>
message.role === 'assistant' && message.contextType === 'userPrompt'
);

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

const chatId = snapshot.getLoadable(aiAnalystCurrentChatAtom).getValue().id;
apiClient.ai.feedback({
chatId,
model: lastAIPromptMessage.model,
messageIndex: promptMessageLength,
like,
});
},
[apiClient]
);

return (
<div className="relative flex flex-row items-center px-2">
<TooltipPopover label="Good response">
<Button
onClick={() => {
setLiked((val) => (val === true ? null : true));
setLike((prev) => {
const newLike = prev === true ? null : true;
handleFeedback(newLike);
return newLike;
});
}}
variant="ghost"
size="icon-sm"
className={cn('hover:text-success', liked === true ? 'text-success' : 'text-muted-foreground')}
disabled={liked === false}
className={cn('hover:text-success', like === true ? 'text-success' : 'text-muted-foreground')}
disabled={like === false}
>
<ThumbUpIcon className="scale-75" />
</Button>
</TooltipPopover>

<TooltipPopover label="Bad response">
<Button
onClick={() => {
setLiked((val) => (val === false ? null : false));
setLike((prev) => {
const newLike = prev === false ? null : false;
handleFeedback(newLike);
return newLike;
});
}}
variant="ghost"
size="icon-sm"
className={cn('hover:text-destructive', liked === false ? 'text-destructive' : 'text-muted-foreground')}
disabled={liked === true}
className={cn('hover:text-destructive', like === false ? 'text-destructive' : 'text-muted-foreground')}
disabled={like === true}
>
<ThumbDownIcon className="scale-75" />
</Button>
Expand Down
8 changes: 5 additions & 3 deletions quadratic-client/src/routes/file.$uuid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,17 @@ export const Component = () => {
// Initialize recoil with the file's permission we get from the server
const { loggedInUser } = useRootRouteLoaderData();
const {
file: { uuid: fileUuid },
team: { settings: teamSettings },
userMakingRequest: { filePermissions },
file: { uuid },
} = useLoaderData() as FileData;
const initializeState = ({ set }: MutableSnapshot) => {
set(editorInteractionStateAtom, (prevState) => ({
...prevState,
user: loggedInUser,
uuid,
permissions: filePermissions,
settings: teamSettings,
user: loggedInUser,
uuid: fileUuid,
}));
};

Expand Down
6 changes: 4 additions & 2 deletions quadratic-shared/typesAndSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ export const ApiSchemas = {
file: FileSchema.extend({
ownerUserId: BaseUserSchema.shape.id.optional(),
}),
team: TeamSchema.pick({ uuid: true, name: true }),
team: TeamSchema.pick({ uuid: true, name: true }).extend({
settings: TeamSettingsSchema,
}),
userMakingRequest: z.object({
id: BaseUserSchema.shape.id.optional(),
filePermissions: z.array(FilePermissionSchema),
Expand Down Expand Up @@ -425,7 +427,7 @@ export const ApiSchemas = {
chatId: z.string().uuid(),
model: z.string(),
messageIndex: z.number(),
like: z.boolean(),
like: z.boolean().nullable(),
}),
'/v0/ai/feedback.POST.response': z.object({
message: z.string(),
Expand Down

0 comments on commit 9311270

Please sign in to comment.