From 9f2a0ac3740ca0bd61680a4ec3cf1b38a677dbdd Mon Sep 17 00:00:00 2001 From: SuperTurk Date: Wed, 4 Oct 2023 15:29:28 +0200 Subject: [PATCH 01/25] fix: Fetching conversation request fail due to pydantic v2 --- backend/chainlit/client/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/chainlit/client/base.py b/backend/chainlit/client/base.py index a1eb581067..83dfac419f 100644 --- a/backend/chainlit/client/base.py +++ b/backend/chainlit/client/base.py @@ -107,13 +107,13 @@ class PaginatedResponse(DataClassJsonMixin, Generic[T]): class Pagination(BaseModel): first: int - cursor: Optional[str] + cursor: Optional[str] = None class ConversationFilter(BaseModel): - feedback: Optional[Literal[-1, 0, 1]] - username: Optional[str] - search: Optional[str] + feedback: Optional[Literal[-1, 0, 1]] = None + username: Optional[str] = None + search: Optional[str] = None class ChainlitGraphQLClient: From 2e309b1011d4046f8049d0ea0bb880092394d327 Mon Sep 17 00:00:00 2001 From: SuperTurk Date: Fri, 6 Oct 2023 12:48:17 +0200 Subject: [PATCH 02/25] feat: Add revalidation options to useApi hook --- frontend/src/hooks/useApi.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/hooks/useApi.tsx b/frontend/src/hooks/useApi.tsx index 2fff8ccbe9..db0d13d8f8 100644 --- a/frontend/src/hooks/useApi.tsx +++ b/frontend/src/hooks/useApi.tsx @@ -1,5 +1,5 @@ import { api } from 'api'; -import useSWR from 'swr'; +import useSWR, { SWRConfiguration } from 'swr'; import { useAuth } from './auth'; @@ -9,12 +9,13 @@ const fetcher = async (endpoint: string, token?: string) => { return res?.json(); }; -function useApi(endpoint: string | null) { +function useApi(endpoint: string | null, options?: SWRConfiguration) { const { accessToken } = useAuth(); return useSWR( endpoint ? [endpoint, accessToken] : null, - ([url, token]: [url: string, token: string]) => fetcher(url, token) + ([url, token]: [url: string, token: string]) => fetcher(url, token), + options ); } From e9d30cd173c790bc5ad4b528137b34dd6384219a Mon Sep 17 00:00:00 2001 From: SuperTurk Date: Wed, 4 Oct 2023 15:31:01 +0200 Subject: [PATCH 03/25] refactor: Improve conversation browsing history --- frontend/package.json | 5 +- frontend/pnpm-lock.yaml | 74 +--- frontend/src/api/chainlitApi.ts | 11 +- .../src/components/organisms/chat/index.tsx | 15 +- .../conversationsHistory/Conversation.tsx | 73 ++++ .../sidebar/ConversationsHistoryList.tsx | 321 ++++++++++++++++++ .../sidebar/DeleteConversationButton.tsx} | 22 +- .../sidebar/filters/FeedbackSelect.tsx | 103 ++++++ .../sidebar/filters/SearchBar.tsx | 70 ++++ .../sidebar/filters/index.tsx | 21 ++ .../conversationsHistory/sidebar/index.tsx | 123 +++++++ .../dataset/filters/FeedbackSelect.tsx | 43 --- .../organisms/dataset/filters/SearchBar.tsx | 92 ----- .../organisms/dataset/filters/index.tsx | 15 - .../components/organisms/dataset/index.tsx | 22 -- .../dataset/openConversationButton.tsx | 22 -- .../components/organisms/dataset/table.tsx | 233 ------------- frontend/src/components/organisms/header.tsx | 16 +- frontend/src/hooks/auth.ts | 4 +- frontend/src/pages/Conversation.tsx | 42 --- frontend/src/pages/Conversations.tsx | 45 +++ frontend/src/pages/Dataset.tsx | 11 - frontend/src/pages/Page.tsx | 8 +- frontend/src/router.tsx | 16 +- frontend/src/state/chatHistory.ts | 13 +- frontend/src/state/conversations.ts | 12 + frontend/src/state/dataset.ts | 12 - frontend/src/state/user.ts | 7 +- frontend/src/types/chat.ts | 2 +- frontend/src/types/chatHistory.ts | 8 + libs/components/src/playground/chat.tsx | 2 +- 31 files changed, 849 insertions(+), 614 deletions(-) create mode 100644 frontend/src/components/organisms/conversationsHistory/Conversation.tsx create mode 100644 frontend/src/components/organisms/conversationsHistory/sidebar/ConversationsHistoryList.tsx rename frontend/src/components/organisms/{dataset/deleteConversationButton.tsx => conversationsHistory/sidebar/DeleteConversationButton.tsx} (87%) create mode 100644 frontend/src/components/organisms/conversationsHistory/sidebar/filters/FeedbackSelect.tsx create mode 100644 frontend/src/components/organisms/conversationsHistory/sidebar/filters/SearchBar.tsx create mode 100644 frontend/src/components/organisms/conversationsHistory/sidebar/filters/index.tsx create mode 100644 frontend/src/components/organisms/conversationsHistory/sidebar/index.tsx delete mode 100644 frontend/src/components/organisms/dataset/filters/FeedbackSelect.tsx delete mode 100644 frontend/src/components/organisms/dataset/filters/SearchBar.tsx delete mode 100644 frontend/src/components/organisms/dataset/filters/index.tsx delete mode 100644 frontend/src/components/organisms/dataset/index.tsx delete mode 100644 frontend/src/components/organisms/dataset/openConversationButton.tsx delete mode 100644 frontend/src/components/organisms/dataset/table.tsx delete mode 100644 frontend/src/pages/Conversation.tsx create mode 100644 frontend/src/pages/Conversations.tsx delete mode 100644 frontend/src/pages/Dataset.tsx create mode 100644 frontend/src/state/conversations.ts delete mode 100644 frontend/src/state/dataset.ts diff --git a/frontend/package.json b/frontend/package.json index 89b090f7a8..df09d46a88 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,11 +25,9 @@ "react-dom": "^18.2.0", "react-hot-toast": "^2.4.1", "react-hotkeys-hook": "^4.4.1", + "react-intersection-observer": "^9.5.2", "react-markdown": "^8.0.7", "react-router-dom": "^6.15.0", - "react-virtualized-auto-sizer": "^1.0.20", - "react-window": "^1.8.9", - "react-window-infinite-loader": "^1.0.9", "recoil": "^0.7.6", "remark-gfm": "^3.0.1", "socket.io-client": "^4.7.2", @@ -42,7 +40,6 @@ "@types/lodash": "^4.14.199", "@types/node": "^20.5.7", "@types/react": "^18.2.0", - "@types/react-window-infinite-loader": "^1.0.6", "@types/uuid": "^9.0.3", "@vitejs/plugin-react-swc": "^3.3.2", "typescript": "^5.2.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b30271bf4c..7aa1012bca 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -44,21 +44,15 @@ dependencies: react-hotkeys-hook: specifier: ^4.4.1 version: 4.4.1(react-dom@18.2.0)(react@18.2.0) + react-intersection-observer: + specifier: ^9.5.2 + version: 9.5.2(react@18.2.0) react-markdown: specifier: ^8.0.7 version: 8.0.7(@types/react@18.2.0)(react@18.2.0) react-router-dom: specifier: ^6.15.0 version: 6.15.0(react-dom@18.2.0)(react@18.2.0) - react-virtualized-auto-sizer: - specifier: ^1.0.20 - version: 1.0.20(react-dom@18.2.0)(react@18.2.0) - react-window: - specifier: ^1.8.9 - version: 1.8.9(react-dom@18.2.0)(react@18.2.0) - react-window-infinite-loader: - specifier: ^1.0.9 - version: 1.0.9(react-dom@18.2.0)(react@18.2.0) recoil: specifier: ^0.7.6 version: 0.7.6(react-dom@18.2.0)(react@18.2.0) @@ -91,9 +85,6 @@ devDependencies: '@types/react': specifier: ^18.2.0 version: 18.2.0 - '@types/react-window-infinite-loader': - specifier: ^1.0.6 - version: 1.0.6 '@types/uuid': specifier: ^9.0.3 version: 9.0.3 @@ -919,19 +910,6 @@ packages: '@types/react': 18.2.0 dev: false - /@types/react-window-infinite-loader@1.0.6: - resolution: {integrity: sha512-V8g8sBDLVeJJAfEENJS7VXZK+DRJ+jzPNtk8jpj2G+obhf+iqGNUDGwNWCbBhLiD+KpHhf3kWQlKBRi0tAeU4Q==} - dependencies: - '@types/react': 18.2.0 - '@types/react-window': 1.8.6 - dev: true - - /@types/react-window@1.8.6: - resolution: {integrity: sha512-AVJr3A5rIO9dQQu5TwTN0lP2c1RtuqyyZGCt7PGP8e5gUpn1PuQRMJb/u3UpdbwTHh4wbEi33UMW5NI0IXt1Mg==} - dependencies: - '@types/react': 18.2.0 - dev: true - /@types/react@18.2.0: resolution: {integrity: sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==} dependencies: @@ -1438,10 +1416,6 @@ packages: '@types/mdast': 3.0.13 dev: false - /memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - dev: false - /micromark-core-commonmark@1.1.0: resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} dependencies: @@ -1801,6 +1775,14 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-intersection-observer@9.5.2(react@18.2.0): + resolution: {integrity: sha512-EmoV66/yvksJcGa1rdW0nDNc4I1RifDWkT50gXSFnPLYQ4xUptuDD4V7k+Rj1OgVAlww628KLGcxPXFlOkkU/Q==} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} dev: false @@ -1873,40 +1855,6 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react-virtualized-auto-sizer@1.0.20(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==} - peerDependencies: - react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc - react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc - dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /react-window-infinite-loader@1.0.9(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-5Hg89IdU4Vrp0RT8kZYKeTIxWZYhNkVXeI1HbKo01Vm/Z7qztDvXljwx16sMzsa9yapRJQW3ODZfMUw38SOWHw==} - engines: {node: '>8.0.0'} - peerDependencies: - react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 - react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 - dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - - /react-window@1.8.9(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==} - engines: {node: '>8.0.0'} - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@babel/runtime': 7.23.1 - memoize-one: 5.2.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false - /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} diff --git a/frontend/src/api/chainlitApi.ts b/frontend/src/api/chainlitApi.ts index 16bbe07d4b..35019cf911 100644 --- a/frontend/src/api/chainlitApi.ts +++ b/frontend/src/api/chainlitApi.ts @@ -1,8 +1,11 @@ import { IPrompt } from '@chainlit/components'; -import { IPageInfo, IPagination } from 'components/organisms/dataset/table'; +import { + IPageInfo, + IPagination +} from 'components/organisms/conversationsHistory/sidebar/ConversationsHistoryList'; -import { IDatasetFilters } from 'state/dataset'; +import { IConversationsFilters } from 'state/conversations'; import { IChat } from 'types/chat'; @@ -69,7 +72,7 @@ const ChainlitAPI = { getConversations: async ( pagination: IPagination, - filter: IDatasetFilters, + filter: IConversationsFilters, accessToken?: string ): Promise<{ pageInfo: IPageInfo; @@ -84,7 +87,7 @@ const ChainlitAPI = { return res?.json(); }, - deleteConversation: async (conversationId: number, accessToken?: string) => { + deleteConversation: async (conversationId: string, accessToken?: string) => { const res = await api.delete( `/project/conversation`, { conversationId }, diff --git a/frontend/src/components/organisms/chat/index.tsx b/frontend/src/components/organisms/chat/index.tsx index 25039dc242..664afa4525 100644 --- a/frontend/src/components/organisms/chat/index.tsx +++ b/frontend/src/components/organisms/chat/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; @@ -12,7 +12,7 @@ import TaskList from 'components/molecules/tasklist'; import { useAuth } from 'hooks/auth'; -import { chatHistoryState } from 'state/chatHistory'; +import { chatHistoryState, conversationsHistoryState } from 'state/chatHistory'; import { projectSettingsState, sideViewState } from 'state/project'; import InputBox from './inputBox'; @@ -24,6 +24,7 @@ const Chat = () => { const pSettings = useRecoilValue(projectSettingsState); const sideViewElement = useRecoilValue(sideViewState); const setChatHistory = useSetRecoilState(chatHistoryState); + const setConversations = useSetRecoilState(conversationsHistoryState); const [autoScroll, setAutoScroll] = useState(true); const { @@ -40,6 +41,13 @@ const Chat = () => { loading } = useChat(); + useEffect(() => { + setConversations((prev) => ({ + ...prev, + currentConversationId: undefined + })); + }, []); + const onSubmit = useCallback( async (msg: string) => { const message: IMessage = { @@ -92,7 +100,8 @@ const Chat = () => { const tasklist = tasklists.at(-1); return ( - + + diff --git a/frontend/src/components/organisms/conversationsHistory/Conversation.tsx b/frontend/src/components/organisms/conversationsHistory/Conversation.tsx new file mode 100644 index 0000000000..070e7fa951 --- /dev/null +++ b/frontend/src/components/organisms/conversationsHistory/Conversation.tsx @@ -0,0 +1,73 @@ +import { Box, Skeleton, Stack } from '@mui/material'; + +import { IAction, nestMessages } from '@chainlit/components'; + +import SideView from 'components/atoms/element/sideView'; +import MessageContainer from 'components/organisms/chat/message/container'; + +import { useApi } from 'hooks/useApi'; + +import { IChat } from 'types/chat'; + +const Conversation = ({ id }: { id: string }) => { + const { + data: conversation, + error, + isLoading + } = useApi(id ? `/project/conversation/${id}` : null, { + revalidateOnFocus: false, + revalidateIfStale: false + }); + + if (isLoading) { + return [1, 2, 3].map((index) => ( + + + + + + + + )); + } + + if (!conversation || error) { + return null; + } + + const elements = conversation.elements; + const actions: IAction[] = []; + + return ( + + + + + + + ); +}; + +export { Conversation }; diff --git a/frontend/src/components/organisms/conversationsHistory/sidebar/ConversationsHistoryList.tsx b/frontend/src/components/organisms/conversationsHistory/sidebar/ConversationsHistoryList.tsx new file mode 100644 index 0000000000..87e0e736cd --- /dev/null +++ b/frontend/src/components/organisms/conversationsHistory/sidebar/ConversationsHistoryList.tsx @@ -0,0 +1,321 @@ +import { ChainlitAPI } from 'api/chainlitApi'; +import { capitalize, map, size, uniqBy } from 'lodash'; +import { useEffect, useState } from 'react'; +import { useInView } from 'react-intersection-observer'; +import { useNavigate } from 'react-router-dom'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +import ChatBubbleOutline from '@mui/icons-material/ChatBubbleOutline'; +import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import List from '@mui/material/List'; +import ListSubheader from '@mui/material/ListSubheader'; +import Skeleton from '@mui/material/Skeleton'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +import { conversationsHistoryState } from 'state/chatHistory'; +import { + IConversationsFilters, + conversationsFiltersState +} from 'state/conversations'; +import { accessTokenState } from 'state/user'; + +import { IChat } from 'types/chat'; + +import { DeleteConversationButton } from './DeleteConversationButton'; + +export interface IPageInfo { + hasNextPage: boolean; + endCursor?: string; +} + +export interface IPagination { + first: number; + cursor?: string | number; +} + +const BATCH_SIZE = 30; + +const groupByDate = (data: IChat[]) => { + const groupedData: { [key: string]: IChat[] } = {}; + + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate(today.getDate() - 1); + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(today.getDate() - 7); + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(today.getDate() - 30); + + data.forEach((item) => { + const createdAt = new Date(item.createdAt); + const isToday = createdAt.toDateString() === today.toDateString(); + const isYesterday = createdAt.toDateString() === yesterday.toDateString(); + const isLast7Days = createdAt >= sevenDaysAgo; + const isLast30Days = createdAt >= thirtyDaysAgo; + + let category: string; + + if (isToday) { + category = 'Today'; + } else if (isYesterday) { + category = 'Yesterday'; + } else if (isLast7Days) { + category = 'Previous 7 days'; + } else if (isLast30Days) { + category = 'Previous 30 days'; + } else { + const monthYear = createdAt.toLocaleString('default', { + month: 'long', + year: 'numeric' + }); + + category = monthYear.split(' ').slice(0, 1).join(' '); + } + + if (!groupedData[category]) { + groupedData[category] = []; + } + + groupedData[category].push(item); + }); + + return groupedData; +}; + +const ConversationsHistoryList = () => { + const navigate = useNavigate(); + const { ref, inView } = useInView(); + + const [conversations, setConversations] = useRecoilState( + conversationsHistoryState + ); + + const accessToken = useRecoilValue(accessTokenState); + const filters = useRecoilValue(conversationsFiltersState); + + const [error, setError] = useState(undefined); + const [prevPageInfo, setPrevPageInfo] = useState(); + const [prevFilters, setPrevFilters] = + useState(filters); + const [isLoading, setIsLoading] = useState(false); + const [isRefetching, setIsRefetching] = useState(false); + + const fetchConversations = async (cursor?: string | number) => { + try { + const { pageInfo, data } = await ChainlitAPI.getConversations( + { first: BATCH_SIZE, cursor }, + filters, + accessToken + ); + setPrevPageInfo(pageInfo); + setError(undefined); + + // Prevent conversations to be duplicated + const allConversations = uniqBy( + conversations?.conversations?.concat(data), + 'id' + ); + + if (allConversations) { + const groupedConversations = groupByDate(allConversations); + + setConversations({ + conversations: allConversations, + groupedConversations + }); + } + } catch (error) { + if (error instanceof Error) setError(error.message); + } finally { + setIsLoading(false); + setIsRefetching(false); + } + }; + + const refetchConversations = async () => { + setIsLoading(true); + setPrevPageInfo(undefined); + fetchConversations(undefined); + }; + + const onDeleteConversation = () => { + setIsRefetching(true); + refetchConversations(); + }; + + useEffect(() => { + const filtersHasChanged = + JSON.stringify(prevFilters) !== JSON.stringify(filters); + + if (size(conversations?.groupedConversations) === 0 || filtersHasChanged) { + if (filtersHasChanged) { + setIsRefetching(true); + setPrevFilters(filters); + } + + refetchConversations(); + } + }, [accessToken, filters]); + + useEffect(() => { + if (inView) loadMoreItems(); + }, [inView]); + + const loadMoreItems = () => { + if (!isLoading && prevPageInfo?.hasNextPage && prevPageInfo.endCursor) { + setIsLoading(true); + fetchConversations(prevPageInfo.endCursor); + } + }; + + if (isRefetching || (!conversations?.groupedConversations && isLoading)) { + console.log('ON REFETCH LAAA'); + return [1, 2, 3].map((index) => ( + + + {[1, 2].map((childIndex) => ( + + + + + ))} + + )); + } + + if (error) { + return ( + + {(error as any).message} + + ); + } + + if (!conversations) { + return null; + } + + if (size(conversations?.groupedConversations) === 0) { + return ( + + No result + + ); + } + + return ( + <> + } + > + {map(conversations.groupedConversations, (items, index) => { + return ( +
  • +
      + + theme.palette.background.paper + }} + > + {index} + + + {map(items, (conversation) => { + const isSelected = + conversations.currentConversationId === conversation.id; + + return ( + ({ + cursor: 'pointer', + p: 1.5, + mb: 0.5, + gap: 0.5, + flexDirection: 'row', + justifyContent: 'space-between', + borderRadius: 1, + backgroundColor: + theme.palette.background[ + isSelected ? 'default' : 'paper' + ], + '&:hover': { + backgroundColor: theme.palette.background.default + } + })} + onClick={() => + navigate(`/conversations/${conversation.id}`) + } + > + + + theme.palette.text.primary, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + }} + > + {capitalize(conversation.messages[0]?.content)} + + + {isSelected ? ( + + ) : null} + + ); + })} +
    +
  • + ); + })} + {prevPageInfo?.hasNextPage ? ( + + + + ) : null} +
    + + ); +}; + +export { ConversationsHistoryList }; diff --git a/frontend/src/components/organisms/dataset/deleteConversationButton.tsx b/frontend/src/components/organisms/conversationsHistory/sidebar/DeleteConversationButton.tsx similarity index 87% rename from frontend/src/components/organisms/dataset/deleteConversationButton.tsx rename to frontend/src/components/organisms/conversationsHistory/sidebar/DeleteConversationButton.tsx index 78e9e0ed54..bf54897cd1 100644 --- a/frontend/src/components/organisms/dataset/deleteConversationButton.tsx +++ b/frontend/src/components/organisms/conversationsHistory/sidebar/DeleteConversationButton.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import toast from 'react-hot-toast'; import { useRecoilValue } from 'recoil'; -import DeleteOutline from '@mui/icons-material/DeleteOutline'; +import { DeleteOutline } from '@mui/icons-material'; import LoadingButton from '@mui/lab/LoadingButton'; import { IconButton } from '@mui/material'; import Button from '@mui/material/Button'; @@ -17,14 +17,11 @@ import DialogTitle from '@mui/material/DialogTitle'; import { accessTokenState } from 'state/user'; interface Props { - conversationId: number; + conversationId: string; onDelete: () => void; } -export default function DeleteConversationButton({ - conversationId, - onDelete -}: Props) { +const DeleteConversationButton = ({ conversationId, onDelete }: Props) => { const [open, setOpen] = useState(false); const accessToken = useRecoilValue(accessTokenState); @@ -57,13 +54,8 @@ export default function DeleteConversationButton({ return (
    - - + + {open && ( ); -} +}; + +export { DeleteConversationButton }; diff --git a/frontend/src/components/organisms/conversationsHistory/sidebar/filters/FeedbackSelect.tsx b/frontend/src/components/organisms/conversationsHistory/sidebar/filters/FeedbackSelect.tsx new file mode 100644 index 0000000000..251922313a --- /dev/null +++ b/frontend/src/components/organisms/conversationsHistory/sidebar/filters/FeedbackSelect.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react'; +import { useRecoilState } from 'recoil'; + +import FilterList from '@mui/icons-material/FilterList'; +import ThumbDown from '@mui/icons-material/ThumbDown'; +import ThumbUp from '@mui/icons-material/ThumbUp'; +import { IconButton } from '@mui/material'; +import Box from '@mui/material/Box'; +import Menu from '@mui/material/Menu'; +import Stack from '@mui/material/Stack'; + +import { conversationsFiltersState } from 'state/conversations'; + +export enum FEEDBACKS { + ALL = 0, + POSITIVE = 1, + NEGATIVE = -1 +} + +export default function FeedbackSelect() { + const [filters, setFilters] = useRecoilState(conversationsFiltersState); + const [anchorEl, setAnchorEl] = useState(null); + + const handleChange = (feedback: number) => { + setFilters((prev) => ({ ...prev, feedback })); + setAnchorEl(null); + }; + + const renderMenuItem = (label: string, feedback: number) => { + return ( + handleChange(feedback)} + sx={{ + cursor: 'pointer', + px: 1.5, + py: 1, + borderRadius: 1, + '&:hover': { + background: (theme) => theme.palette.background.default + } + }} + > + {label} + + ); + }; + + const renderIcon = () => { + const sx = { width: 16, height: 16 }; + + switch (filters.feedback) { + case FEEDBACKS.POSITIVE: + return ; + case FEEDBACKS.NEGATIVE: + return ; + default: + return ; + } + }; + + return ( + <> + setAnchorEl(event.currentTarget)} + sx={{ + borderRadius: 1, + backgroundColor: (theme) => theme.palette.background.default + }} + > + {renderIcon()} + + setAnchorEl(null)} + sx={{ mt: 1 }} + MenuListProps={{ + sx: { p: 0.5 } + }} + slotProps={{ + paper: { + sx: { + background: (theme) => theme.palette.background.paper, + border: (theme) => `1px solid ${theme.palette.divider}`, + boxShadow: (theme) => + theme.palette.mode === 'light' + ? '0px 2px 4px 0px #0000000D' + : '0px 10px 10px 0px #0000000D' + } + } + }} + > + + {renderMenuItem('Feedback: All', FEEDBACKS.ALL)} + {renderMenuItem('Feedback: Positive', FEEDBACKS.POSITIVE)} + {renderMenuItem('Feedback: Negative', FEEDBACKS.NEGATIVE)} + + + + ); +} diff --git a/frontend/src/components/organisms/conversationsHistory/sidebar/filters/SearchBar.tsx b/frontend/src/components/organisms/conversationsHistory/sidebar/filters/SearchBar.tsx new file mode 100644 index 0000000000..40be64dc0a --- /dev/null +++ b/frontend/src/components/organisms/conversationsHistory/sidebar/filters/SearchBar.tsx @@ -0,0 +1,70 @@ +import { debounce } from 'lodash'; +import { useRef } from 'react'; +import { useRecoilState } from 'recoil'; + +import CloseIcon from '@mui/icons-material/Close'; +import SearchIcon from '@mui/icons-material/Search'; +import IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import TextField from '@mui/material/TextField'; + +import { conversationsFiltersState } from 'state/conversations'; + +export default function SearchBar() { + const [filters, setFilters] = useRecoilState(conversationsFiltersState); + + const handleChange = (value: string) => { + value = value.trim(); + const search = value === '' ? undefined : value; + setFilters((prev) => ({ ...prev, search })); + }; + + const _onChange = debounce(handleChange, 300); + const inputRef = useRef(); + + const clear = () => { + _onChange(''); + if (inputRef.current) { + inputRef.current.value = ''; + } + }; + + return ( + theme.palette.background.default + }} + InputProps={{ + sx: { + px: 1.5, + py: 1 + }, + disableUnderline: true, + startAdornment: ( + + + + ), + endAdornment: filters.search ? ( + + + + ) : null + }} + placeholder="Search" + inputProps={{ + 'aria-label': 'search', + ref: inputRef, + sx: { p: 0 } + }} + onChange={(e) => _onChange(e.target.value)} + /> + ); +} diff --git a/frontend/src/components/organisms/conversationsHistory/sidebar/filters/index.tsx b/frontend/src/components/organisms/conversationsHistory/sidebar/filters/index.tsx new file mode 100644 index 0000000000..0548469869 --- /dev/null +++ b/frontend/src/components/organisms/conversationsHistory/sidebar/filters/index.tsx @@ -0,0 +1,21 @@ +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; + +import FeedbackSelect from './FeedbackSelect'; +import SearchBar from './SearchBar'; + +export default function Filters() { + return ( + + + + + + + ); +} diff --git a/frontend/src/components/organisms/conversationsHistory/sidebar/index.tsx b/frontend/src/components/organisms/conversationsHistory/sidebar/index.tsx new file mode 100644 index 0000000000..19a9de622a --- /dev/null +++ b/frontend/src/components/organisms/conversationsHistory/sidebar/index.tsx @@ -0,0 +1,123 @@ +import { useEffect, useRef, useState } from 'react'; +import { useRecoilValue } from 'recoil'; + +import KeyboardDoubleArrowLeftIcon from '@mui/icons-material/KeyboardDoubleArrowLeft'; +import KeyboardDoubleArrowRightIcon from '@mui/icons-material/KeyboardDoubleArrowRight'; +import Box from '@mui/material/Box'; +import MDrawer from '@mui/material/Drawer'; +import IconButton from '@mui/material/IconButton'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import styled from '@mui/material/styles/styled'; +import useMediaQuery from '@mui/material/useMediaQuery'; + +import { useAuth } from 'hooks/auth'; + +import { projectSettingsState } from 'state/project'; + +import { ConversationsHistoryList } from './ConversationsHistoryList'; +import Filters from './filters'; + +const DRAWER_WIDTH = 240; + +let scrollTop = 0; + +const ConversationsHistorySidebar = (): JSX.Element | null => { + const isMobile = useMediaQuery('(max-width:800px)'); + const [open, setOpen] = useState(true); + const pSettings = useRecoilValue(projectSettingsState); + const { user } = useAuth(); + + const ref = useRef(null); + + useEffect(() => { + const saveScroll = () => { + console.log('REF', ref.current?.scrollTop); + scrollTop = ref.current?.scrollTop || 0; + }; + + ref.current?.scrollTo({ + top: scrollTop + }); + ref.current?.addEventListener('scroll', saveScroll); + + return () => ref.current?.removeEventListener('scroll', saveScroll); + }, []); + + if (!pSettings?.dataPersistence || !user) { + return null; + } + + return ( + <> + + + theme.palette.text.primary + }} + > + Chat History + + setOpen(false)}> + + + + + + + + theme.palette.background.paper + }} + onClick={() => setOpen(true)} + > + + + + + ); +}; + +const Drawer = styled(MDrawer, { + shouldForwardProp: (prop) => prop !== 'isSmallScreen' +})<{ open: boolean }>(({ open }) => ({ + width: open ? DRAWER_WIDTH : 0, + '& .MuiDrawer-paper': { + position: 'inherit', + gap: 10, + display: 'flex', + padding: '0px 4px', + backgroundImage: 'none' + } +})); + +export { ConversationsHistorySidebar }; diff --git a/frontend/src/components/organisms/dataset/filters/FeedbackSelect.tsx b/frontend/src/components/organisms/dataset/filters/FeedbackSelect.tsx deleted file mode 100644 index 219b489811..0000000000 --- a/frontend/src/components/organisms/dataset/filters/FeedbackSelect.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useRecoilState } from 'recoil'; - -import Box from '@mui/material/Box'; -import { SelectChangeEvent } from '@mui/material/Select'; - -import { SelectInput, SelectItem } from '@chainlit/components'; - -import { datasetFiltersState } from 'state/dataset'; - -const items: SelectItem[] = [ - { - value: 0, - label: 'All' - }, - { - value: 1, - label: 'Good' - }, - { - value: -1, - label: 'Bad' - } -]; - -export default function FeedbackSelect() { - const [df, setDf] = useRecoilState(datasetFiltersState); - - const handleChange = (event: SelectChangeEvent) => { - setDf({ ...df, feedback: parseInt(event.target.value) }); - }; - - return ( - - - - ); -} diff --git a/frontend/src/components/organisms/dataset/filters/SearchBar.tsx b/frontend/src/components/organisms/dataset/filters/SearchBar.tsx deleted file mode 100644 index adf0f2238d..0000000000 --- a/frontend/src/components/organisms/dataset/filters/SearchBar.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { debounce } from 'lodash'; -import { useRef } from 'react'; -import { useRecoilState } from 'recoil'; - -import CloseIcon from '@mui/icons-material/Close'; -import SearchIcon from '@mui/icons-material/Search'; -import IconButton from '@mui/material/IconButton'; -import InputBase from '@mui/material/InputBase'; -import { styled } from '@mui/material/styles'; - -import { datasetFiltersState } from 'state/dataset'; - -const Search = styled('div')(({ theme }) => ({ - position: 'relative', - color: theme.palette.text.primary, - borderRadius: theme.shape.borderRadius, - border: `${theme.palette.divider} solid 1px`, - '&:hover': { - border: `${theme.palette.primary} solid 1px !important` - }, - - marginLeft: 0, - display: 'flex', - [theme.breakpoints.up('sm')]: { - width: 'auto' - } -})); - -const SearchIconWrapper = styled('div')(() => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'center' -})); - -const StyledInputBase = styled(InputBase)(({ theme }) => ({ - color: 'inherit', - '& .MuiInputBase-input': { - padding: theme.spacing(1, 1, 1, 0), - // vertical padding + font size from searchIcon - paddingLeft: `calc(1em + ${theme.spacing(3)})`, - transition: theme.transitions.create('width'), - width: '100%', - [theme.breakpoints.up('md')]: { - width: '20ch' - } - } -})); - -export default function SearchBar() { - const [df, setDf] = useRecoilState(datasetFiltersState); - - const handleChange = (value: string) => { - value = value.trim(); - const search = value === '' ? undefined : value; - setDf({ ...df, search }); - }; - - const _onChange = debounce(handleChange, 300); - const inputRef = useRef(); - - const clear = () => { - _onChange(''); - if (inputRef.current) { - inputRef.current.value = ''; - } - }; - - return ( - - - - - _onChange(e.target.value)} - /> - - - - - - - ); -} diff --git a/frontend/src/components/organisms/dataset/filters/index.tsx b/frontend/src/components/organisms/dataset/filters/index.tsx deleted file mode 100644 index ad93817e39..0000000000 --- a/frontend/src/components/organisms/dataset/filters/index.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Stack } from '@mui/material'; - -import FeedbackSelect from './FeedbackSelect'; -import SearchBar from './SearchBar'; - -export default function Filters() { - return ( - - - - - - - ); -} diff --git a/frontend/src/components/organisms/dataset/index.tsx b/frontend/src/components/organisms/dataset/index.tsx deleted file mode 100644 index 2eb87442cd..0000000000 --- a/frontend/src/components/organisms/dataset/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Box } from '@mui/material'; - -import Filters from './filters'; -import ConversationTable from './table'; - -export default function Conversation() { - return ( - - - - - - - ); -} diff --git a/frontend/src/components/organisms/dataset/openConversationButton.tsx b/frontend/src/components/organisms/dataset/openConversationButton.tsx deleted file mode 100644 index 0187097828..0000000000 --- a/frontend/src/components/organisms/dataset/openConversationButton.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Link } from 'react-router-dom'; - -import VisibilityIcon from '@mui/icons-material/Visibility'; -import { IconButton } from '@mui/material'; - -interface Props { - conversationId: number; -} - -export default function OpenConversationButton({ conversationId }: Props) { - return ( - - - - ); -} diff --git a/frontend/src/components/organisms/dataset/table.tsx b/frontend/src/components/organisms/dataset/table.tsx deleted file mode 100644 index 9fafb922ff..0000000000 --- a/frontend/src/components/organisms/dataset/table.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { ChainlitAPI } from 'api/chainlitApi'; -import { useCallback, useEffect, useState } from 'react'; -import AutoSizer from 'react-virtualized-auto-sizer'; -import { FixedSizeList } from 'react-window'; -import InfiniteLoader from 'react-window-infinite-loader'; -import { useRecoilValue } from 'recoil'; - -import { Alert, Box, Stack, Typography } from '@mui/material'; - -import { datasetFiltersState } from 'state/dataset'; -import { accessTokenState } from 'state/user'; - -import { IChat } from 'types/chat'; - -import DeleteConversationButton from './deleteConversationButton'; -import OpenConversationButton from './openConversationButton'; - -export interface IPageInfo { - hasNextPage: boolean; - endCursor?: string; -} - -export interface IPagination { - first: number; - cursor?: string | number; -} - -const BATCH_SIZE = 30; - -const serializeDate = (timestamp: number | string) => { - const dateOptions: Intl.DateTimeFormatOptions = { - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric' - }; - return new Date(timestamp).toLocaleDateString(undefined, dateOptions); -}; - -export default function ConversationTable() { - const df = useRecoilValue(datasetFiltersState); - const accessToken = useRecoilValue(accessTokenState); - - const [conversations, setConversations] = useState([]); - const [prevPageInfo, setPrevPageInfo] = useState(); - const [error, setError] = useState(undefined); - const [loading, setLoading] = useState(true); - - const fetchConversations = useCallback( - async (cursor?: string | number) => { - try { - const { pageInfo, data } = await ChainlitAPI.getConversations( - { first: BATCH_SIZE, cursor }, - df, - accessToken - ); - setPrevPageInfo(pageInfo); - setError(undefined); - setConversations((prev) => [...prev, ...data]); - } catch (error) { - if (error instanceof Error) setError(error.message); - } finally { - setLoading(false); - } - }, - [accessToken, df] - ); - - const refetchConversations = useCallback(async () => { - setConversations([]); - setLoading(true); - setPrevPageInfo(undefined); - fetchConversations(undefined); - }, [fetchConversations]); - - useEffect(() => { - refetchConversations(); - }, [refetchConversations]); - - const loadMoreItems = useCallback(() => { - if (prevPageInfo?.hasNextPage && prevPageInfo.endCursor) { - fetchConversations(prevPageInfo.endCursor); - } - }, [prevPageInfo, fetchConversations]); - - if (error) { - return {(error as any).message}; - } - if (loading) { - return Loading...; - } - - const itemCount = conversations.length; - - if (itemCount === 0) { - return No result; - } - - const columns = { - Id: { - minWidth: '60px', - width: '5%' - }, - Author: { - minWidth: '130px', - width: '25%' - }, - Input: { - minWidth: '200px', - width: '35%' - }, - Date: { - minWidth: '120px', - width: '25%' - }, - Actions: { - minWidth: '80px', - width: '10%' - } - }; - - const RowText = ({ text, col }: any) => { - return ( - - {text} - - ); - }; - - const Row = ({ index, style }: any) => { - const conversation = conversations[index]; - return ( - `1px solid ${theme.palette.divider}` - }} - > - - - - - - - refetchConversations()} - /> - - - ); - }; - - const Header = Object.entries(columns).map(([key, value]) => ( - - {key} - - )); - - return ( - - `1px solid ${theme.palette.divider}` - }} - > - {Header} - - - - {({ height, width }) => ( - !!conversations[index]} - itemCount={prevPageInfo?.hasNextPage ? itemCount + 1 : itemCount} - loadMoreItems={loadMoreItems} - > - {({ onItemsRendered, ref }) => ( - - {Row} - - )} - - )} - - - - ); -} diff --git a/frontend/src/components/organisms/header.tsx b/frontend/src/components/organisms/header.tsx index f252bca889..e35e542fa8 100644 --- a/frontend/src/components/organisms/header.tsx +++ b/frontend/src/components/organisms/header.tsx @@ -23,8 +23,6 @@ import UserButton from 'components/atoms/buttons/userButton'; import { Logo } from 'components/atoms/logo'; import NewChatButton from 'components/molecules/newChatButton'; -import { useAuth } from 'hooks/auth'; - import { projectSettingsState } from 'state/project'; interface INavItem { @@ -60,11 +58,10 @@ function NavItem({ to, label }: INavItem) { } interface NavProps { - hasDb?: boolean; hasReadme?: boolean; } -function Nav({ hasDb, hasReadme }: NavProps) { +function Nav({ hasReadme }: NavProps) { const location = useLocation(); const theme = useTheme(); const [open, setOpen] = useState(false); @@ -80,10 +77,6 @@ function Nav({ hasDb, hasReadme }: NavProps) { const tabs = [{ to: '/', label: 'Chat' }]; - if (hasDb) { - tabs.push({ to: '/dataset', label: 'History' }); - } - if (hasReadme) { tabs.push({ to: '/readme', label: 'Readme' }); } @@ -146,16 +139,13 @@ function Nav({ hasDb, hasReadme }: NavProps) { export default function Header() { const pSettings = useRecoilValue(projectSettingsState); - - const { user } = useAuth(); const matches = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')); - const hasHistory = !!(user && pSettings?.dataPersistence); - return ( `0 ${theme.spacing(2)} !important`, minHeight: '60px !important', borderBottomWidth: '1px', borderBottomStyle: 'solid', @@ -165,7 +155,7 @@ export default function Header() { > {!matches ? : null} -