From 9604c30388f177c8a0073196c4a64e1ef5b74ddf Mon Sep 17 00:00:00 2001 From: Sasha Date: Mon, 4 Nov 2024 11:46:12 +0100 Subject: [PATCH 01/45] [OPIK-347]: add a prompts list page with fake data; --- apps/opik-frontend/src/api/api.ts | 2 + .../api/prompts/usePromptCreateMutation.ts | 60 +++++ .../api/prompts/usePromptDeleteMutation.ts | 38 +++ .../src/api/prompts/usePromptsList.ts | 131 ++++++++++ .../src/components/layout/SideBar/SideBar.tsx | 23 ++ .../pages/PromptsPage/AddPromptDialog.tsx | 110 +++++++++ .../PromptsPage/PromptRowActionsCell.tsx | 67 ++++++ .../pages/PromptsPage/PromptsPage.tsx | 227 ++++++++++++++++++ .../pages/PromptsPage/TagNameCell.tsx | 25 ++ apps/opik-frontend/src/router.tsx | 19 ++ apps/opik-frontend/src/types/prompts.ts | 8 + 11 files changed, 710 insertions(+) create mode 100644 apps/opik-frontend/src/api/prompts/usePromptCreateMutation.ts create mode 100644 apps/opik-frontend/src/api/prompts/usePromptDeleteMutation.ts create mode 100644 apps/opik-frontend/src/api/prompts/usePromptsList.ts create mode 100644 apps/opik-frontend/src/components/pages/PromptsPage/AddPromptDialog.tsx create mode 100644 apps/opik-frontend/src/components/pages/PromptsPage/PromptRowActionsCell.tsx create mode 100644 apps/opik-frontend/src/components/pages/PromptsPage/PromptsPage.tsx create mode 100644 apps/opik-frontend/src/components/pages/PromptsPage/TagNameCell.tsx create mode 100644 apps/opik-frontend/src/types/prompts.ts diff --git a/apps/opik-frontend/src/api/api.ts b/apps/opik-frontend/src/api/api.ts index 10ad7e277..34e94eb11 100644 --- a/apps/opik-frontend/src/api/api.ts +++ b/apps/opik-frontend/src/api/api.ts @@ -15,6 +15,8 @@ export const FEEDBACK_DEFINITIONS_REST_ENDPOINT = "/v1/private/feedback-definitions/"; export const TRACES_REST_ENDPOINT = "/v1/private/traces/"; export const SPANS_REST_ENDPOINT = "/v1/private/spans/"; +export const PROMPTS_REST_ENDPOINT = "/v1/private/prompts/"; + export type QueryConfig = Omit< UseQueryOptions< diff --git a/apps/opik-frontend/src/api/prompts/usePromptCreateMutation.ts b/apps/opik-frontend/src/api/prompts/usePromptCreateMutation.ts new file mode 100644 index 000000000..dc01359e9 --- /dev/null +++ b/apps/opik-frontend/src/api/prompts/usePromptCreateMutation.ts @@ -0,0 +1,60 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import get from "lodash/get"; + +import api, {PROMPTS_REST_ENDPOINT} from "@/api/api"; +import { useToast } from "@/components/ui/use-toast"; +import {Prompt} from "@/types/prompts"; + +interface CreatePromptTemplate { + template: string; +} + +type UsePromptCreateMutationParams = { + prompt: Partial & CreatePromptTemplate; + workspaceName: string; +}; + +const usePromptCreateMutation = () => { + const queryClient = useQueryClient(); + const { toast } = useToast(); + + return useMutation({ + mutationFn: async ({ + prompt, + workspaceName, + }: UsePromptCreateMutationParams) => { + const { data, headers } = await api.post(PROMPTS_REST_ENDPOINT, { + ...prompt, + workspace_name: workspaceName, + }); + + return data; + }, + onMutate: async (params: UsePromptCreateMutationParams) => { + return { + queryKey: ["prompts", { workspaceName: params.workspaceName }], + }; + }, + onError: (error: AxiosError) => { + const message = get( + error, + ["response", "data", "message"], + error.message, + ); + + toast({ + title: "Error", + description: message, + variant: "destructive", + }); + }, + onSettled: (data, error, variables, context) => { + if (context) { + return queryClient.invalidateQueries({ queryKey: context.queryKey }); + } + }, + }); +}; + +export default usePromptCreateMutation; diff --git a/apps/opik-frontend/src/api/prompts/usePromptDeleteMutation.ts b/apps/opik-frontend/src/api/prompts/usePromptDeleteMutation.ts new file mode 100644 index 000000000..509a31008 --- /dev/null +++ b/apps/opik-frontend/src/api/prompts/usePromptDeleteMutation.ts @@ -0,0 +1,38 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import get from "lodash/get"; +import { useToast } from "@/components/ui/use-toast"; +import api, {PROMPTS_REST_ENDPOINT} from "@/api/api"; + +type UsePromptDeleteMutationParams = { + promptId: string; +}; + +const usePromptDeleteMutation = () => { + const queryClient = useQueryClient(); + const { toast } = useToast(); + + return useMutation({ + mutationFn: async ({ promptId }: UsePromptDeleteMutationParams) => { + const { data } = await api.delete(PROMPTS_REST_ENDPOINT + promptId); + return data; + }, + onError: (error) => { + const message = get( + error, + ["response", "data", "message"], + error.message, + ); + + toast({ + title: "Error", + description: message, + variant: "destructive", + }); + }, + onSettled: () => { + return queryClient.invalidateQueries({ queryKey: ["prompts"] }); + }, + }); +}; + +export default usePromptDeleteMutation; diff --git a/apps/opik-frontend/src/api/prompts/usePromptsList.ts b/apps/opik-frontend/src/api/prompts/usePromptsList.ts new file mode 100644 index 000000000..9007f2769 --- /dev/null +++ b/apps/opik-frontend/src/api/prompts/usePromptsList.ts @@ -0,0 +1,131 @@ +import { QueryFunctionContext, useQuery } from "@tanstack/react-query"; +import api, {PROMPTS_REST_ENDPOINT, QueryConfig} from "@/api/api"; +import {Prompt} from "@/types/prompts"; + +type UsePromptsListParams = { + workspaceName: string; + search?: string; + page: number; + size: number; +}; + +type UsePromptsListResponse = { + content: Prompt[]; + total: number; +}; + +// ALEX +const FAKE_PROMPTS: Prompt[] = [ + { + "id": "4338ec03-6ee9-4635-b071-77849626b948", + "name": "Item 1123421342134341234812349123841239048132094812304123804301294", + "description": "This is a description for Item 1.", + "last_updated_at": "2022-04-15 00:00:00", + "created_at": "2020-09-09 00:00:00", + "versions_count": 6 + }, + { + "id": "2ac76571-520f-4664-9ad5-1439492ad557", + "name": "Item 2", + "description": "This is a description for Item 2.", + "last_updated_at": "2022-08-19 00:00:00", + "created_at": "2021-10-27 00:00:00", + "versions_count": 4 + }, + { + "id": "3ba74f16-698f-4318-9427-d26403bc117b", + "name": "Item 3", + "description": "This is a description for Item 3.", + "last_updated_at": "2023-12-12 00:00:00", + "created_at": "2021-10-08 00:00:00", + "versions_count": 5 + }, + { + "id": "bbef9b6c-d991-4132-9c08-24e1584ccea2", + "name": "Item 4", + "description": "This is a description for Item 4.", + "last_updated_at": "2023-05-14 00:00:00", + "created_at": "2023-01-04 00:00:00", + "versions_count": 10 + }, + { + "id": "57c82e2b-3d75-4f16-87ac-3201554ea84d", + "name": "Item 5", + "description": "This is a description for Item 5.", + "last_updated_at": "2022-06-10 00:00:00", + "created_at": "2021-10-06 00:00:00", + "versions_count": 4 + }, + { + "id": "caad2d07-6be4-4e30-8e54-45928c4c675c", + "name": "Item 6", + "description": "This is a description for Item 6.", + "last_updated_at": "2023-11-07 00:00:00", + "created_at": "2022-04-26 00:00:00", + "versions_count": 7 + }, + { + "id": "955439b8-2199-4edb-bfff-0becef27f477", + "name": "Item 7", + "description": "This is a description for Item 7.", + "last_updated_at": "2023-09-21 00:00:00", + "created_at": "2023-08-15 00:00:00", + "versions_count": 5 + }, + { + "id": "059452b9-9050-4b99-893f-8e809adae877", + "name": "Item 8", + "description": "This is a description for Item 8.", + "last_updated_at": "2023-04-10 00:00:00", + "created_at": "2022-09-22 00:00:00", + "versions_count": 8 + }, + { + "id": "c46f907a-7e35-4d35-a482-fb4a7fee3967", + "name": "Item 9", + "description": "This is a description for Item 9.", + "last_updated_at": "2023-12-22 00:00:00", + "created_at": "2023-05-13 00:00:00", + "versions_count": 8 + }, + { + "id": "c4365771-f654-4434-9272-8e22f623906b", + "name": "Item 10", + "description": "This is a description for Item 10.", + "last_updated_at": "2023-09-09 00:00:00", + "created_at": "2022-12-02 00:00:00", + "versions_count": 6 + } +] + + +const getPromptsList = async ( + { signal }: QueryFunctionContext, + { workspaceName, search, size, page }: UsePromptsListParams, +) => { + // const { data } = await api.get(PROMPTS_REST_ENDPOINT, { + // signal, + // params: { + // workspace_name: workspaceName, + // ...(search && { name: search }), + // size, + // page, + // }, + // }); + + return { + content: FAKE_PROMPTS, + total: FAKE_PROMPTS.length, + }; +}; + +export default function usePromptsList( + params: UsePromptsListParams, + options?: QueryConfig, +) { + return useQuery({ + queryKey: ["prompts", params], + queryFn: (context) => getPromptsList(context, params), + ...options, + }); +} diff --git a/apps/opik-frontend/src/components/layout/SideBar/SideBar.tsx b/apps/opik-frontend/src/components/layout/SideBar/SideBar.tsx index e3effc3cd..a45bcf1db 100644 --- a/apps/opik-frontend/src/components/layout/SideBar/SideBar.tsx +++ b/apps/opik-frontend/src/components/layout/SideBar/SideBar.tsx @@ -11,6 +11,7 @@ import { MessageSquare, PanelLeftClose, MessageCircleQuestion, + FileTerminal } from "lucide-react"; import { keepPreviousData } from "@tanstack/react-query"; @@ -27,6 +28,7 @@ import { buildDocsUrl, cn } from "@/lib/utils"; import Logo from "@/components/layout/Logo/Logo"; import usePluginsStore from "@/store/PluginsStore"; import ProvideFeedbackDialog from "@/components/layout/SideBar/FeedbackDialog/ProvideFeedbackDialog"; +import usePromptsList from "@/api/prompts/usePromptsList"; enum MENU_ITEM_TYPE { link = "link", @@ -53,6 +55,14 @@ const MAIN_MENU_ITEMS: MenuItem[] = [ label: "Projects", count: "projects", }, + { + id: "prompts", + path: "/$workspaceName/prompts", + type: MENU_ITEM_TYPE.router, + icon: FileTerminal, + label: "Prompts", + count: "prompts", + }, { id: "datasets", path: "/$workspaceName/datasets", @@ -204,11 +214,24 @@ const SideBar: React.FunctionComponent = ({ }, ); + const { data: promptsData } = usePromptsList( + { + workspaceName, + page: 1, + size: 1, + }, + { + placeholderData: keepPreviousData, + enabled: expanded, + }, + ); + const countDataMap: Record = { projects: projectData?.total, datasets: datasetsData?.total, experiments: experimentsData?.total, feedbackDefinitions: feedbackDefinitions?.total, + prompts: promptsData?.total }; const bottomMenuItems: MenuItem[] = [ diff --git a/apps/opik-frontend/src/components/pages/PromptsPage/AddPromptDialog.tsx b/apps/opik-frontend/src/components/pages/PromptsPage/AddPromptDialog.tsx new file mode 100644 index 000000000..437497c60 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/PromptsPage/AddPromptDialog.tsx @@ -0,0 +1,110 @@ +import React, {useCallback, useState} from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import useAppStore from "@/store/AppStore"; +import { Textarea } from "@/components/ui/textarea"; +import usePromptCreateMutation from "@/api/prompts/usePromptCreateMutation"; +import {Prompt} from "@/types/prompts"; +import {AccordionContent, AccordionItem, AccordionTrigger, Accordion} from "@/components/ui/accordion"; + +type AddPromptDialogProps = { + open: boolean; + setOpen: (open: boolean) => void; + onPromptCreated?: (prompt: Prompt) => void; +}; + +const AddPromptDialog: React.FunctionComponent = ({ + open, + setOpen, + onPromptCreated, +}) => { + const workspaceName = useAppStore((state) => state.activeWorkspaceName); + const promptCreateMutation = usePromptCreateMutation(); + const [name, setName] = useState(""); + const [prompt, setPrompt] = useState(""); + const [description, setDescription] = useState(""); + + const isValid = Boolean(name.length && prompt.length); + + const createPrompt = useCallback(() => { + promptCreateMutation.mutate( + { + prompt: { + name, + template: prompt, + ...(description ? { description } : {}), + }, + workspaceName, + }, + { onSuccess: onPromptCreated }, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [name, description, workspaceName, onPromptCreated]); + + return ( + + + + Create a new prompt + +
+ + setName(event.target.value)} + /> +
+
+ +