This commit is contained in:
ManishMadan2882
2025-09-16 14:59:18 +05:30
42 changed files with 2463 additions and 414 deletions

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64" color="#000000" fill="none">
<path d="M3.49994 11.7501L11.6717 3.57855C12.7762 2.47398 14.5672 2.47398 15.6717 3.57855C16.7762 4.68312 16.7762 6.47398 15.6717 7.57855M15.6717 7.57855L9.49994 13.7501M15.6717 7.57855C16.7762 6.47398 18.5672 6.47398 19.6717 7.57855C20.7762 8.68312 20.7762 10.474 19.6717 11.5785L12.7072 18.543C12.3167 18.9335 12.3167 19.5667 12.7072 19.9572L13.9999 21.2499" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M17.4999 9.74921L11.3282 15.921C10.2237 17.0255 8.43272 17.0255 7.32823 15.921C6.22373 14.8164 6.22373 13.0255 7.32823 11.921L13.4999 5.74939" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@@ -45,6 +45,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
description: '',
image: '',
source: '',
sources: [],
chunks: '',
retriever: '',
prompt_id: 'default',
@@ -150,7 +151,41 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
const formData = new FormData();
formData.append('name', agent.name);
formData.append('description', agent.description);
formData.append('source', agent.source);
if (selectedSourceIds.size > 1) {
const sourcesArray = Array.from(selectedSourceIds)
.map((id) => {
const sourceDoc = sourceDocs?.find(
(source) =>
source.id === id || source.retriever === id || source.name === id,
);
if (sourceDoc?.name === 'Default' && !sourceDoc?.id) {
return 'default';
}
return sourceDoc?.id || id;
})
.filter(Boolean);
formData.append('sources', JSON.stringify(sourcesArray));
formData.append('source', '');
} else if (selectedSourceIds.size === 1) {
const singleSourceId = Array.from(selectedSourceIds)[0];
const sourceDoc = sourceDocs?.find(
(source) =>
source.id === singleSourceId ||
source.retriever === singleSourceId ||
source.name === singleSourceId,
);
let finalSourceId;
if (sourceDoc?.name === 'Default' && !sourceDoc?.id)
finalSourceId = 'default';
else finalSourceId = sourceDoc?.id || singleSourceId;
formData.append('source', String(finalSourceId));
formData.append('sources', JSON.stringify([]));
} else {
formData.append('source', '');
formData.append('sources', JSON.stringify([]));
}
formData.append('chunks', agent.chunks);
formData.append('retriever', agent.retriever);
formData.append('prompt_id', agent.prompt_id);
@@ -196,7 +231,41 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
const formData = new FormData();
formData.append('name', agent.name);
formData.append('description', agent.description);
formData.append('source', agent.source);
if (selectedSourceIds.size > 1) {
const sourcesArray = Array.from(selectedSourceIds)
.map((id) => {
const sourceDoc = sourceDocs?.find(
(source) =>
source.id === id || source.retriever === id || source.name === id,
);
if (sourceDoc?.name === 'Default' && !sourceDoc?.id) {
return 'default';
}
return sourceDoc?.id || id;
})
.filter(Boolean);
formData.append('sources', JSON.stringify(sourcesArray));
formData.append('source', '');
} else if (selectedSourceIds.size === 1) {
const singleSourceId = Array.from(selectedSourceIds)[0];
const sourceDoc = sourceDocs?.find(
(source) =>
source.id === singleSourceId ||
source.retriever === singleSourceId ||
source.name === singleSourceId,
);
let finalSourceId;
if (sourceDoc?.name === 'Default' && !sourceDoc?.id)
finalSourceId = 'default';
else finalSourceId = sourceDoc?.id || singleSourceId;
formData.append('source', String(finalSourceId));
formData.append('sources', JSON.stringify([]));
} else {
formData.append('source', '');
formData.append('sources', JSON.stringify([]));
}
formData.append('chunks', agent.chunks);
formData.append('retriever', agent.retriever);
formData.append('prompt_id', agent.prompt_id);
@@ -293,9 +362,33 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
throw new Error('Failed to fetch agent');
}
const data = await response.json();
if (data.source) setSelectedSourceIds(new Set([data.source]));
else if (data.retriever)
if (data.sources && data.sources.length > 0) {
const mappedSources = data.sources.map((sourceId: string) => {
if (sourceId === 'default') {
const defaultSource = sourceDocs?.find(
(source) => source.name === 'Default',
);
return defaultSource?.retriever || 'classic';
}
return sourceId;
});
setSelectedSourceIds(new Set(mappedSources));
} else if (data.source) {
if (data.source === 'default') {
const defaultSource = sourceDocs?.find(
(source) => source.name === 'Default',
);
setSelectedSourceIds(
new Set([defaultSource?.retriever || 'classic']),
);
} else {
setSelectedSourceIds(new Set([data.source]));
}
} else if (data.retriever) {
setSelectedSourceIds(new Set([data.retriever]));
}
if (data.tools) setSelectedToolIds(new Set(data.tools));
if (data.status === 'draft') setEffectiveMode('draft');
if (data.json_schema) {
@@ -311,25 +404,57 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
}, [agentId, mode, token]);
useEffect(() => {
const selectedSource = Array.from(selectedSourceIds).map((id) =>
sourceDocs?.find(
(source) =>
source.id === id || source.retriever === id || source.name === id,
),
);
if (selectedSource[0]?.model === embeddingsName) {
if (selectedSource[0] && 'id' in selectedSource[0]) {
const selectedSources = Array.from(selectedSourceIds)
.map((id) =>
sourceDocs?.find(
(source) =>
source.id === id || source.retriever === id || source.name === id,
),
)
.filter(Boolean);
if (selectedSources.length > 0) {
// Handle multiple sources
if (selectedSources.length > 1) {
// Multiple sources selected - store in sources array
const sourceIds = selectedSources
.map((source) => source?.id)
.filter((id): id is string => Boolean(id));
setAgent((prev) => ({
...prev,
source: selectedSource[0]?.id || 'default',
sources: sourceIds,
source: '', // Clear single source for multiple sources
retriever: '',
}));
} else
setAgent((prev) => ({
...prev,
source: '',
retriever: selectedSource[0]?.retriever || 'classic',
}));
} else {
// Single source selected - maintain backward compatibility
const selectedSource = selectedSources[0];
if (selectedSource?.model === embeddingsName) {
if (selectedSource && 'id' in selectedSource) {
setAgent((prev) => ({
...prev,
source: selectedSource?.id || 'default',
sources: [], // Clear sources array for single source
retriever: '',
}));
} else {
setAgent((prev) => ({
...prev,
source: '',
sources: [], // Clear sources array
retriever: selectedSource?.retriever || 'classic',
}));
}
}
}
} else {
// No sources selected
setAgent((prev) => ({
...prev,
source: '',
sources: [],
retriever: '',
}));
}
}, [selectedSourceIds]);
@@ -510,7 +635,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
)
.filter(Boolean)
.join(', ')
: 'Select source'}
: 'Select sources'}
</button>
<MultiSelectPopup
isOpen={isSourcePopupOpen}
@@ -526,12 +651,10 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
selectedIds={selectedSourceIds}
onSelectionChange={(newSelectedIds: Set<string | number>) => {
setSelectedSourceIds(newSelectedIds);
setIsSourcePopupOpen(false);
}}
title="Select Source"
title="Select Sources"
searchPlaceholder="Search sources..."
noOptionsMessage="No source available"
singleSelect={true}
noOptionsMessage="No sources available"
/>
</div>
<div className="mt-3">

