diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2b868da2..e89a334c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1490,7 +1490,7 @@ "version": "18.0.10", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index ebb3f88a..22ab5017 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -1,46 +1,48 @@ import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { NavLink, useNavigate } from 'react-router-dom'; + +import conversationService from './api/services/conversationService'; +import userService from './api/services/userService'; +import Add from './assets/add.svg'; import DocsGPT3 from './assets/cute_docsgpt3.svg'; import Discord from './assets/discord.svg'; import Expand from './assets/expand.svg'; import Github from './assets/github.svg'; -import Hamburger from './assets/hamburger.svg'; import HamburgerDark from './assets/hamburger-dark.svg'; +import Hamburger from './assets/hamburger.svg'; import Info from './assets/info.svg'; import SettingGear from './assets/settingGear.svg'; import Twitter from './assets/TwitterX.svg'; -import Add from './assets/add.svg'; import UploadIcon from './assets/upload.svg'; -import { ActiveState } from './models/misc'; -import APIKeyModal from './preferences/APIKeyModal'; -import DeleteConvModal from './modals/DeleteConvModal'; - -import { - selectApiKeyStatus, - selectSelectedDocs, - selectSelectedDocsStatus, - selectSourceDocs, - setSelectedDocs, - selectConversations, - setConversations, - selectConversationId, - selectModalStateDeleteConv, - setModalStateDeleteConv, - setSourceDocs, -} from './preferences/preferenceSlice'; +import SourceDropdown from './components/SourceDropdown'; import { setConversation, updateConversationId, } from './conversation/conversationSlice'; -import { useMediaQuery, useOutsideAlerter } from './hooks'; -import Upload from './upload/Upload'; -import { Doc, getConversations, getDocs } from './preferences/preferenceApi'; -import SelectDocsModal from './preferences/SelectDocsModal'; import ConversationTile from './conversation/ConversationTile'; -import { useDarkTheme } from './hooks'; -import SourceDropdown from './components/SourceDropdown'; -import { useTranslation } from 'react-i18next'; +import { useDarkTheme, useMediaQuery, useOutsideAlerter } from './hooks'; +import DeleteConvModal from './modals/DeleteConvModal'; +import { ActiveState } from './models/misc'; +import APIKeyModal from './preferences/APIKeyModal'; +import { Doc, getConversations, getDocs } from './preferences/preferenceApi'; +import { + selectApiKeyStatus, + selectConversationId, + selectConversations, + selectModalStateDeleteConv, + selectSelectedDocs, + selectSelectedDocsStatus, + selectSourceDocs, + setConversations, + setModalStateDeleteConv, + setSelectedDocs, + setSourceDocs, +} from './preferences/preferenceSlice'; +import SelectDocsModal from './preferences/SelectDocsModal'; +import Upload from './upload/Upload'; + interface NavigationProps { navOpen: boolean; setNavOpen: React.Dispatch>; @@ -85,7 +87,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { useState('INACTIVE'); const navRef = useRef(null); - const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; const navigate = useNavigate(); @@ -106,9 +107,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { } const handleDeleteAllConversations = () => { - fetch(`${apiHost}/api/delete_all_conversations`, { - method: 'POST', - }) + conversationService + .deleteAll({}) .then(() => { fetchConversations(); }) @@ -116,9 +116,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { }; const handleDeleteConversation = (id: string) => { - fetch(`${apiHost}/api/delete_conversation?id=${id}`, { - method: 'POST', - }) + conversationService + .delete(id, {}) .then(() => { fetchConversations(); }) @@ -128,17 +127,9 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { const handleDeleteClick = (doc: Doc) => { const docPath = `indexes/local/${doc.name}`; - fetch(`${apiHost}/api/delete_old?path=${docPath}`, { - method: 'GET', - }) + userService + .deletePath(docPath) .then(() => { - // remove the image element from the DOM - // const imageElement = document.querySelector( - // `#img-${index}`, - // ) as HTMLElement; - // const parentElement = imageElement.parentNode as HTMLElement; - // parentElement.parentNode?.removeChild(parentElement); - return getDocs(); }) .then((updatedDocs) => { @@ -153,10 +144,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { }; const handleConversationClick = (index: string) => { - // fetch the conversation from the server and setConversation in the store - fetch(`${apiHost}/api/get_single_conversation?id=${index}`, { - method: 'GET', - }) + conversationService + .getConversation(index) .then((response) => response.json()) .then((data) => { navigate('/'); @@ -173,13 +162,8 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) { name: string; id: string; }) { - await fetch(`${apiHost}/api/update_conversation_name`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(updatedConversation), - }) + await conversationService + .update(updatedConversation) .then((response) => response.json()) .then((data) => { if (data) { diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 00000000..21699721 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,69 @@ +const baseURL = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +const apiClient = { + get: (url: string, headers = {}, signal?: AbortSignal): Promise => + fetch(`${baseURL}${url}`, { + method: 'GET', + headers: { + ...defaultHeaders, + ...headers, + }, + signal, + }).then((response) => { + return response; + }), + + post: ( + url: string, + data: any, + headers = {}, + signal?: AbortSignal, + ): Promise => + fetch(`${baseURL}${url}`, { + method: 'POST', + headers: { + ...defaultHeaders, + ...headers, + }, + body: JSON.stringify(data), + signal, + }).then((response) => { + return response; + }), + + put: ( + url: string, + data: any, + headers = {}, + signal?: AbortSignal, + ): Promise => + fetch(`${baseURL}${url}`, { + method: 'PUT', + headers: { + ...defaultHeaders, + ...headers, + }, + body: JSON.stringify(data), + signal, + }).then((response) => { + return response; + }), + + delete: (url: string, headers = {}, signal?: AbortSignal): Promise => + fetch(`${baseURL}${url}`, { + method: 'DELETE', + headers: { + ...defaultHeaders, + ...headers, + }, + signal, + }).then((response) => { + return response; + }), +}; + +export default apiClient; diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts new file mode 100644 index 00000000..af2fb920 --- /dev/null +++ b/frontend/src/api/endpoints.ts @@ -0,0 +1,33 @@ +const endpoints = { + USER: { + DOCS: '/api/combine', + DOCS_CHECK: '/api/docs_check', + API_KEYS: '/api/get_api_keys', + CREATE_API_KEY: '/api/create_api_key', + DELETE_API_KEY: '/api/delete_api_key', + PROMPTS: '/api/get_prompts', + CREATE_PROMPT: '/api/create_prompt', + DELETE_PROMPT: '/api/delete_prompt', + UPDATE_PROMPT: '/api/update_prompt', + SINGLE_PROMPT: (id: string) => `/api/get_single_prompt?id=${id}`, + DELETE_PATH: (docPath: string) => `/api/delete_old?path=${docPath}`, + TASK_STATUS: (task_id: string) => `/api/task_status?task_id=${task_id}`, + }, + CONVERSATION: { + ANSWER: '/api/answer', + ANSWER_STREAMING: '/stream', + SEARCH: '/api/search', + FEEDBACK: '/api/feedback', + CONVERSATION: (id: string) => `/api/get_single_conversation?id=${id}`, + CONVERSATIONS: '/api/get_conversations', + SHARE_CONVERSATION: (isPromptable: boolean) => + `/api/share?isPromptable=${isPromptable}`, + SHARED_CONVERSATION: (identifier: string) => + `/api/shared_conversation/${identifier}`, + DELETE: (id: string) => `/api/delete_conversation?id=${id}`, + DELETE_ALL: '/api/delete_all_conversations', + UPDATE: '/api/update_conversation_name', + }, +}; + +export default endpoints; diff --git a/frontend/src/api/services/conversationService.ts b/frontend/src/api/services/conversationService.ts new file mode 100644 index 00000000..9e31df84 --- /dev/null +++ b/frontend/src/api/services/conversationService.ts @@ -0,0 +1,32 @@ +import apiClient from '../client'; +import endpoints from '../endpoints'; + +const conversationService = { + answer: (data: any, signal: AbortSignal): Promise => + apiClient.post(endpoints.CONVERSATION.ANSWER, data, {}, signal), + answerStream: (data: any, signal: AbortSignal): Promise => + apiClient.post(endpoints.CONVERSATION.ANSWER_STREAMING, data, {}, signal), + search: (data: any): Promise => + apiClient.post(endpoints.CONVERSATION.SEARCH, data), + feedback: (data: any): Promise => + apiClient.post(endpoints.CONVERSATION.FEEDBACK, data), + getConversation: (id: string): Promise => + apiClient.get(endpoints.CONVERSATION.CONVERSATION(id)), + getConversations: (): Promise => + apiClient.get(endpoints.CONVERSATION.CONVERSATIONS), + shareConversation: (isPromptable: boolean, data: any): Promise => + apiClient.post( + endpoints.CONVERSATION.SHARE_CONVERSATION(isPromptable), + data, + ), + getSharedConversation: (identifier: string): Promise => + apiClient.get(endpoints.CONVERSATION.SHARED_CONVERSATION(identifier)), + delete: (id: string, data: any): Promise => + apiClient.post(endpoints.CONVERSATION.DELETE(id), data), + deleteAll: (data: any): Promise => + apiClient.post(endpoints.CONVERSATION.DELETE_ALL, data), + update: (data: any): Promise => + apiClient.post(endpoints.CONVERSATION.UPDATE, data), +}; + +export default conversationService; diff --git a/frontend/src/api/services/userService.ts b/frontend/src/api/services/userService.ts new file mode 100644 index 00000000..193fe6ad --- /dev/null +++ b/frontend/src/api/services/userService.ts @@ -0,0 +1,28 @@ +import apiClient from '../client'; +import endpoints from '../endpoints'; + +const userService = { + getDocs: (): Promise => apiClient.get(endpoints.USER.DOCS), + checkDocs: (data: any): Promise => + apiClient.post(endpoints.USER.DOCS_CHECK, data), + getAPIKeys: (): Promise => apiClient.get(endpoints.USER.API_KEYS), + createAPIKey: (data: any): Promise => + apiClient.post(endpoints.USER.CREATE_API_KEY, data), + deleteAPIKey: (data: any): Promise => + apiClient.post(endpoints.USER.DELETE_API_KEY, data), + getPrompts: (): Promise => apiClient.get(endpoints.USER.PROMPTS), + createPrompt: (data: any): Promise => + apiClient.post(endpoints.USER.CREATE_PROMPT, data), + deletePrompt: (data: any): Promise => + apiClient.post(endpoints.USER.DELETE_PROMPT, data), + updatePrompt: (data: any): Promise => + apiClient.post(endpoints.USER.UPDATE_PROMPT, data), + getSinglePrompt: (id: string): Promise => + apiClient.get(endpoints.USER.SINGLE_PROMPT(id)), + deletePath: (docPath: string): Promise => + apiClient.get(endpoints.USER.DELETE_PATH(docPath)), + getTaskStatus: (task_id: string): Promise => + apiClient.get(endpoints.USER.TASK_STATUS(task_id)), +}; + +export default userService; diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index dddd945e..4ceddbff 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -1,9 +1,22 @@ import { Fragment, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import { useDarkTheme } from '../hooks'; + +import ArrowDown from '../assets/arrow-down.svg'; +import Send from '../assets/send.svg'; +import SendDark from '../assets/send_dark.svg'; +import ShareIcon from '../assets/share.svg'; +import SpinnerDark from '../assets/spinner-dark.svg'; +import Spinner from '../assets/spinner.svg'; +import RetryIcon from '../components/RetryIcon'; import Hero from '../Hero'; +import { useDarkTheme } from '../hooks'; +import { ShareConversationModal } from '../modals/ShareConversationModal'; +import { selectConversationId } from '../preferences/preferenceSlice'; import { AppDispatch } from '../store'; import ConversationBubble from './ConversationBubble'; +import { handleSendFeedback } from './conversationHandlers'; +import { FEEDBACK, Query } from './conversationModels'; import { addQuery, fetchAnswer, @@ -11,18 +24,6 @@ import { selectStatus, updateQuery, } from './conversationSlice'; -import { selectConversationId } from '../preferences/preferenceSlice'; -import Send from './../assets/send.svg'; -import SendDark from './../assets/send_dark.svg'; -import Spinner from './../assets/spinner.svg'; -import SpinnerDark from './../assets/spinner-dark.svg'; -import { FEEDBACK, Query } from './conversationModels'; -import { sendFeedback } from './conversationApi'; -import { useTranslation } from 'react-i18next'; -import ArrowDown from './../assets/arrow-down.svg'; -import RetryIcon from '../components/RetryIcon'; -import ShareIcon from '../assets/share.svg'; -import { ShareConversationModal } from '../modals/ShareConversationModal'; export default function Conversation() { const queries = useSelector(selectQueries); @@ -112,7 +113,7 @@ export default function Conversation() { const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => { const prevFeedback = query.feedback; dispatch(updateQuery({ index, query: { feedback } })); - sendFeedback(query.prompt, query.response!, feedback).catch(() => + handleSendFeedback(query.prompt, query.response!, feedback).catch(() => dispatch(updateQuery({ index, query: { feedback: prevFeedback } })), ); }; diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index dea1258f..bd484c4b 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -1,8 +1,9 @@ -import { useEffect, useRef } from 'react'; -import { useParams } from 'react-router-dom'; -import { useNavigate } from 'react-router-dom'; import { Query } from './conversationModels'; +import { Fragment, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; +import { useNavigate, useParams } from 'react-router-dom'; + +import conversationService from '../api/services/conversationService'; import ConversationBubble from './ConversationBubble'; import Send from '../assets/send.svg'; import Spinner from '../assets/spinner.svg'; @@ -15,7 +16,6 @@ import { selectQueries, } from './sharedConversationSlice'; import { useSelector } from 'react-redux'; -import { Fragment } from 'react'; const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; const SharedConversation = () => { const params = useParams(); @@ -64,7 +64,8 @@ const SharedConversation = () => { } const fetchQueris = () => { identifier && - fetch(`${apiHost}/api/shared_conversation/${identifier}`) + conversationService + .getSharedConversation(identifier || '') .then((res) => { if (res.status === 404 || res.status === 400) navigate('/pagenotfound'); @@ -200,5 +201,3 @@ const SharedConversation = () => { ); }; - -export default SharedConversation; diff --git a/frontend/src/conversation/conversationApi.ts b/frontend/src/conversation/conversationHandlers.ts similarity index 79% rename from frontend/src/conversation/conversationApi.ts rename to frontend/src/conversation/conversationHandlers.ts index 3010a63d..05c2db0d 100644 --- a/frontend/src/conversation/conversationApi.ts +++ b/frontend/src/conversation/conversationHandlers.ts @@ -1,11 +1,9 @@ -import { Answer, FEEDBACK } from './conversationModels'; +import conversationService from '../api/services/conversationService'; import { Doc } from '../preferences/preferenceApi'; - -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; +import { Answer, FEEDBACK } from './conversationModels'; function getDocPath(selectedDocs: Doc | null): string { let docPath = 'default'; - if (selectedDocs) { let namePath = selectedDocs.name; if (selectedDocs.language === namePath) { @@ -27,10 +25,10 @@ function getDocPath(selectedDocs: Doc | null): string { docPath = selectedDocs.docLink; } } - return docPath; } -export function fetchAnswerApi( + +export function handleFetchAnswer( question: string, signal: AbortSignal, selectedDocs: Doc | null, @@ -57,27 +55,22 @@ export function fetchAnswerApi( } > { const docPath = getDocPath(selectedDocs); - //in history array remove all keys except prompt and response history = history.map((item) => { return { prompt: item.prompt, response: item.response }; }); - - return fetch(apiHost + '/api/answer', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - question: question, - history: history, - active_docs: docPath, - conversation_id: conversationId, - prompt_id: promptId, - chunks: chunks, - token_limit: token_limit, - }), - signal, - }) + return conversationService + .answer( + { + question: question, + history: history, + active_docs: docPath, + conversation_id: conversationId, + prompt_id: promptId, + chunks: chunks, + token_limit: token_limit, + }, + signal, + ) .then((response) => { if (response.ok) { return response.json(); @@ -97,7 +90,7 @@ export function fetchAnswerApi( }); } -export function fetchAnswerSteaming( +export function handleFetchAnswerSteaming( question: string, signal: AbortSignal, selectedDocs: Doc | null, @@ -109,29 +102,23 @@ export function fetchAnswerSteaming( onEvent: (event: MessageEvent) => void, ): Promise { const docPath = getDocPath(selectedDocs); - history = history.map((item) => { return { prompt: item.prompt, response: item.response }; }); - return new Promise((resolve, reject) => { - const body = { - question: question, - active_docs: docPath, - history: JSON.stringify(history), - conversation_id: conversationId, - prompt_id: promptId, - chunks: chunks, - token_limit: token_limit, - }; - fetch(apiHost + '/stream', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - signal, - }) + conversationService + .answerStream( + { + question: question, + active_docs: docPath, + history: JSON.stringify(history), + conversation_id: conversationId, + prompt_id: promptId, + chunks: chunks, + token_limit: token_limit, + }, + signal, + ) .then((response) => { if (!response.body) throw Error('No response body'); @@ -179,7 +166,8 @@ export function fetchAnswerSteaming( }); }); } -export function searchEndpoint( + +export function handleSearch( question: string, selectedDocs: Doc | null, conversation_id: string | null, @@ -188,50 +176,40 @@ export function searchEndpoint( token_limit: number, ) { const docPath = getDocPath(selectedDocs); - - const body = { - question: question, - active_docs: docPath, - conversation_id, - history, - chunks: chunks, - token_limit: token_limit, - }; - return fetch(`${apiHost}/api/search`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }) + return conversationService + .search({ + question: question, + active_docs: docPath, + conversation_id, + history, + chunks: chunks, + token_limit: token_limit, + }) .then((response) => response.json()) .then((data) => { return data; }) .catch((err) => console.log(err)); } -export function sendFeedback( + +export function handleSendFeedback( prompt: string, response: string, feedback: FEEDBACK, ) { - return fetch(`${apiHost}/api/feedback`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + return conversationService + .feedback({ question: prompt, answer: response, feedback: feedback, - }), - }).then((response) => { - if (response.ok) { - return Promise.resolve(); - } else { - return Promise.reject(); - } - }); + }) + .then((response) => { + if (response.ok) { + return Promise.resolve(); + } else { + return Promise.reject(); + } + }); } export function fetchSharedAnswerSteaming( //for shared conversations diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts index 7ab9f8fe..75c457a9 100644 --- a/frontend/src/conversation/conversationSlice.ts +++ b/frontend/src/conversation/conversationSlice.ts @@ -1,10 +1,14 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import store from '../store'; -import { fetchAnswerApi, fetchAnswerSteaming } from './conversationApi'; -import { searchEndpoint } from './conversationApi'; -import { Answer, ConversationState, Query, Status } from './conversationModels'; + import { getConversations } from '../preferences/preferenceApi'; import { setConversations } from '../preferences/preferenceSlice'; +import store from '../store'; +import { + handleFetchAnswer, + handleFetchAnswerSteaming, + handleSearch, +} from './conversationHandlers'; +import { Answer, ConversationState, Query, Status } from './conversationModels'; const initialState: ConversationState = { queries: [], @@ -20,7 +24,7 @@ export const fetchAnswer = createAsyncThunk( const state = getState() as RootState; if (state.preference) { if (API_STREAMING) { - await fetchAnswerSteaming( + await handleFetchAnswerSteaming( question, signal, state.preference.selectedDocs!, @@ -45,7 +49,7 @@ export const fetchAnswer = createAsyncThunk( console.error('Failed to fetch conversations: ', error); }); - searchEndpoint( + handleSearch( //search for sources post streaming question, state.preference.selectedDocs!, @@ -89,7 +93,7 @@ export const fetchAnswer = createAsyncThunk( }, ); } else { - const answer = await fetchAnswerApi( + const answer = await handleFetchAnswer( question, signal, state.preference.selectedDocs!, diff --git a/frontend/src/modals/CreateAPIKeyModal.tsx b/frontend/src/modals/CreateAPIKeyModal.tsx new file mode 100644 index 00000000..2f67d83b --- /dev/null +++ b/frontend/src/modals/CreateAPIKeyModal.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; + +import userService from '../api/services/userService'; +import Exit from '../assets/exit.svg'; +import Dropdown from '../components/Dropdown'; +import Input from '../components/Input'; +import { CreateAPIKeyModalProps, Doc } from '../models/misc'; +import { selectSourceDocs } from '../preferences/preferenceSlice'; + +const embeddingsName = + import.meta.env.VITE_EMBEDDINGS_NAME || + 'huggingface_sentence-transformers/all-mpnet-base-v2'; + +export default function CreateAPIKeyModal({ + close, + createAPIKey, +}: CreateAPIKeyModalProps) { + const { t } = useTranslation(); + const docs = useSelector(selectSourceDocs); + + const [APIKeyName, setAPIKeyName] = React.useState(''); + const [sourcePath, setSourcePath] = React.useState<{ + label: string; + value: string; + } | null>(null); + const [prompt, setPrompt] = React.useState<{ + name: string; + id: string; + type: string; + } | null>(null); + const [activePrompts, setActivePrompts] = React.useState< + { name: string; id: string; type: string }[] + >([]); + const [chunk, setChunk] = React.useState('2'); + const chunkOptions = ['0', '2', '4', '6', '8', '10']; + + const extractDocPaths = () => + docs + ? docs + .filter((doc) => doc.model === embeddingsName) + .map((doc: Doc) => { + let namePath = doc.name; + if (doc.language === namePath) { + namePath = '.project'; + } + let docPath = 'default'; + if (doc.location === 'local') { + docPath = 'local' + '/' + doc.name + '/'; + } else if (doc.location === 'remote') { + docPath = + doc.language + + '/' + + namePath + + '/' + + doc.version + + '/' + + doc.model + + '/'; + } + return { + label: doc.name, + value: docPath, + }; + }) + : []; + + React.useEffect(() => { + const handleFetchPrompts = async () => { + try { + const response = await userService.getPrompts(); + if (!response.ok) { + throw new Error('Failed to fetch prompts'); + } + const promptsData = await response.json(); + setActivePrompts(promptsData); + } catch (error) { + console.error(error); + } + }; + handleFetchPrompts(); + }, []); + return ( +
+
+ +
+ + {t('modals.createAPIKey.label')} + +
+
+ + {t('modals.createAPIKey.apiKeyName')} + + setAPIKeyName(e.target.value)} + > +
+
+ + setSourcePath(selection) + } + options={extractDocPaths()} + size="w-full" + rounded="xl" + border="border" + /> +
+
+ + setPrompt(value) + } + size="w-full" + border="border" + /> +
+
+

+ {t('modals.createAPIKey.chunks')} +

+ setChunk(value)} + size="w-full" + border="border" + /> +
+ +
+
+ ); +} diff --git a/frontend/src/modals/SaveAPIKeyModal.tsx b/frontend/src/modals/SaveAPIKeyModal.tsx new file mode 100644 index 00000000..d91d0c2d --- /dev/null +++ b/frontend/src/modals/SaveAPIKeyModal.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import Exit from '../assets/exit.svg'; +import { SaveAPIKeyModalProps } from '../models/misc'; + +export default function SaveAPIKeyModal({ + apiKey, + close, +}: SaveAPIKeyModalProps) { + const { t } = useTranslation(); + const [isCopied, setIsCopied] = React.useState(false); + + const handleCopyKey = () => { + navigator.clipboard.writeText(apiKey); + setIsCopied(true); + }; + return ( +
+
+ +

+ {' '} + {t('modals.saveKey.note')} +

+

+ {t('modals.saveKey.disclaimer')} +

+
+
+

API Key

+ {apiKey} +
+ +
+ +
+
+ ); +} diff --git a/frontend/src/modals/ShareConversationModal.tsx b/frontend/src/modals/ShareConversationModal.tsx index cffe477b..0015402e 100644 --- a/frontend/src/modals/ShareConversationModal.tsx +++ b/frontend/src/modals/ShareConversationModal.tsx @@ -18,6 +18,8 @@ const embeddingsName = type StatusType = 'loading' | 'idle' | 'fetched' | 'failed'; +import conversationService from '../api/services/conversationService'; + export const ShareConversationModal = ({ close, conversationId, @@ -100,13 +102,8 @@ export const ShareConversationModal = ({ payload.prompt_id = selectedPrompt.id; sourcePath && (payload.source = sourcePath.value); } - fetch(`${apiHost}/api/share?isPromptable=${isPromptable}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }) + conversationService + .shareConversation(isPromptable, payload) .then((res) => { console.log(res.status); return res.json(); diff --git a/frontend/src/models/misc.ts b/frontend/src/models/misc.ts index 52787932..ab8d6b85 100644 --- a/frontend/src/models/misc.ts +++ b/frontend/src/models/misc.ts @@ -21,7 +21,6 @@ export type PromptProps = { selectedPrompt: { name: string; id: string; type: string }; onSelectPrompt: (name: string, id: string, type: string) => void; setPrompts: (prompts: { name: string; id: string; type: string }[]) => void; - apiHost: string; }; export type DocumentsProps = { diff --git a/frontend/src/preferences/preferenceApi.ts b/frontend/src/preferences/preferenceApi.ts index 81fd3131..29a41645 100644 --- a/frontend/src/preferences/preferenceApi.ts +++ b/frontend/src/preferences/preferenceApi.ts @@ -1,3 +1,6 @@ +import conversationService from '../api/services/conversationService'; +import userService from '../api/services/userService'; + // not all properties in Doc are going to be present. Make some optional export type Doc = { location: string; @@ -14,10 +17,7 @@ export type Doc = { //Fetches all JSON objects from the source. We only use the objects with the "model" property in SelectDocsModal.tsx. Hopefully can clean up the source file later. export async function getDocs(): Promise { try { - const apiHost = - import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; - - const response = await fetch(apiHost + '/api/combine'); + const response = await userService.getDocs(); const data = await response.json(); const docs: Doc[] = []; @@ -37,10 +37,7 @@ export async function getConversations(): Promise< { name: string; id: string }[] | null > { try { - const apiHost = - import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; - - const response = await fetch(apiHost + '/api/get_conversations'); + const response = await conversationService.getConversations(); const data = await response.json(); const conversations: { name: string; id: string }[] = []; @@ -93,14 +90,9 @@ export function setLocalRecentDocs(doc: Doc): void { docPath = doc.language + '/' + namePath + '/' + doc.version + '/' + doc.model + '/'; } - const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; - fetch(apiHost + '/api/docs_check', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + userService + .checkDocs({ docs: docPath, - }), - }).then((response) => response.json()); + }) + .then((response) => response.json()); } diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx index d87e0abe..18904d24 100644 --- a/frontend/src/settings/APIKeys.tsx +++ b/frontend/src/settings/APIKeys.tsx @@ -1,22 +1,12 @@ import React from 'react'; -import { useSelector } from 'react-redux'; -import Dropdown from '../components/Dropdown'; -import { - Doc, - CreateAPIKeyModalProps, - SaveAPIKeyModalProps, -} from '../models/misc'; -import { selectSourceDocs } from '../preferences/preferenceSlice'; -import Exit from '../assets/exit.svg'; -import Trash from '../assets/trash.svg'; import { useTranslation } from 'react-i18next'; -import Input from '../components/Input'; -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; -const embeddingsName = - import.meta.env.VITE_EMBEDDINGS_NAME || - 'huggingface_sentence-transformers/all-mpnet-base-v2'; -const APIKeys: React.FC = () => { +import userService from '../api/services/userService'; +import Trash from '../assets/trash.svg'; +import CreateAPIKeyModal from '../modals/CreateAPIKeyModal'; +import SaveAPIKeyModal from '../modals/SaveAPIKeyModal'; + +export default function APIKeys() { const { t } = useTranslation(); const [isCreateModalOpen, setCreateModal] = React.useState(false); const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false); @@ -24,14 +14,23 @@ const APIKeys: React.FC = () => { const [apiKeys, setApiKeys] = React.useState< { name: string; key: string; source: string; id: string }[] >([]); + + const handleFetchKeys = async () => { + try { + const response = await userService.getAPIKeys(); + if (!response.ok) { + throw new Error('Failed to fetch API Keys'); + } + const apiKeys = await response.json(); + setApiKeys(apiKeys); + } catch (error) { + console.log(error); + } + }; + const handleDeleteKey = (id: string) => { - fetch(`${apiHost}/api/delete_api_key`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id }), - }) + userService + .deleteAPIKey({ id }) .then((response) => { if (!response.ok) { throw new Error('Failed to delete API Key'); @@ -46,34 +45,15 @@ const APIKeys: React.FC = () => { console.error(error); }); }; - React.useEffect(() => { - fetchAPIKeys(); - }, []); - const fetchAPIKeys = async () => { - try { - const response = await fetch(`${apiHost}/api/get_api_keys`); - if (!response.ok) { - throw new Error('Failed to fetch API Keys'); - } - const apiKeys = await response.json(); - setApiKeys(apiKeys); - } catch (error) { - console.log(error); - } - }; - const createAPIKey = (payload: { + + const handleCreateKey = (payload: { name: string; source: string; prompt_id: string; chunks: string; }) => { - fetch(`${apiHost}/api/create_api_key`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }) + userService + .createAPIKey(payload) .then((response) => { if (!response.ok) { throw new Error('Failed to create API Key'); @@ -85,12 +65,16 @@ const APIKeys: React.FC = () => { setCreateModal(false); setNewKey(data.key); setSaveKeyModal(true); - fetchAPIKeys(); + handleFetchKeys(); }) .catch((error) => { console.error(error); }); }; + + React.useEffect(() => { + handleFetchKeys(); + }, []); return (
@@ -104,8 +88,8 @@ const APIKeys: React.FC = () => {
{isCreateModalOpen && ( setCreateModal(false)} - createAPIKey={createAPIKey} /> )} {isSaveKeyModalOpen && ( @@ -155,192 +139,4 @@ const APIKeys: React.FC = () => {
); -}; - -const CreateAPIKeyModal: React.FC = ({ - close, - createAPIKey, -}) => { - const [APIKeyName, setAPIKeyName] = React.useState(''); - const [sourcePath, setSourcePath] = React.useState<{ - label: string; - value: string; - } | null>(null); - - const chunkOptions = ['0', '2', '4', '6', '8', '10']; - const [chunk, setChunk] = React.useState('2'); - const [activePrompts, setActivePrompts] = React.useState< - { name: string; id: string; type: string }[] - >([]); - const [prompt, setPrompt] = React.useState<{ - name: string; - id: string; - type: string; - } | null>(null); - const docs = useSelector(selectSourceDocs); - React.useEffect(() => { - const fetchPrompts = async () => { - try { - const response = await fetch(`${apiHost}/api/get_prompts`); - if (!response.ok) { - throw new Error('Failed to fetch prompts'); - } - const promptsData = await response.json(); - setActivePrompts(promptsData); - } catch (error) { - console.error(error); - } - }; - fetchPrompts(); - }, []); - const extractDocPaths = () => - docs - ? docs - .filter((doc) => doc.model === embeddingsName) - .map((doc: Doc) => { - let namePath = doc.name; - if (doc.language === namePath) { - namePath = '.project'; - } - let docPath = 'default'; - if (doc.location === 'local') { - docPath = 'local' + '/' + doc.name + '/'; - } else if (doc.location === 'remote') { - docPath = - doc.language + - '/' + - namePath + - '/' + - doc.version + - '/' + - doc.model + - '/'; - } - return { - label: doc.name, - value: docPath, - }; - }) - : []; - const { t } = useTranslation(); - return ( -
-
- -
- - {t('modals.createAPIKey.label')} - -
-
- - {t('modals.createAPIKey.apiKeyName')} - - setAPIKeyName(e.target.value)} - > -
-
- - setSourcePath(selection) - } - options={extractDocPaths()} - size="w-full" - rounded="xl" - /> -
-
- - setPrompt(value) - } - size="w-full" - /> -
-
-

- {t('modals.createAPIKey.chunks')} -

- setChunk(value)} - size="w-full" - /> -
- -
-
- ); -}; - -const SaveAPIKeyModal: React.FC = ({ apiKey, close }) => { - const [isCopied, setIsCopied] = React.useState(false); - const { t } = useTranslation(); - const handleCopyKey = () => { - navigator.clipboard.writeText(apiKey); - setIsCopied(true); - }; - return ( -
-
- -

- {' '} - {t('modals.saveKey.note')} -

-

- {t('modals.saveKey.disclaimer')} -

-
-
-

API Key

- {apiKey} -
- -
- -
-
- ); -}; - -export default APIKeys; +} diff --git a/frontend/src/settings/General.tsx b/frontend/src/settings/General.tsx index bf917d91..2d0c466d 100644 --- a/frontend/src/settings/General.tsx +++ b/frontend/src/settings/General.tsx @@ -1,22 +1,22 @@ import React from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import Prompts from './Prompts'; -import { useDarkTheme } from '../hooks'; import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; + +import userService from '../api/services/userService'; import Dropdown from '../components/Dropdown'; +import { useDarkTheme } from '../hooks'; import { - selectPrompt, - setPrompt, - setChunks, selectChunks, - setTokenLimit, + selectPrompt, selectTokenLimit, + setChunks, setModalStateDeleteConv, + setPrompt, + setTokenLimit, } from '../preferences/preferenceSlice'; +import Prompts from './Prompts'; -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; - -const General: React.FC = () => { +export default function General() { const { t, i18n: { changeLanguage, language }, @@ -69,9 +69,9 @@ const General: React.FC = () => { const selectedPrompt = useSelector(selectPrompt); React.useEffect(() => { - const fetchPrompts = async () => { + const handleFetchPrompts = async () => { try { - const response = await fetch(`${apiHost}/api/get_prompts`); + const response = await userService.getPrompts(); if (!response.ok) { throw new Error('Failed to fetch prompts'); } @@ -81,14 +81,13 @@ const General: React.FC = () => { console.error(error); } }; - fetchPrompts(); + handleFetchPrompts(); }, []); React.useEffect(() => { localStorage.setItem('docsgpt-locale', selectedLanguage?.value as string); changeLanguage(selectedLanguage?.value); }, [selectedLanguage, changeLanguage]); - return (
@@ -171,7 +170,6 @@ const General: React.FC = () => { dispatch(setPrompt({ name: name, id: id, type: type })) } setPrompts={setPrompts} - apiHost={apiHost} />
@@ -189,6 +187,4 @@ const General: React.FC = () => {
); -}; - -export default General; +} diff --git a/frontend/src/settings/Prompts.tsx b/frontend/src/settings/Prompts.tsx index 2bae07eb..3d041555 100644 --- a/frontend/src/settings/Prompts.tsx +++ b/frontend/src/settings/Prompts.tsx @@ -1,15 +1,17 @@ import React from 'react'; -import { PromptProps, ActiveState } from '../models/misc'; -import Dropdown from '../components/Dropdown'; -import PromptsModal from '../preferences/PromptsModal'; import { useTranslation } from 'react-i18next'; -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; -const Prompts: React.FC = ({ + +import userService from '../api/services/userService'; +import Dropdown from '../components/Dropdown'; +import { ActiveState, PromptProps } from '../models/misc'; +import PromptsModal from '../preferences/PromptsModal'; + +export default function Prompts({ prompts, selectedPrompt, onSelectPrompt, setPrompts, -}) => { +}: PromptProps) { const handleSelectPrompt = ({ name, id, @@ -37,17 +39,12 @@ const Prompts: React.FC = ({ t, i18n: { changeLanguage, language }, } = useTranslation(); + const handleAddPrompt = async () => { try { - const response = await fetch(`${apiHost}/api/create_prompt`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: newPromptName, - content: newPromptContent, - }), + const response = await userService.createPrompt({ + name: newPromptName, + content: newPromptContent, }); if (!response.ok) { throw new Error('Failed to add prompt'); @@ -69,18 +66,12 @@ const Prompts: React.FC = ({ const handleDeletePrompt = (id: string) => { setPrompts(prompts.filter((prompt) => prompt.id !== id)); - fetch(`${apiHost}/api/delete_prompt`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id: id }), - }) + userService + .deletePrompt({ id }) .then((response) => { if (!response.ok) { throw new Error('Failed to delete prompt'); } - // get 1st prompt and set it as selected if (prompts.length > 0) { onSelectPrompt(prompts[0].name, prompts[0].id, prompts[0].type); } @@ -90,18 +81,9 @@ const Prompts: React.FC = ({ }); }; - const fetchPromptContent = async (id: string) => { - console.log('fetching prompt content'); + const handleFetchPromptContent = async (id: string) => { try { - const response = await fetch( - `${apiHost}/api/get_single_prompt?id=${id}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }, - ); + const response = await userService.getSinglePrompt(id); if (!response.ok) { throw new Error('Failed to fetch prompt content'); } @@ -113,17 +95,12 @@ const Prompts: React.FC = ({ }; const handleSaveChanges = (id: string, type: string) => { - fetch(`${apiHost}/api/update_prompt`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + userService + .updatePrompt({ id: id, name: editPromptName, content: editPromptContent, - }), - }) + }) .then((response) => { if (!response.ok) { throw new Error('Failed to update prompt'); @@ -154,7 +131,6 @@ const Prompts: React.FC = ({ console.error(error); }); }; - return ( <>
@@ -183,7 +159,7 @@ const Prompts: React.FC = ({ }) => { setModalType('EDIT'); setEditPromptName(name); - fetchPromptContent(id); + handleFetchPromptContent(id); setCurrentPromptEdit({ id: id, name: name, type: type }); setModalState('ACTIVE'); }} @@ -219,6 +195,4 @@ const Prompts: React.FC = ({ /> ); -}; - -export default Prompts; +} diff --git a/frontend/src/settings/index.tsx b/frontend/src/settings/index.tsx index 3969fce6..226ebb3b 100644 --- a/frontend/src/settings/index.tsx +++ b/frontend/src/settings/index.tsx @@ -1,22 +1,22 @@ import React from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import General from './General'; -import Documents from './Documents'; -import APIKeys from './APIKeys'; -import Widgets from './Widgets'; +import { useTranslation } from 'react-i18next'; +import { useDispatch, useSelector } from 'react-redux'; + +import userService from '../api/services/userService'; +import ArrowLeft from '../assets/arrow-left.svg'; +import ArrowRight from '../assets/arrow-right.svg'; +import i18n from '../locale/i18n'; +import { Doc } from '../preferences/preferenceApi'; import { selectSourceDocs, setSourceDocs, } from '../preferences/preferenceSlice'; -import { Doc } from '../preferences/preferenceApi'; -import ArrowLeft from '../assets/arrow-left.svg'; -import ArrowRight from '../assets/arrow-right.svg'; -import { useTranslation } from 'react-i18next'; -import i18n from '../locale/i18n'; +import APIKeys from './APIKeys'; +import Documents from './Documents'; +import General from './General'; +import Widgets from './Widgets'; -const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; - -const Settings: React.FC = () => { +export default function Settings() { const dispatch = useDispatch(); const { t } = useTranslation(); const tabs = [ @@ -33,11 +33,11 @@ const Settings: React.FC = () => { const updateWidgetScreenshot = (screenshot: File | null) => { setWidgetScreenshot(screenshot); }; + const handleDeleteClick = (index: number, doc: Doc) => { const docPath = 'indexes/' + 'local' + '/' + doc.name; - fetch(`${apiHost}/api/delete_old?path=${docPath}`, { - method: 'GET', - }) + userService + .deletePath(docPath) .then((response) => { if (response.ok && documents) { const updatedDocuments = [ @@ -50,7 +50,6 @@ const Settings: React.FC = () => { .catch((error) => console.error(error)); }; - // persist active tab as the translated version of 'general' per language change React.useEffect(() => { setActiveTab(t('settings.general.label')); }, [i18n.language]); @@ -134,6 +133,4 @@ const Settings: React.FC = () => { return null; } } -}; - -export default Settings; +} diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx index a8e1929d..3bb3e7ae 100644 --- a/frontend/src/upload/Upload.tsx +++ b/frontend/src/upload/Upload.tsx @@ -1,13 +1,14 @@ -import React, { useRef } from 'react'; -import { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDropzone } from 'react-dropzone'; +import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; + +import userService from '../api/services/userService'; +import Dropdown from '../components/Dropdown'; +import Input from '../components/Input'; import { ActiveState } from '../models/misc'; import { getDocs } from '../preferences/preferenceApi'; import { setSelectedDocs, setSourceDocs } from '../preferences/preferenceSlice'; -import Dropdown from '../components/Dropdown'; -import { useTranslation } from 'react-i18next'; -import Input from '../components/Input'; function Upload({ modalState, @@ -95,20 +96,6 @@ function Upload({ {/* progress bar */} - -
); } @@ -125,8 +112,8 @@ function Upload({ if ((progress?.percentage ?? 0) < 100) { timeoutID = setTimeout(() => { - const apiHost = import.meta.env.VITE_API_HOST; - fetch(`${apiHost}/api/task_status?task_id=${progress?.taskId}`) + userService + .getTaskStatus(progress?.taskId as string) .then((data) => data.json()) .then((data) => { if (data.status == 'SUCCESS') { @@ -164,6 +151,10 @@ function Upload({ failed: false, }, ); + setDocName(''); + setfiles([]); + setProgress(undefined); + setModalState('INACTIVE'); } } else if (data.status == 'PROGRESS') { setProgress(