View File

@@ -10,6 +10,7 @@ export type Agent = {
description: string;
image: string;
source: string;
sources?: string[];
chunks: string;
retriever: string;
prompt_id: string;

View File

@@ -57,6 +57,8 @@ const endpoints = {
DIRECTORY_STRUCTURE: (docId: string) =>
`/api/directory_structure?id=${docId}`,
MANAGE_SOURCE_FILES: '/api/manage_source_files',
MCP_TEST_CONNECTION: '/api/mcp_server/test',
MCP_SAVE_SERVER: '/api/mcp_server/save',
},
CONVERSATION: {
ANSWER: '/api/answer',

View File

@@ -90,7 +90,10 @@ const userService = {
path?: string,
search?: string,
): Promise<any> =>
apiClient.get(endpoints.USER.GET_CHUNKS(docId, page, perPage, path, search), token),
apiClient.get(
endpoints.USER.GET_CHUNKS(docId, page, perPage, path, search),
token,
),
addChunk: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.ADD_CHUNK, data, token),
deleteChunk: (
@@ -105,16 +108,24 @@ const userService = {
apiClient.get(endpoints.USER.DIRECTORY_STRUCTURE(docId), token),
manageSourceFiles: (data: FormData, token: string | null): Promise<any> =>
apiClient.postFormData(endpoints.USER.MANAGE_SOURCE_FILES, data, token),
syncConnector: (docId: string, provider: string, token: string | null): Promise<any> => {
testMCPConnection: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.MCP_TEST_CONNECTION, data, token),
saveMCPServer: (data: any, token: string | null): Promise<any> =>
apiClient.post(endpoints.USER.MCP_SAVE_SERVER, data, token),
syncConnector: (
docId: string,
provider: string,
token: string | null,
): Promise<any> => {
const sessionToken = getSessionToken(provider);
return apiClient.post(
endpoints.USER.SYNC_CONNECTOR,
{
source_id: docId,
session_token: sessionToken,
provider: provider
provider: provider,
},
token
token,
);
},
};

View File

View File

@@ -16,7 +16,12 @@ const providerLabel = (provider: string) => {
return map[provider] || provider.replace(/_/g, ' ');
};
const ConnectorAuth: React.FC<ConnectorAuthProps> = ({ provider, onSuccess, onError, label }) => {
const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
provider,
onSuccess,
onError,
label,
}) => {
const token = useSelector(selectToken);
const completedRef = useRef(false);
const intervalRef = useRef<number | null>(null);
@@ -31,8 +36,12 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({ provider, onSuccess, onEr
const handleAuthMessage = (event: MessageEvent) => {
const successGeneric = event.data?.type === 'connector_auth_success';
const successProvider = event.data?.type === `${provider}_auth_success` || event.data?.type === 'google_drive_auth_success';
const errorProvider = event.data?.type === `${provider}_auth_error` || event.data?.type === 'google_drive_auth_error';
const successProvider =
event.data?.type === `${provider}_auth_success` ||
event.data?.type === 'google_drive_auth_success';
const errorProvider =
event.data?.type === `${provider}_auth_error` ||
event.data?.type === 'google_drive_auth_error';
if (successGeneric || successProvider) {
completedRef.current = true;
@@ -54,12 +63,17 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({ provider, onSuccess, onEr
cleanup();
const apiHost = import.meta.env.VITE_API_HOST;
const authResponse = await fetch(`${apiHost}/api/connectors/auth?provider=${provider}`, {
headers: { Authorization: `Bearer ${token}` },
});
const authResponse = await fetch(
`${apiHost}/api/connectors/auth?provider=${provider}`,
{
headers: { Authorization: `Bearer ${token}` },
},
);
if (!authResponse.ok) {
throw new Error(`Failed to get authorization URL: ${authResponse.status}`);
throw new Error(
`Failed to get authorization URL: ${authResponse.status}`,
);
}
const authData = await authResponse.json();
@@ -70,10 +84,12 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({ provider, onSuccess, onEr
const authWindow = window.open(
authData.authorization_url,
`${provider}-auth`,
'width=500,height=600,scrollbars=yes,resizable=yes'
'width=500,height=600,scrollbars=yes,resizable=yes',
);
if (!authWindow) {
throw new Error('Failed to open authentication window. Please allow popups.');
throw new Error(
'Failed to open authentication window. Please allow popups.',
);
}
window.addEventListener('message', handleAuthMessage as any);
@@ -98,10 +114,13 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({ provider, onSuccess, onEr
return (
<button
onClick={handleAuth}
className="w-full flex items-center justify-center gap-2 rounded-lg bg-blue-500 px-4 py-3 text-white hover:bg-blue-600 transition-colors"
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-500 px-4 py-3 text-white transition-colors hover:bg-blue-600"
>
<svg className="h-5 w-5" viewBox="0 0 24 24">
<path fill="currentColor" d="M6.28 3l5.72 10H24l-5.72-10H6.28zm11.44 0L12 13l5.72 10H24L18.28 3h-.56zM0 13l5.72 10h5.72L5.72 13H0z"/>
<path
fill="currentColor"
d="M6.28 3l5.72 10H24l-5.72-10H6.28zm11.44 0L12 13l5.72 10H24L18.28 3h-.56zM0 13l5.72 10h5.72L5.72 13H0z"
/>
</svg>
{buttonLabel}
</button>
@@ -109,4 +128,3 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({ provider, onSuccess, onEr
};
export default ConnectorAuth;

View File

@@ -240,8 +240,6 @@ const ConnectorTreeComponent: React.FC<ConnectorTreeComponentProps> = ({
return current;
};
const getMenuRef = (id: string) => {
if (!menuRefs.current[id]) {
menuRefs.current[id] = React.createRef();

View File

@@ -136,8 +136,6 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
}
}, [docId, token]);
const navigateToDirectory = (dirName: string) => {
setCurrentPath((prev) => [...prev, dirName]);
};
@@ -445,18 +443,18 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
const renderPathNavigation = () => {
return (
<div className="mb-0 min-h-[38px] flex flex-col gap-2 text-base sm:flex-row sm:items-center sm:justify-between">
<div className="mb-0 flex min-h-[38px] flex-col gap-2 text-base sm:flex-row sm:items-center sm:justify-between">
{/* Left side with path navigation */}
<div className="flex w-full items-center sm:w-auto">
<button
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34] font-medium"
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm font-medium text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
onClick={handleBackNavigation}
>
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
</button>
<div className="flex flex-wrap items-center">
<span className="text-[#7D54D1] font-semibold break-words">
<span className="font-semibold break-words text-[#7D54D1]">
{sourceName}
</span>
{currentPath.length > 0 && (
@@ -487,8 +485,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
</div>
</div>
<div className="flex relative flex-row flex-nowrap items-center gap-2 w-full sm:w-auto justify-end mt-2 sm:mt-0">
<div className="relative mt-2 flex w-full flex-row flex-nowrap items-center justify-end gap-2 sm:mt-0 sm:w-auto">
{processingRef.current && (
<div className="text-sm text-gray-500">
{currentOpRef.current === 'add'
@@ -497,13 +494,13 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
</div>
)}
{renderFileSearch()}
{renderFileSearch()}
{/* Add file button */}
{!processingRef.current && (
<button
onClick={handleAddFile}
className="bg-purple-30 hover:bg-violets-are-blue flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 text-[14px] whitespace-nowrap text-white font-medium"
className="bg-purple-30 hover:bg-violets-are-blue flex h-[38px] min-w-[108px] items-center justify-center rounded-full px-4 text-[14px] font-medium whitespace-nowrap text-white"
title={t('settings.sources.addFile')}
>
{t('settings.sources.addFile')}
@@ -609,7 +606,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
<div ref={menuRef} className="relative">
<button
onClick={(e) => handleMenuClick(e, itemId)}
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E] font-medium"
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E]"
aria-label={t('settings.sources.menuAlt')}
>
<img
@@ -664,7 +661,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
<div ref={menuRef} className="relative">
<button
onClick={(e) => handleMenuClick(e, itemId)}
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E] font-medium"
className="inline-flex h-[35px] w-[24px] shrink-0 items-center justify-center rounded-md font-medium transition-colors hover:bg-[#EBEBEB] dark:hover:bg-[#26272E]"
aria-label={t('settings.sources.menuAlt')}
>
<img
@@ -756,14 +753,12 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
}
}}
placeholder={t('settings.sources.searchFiles')}
className={`w-full h-[38px] border border-[#D1D9E0] px-4 py-2 dark:border-[#6A6A6A]
${searchQuery ? 'rounded-t-[24px]' : 'rounded-[24px]'}
bg-transparent focus:outline-none dark:text-[#E0E0E0]`}
className={`h-[38px] w-full border border-[#D1D9E0] px-4 py-2 dark:border-[#6A6A6A] ${searchQuery ? 'rounded-t-[24px]' : 'rounded-[24px]'} bg-transparent focus:outline-none dark:text-[#E0E0E0]`}
/>
{searchQuery && (
<div className="absolute top-full left-0 right-0 z-10 max-h-[calc(100vh-200px)] w-full overflow-hidden rounded-b-[12px] border border-t-0 border-[#D1D9E0] bg-white shadow-lg dark:border-[#6A6A6A] dark:bg-[#1F2023] transition-all duration-200">
<div className="max-h-[calc(100vh-200px)] overflow-y-auto overflow-x-hidden overscroll-contain">
<div className="absolute top-full right-0 left-0 z-10 max-h-[calc(100vh-200px)] w-full overflow-hidden rounded-b-[12px] border border-t-0 border-[#D1D9E0] bg-white shadow-lg transition-all duration-200 dark:border-[#6A6A6A] dark:bg-[#1F2023]">
<div className="max-h-[calc(100vh-200px)] overflow-x-hidden overflow-y-auto overscroll-contain">
{searchResults.length === 0 ? (
<div className="py-2 text-center text-sm text-gray-500 dark:text-gray-400">
{t('settings.sources.noResults')}
@@ -774,10 +769,11 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
key={index}
onClick={() => handleSearchSelect(result)}
title={result.path}
className={`flex min-w-0 cursor-pointer items-center px-3 py-2 hover:bg-[#ECEEEF] dark:hover:bg-[#27282D] ${index !== searchResults.length - 1
className={`flex min-w-0 cursor-pointer items-center px-3 py-2 hover:bg-[#ECEEEF] dark:hover:bg-[#27282D] ${
index !== searchResults.length - 1
? 'border-b border-[#D1D9E0] dark:border-[#6A6A6A]'
: ''
}`}
}`}
>
<img
src={result.isFile ? FileIcon : FolderIcon}
@@ -788,7 +784,7 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
}
className="mr-2 h-4 w-4 flex-shrink-0"
/>
<span className="text-sm dark:text-[#E0E0E0] truncate flex-1">
<span className="flex-1 truncate text-sm dark:text-[#E0E0E0]">
{result.path.split('/').pop() || result.path}
</span>
</div>
@@ -870,7 +866,9 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
message={
itemToDelete?.isFile
? t('settings.sources.confirmDelete')
: t('settings.sources.deleteDirectoryWarning', { name: itemToDelete?.name })
: t('settings.sources.deleteDirectoryWarning', {
name: itemToDelete?.name,
})
}
modalState={deleteModalState}
setModalState={setDeleteModalState}

View File

@@ -368,8 +368,8 @@ export default function MessageInput({
className="xs:px-3 xs:py-1.5 dark:border-purple-taupe flex max-w-[130px] items-center rounded-[32px] border border-[#AAAAAA] px-2 py-1 transition-colors hover:bg-gray-100 sm:max-w-[150px] dark:hover:bg-[#2C2E3C]"
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
title={
selectedDocs
? selectedDocs.name
selectedDocs && selectedDocs.length > 0
? selectedDocs.map((doc) => doc.name).join(', ')
: t('conversation.sources.title')
}
>
@@ -379,8 +379,10 @@ export default function MessageInput({
className="mr-1 h-3.5 w-3.5 shrink-0 sm:mr-1.5 sm:h-4"
/>
<span className="xs:text-[12px] dark:text-bright-gray truncate overflow-hidden text-[10px] font-medium text-[#5D5D5D] sm:text-[14px]">
{selectedDocs
? selectedDocs.name
{selectedDocs && selectedDocs.length > 0
? selectedDocs.length === 1
? selectedDocs[0].name
: `${selectedDocs.length} sources selected`
: t('conversation.sources.title')}
</span>
{!isTouch && (

View File

@@ -17,7 +17,7 @@ type SourcesPopupProps = {
isOpen: boolean;
onClose: () => void;
anchorRef: React.RefObject<HTMLButtonElement | null>;
handlePostDocumentSelect: (doc: Doc | null) => void;
handlePostDocumentSelect: (doc: Doc[] | null) => void;
setUploadModalState: React.Dispatch<React.SetStateAction<ActiveState>>;
};
@@ -149,9 +149,13 @@ export default function SourcesPopup({
if (option.model === embeddingsName) {
const isSelected =
selectedDocs &&
(option.id
? selectedDocs.id === option.id
: selectedDocs.date === option.date);
Array.isArray(selectedDocs) &&
selectedDocs.length > 0 &&
selectedDocs.some((doc) =>
option.id
? doc.id === option.id
: doc.date === option.date,
);
return (
<div
@@ -159,11 +163,29 @@ export default function SourcesPopup({
className="border-opacity-80 dark:border-dim-gray flex cursor-pointer items-center border-b border-[#D9D9D9] p-3 transition-colors hover:bg-gray-100 dark:text-[14px] dark:hover:bg-[#2C2E3C]"
onClick={() => {
if (isSelected) {
dispatch(setSelectedDocs(null));
handlePostDocumentSelect(null);
const updatedDocs =
selectedDocs && Array.isArray(selectedDocs)
? selectedDocs.filter((doc) =>
option.id
? doc.id !== option.id
: doc.date !== option.date,
)
: [];
dispatch(
setSelectedDocs(
updatedDocs.length > 0 ? updatedDocs : null,
),
);
handlePostDocumentSelect(
updatedDocs.length > 0 ? updatedDocs : null,
);
} else {
dispatch(setSelectedDocs(option));
handlePostDocumentSelect(option);
const updatedDocs =
selectedDocs && Array.isArray(selectedDocs)
? [...selectedDocs, option]
: [option];
dispatch(setSelectedDocs(updatedDocs));
handlePostDocumentSelect(updatedDocs);
}
}}
>

View File

@@ -1,6 +1,6 @@
import 'katex/dist/katex.min.css';
import { forwardRef, Fragment, useRef, useState, useEffect } from 'react';
import { forwardRef, Fragment, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import { useSelector } from 'react-redux';
@@ -12,12 +12,13 @@ import {
import rehypeKatex from 'rehype-katex';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import DocumentationDark from '../assets/documentation-dark.svg';
import ChevronDown from '../assets/chevron-down.svg';
import Cloud from '../assets/cloud.svg';
import DocsGPT3 from '../assets/cute_docsgpt3.svg';
import Dislike from '../assets/dislike.svg?react';
import Document from '../assets/document.svg';
import DocumentationDark from '../assets/documentation-dark.svg';
import Edit from '../assets/edit.svg';
import Like from '../assets/like.svg?react';
import Link from '../assets/link.svg';
@@ -761,7 +762,11 @@ function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) {
Response
</span>{' '}
<CopyButton
textToCopy={JSON.stringify(toolCall.result, null, 2)}
textToCopy={
toolCall.status === 'error'
? toolCall.error || 'Unknown error'
: JSON.stringify(toolCall.result, null, 2)
}
/>
</p>
{toolCall.status === 'pending' && (
@@ -779,6 +784,16 @@ function ToolCalls({ toolCalls }: { toolCalls: ToolCallsType[] }) {
</span>
</p>
)}
{toolCall.status === 'error' && (
<p className="dark:bg-raisin-black rounded-b-2xl p-2 font-mono text-sm break-words">
<span
className="leading-[23px] text-red-500 dark:text-red-400"
style={{ fontFamily: 'IBMPlexMono-Medium' }}
>
{toolCall.error}
</span>
</p>
)}
</div>
</div>
</Accordion>

View File

@@ -7,7 +7,7 @@ export function handleFetchAnswer(
question: string,
signal: AbortSignal,
token: string | null,
selectedDocs: Doc | null,
selectedDocs: Doc[] | null,
conversationId: string | null,
promptId: string | null,
chunks: string,
@@ -52,10 +52,17 @@ export function handleFetchAnswer(
payload.attachments = attachments;
}
if (selectedDocs && 'id' in selectedDocs) {
payload.active_docs = selectedDocs.id as string;
if (selectedDocs && Array.isArray(selectedDocs)) {
if (selectedDocs.length > 1) {
// Handle multiple documents
payload.active_docs = selectedDocs.map((doc) => doc.id!);
payload.retriever = selectedDocs[0]?.retriever as string;
} else if (selectedDocs.length === 1 && 'id' in selectedDocs[0]) {
// Handle single document (backward compatibility)
payload.active_docs = selectedDocs[0].id as string;
payload.retriever = selectedDocs[0].retriever as string;
}
}
payload.retriever = selectedDocs?.retriever as string;
return conversationService
.answer(payload, token, signal)
.then((response) => {
@@ -84,7 +91,7 @@ export function handleFetchAnswerSteaming(
question: string,
signal: AbortSignal,
token: string | null,
selectedDocs: Doc | null,
selectedDocs: Doc[] | null,
conversationId: string | null,
promptId: string | null,
chunks: string,
@@ -112,10 +119,17 @@ export function handleFetchAnswerSteaming(
payload.attachments = attachments;
}
if (selectedDocs && 'id' in selectedDocs) {
payload.active_docs = selectedDocs.id as string;
if (selectedDocs && Array.isArray(selectedDocs)) {
if (selectedDocs.length > 1) {
// Handle multiple documents
payload.active_docs = selectedDocs.map((doc) => doc.id!);
payload.retriever = selectedDocs[0]?.retriever as string;
} else if (selectedDocs.length === 1 && 'id' in selectedDocs[0]) {
// Handle single document (backward compatibility)
payload.active_docs = selectedDocs[0].id as string;
payload.retriever = selectedDocs[0].retriever as string;
}
}
payload.retriever = selectedDocs?.retriever as string;
return new Promise<Answer>((resolve, reject) => {
conversationService
@@ -171,7 +185,7 @@ export function handleFetchAnswerSteaming(
export function handleSearch(
question: string,
token: string | null,
selectedDocs: Doc | null,
selectedDocs: Doc[] | null,
conversation_id: string | null,
chunks: string,
token_limit: number,
@@ -183,9 +197,17 @@ export function handleSearch(
token_limit: token_limit,
isNoneDoc: selectedDocs === null,
};
if (selectedDocs && 'id' in selectedDocs)
payload.active_docs = selectedDocs.id as string;
payload.retriever = selectedDocs?.retriever as string;
if (selectedDocs && Array.isArray(selectedDocs)) {
if (selectedDocs.length > 1) {
// Handle multiple documents
payload.active_docs = selectedDocs.map((doc) => doc.id!);
payload.retriever = selectedDocs[0]?.retriever as string;
} else if (selectedDocs.length === 1 && 'id' in selectedDocs[0]) {
// Handle single document (backward compatibility)
payload.active_docs = selectedDocs[0].id as string;
payload.retriever = selectedDocs[0].retriever as string;
}
}
return conversationService
.search(payload, token)
.then((response) => response.json())

View File

@@ -54,7 +54,7 @@ export interface Query {
export interface RetrievalPayload {
question: string;
active_docs?: string;
active_docs?: string | string[];
retriever?: string;
conversation_id: string | null;
prompt_id?: string | null;

View File

@@ -4,5 +4,6 @@ export type ToolCallsType = {
call_id: string;
arguments: Record<string, any>;
result?: Record<string, any>;
status?: 'pending' | 'completed';
error?: string;
status?: 'pending' | 'completed' | 'error';
};

View File

@@ -18,11 +18,14 @@ export default function useDefaultDocument() {
const fetchDocs = () => {
getDocs(token).then((data) => {
dispatch(setSourceDocs(data));
if (!selectedDoc)
if (
!selectedDoc ||
(Array.isArray(selectedDoc) && selectedDoc.length === 0)
)
Array.isArray(data) &&
data?.forEach((doc: Doc) => {
if (doc.model && doc.name === 'default') {
dispatch(setSelectedDocs(doc));
dispatch(setSelectedDocs([doc]));
}
});
});

View File

@@ -185,7 +185,39 @@
"cancel": "Cancel",
"addNew": "Add New",
"name": "Name",
"type": "Type"
"type": "Type",
"mcp": {
"addServer": "Add MCP Server",
"editServer": "Edit Server",
"serverName": "Server Name",
"serverUrl": "Server URL",
"headerName": "Header Name",
"timeout": "Timeout (seconds)",
"testConnection": "Test Connection",
"testing": "Testing...",
"saving": "Saving...",
"save": "Save",
"cancel": "Cancel",
"noAuth": "No Authentication",
"placeholders": {
"serverUrl": "https://api.example.com",
"apiKey": "Your secret API key",
"bearerToken": "Your secret token",
"username": "Your username",
"password": "Your password"
},
"errors": {
"nameRequired": "Server name is required",
"urlRequired": "Server URL is required",
"invalidUrl": "Please enter a valid URL",
"apiKeyRequired": "API key is required",
"tokenRequired": "Bearer token is required",
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"testFailed": "Connection test failed",
"saveFailed": "Failed to save MCP server"
}
}
}
},
"modals": {

View File

@@ -8,6 +8,7 @@ import { useOutsideAlerter } from '../hooks';
import { ActiveState } from '../models/misc';
import { selectToken } from '../preferences/preferenceSlice';
import ConfigToolModal from './ConfigToolModal';
import MCPServerModal from './MCPServerModal';
import { AvailableToolType } from './types';
import WrapperComponent from './WrapperModal';
@@ -34,6 +35,8 @@ export default function AddToolModal({
React.useState<AvailableToolType | null>(null);
const [configModalState, setConfigModalState] =
React.useState<ActiveState>('INACTIVE');
const [mcpModalState, setMcpModalState] =
React.useState<ActiveState>('INACTIVE');
const [loading, setLoading] = React.useState(false);
useOutsideAlerter(modalRef, () => {
@@ -86,6 +89,9 @@ export default function AddToolModal({
.catch((error) => {
console.error('Failed to create tool:', error);
});
} else if (tool.name === 'mcp_tool') {
setModalState('INACTIVE');
setMcpModalState('ACTIVE');
} else {
setModalState('INACTIVE');
setConfigModalState('ACTIVE');
@@ -95,6 +101,12 @@ export default function AddToolModal({
React.useEffect(() => {
if (modalState === 'ACTIVE') getAvailableTools();
}, [modalState]);
const handleMcpServerAdded = () => {
getUserTools();
setMcpModalState('INACTIVE');
};
return (
<>
{modalState === 'ACTIVE' && (
@@ -166,6 +178,11 @@ export default function AddToolModal({
tool={selectedTool}
getUserTools={getUserTools}
/>
<MCPServerModal
modalState={mcpModalState}
setModalState={setMcpModalState}
onServerSaved={handleMcpServerAdded}
/>
</>
);
}

View File

@@ -0,0 +1,482 @@
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import userService from '../api/services/userService';
import Dropdown from '../components/Dropdown';
import Input from '../components/Input';
import Spinner from '../components/Spinner';
import { useOutsideAlerter } from '../hooks';
import { ActiveState } from '../models/misc';
import { selectToken } from '../preferences/preferenceSlice';
import WrapperComponent from './WrapperModal';
interface MCPServerModalProps {
modalState: ActiveState;
setModalState: (state: ActiveState) => void;
server?: any;
onServerSaved: () => void;
}
const authTypes = [
{ label: 'No Authentication', value: 'none' },
{ label: 'API Key', value: 'api_key' },
{ label: 'Bearer Token', value: 'bearer' },
// { label: 'Basic Authentication', value: 'basic' },
];
export default function MCPServerModal({
modalState,
setModalState,
server,
onServerSaved,
}: MCPServerModalProps) {
const { t } = useTranslation();
const token = useSelector(selectToken);
const modalRef = useRef<HTMLDivElement>(null);
const [formData, setFormData] = useState({
name: server?.displayName || 'My MCP Server',
server_url: server?.server_url || '',
auth_type: server?.auth_type || 'none',
api_key: '',
header_name: 'X-API-Key',
bearer_token: '',
username: '',
password: '',
timeout: server?.timeout || 30,
});
const [loading, setLoading] = useState(false);
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<{
success: boolean;
message: string;
} | null>(null);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
useOutsideAlerter(modalRef, () => {
if (modalState === 'ACTIVE') {
setModalState('INACTIVE');
resetForm();
}
}, [modalState]);
const resetForm = () => {
setFormData({
name: 'My MCP Server',
server_url: '',
auth_type: 'none',
api_key: '',
header_name: 'X-API-Key',
bearer_token: '',
username: '',
password: '',
timeout: 30,
});
setErrors({});
setTestResult(null);
};
const validateForm = () => {
const requiredFields: { [key: string]: boolean } = {
name: !formData.name.trim(),
server_url: !formData.server_url.trim(),
};
const authFieldChecks: { [key: string]: () => void } = {
api_key: () => {
if (!formData.api_key.trim())
newErrors.api_key = t('settings.tools.mcp.errors.apiKeyRequired');
},
bearer: () => {
if (!formData.bearer_token.trim())
newErrors.bearer_token = t('settings.tools.mcp.errors.tokenRequired');
},
basic: () => {
if (!formData.username.trim())
newErrors.username = t('settings.tools.mcp.errors.usernameRequired');
if (!formData.password.trim())
newErrors.password = t('settings.tools.mcp.errors.passwordRequired');
},
};
const newErrors: { [key: string]: string } = {};
Object.entries(requiredFields).forEach(([field, isEmpty]) => {
if (isEmpty)
newErrors[field] = t(
`settings.tools.mcp.errors.${field === 'name' ? 'nameRequired' : 'urlRequired'}`,
);
});
if (formData.server_url.trim()) {
try {
new URL(formData.server_url);
} catch {
newErrors.server_url = t('settings.tools.mcp.errors.invalidUrl');
}
}
const timeoutValue = formData.timeout === '' ? 30 : formData.timeout;
if (
typeof timeoutValue === 'number' &&
(timeoutValue < 1 || timeoutValue > 300)
)
newErrors.timeout = 'Timeout must be between 1 and 300 seconds';
if (authFieldChecks[formData.auth_type])
authFieldChecks[formData.auth_type]();
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleInputChange = (name: string, value: string | number) => {
setFormData((prev) => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: '' }));
}
setTestResult(null);
};
const buildToolConfig = () => {
const config: any = {
server_url: formData.server_url.trim(),
auth_type: formData.auth_type,
timeout: formData.timeout === '' ? 30 : formData.timeout,
};
if (formData.auth_type === 'api_key') {
config.api_key = formData.api_key.trim();
config.api_key_header = formData.header_name.trim() || 'X-API-Key';
} else if (formData.auth_type === 'bearer') {
config.bearer_token = formData.bearer_token.trim();
} else if (formData.auth_type === 'basic') {
config.username = formData.username.trim();
config.password = formData.password.trim();
}
return config;
};
const testConnection = async () => {
if (!validateForm()) return;
setTesting(true);
setTestResult(null);
try {
const config = buildToolConfig();
const response = await userService.testMCPConnection({ config }, token);
const result = await response.json();
setTestResult(result);
} catch (error) {
setTestResult({
success: false,
message: t('settings.tools.mcp.errors.testFailed'),
});
} finally {
setTesting(false);
}
};
const handleSave = async () => {
if (!validateForm()) return;
setLoading(true);
try {
const config = buildToolConfig();
const serverData = {
displayName: formData.name,
config,
status: true,
...(server?.id && { id: server.id }),
};
const response = await userService.saveMCPServer(serverData, token);
const result = await response.json();
if (response.ok && result.success) {
setTestResult({
success: true,
message: result.message,
});
onServerSaved();
setModalState('INACTIVE');
resetForm();
} else {
setErrors({
general: result.error || t('settings.tools.mcp.errors.saveFailed'),
});
}
} catch (error) {
console.error('Error saving MCP server:', error);
setErrors({ general: t('settings.tools.mcp.errors.saveFailed') });
} finally {
setLoading(false);
}
};
const renderAuthFields = () => {
switch (formData.auth_type) {
case 'api_key':
return (
<div className="mb-10">
<div className="mt-6">
<Input
name="api_key"
type="text"
className="rounded-md"
value={formData.api_key}
onChange={(e) => handleInputChange('api_key', e.target.value)}
placeholder={t('settings.tools.mcp.placeholders.apiKey')}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
{errors.api_key && (
<p className="mt-1 text-sm text-red-600">{errors.api_key}</p>
)}
</div>
<div className="mt-5">
<Input
name="header_name"
type="text"
className="rounded-md"
value={formData.header_name}
onChange={(e) =>
handleInputChange('header_name', e.target.value)
}
placeholder={t('settings.tools.mcp.headerName')}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
</div>
</div>
);
case 'bearer':
return (
<div className="mb-10">
<Input
name="bearer_token"
type="text"
className="rounded-md"
value={formData.bearer_token}
onChange={(e) =>
handleInputChange('bearer_token', e.target.value)
}
placeholder={t('settings.tools.mcp.placeholders.bearerToken')}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
{errors.bearer_token && (
<p className="mt-1 text-sm text-red-600">{errors.bearer_token}</p>
)}
</div>
);
case 'basic':
return (
<div className="mb-10">
<div className="mt-6">
<Input
name="username"
type="text"
className="rounded-md"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
placeholder={t('settings.tools.mcp.username')}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
{errors.username && (
<p className="mt-1 text-sm text-red-600">{errors.username}</p>
)}
</div>
<div className="mt-5">
<Input
name="password"
type="text"
className="rounded-md"
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
placeholder={t('settings.tools.mcp.password')}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
)}
</div>
</div>
);
default:
return null;
}
};
return (
modalState === 'ACTIVE' && (
<WrapperComponent
close={() => {
setModalState('INACTIVE');
resetForm();
}}
className="max-w-[600px] md:w-[80vw] lg:w-[60vw]"
>
<div className="flex h-full flex-col">
<div className="px-6 py-4">
<h2 className="text-jet dark:text-bright-gray text-xl font-semibold">
{server
? t('settings.tools.mcp.editServer')
: t('settings.tools.mcp.addServer')}
</h2>
</div>
<div className="flex-1 px-6">
<div className="space-y-6 py-6">
<div>
<Input
name="name"
type="text"
className="rounded-md"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
borderVariant="thin"
placeholder={t('settings.tools.mcp.serverName')}
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
)}
</div>
<div>
<Input
name="server_url"
type="text"
className="rounded-md"
value={formData.server_url}
onChange={(e) =>
handleInputChange('server_url', e.target.value)
}
placeholder={t('settings.tools.mcp.serverUrl')}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
{errors.server_url && (
<p className="mt-1 text-sm text-red-600">
{errors.server_url}
</p>
)}
</div>
<Dropdown
placeholder={t('settings.tools.mcp.authType')}
selectedValue={
authTypes.find((type) => type.value === formData.auth_type)
?.label || null
}
onSelect={(selection: { label: string; value: string }) => {
handleInputChange('auth_type', selection.value);
}}
options={authTypes}
size="w-full"
rounded="3xl"
border="border"
/>
{renderAuthFields()}
<div>
<Input
name="timeout"
type="number"
className="rounded-md"
value={formData.timeout}
onChange={(e) => {
const value = e.target.value;
if (value === '') {
handleInputChange('timeout', '');
} else {
const numValue = parseInt(value);
if (!isNaN(numValue) && numValue >= 1) {
handleInputChange('timeout', numValue);
}
}
}}
placeholder={t('settings.tools.mcp.timeout')}
borderVariant="thin"
labelBgClassName="bg-white dark:bg-charleston-green-2"
/>
{errors.timeout && (
<p className="mt-2 text-sm text-red-600">{errors.timeout}</p>
)}
</div>
{testResult && (
<div
className={`rounded-md p-5 ${
testResult.success
? 'bg-green-50 text-green-700 dark:bg-green-900/40 dark:text-green-300'
: 'bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300'
}`}
>
{testResult.message}
</div>
)}
{errors.general && (
<div className="rounded-2xl bg-red-50 p-5 text-red-700 dark:bg-red-900 dark:text-red-300">
{errors.general}
</div>
)}
</div>
</div>
<div className="px-6 py-2">
<div className="flex flex-col gap-4 sm:flex-row sm:justify-between">
<button
onClick={testConnection}
disabled={testing}
className="border-silver dark:border-dim-gray dark:text-light-gray w-full rounded-3xl border px-6 py-2 text-sm font-medium transition-all hover:bg-gray-100 disabled:opacity-50 sm:w-auto dark:hover:bg-[#767183]/50"
>
{testing ? (
<div className="flex items-center justify-center">
<Spinner size="small" />
<span className="ml-2">
{t('settings.tools.mcp.testing')}
</span>
</div>
) : (
t('settings.tools.mcp.testConnection')
)}
</button>
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:gap-3">
<button
onClick={() => {
setModalState('INACTIVE');
resetForm();
}}
className="dark:text-light-gray w-full cursor-pointer rounded-3xl px-6 py-2 text-sm font-medium hover:bg-gray-100 sm:w-auto dark:bg-transparent dark:hover:bg-[#767183]/50"
>
{t('settings.tools.mcp.cancel')}
</button>
<button
onClick={handleSave}
disabled={loading}
className="bg-purple-30 hover:bg-violets-are-blue w-full rounded-3xl px-6 py-2 text-sm font-medium text-white transition-all disabled:opacity-50 sm:w-auto"
>
{loading ? (
<div className="flex items-center justify-center">
<Spinner size="small" />
<span className="ml-2">
{t('settings.tools.mcp.saving')}
</span>
</div>
) : (
t('settings.tools.mcp.save')
)}
</button>
</div>
</div>
</div>
</div>
</WrapperComponent>
)
);
}

View File

@@ -60,7 +60,7 @@ export const ShareConversationModal = ({
const [sourcePath, setSourcePath] = useState<{
label: string;
value: string;
} | null>(preSelectedDoc ? extractDocPaths([preSelectedDoc])[0] : null);
} | null>(preSelectedDoc ? extractDocPaths(preSelectedDoc)[0] : null);
const handleCopyKey = (url: string) => {
navigator.clipboard.writeText(url);
@@ -105,14 +105,14 @@ export const ShareConversationModal = ({
return (
<WrapperModal close={close}>
<div className="flex flex-col gap-2">
<h2 className="text-xl font-medium text-eerie-black dark:text-chinese-white">
<h2 className="text-eerie-black dark:text-chinese-white text-xl font-medium">
{t('modals.shareConv.label')}
</h2>
<p className="text-sm text-eerie-black dark:text-silver/60">
<p className="text-eerie-black dark:text-silver/60 text-sm">
{t('modals.shareConv.note')}
</p>
<div className="flex items-center justify-between">
<span className="text-lg text-eerie-black dark:text-white">
<span className="text-eerie-black text-lg dark:text-white">
{t('modals.shareConv.option')}
</span>
<ToggleSwitch
@@ -136,19 +136,19 @@ export const ShareConversationModal = ({
</div>
)}
<div className="flex items-baseline justify-between gap-2">
<span className="no-scrollbar w-full overflow-x-auto whitespace-nowrap rounded-full border-2 border-silver px-4 py-3 text-eerie-black dark:border-silver/40 dark:text-white">
<span className="no-scrollbar border-silver text-eerie-black dark:border-silver/40 w-full overflow-x-auto rounded-full border-2 px-4 py-3 whitespace-nowrap dark:text-white">
{`${domain}/share/${identifier ?? '....'}`}
</span>
{status === 'fetched' ? (
<button
className="my-1 h-10 w-28 rounded-full bg-purple-30 p-2 text-sm text-white hover:bg-violets-are-blue"
className="bg-purple-30 hover:bg-violets-are-blue my-1 h-10 w-28 rounded-full p-2 text-sm text-white"
onClick={() => handleCopyKey(`${domain}/share/${identifier}`)}
>
{isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
</button>
) : (
<button
className="my-1 flex h-10 w-28 items-center justify-evenly rounded-full bg-purple-30 p-2 text-center text-sm font-normal text-white hover:bg-violets-are-blue"
className="bg-purple-30 hover:bg-violets-are-blue my-1 flex h-10 w-28 items-center justify-evenly rounded-full p-2 text-center text-sm font-normal text-white"
onClick={() => {
shareCoversationPublicly(allowPrompt);
}}

View File

@@ -90,9 +90,9 @@ export function getLocalApiKey(): string | null {
return key;
}
export function getLocalRecentDocs(): string | null {
const doc = localStorage.getItem('DocsGPTRecentDocs');
return doc;
export function getLocalRecentDocs(): Doc[] | null {
const docs = localStorage.getItem('DocsGPTRecentDocs');
return docs ? (JSON.parse(docs) as Doc[]) : null;
}
export function getLocalPrompt(): string | null {
@@ -108,19 +108,20 @@ export function setLocalPrompt(prompt: string): void {
localStorage.setItem('DocsGPTPrompt', prompt);
}
export function setLocalRecentDocs(doc: Doc | null): void {
localStorage.setItem('DocsGPTRecentDocs', JSON.stringify(doc));
export function setLocalRecentDocs(docs: Doc[] | null): void {
if (docs && docs.length > 0) {
localStorage.setItem('DocsGPTRecentDocs', JSON.stringify(docs));
let docPath = 'default';
if (doc?.type === 'local') {
docPath = 'local' + '/' + doc.name + '/';
docs.forEach((doc) => {
let docPath = 'default';
if (doc.type === 'local') {
docPath = 'local' + '/' + doc.name + '/';
}
userService
.checkDocs({ docs: docPath }, null)
.then((response) => response.json());
});
} else {
localStorage.removeItem('DocsGPTRecentDocs');
}
userService
.checkDocs(
{
docs: docPath,
},
null,
)
.then((response) => response.json());
}

View File

@@ -15,7 +15,7 @@ export interface Preference {
prompt: { name: string; id: string; type: string };
chunks: string;
token_limit: number;
selectedDocs: Doc | null;
selectedDocs: Doc[] | null;
sourceDocs: Doc[] | null;
conversations: {
data: { name: string; id: string }[] | null;
@@ -34,15 +34,16 @@ const initialState: Preference = {
prompt: { name: 'default', id: 'default', type: 'public' },
chunks: '2',
token_limit: 2000,
selectedDocs: {
id: 'default',
name: 'default',
type: 'remote',
date: 'default',
docLink: 'default',
model: 'openai_text-embedding-ada-002',
retriever: 'classic',
} as Doc,
selectedDocs: [
{
id: 'default',
name: 'default',
type: 'remote',
date: 'default',
model: 'openai_text-embedding-ada-002',
retriever: 'classic',
},
] as Doc[],
sourceDocs: null,
conversations: {
data: null,

View File

@@ -1,4 +1,3 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
@@ -319,7 +318,7 @@ export default function Sources({
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
className="w-full h-[32px] rounded-full border border-silver dark:border-silver/40 bg-transparent px-3 text-sm text-jet dark:text-bright-gray placeholder:text-gray-400 dark:placeholder:text-gray-500 outline-none focus:border-silver dark:focus:border-silver/60"
className="border-silver dark:border-silver/40 text-jet dark:text-bright-gray focus:border-silver dark:focus:border-silver/60 h-[32px] w-full rounded-full border bg-transparent px-3 text-sm outline-none placeholder:text-gray-400 dark:placeholder:text-gray-500"
/>
</div>
</div>
@@ -336,7 +335,7 @@ export default function Sources({
</div>
<div className="relative w-full">
{loading ? (
<div className="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 px-2 py-4">
<div className="grid w-full grid-cols-1 gap-6 px-2 py-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<SkeletonLoader component="sourceCards" count={rowsPerPage} />
</div>
) : !currentDocuments?.length ? (
@@ -351,17 +350,18 @@ export default function Sources({
</p>
</div>
) : (
<div className="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 px-2 py-4">
<div className="grid w-full grid-cols-1 gap-6 px-2 py-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{currentDocuments.map((document, index) => {
const docId = document.id ? document.id.toString() : '';
return (
<div key={docId} className="relative">
<div
className={`flex h-[130px] w-full flex-col rounded-2xl bg-[#F9F9F9] p-3 transition-all duration-200 dark:bg-[#383838] ${activeMenuId === docId || syncMenuState.docId === docId
className={`flex h-[130px] w-full flex-col rounded-2xl bg-[#F9F9F9] p-3 transition-all duration-200 dark:bg-[#383838] ${
activeMenuId === docId || syncMenuState.docId === docId
? 'scale-[1.05]'
: 'hover:scale-[1.05]'
}`}
}`}
>
<div className="w-full flex-1">
<div className="flex w-full items-center justify-between gap-2">
@@ -426,7 +426,7 @@ export default function Sources({
<img
src={CalendarIcon}
alt=""
className="w-[14px] h-[14px]"
className="h-[14px] w-[14px]"
/>
<span className="font-inter text-[12px] leading-[18px] font-[500] text-[#848484] dark:text-[#848484]">
{document.date ? formatDate(document.date) : ''}
@@ -436,7 +436,7 @@ export default function Sources({
<img
src={DiscIcon}
alt=""
className="w-[14px] h-[14px]"
className="h-[14px] w-[14px]"
/>
<span className="font-inter text-[12px] leading-[18px] font-[500] text-[#848484] dark:text-[#848484]">
{document.tokens

View File

@@ -30,9 +30,22 @@ export default function ToolConfig({
handleGoBack: () => void;
}) {
const token = useSelector(selectToken);
const [authKey, setAuthKey] = React.useState<string>(
'token' in tool.config ? tool.config.token : '',
);
const [authKey, setAuthKey] = React.useState<string>(() => {
if (tool.name === 'mcp_tool') {
const config = tool.config as any;
if (config.auth_type === 'api_key') {
return config.api_key || '';
} else if (config.auth_type === 'bearer') {
return config.encrypted_token || '';
} else if (config.auth_type === 'basic') {
return config.password || '';
}
return '';
} else if ('token' in tool.config) {
return tool.config.token;
}
return '';
});
const [customName, setCustomName] = React.useState<string>(
tool.customName || '',
);
@@ -97,6 +110,26 @@ export default function ToolConfig({
};
const handleSaveChanges = () => {
let configToSave;
if (tool.name === 'api_tool') {
configToSave = tool.config;
} else if (tool.name === 'mcp_tool') {
configToSave = { ...tool.config } as any;
const mcpConfig = tool.config as any;
if (authKey.trim()) {
if (mcpConfig.auth_type === 'api_key') {
configToSave.api_key = authKey;
} else if (mcpConfig.auth_type === 'bearer') {
configToSave.encrypted_token = authKey;
} else if (mcpConfig.auth_type === 'basic') {
configToSave.password = authKey;
}
}
} else {
configToSave = { token: authKey };
}
userService
.updateTool(
{
@@ -105,7 +138,7 @@ export default function ToolConfig({
displayName: tool.displayName,
customName: customName,
description: tool.description,
config: tool.name === 'api_tool' ? tool.config : { token: authKey },
config: configToSave,
actions: 'actions' in tool ? tool.actions : [],
status: tool.status,
},
@@ -196,7 +229,15 @@ export default function ToolConfig({
<div className="mt-1">
{Object.keys(tool?.config).length !== 0 && tool.name !== 'api_tool' && (
<p className="text-eerie-black dark:text-bright-gray text-sm font-semibold">
{t('settings.tools.authentication')}
{tool.name === 'mcp_tool'
? (tool.config as any)?.auth_type === 'bearer'
? 'Bearer Token'
: (tool.config as any)?.auth_type === 'api_key'
? 'API Key'
: (tool.config as any)?.auth_type === 'basic'
? 'Password'
: t('settings.tools.authentication')
: t('settings.tools.authentication')}
</p>
)}
<div className="mt-4 flex flex-col items-start gap-2 sm:flex-row sm:items-center">
@@ -208,7 +249,17 @@ export default function ToolConfig({
value={authKey}
onChange={(e) => setAuthKey(e.target.value)}
borderVariant="thin"
placeholder={t('modals.configTool.apiKeyPlaceholder')}
placeholder={
tool.name === 'mcp_tool'
? (tool.config as any)?.auth_type === 'bearer'
? 'Bearer Token'
: (tool.config as any)?.auth_type === 'api_key'
? 'API Key'
: (tool.config as any)?.auth_type === 'basic'
? 'Password'
: t('modals.configTool.apiKeyPlaceholder')
: t('modals.configTool.apiKeyPlaceholder')
}
/>
</div>
)}
@@ -450,6 +501,26 @@ export default function ToolConfig({
setModalState={(state) => setShowUnsavedModal(state === 'ACTIVE')}
submitLabel={t('settings.tools.saveAndLeave')}
handleSubmit={() => {
let configToSave;
if (tool.name === 'api_tool') {
configToSave = tool.config;
} else if (tool.name === 'mcp_tool') {
configToSave = { ...tool.config } as any;
const mcpConfig = tool.config as any;
if (authKey.trim()) {
if (mcpConfig.auth_type === 'api_key') {
configToSave.api_key = authKey;
} else if (mcpConfig.auth_type === 'bearer') {
configToSave.encrypted_token = authKey;
} else if (mcpConfig.auth_type === 'basic') {
configToSave.password = authKey;
}
}
} else {
configToSave = { token: authKey };
}
userService
.updateTool(
{
@@ -458,10 +529,7 @@ export default function ToolConfig({
displayName: tool.displayName,
customName: customName,
description: tool.description,
config:
tool.name === 'api_tool'
? tool.config
: { token: authKey },
config: configToSave,
actions: 'actions' in tool ? tool.actions : [],
status: tool.status,
},

View File

@@ -4,8 +4,15 @@ import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import userService from '../api/services/userService';
import { getSessionToken } from '../utils/providerUtils';
import {
getSessionToken,
setSessionToken,
removeSessionToken,
} from '../utils/providerUtils';
import { formatDate } from '../utils/dateTimeUtils';
import { formatBytes } from '../utils/stringUtils';
import FileUpload from '../assets/file_upload.svg';
import WebsiteCollect from '../assets/website_collect.svg';
import Dropdown from '../components/Dropdown';
import Input from '../components/Input';
import ToggleSwitch from '../components/ToggleSwitch';
@@ -377,7 +384,8 @@ function Upload({
data?.find(
(d: Doc) => d.type?.toLowerCase() === 'local',
),
));
),
);
});
setProgress(
(progress) =>

View File

@@ -3,7 +3,6 @@
* Follows the convention: {provider}_session_token
*/
export const getSessionToken = (provider: string): string | null => {
return localStorage.getItem(`${provider}_session_token`);
};
@@ -14,4 +13,4 @@ export const setSessionToken = (provider: string, token: string): void => {
export const removeSessionToken = (provider: string): void => {
localStorage.removeItem(`${provider}_session_token`);
};
};