mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-12-01 01:23:14 +00:00
Merge branch 'main' into feat/jwt-auth
This commit is contained in:
@@ -112,7 +112,7 @@ export default function APIKeys() {
|
||||
<div className="mb-6 flex flex-col sm:flex-row justify-end items-start sm:items-center gap-3">
|
||||
<button
|
||||
onClick={() => setCreateModal(true)}
|
||||
className="rounded-full w-full sm:w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
|
||||
className="rounded-full text-sm w-[108px] h-[30px] bg-purple-30 text-white hover:bg-violets-are-blue flex items-center justify-center"
|
||||
title={t('settings.apiKeys.createNew')}
|
||||
>
|
||||
{t('settings.apiKeys.createNew')}
|
||||
@@ -125,13 +125,13 @@ export default function APIKeys() {
|
||||
<table className="w-full table-auto">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-300 dark:border-silver/40">
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver uppercase w-[35%]">
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[35%]">
|
||||
{t('settings.apiKeys.name')}
|
||||
</th>
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver uppercase w-[35%]">
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[35%]">
|
||||
{t('settings.apiKeys.sourceDoc')}
|
||||
</th>
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver uppercase w-[25%]">
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[25%]">
|
||||
<span className="hidden sm:inline">
|
||||
{t('settings.apiKeys.key')}
|
||||
</span>
|
||||
@@ -139,7 +139,7 @@ export default function APIKeys() {
|
||||
{t('settings.apiKeys.key')}
|
||||
</span>
|
||||
</th>
|
||||
<th className="py-3 px-4 text-right text-xs font-medium text-gray-700 dark:text-[#E0E0E0] uppercase w-[5%]">
|
||||
<th className="py-3 px-4 text-right text-xs font-medium text-gray-700 dark:text-[#E0E0E0] w-[5%]">
|
||||
<span className="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -163,7 +163,7 @@ export default function APIKeys() {
|
||||
key={element.id}
|
||||
className="group transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
>
|
||||
<td className="py-4 px-4 text-sm text-gray-700 dark:text-[#E0E0E0] w-[35%] min-w-48 max-w-0">
|
||||
<td className="py-4 px-4 text-sm font-semibold text-gray-700 dark:text-[#E0E0E0] w-[35%] min-w-48 max-w-0">
|
||||
<div className="truncate" title={element.name}>
|
||||
{element.name}
|
||||
</div>
|
||||
@@ -225,6 +225,7 @@ export default function APIKeys() {
|
||||
submitLabel={t('modals.deleteConv.delete')}
|
||||
handleSubmit={() => handleDeleteKey(keyToDelete.id)}
|
||||
handleCancel={() => setKeyToDelete(null)}
|
||||
variant="danger"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -234,7 +234,7 @@ export default function Analytics() {
|
||||
}
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
borderColor="gray-700"
|
||||
darkBorderColor="dim-gray"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -348,7 +348,7 @@ export default function Analytics() {
|
||||
<div className="h-[345px] w-full px-6 py-5 border rounded-2xl border-silver dark:border-silver/40 overflow-hidden">
|
||||
<div className="flex flex-row items-center justify-start gap-3">
|
||||
<p className="font-bold text-jet dark:text-bright-gray">
|
||||
{t('settings.analytics.feedback')}
|
||||
{t('settings.analytics.userFeedback')}
|
||||
</p>
|
||||
<Dropdown
|
||||
size="w-[125px]"
|
||||
@@ -461,5 +461,17 @@ function AnalyticsChart({
|
||||
},
|
||||
},
|
||||
};
|
||||
return <Bar options={options} plugins={[htmlLegendPlugin]} data={data} />;
|
||||
return (
|
||||
<Bar
|
||||
options={options}
|
||||
plugins={[htmlLegendPlugin]}
|
||||
data={{
|
||||
...data,
|
||||
datasets: data.datasets.map((dataset) => ({
|
||||
...dataset,
|
||||
hoverBackgroundColor: `${dataset.backgroundColor}CC`, // 80% opacity
|
||||
})),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
@@ -6,10 +6,11 @@ import userService from '../api/services/userService';
|
||||
import ArrowLeft from '../assets/arrow-left.svg';
|
||||
import caretSort from '../assets/caret-sort.svg';
|
||||
import Edit from '../assets/edit.svg';
|
||||
import EyeView from '../assets/eye-view.svg';
|
||||
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
||||
import NoFilesIcon from '../assets/no-files.svg';
|
||||
import SyncIcon from '../assets/sync.svg';
|
||||
import Trash from '../assets/trash.svg';
|
||||
import Trash from '../assets/red-trash.svg';
|
||||
import Pagination from '../components/DocumentPagination';
|
||||
import DropdownMenu from '../components/DropdownMenu';
|
||||
import Input from '../components/Input';
|
||||
@@ -28,6 +29,8 @@ import {
|
||||
import Upload from '../upload/Upload';
|
||||
import { formatDate } from '../utils/dateTimeUtils';
|
||||
import { ChunkType } from './types';
|
||||
import ContextMenu, { MenuOption } from '../components/ContextMenu';
|
||||
import ThreeDots from '../assets/three-dots.svg';
|
||||
|
||||
const formatTokens = (tokens: number): string => {
|
||||
const roundToTwoDecimals = (num: number): string => {
|
||||
@@ -63,6 +66,53 @@ export default function Documents({
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
|
||||
const [totalPages, setTotalPages] = useState<number>(1);
|
||||
|
||||
const [activeMenuId, setActiveMenuId] = useState<string | null>(null);
|
||||
const menuRefs = useRef<{ [key: string]: React.RefObject<HTMLDivElement> }>(
|
||||
{},
|
||||
);
|
||||
|
||||
// Create or get a ref for each document wrapper div (not the td)
|
||||
const getMenuRef = (docId: string) => {
|
||||
if (!menuRefs.current[docId]) {
|
||||
menuRefs.current[docId] = React.createRef<HTMLDivElement>();
|
||||
}
|
||||
return menuRefs.current[docId];
|
||||
};
|
||||
|
||||
const handleMenuClick = (e: React.MouseEvent, docId: string) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const isAnyMenuOpen =
|
||||
(syncMenuState.isOpen && syncMenuState.docId === docId) ||
|
||||
activeMenuId === docId;
|
||||
|
||||
if (isAnyMenuOpen) {
|
||||
setSyncMenuState((prev) => ({ ...prev, isOpen: false, docId: null }));
|
||||
setActiveMenuId(null);
|
||||
return;
|
||||
}
|
||||
setActiveMenuId(docId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (activeMenuId) {
|
||||
const activeRef = menuRefs.current[activeMenuId];
|
||||
if (
|
||||
activeRef?.current &&
|
||||
!activeRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setActiveMenuId(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [activeMenuId]);
|
||||
|
||||
const currentDocuments = paginatedDocuments ?? [];
|
||||
const syncOptions = [
|
||||
{ label: t('settings.documents.syncFrequency.never'), value: 'never' },
|
||||
@@ -71,6 +121,16 @@ export default function Documents({
|
||||
{ label: t('settings.documents.syncFrequency.monthly'), value: 'monthly' },
|
||||
];
|
||||
const [showDocumentChunks, setShowDocumentChunks] = useState<Doc>();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [syncMenuState, setSyncMenuState] = useState<{
|
||||
isOpen: boolean;
|
||||
docId: string | null;
|
||||
document: Doc | null;
|
||||
}>({
|
||||
isOpen: false,
|
||||
docId: null,
|
||||
document: null,
|
||||
});
|
||||
|
||||
const refreshDocs = useCallback(
|
||||
(
|
||||
@@ -169,6 +229,50 @@ export default function Documents({
|
||||
}
|
||||
};
|
||||
|
||||
const getActionOptions = (index: number, document: Doc): MenuOption[] => {
|
||||
const actions: MenuOption[] = [
|
||||
{
|
||||
icon: EyeView,
|
||||
label: t('settings.documents.view'),
|
||||
onClick: () => {
|
||||
setShowDocumentChunks(document);
|
||||
},
|
||||
iconWidth: 18,
|
||||
iconHeight: 18,
|
||||
variant: 'primary',
|
||||
},
|
||||
];
|
||||
|
||||
if (document.syncFrequency) {
|
||||
actions.push({
|
||||
icon: SyncIcon,
|
||||
label: t('settings.documents.sync'),
|
||||
onClick: () => {
|
||||
setSyncMenuState({
|
||||
isOpen: true,
|
||||
docId: document.id ?? null,
|
||||
document: document,
|
||||
});
|
||||
},
|
||||
iconWidth: 14,
|
||||
iconHeight: 14,
|
||||
variant: 'primary',
|
||||
});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
icon: Trash,
|
||||
label: t('convTile.delete'),
|
||||
onClick: () => {
|
||||
handleDeleteConfirmation(index, document);
|
||||
},
|
||||
iconWidth: 18,
|
||||
iconHeight: 18,
|
||||
variant: 'danger',
|
||||
});
|
||||
|
||||
return actions;
|
||||
};
|
||||
useEffect(() => {
|
||||
refreshDocs(undefined, 1, rowsPerPage);
|
||||
}, [searchTerm]);
|
||||
@@ -208,7 +312,7 @@ export default function Documents({
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-full w-full sm:w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
|
||||
className="rounded-full w-[108px] h-[32px] text-sm bg-purple-30 text-white hover:bg-violets-are-blue flex items-center justify-center"
|
||||
title={t('settings.documents.addNew')}
|
||||
onClick={() => {
|
||||
setIsOnboarding(false);
|
||||
@@ -224,11 +328,11 @@ export default function Documents({
|
||||
<table className="w-full table-auto">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-300 dark:border-silver/40">
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver uppercase w-[45%]">
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[45%]">
|
||||
{t('settings.documents.name')}
|
||||
</th>
|
||||
<th className="py-3 px-4 text-center text-xs font-medium text-sonic-silver uppercase w-[20%]">
|
||||
<div className="flex justify-center items-center">
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[30%]">
|
||||
<div className="flex justify-start items-center">
|
||||
{t('settings.documents.date')}
|
||||
<img
|
||||
className="cursor-pointer ml-2"
|
||||
@@ -238,8 +342,8 @@ export default function Documents({
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th className="py-3 px-4 text-center text-xs font-medium text-sonic-silver uppercase w-[25%]">
|
||||
<div className="flex justify-center items-center">
|
||||
<th className="py-3 px-4 text-left text-xs font-medium text-sonic-silver w-[15%]">
|
||||
<div className="flex justify-start items-center">
|
||||
<span className="hidden sm:inline">
|
||||
{t('settings.documents.tokenUsage')}
|
||||
</span>
|
||||
@@ -254,10 +358,8 @@ export default function Documents({
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th className="py-3 px-4 text-right text-xs font-medium text-gray-700 dark:text-[#E0E0E0] uppercase w-[10%]">
|
||||
<span className="sr-only">
|
||||
{t('settings.documents.actions')}
|
||||
</span>
|
||||
<th className="py-3 px-4 sr-only w-[10%]">
|
||||
{t('settings.documents.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -274,61 +376,88 @@ export default function Documents({
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
currentDocuments.map((document, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="group transition-colors cursor-pointer"
|
||||
onClick={() => setShowDocumentChunks(document)}
|
||||
>
|
||||
<td
|
||||
className="py-4 px-4 text-sm text-gray-700 dark:text-[#E0E0E0] w-[45%] min-w-48 max-w-0 truncate group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50"
|
||||
title={document.name}
|
||||
>
|
||||
{document.name}
|
||||
</td>
|
||||
<td className="py-4 px-4 text-center text-sm text-gray-700 dark:text-[#E0E0E0] whitespace-nowrap w-[20%] group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
||||
{document.date ? formatDate(document.date) : ''}
|
||||
</td>
|
||||
<td className="py-4 px-4 text-center text-sm text-gray-700 dark:text-[#E0E0E0] whitespace-nowrap w-[25%] group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
||||
{document.tokens
|
||||
? formatTokens(+document.tokens)
|
||||
: ''}
|
||||
</td>
|
||||
<td
|
||||
className="py-4 px-4 text-right w-[10%] group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50"
|
||||
onClick={(e) => e.stopPropagation()} // Stop event propagation for the entire actions cell
|
||||
>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{!document.syncFrequency && (
|
||||
<div className="w-8"></div>
|
||||
)}
|
||||
{document.syncFrequency && (
|
||||
<DropdownMenu
|
||||
name={t('settings.documents.sync')}
|
||||
options={syncOptions}
|
||||
onSelect={(value: string) => {
|
||||
handleManageSync(document, value);
|
||||
}}
|
||||
defaultValue={document.syncFrequency}
|
||||
icon={SyncIcon}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
handleDeleteConfirmation(index, document);
|
||||
}}
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex-shrink-0"
|
||||
currentDocuments.map((document, index) => {
|
||||
const docId = document.id ? document.id.toString() : '';
|
||||
|
||||
return (
|
||||
<tr key={docId} className="group transition-colors">
|
||||
<td
|
||||
className="py-4 px-4 text-sm font-semibold text-gray-700 dark:text-[#E0E0E0] min-w-48 max-w-0 truncate group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50"
|
||||
title={document.name}
|
||||
>
|
||||
{document.name}
|
||||
</td>
|
||||
<td className="py-4 px-4 text-sm text-gray-700 dark:text-[#E0E0E0] whitespace-nowrap group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
||||
{document.date ? formatDate(document.date) : ''}
|
||||
</td>
|
||||
<td className="py-4 px-4 text-sm text-gray-700 dark:text-[#E0E0E0] whitespace-nowrap group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50">
|
||||
{document.tokens
|
||||
? formatTokens(+document.tokens)
|
||||
: ''}
|
||||
</td>
|
||||
<td
|
||||
className="py-4 px-4 text-right group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
ref={getMenuRef(docId)}
|
||||
className="flex items-center justify-end gap-3 relative"
|
||||
>
|
||||
<img
|
||||
src={Trash}
|
||||
alt={t('convTile.delete')}
|
||||
className="h-4 w-4 opacity-60 hover:opacity-100"
|
||||
{document.syncFrequency && (
|
||||
<DropdownMenu
|
||||
name={t('settings.documents.sync')}
|
||||
options={syncOptions}
|
||||
onSelect={(value: string) => {
|
||||
handleManageSync(document, value);
|
||||
}}
|
||||
defaultValue={document.syncFrequency}
|
||||
icon={SyncIcon}
|
||||
isOpen={
|
||||
syncMenuState.docId === docId &&
|
||||
syncMenuState.isOpen
|
||||
}
|
||||
onOpenChange={(isOpen) => {
|
||||
setSyncMenuState((prev) => ({
|
||||
...prev,
|
||||
isOpen,
|
||||
docId: isOpen ? docId : null,
|
||||
document: isOpen ? document : null,
|
||||
}));
|
||||
}}
|
||||
anchorRef={getMenuRef(docId)}
|
||||
position="bottom-left"
|
||||
offset={{ x: 24, y: -24 }}
|
||||
className="min-w-[120px]"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => handleMenuClick(e, docId)}
|
||||
className="inline-flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex-shrink-0"
|
||||
aria-label="Open menu"
|
||||
data-testid={`menu-button-${docId}`}
|
||||
>
|
||||
<img
|
||||
src={ThreeDots}
|
||||
alt={t('convTile.menu')}
|
||||
className="h-4 w-4 opacity-60 hover:opacity-100"
|
||||
/>
|
||||
</button>
|
||||
<ContextMenu
|
||||
isOpen={activeMenuId === docId}
|
||||
setIsOpen={(isOpen) => {
|
||||
setActiveMenuId(isOpen ? docId : null);
|
||||
}}
|
||||
options={getActionOptions(index, document)}
|
||||
anchorRef={getMenuRef(docId)}
|
||||
position="bottom-left"
|
||||
offset={{ x: 48, y: -24 }}
|
||||
className="z-50"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -380,6 +509,7 @@ export default function Documents({
|
||||
setDocumentToDelete(null);
|
||||
}}
|
||||
submitLabel={t('convTile.delete')}
|
||||
variant="danger"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -533,7 +663,7 @@ function DocumentChunks({
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-full w-full sm:w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
|
||||
className="rounded-full w-[108px] h-[32px] text-sm bg-purple-30 text-white hover:bg-violets-are-blue flex items-center justify-center"
|
||||
title={t('settings.documents.addNew')}
|
||||
onClick={() => setAddModal('ACTIVE')}
|
||||
>
|
||||
|
||||
@@ -84,9 +84,11 @@ export default function General() {
|
||||
changeLanguage(selectedLanguage?.value);
|
||||
}, [selectedLanguage, changeLanguage]);
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<div className="mb-5">
|
||||
<label className="block mb-2 font-bold text-jet dark:text-bright-gray">
|
||||
<div className="mt-12 flex flex-col gap-4">
|
||||
{' '}
|
||||
<div className="flex flex-col gap-4">
|
||||
{' '}
|
||||
<label className="font-medium text-base text-jet dark:text-bright-gray">
|
||||
{t('settings.general.selectTheme')}
|
||||
</label>
|
||||
<Dropdown
|
||||
@@ -103,8 +105,8 @@ export default function General() {
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label className="block mb-2 font-bold text-jet dark:text-bright-gray">
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="font-medium text-base text-jet dark:text-bright-gray">
|
||||
{t('settings.general.selectLanguage')}
|
||||
</label>
|
||||
<Dropdown
|
||||
@@ -121,8 +123,8 @@ export default function General() {
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label className="block font-bold text-jet dark:text-bright-gray">
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="font-medium text-base text-jet dark:text-bright-gray">
|
||||
{t('settings.general.chunks')}
|
||||
</label>
|
||||
<Dropdown
|
||||
@@ -134,8 +136,8 @@ export default function General() {
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<label className="mb-2 block font-bold text-jet dark:text-bright-gray">
|
||||
<div className="flex flex-col gap-4">
|
||||
<label className="font-medium text-base text-jet dark:text-bright-gray">
|
||||
{t('settings.general.convHistory')}
|
||||
</label>
|
||||
<Dropdown
|
||||
@@ -159,7 +161,7 @@ export default function General() {
|
||||
border="border"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Prompts
|
||||
prompts={prompts}
|
||||
selectedPrompt={selectedPrompt}
|
||||
@@ -169,12 +171,11 @@ export default function General() {
|
||||
setPrompts={setPrompts}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-56">
|
||||
<label className="block font-bold text-jet dark:text-bright-gray">
|
||||
{t('settings.general.deleteAllLabel')}
|
||||
</label>
|
||||
<hr className="border-t w-[calc(min(665px,100%))] my-4 border-silver dark:border-silver/40" />
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
className="mt-2 flex w-full cursor-pointer items-center justify-between rounded-3xl border border-solid border-red-700 px-5 py-3 text-red-700 transition-colors hover:bg-red-700 hover:text-white dark:border-red-600 dark:text-red-600 dark:hover:bg-red-600 dark:hover:text-white"
|
||||
title={t('settings.general.deleteAllLabel')}
|
||||
className="flex font-medium text-sm w-fit cursor-pointer items-center justify-between rounded-3xl border border-solid border-rosso-corsa bg-transparent px-5 py-3 text-rosso-corsa transition-colors hover:bg-rosso-corsa hover:text-white hover:font-bold tracking-[0.015em] hover:tracking-normal"
|
||||
onClick={() => dispatch(setModalStateDeleteConv('ACTIVE'))}
|
||||
>
|
||||
{t('settings.general.deleteAllBtn')}
|
||||
|
||||
@@ -110,6 +110,7 @@ export default function Logs() {
|
||||
}
|
||||
rounded="3xl"
|
||||
border="border"
|
||||
darkBorderColor="dim-gray"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -127,46 +128,78 @@ type LogsTableProps = {
|
||||
setPage: React.Dispatch<React.SetStateAction<number>>;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
function LogsTable({ logs, setPage, loading }: LogsTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const observerRef = useRef<any>();
|
||||
const firstObserver = useCallback((node: HTMLDivElement) => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) setPage((prev) => prev + 1);
|
||||
});
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const [openLogId, setOpenLogId] = useState<string | null>(null);
|
||||
|
||||
const handleLogToggle = (logId: string) => {
|
||||
if (openLogId && openLogId !== logId) {
|
||||
// If a different log is being opened, close the current one
|
||||
const currentOpenLog = document.getElementById(
|
||||
openLogId,
|
||||
) as HTMLDetailsElement;
|
||||
if (currentOpenLog) {
|
||||
currentOpenLog.open = false;
|
||||
}
|
||||
}
|
||||
if (node && observerRef.current) observerRef.current.observe(node);
|
||||
setOpenLogId(logId);
|
||||
};
|
||||
|
||||
const firstObserver = useCallback((node: HTMLDivElement | null) => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
}
|
||||
|
||||
if (!node) return;
|
||||
|
||||
observerRef.current = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
setPage((prev) => prev + 1);
|
||||
}
|
||||
});
|
||||
|
||||
observerRef.current.observe(node);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="logs-table border rounded-2xl h-[55vh] w-full overflow-hidden border-silver dark:border-silver/40">
|
||||
<div className="h-8 bg-black/10 dark:bg-chinese-black flex flex-col items-start justify-center">
|
||||
<div className="logs-table rounded-xl h-[55vh] w-full overflow-hidden bg-white dark:bg-black border border-light-silver dark:border-transparent">
|
||||
<div className="h-8 bg-black/10 dark:bg-[#191919] flex flex-col items-start justify-center">
|
||||
<p className="px-3 text-xs dark:text-gray-6000">
|
||||
{t('settings.logs.tableHeader')}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
ref={observerRef}
|
||||
className="flex flex-col items-start h-[51vh] overflow-y-auto bg-transparent flex-grow gap-px"
|
||||
>
|
||||
<div className="flex flex-col items-start h-[51vh] overflow-y-auto bg-transparent flex-grow gap-2 p-4">
|
||||
{logs?.map((log, index) => {
|
||||
if (index === logs.length - 1) {
|
||||
return (
|
||||
<div ref={firstObserver} key={index} className="w-full">
|
||||
<Log log={log} />
|
||||
<Log log={log} onToggle={handleLogToggle} />
|
||||
</div>
|
||||
);
|
||||
} else return <Log key={index} log={log} />;
|
||||
} else
|
||||
return <Log key={index} log={log} onToggle={handleLogToggle} />;
|
||||
})}
|
||||
{loading && <SkeletonLoader component="logs" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Log({ log }: { log: LogData }) {
|
||||
function Log({
|
||||
log,
|
||||
onToggle,
|
||||
}: {
|
||||
log: LogData;
|
||||
onToggle: (id: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const logLevelColor = {
|
||||
info: 'text-green-500',
|
||||
@@ -174,9 +207,18 @@ function Log({ log }: { log: LogData }) {
|
||||
warning: 'text-yellow-500',
|
||||
};
|
||||
const { id, action, timestamp, ...filteredLog } = log;
|
||||
|
||||
return (
|
||||
<details className="group bg-transparent [&_summary::-webkit-details-marker]:hidden w-full hover:bg-[#F9F9F9] hover:dark:bg-dark-charcoal">
|
||||
<summary className="flex flex-row items-start gap-2 text-gray-900 cursor-pointer p-2 group-open:bg-[#F9F9F9] dark:group-open:bg-dark-charcoal">
|
||||
<details
|
||||
id={log.id}
|
||||
className="group bg-transparent [&_summary::-webkit-details-marker]:hidden w-full hover:bg-[#F9F9F9] hover:dark:bg-dark-charcoal rounded-xl group-open:opacity-80 [&[open]]:border [&[open]]:border-[#d9d9d9]"
|
||||
onToggle={(e) => {
|
||||
if ((e.target as HTMLDetailsElement).open) {
|
||||
onToggle(log.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<summary className="flex flex-row items-start gap-2 text-gray-900 cursor-pointer px-4 py-3 group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B] group-open:rounded-t-xl p-2">
|
||||
<img
|
||||
src={ChevronRight}
|
||||
alt="Expand log entry"
|
||||
@@ -194,14 +236,15 @@ function Log({ log }: { log: LogData }) {
|
||||
</h2>
|
||||
</span>
|
||||
</summary>
|
||||
<div className="px-4 group-open:bg-[#F9F9F9] dark:group-open:bg-dark-charcoal">
|
||||
<div className="px-4 py-3 group-open:bg-[#F1F1F1] dark:group-open:bg-[#1B1B1B] group-open:rounded-b-xl">
|
||||
<p className="px-2 leading-relaxed text-gray-700 dark:text-gray-400 text-xs break-words">
|
||||
{JSON.stringify(filteredLog, null, 2)}
|
||||
</p>
|
||||
<div className="my-px w-8">
|
||||
<div className="my-px w-fit">
|
||||
<CopyButton
|
||||
text={JSON.stringify(filteredLog)}
|
||||
colorLight="transparent"
|
||||
showText={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,10 +38,7 @@ export default function Prompts({
|
||||
});
|
||||
const [modalType, setModalType] = React.useState<'ADD' | 'EDIT'>('ADD');
|
||||
const [modalState, setModalState] = React.useState<ActiveState>('INACTIVE');
|
||||
const {
|
||||
t,
|
||||
i18n: { changeLanguage, language },
|
||||
} = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleAddPrompt = async () => {
|
||||
try {
|
||||
@@ -144,11 +141,11 @@ export default function Prompts({
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="flex flex-row items-center gap-8">
|
||||
<div>
|
||||
<p className="font-semibold dark:text-bright-gray">
|
||||
{t('settings.general.prompt')}
|
||||
</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="font-medium dark:text-bright-gray">
|
||||
{t('settings.general.prompt')}
|
||||
</p>
|
||||
<div className="flex flex-row justify-start items-baseline gap-6">
|
||||
<Dropdown
|
||||
options={prompts}
|
||||
selectedValue={selectedPrompt.name}
|
||||
@@ -175,16 +172,17 @@ export default function Prompts({
|
||||
}}
|
||||
onDelete={handleDeletePrompt}
|
||||
/>
|
||||
|
||||
<button
|
||||
className="rounded-3xl w-20 h-10 text-sm border border-solid border-violets-are-blue text-violets-are-blue transition-colors hover:text-white hover:bg-violets-are-blue"
|
||||
onClick={() => {
|
||||
setModalType('ADD');
|
||||
setModalState('ACTIVE');
|
||||
}}
|
||||
>
|
||||
{t('settings.general.add')}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="mt-[24px] rounded-3xl border border-solid border-purple-30 px-5 py-3 text-purple-30 transition-colors hover:text-white hover:bg-[#6F3FD1] dark:border-purple-30 dark:text-purple-30 dark:hover:bg-purple-30 dark:hover:text-white"
|
||||
onClick={() => {
|
||||
setModalType('ADD');
|
||||
setModalState('ACTIVE');
|
||||
}}
|
||||
>
|
||||
{t('settings.general.addNew')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<PromptsModal
|
||||
|
||||
@@ -8,10 +8,12 @@ import CircleX from '../assets/circle-x.svg';
|
||||
import Trash from '../assets/trash.svg';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import Input from '../components/Input';
|
||||
import ToggleSwitch from '../components/ToggleSwitch';
|
||||
import AddActionModal from '../modals/AddActionModal';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { APIActionType, APIToolType, UserToolType } from './types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ToolConfig({
|
||||
tool,
|
||||
@@ -28,7 +30,7 @@ export default function ToolConfig({
|
||||
);
|
||||
const [actionModalState, setActionModalState] =
|
||||
React.useState<ActiveState>('INACTIVE');
|
||||
|
||||
const { t } = useTranslation();
|
||||
const handleCheckboxChange = (actionIndex: number, property: string) => {
|
||||
setTool({
|
||||
...tool,
|
||||
@@ -140,21 +142,18 @@ export default function ToolConfig({
|
||||
{Object.keys(tool?.config).length !== 0 &&
|
||||
tool.name !== 'api_tool' && (
|
||||
<div className="relative w-96">
|
||||
<span className="z-10 absolute left-5 -top-2 bg-white px-2 text-xs text-gray-4000 dark:bg-[#26272E] dark:text-silver">
|
||||
API Key / Oauth
|
||||
</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={authKey}
|
||||
onChange={(e) => setAuthKey(e.target.value)}
|
||||
borderVariant="thin"
|
||||
placeholder="Enter API Key / Oauth"
|
||||
></Input>
|
||||
placeholder={t('modals.configTool.apiKeyPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="rounded-full px-5 py-[10px] bg-purple-30 text-white hover:bg-[#6F3FD1] text-nowrap text-sm"
|
||||
className="rounded-full px-5 py-[10px] bg-purple-30 text-white hover:bg-violets-are-blue text-nowrap text-sm"
|
||||
onClick={handleSaveChanges}
|
||||
>
|
||||
Save changes
|
||||
@@ -178,7 +177,7 @@ export default function ToolConfig({
|
||||
onClick={() => {
|
||||
setActionModalState('ACTIVE');
|
||||
}}
|
||||
className="border border-solid border-purple-30 text-purple-30 dark:border-purple-30 dark:text-purple-30 transition-colors hover:bg-[#6F3FD1] hover:text-white dark:hover:bg-purple-30 dark:hover:text-white rounded-full text-sm px-5 py-1"
|
||||
className="border border-solid border-violets-are-blue text-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white rounded-full text-sm px-5 py-1"
|
||||
>
|
||||
Add action
|
||||
</button>
|
||||
@@ -198,33 +197,27 @@ export default function ToolConfig({
|
||||
<p className="font-semibold text-eerie-black dark:text-bright-gray">
|
||||
{action.name}
|
||||
</p>
|
||||
<label
|
||||
htmlFor={`actionToggle-${actionIndex}`}
|
||||
className="relative inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-300 dark:bg-[#D2D5DA33]/20 transition [-webkit-tap-highlight-color:_transparent] has-[:checked]:bg-[#0C9D35CC] has-[:checked]:dark:bg-[#0C9D35CC]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`actionToggle-${actionIndex}`}
|
||||
className="peer sr-only"
|
||||
checked={action.active}
|
||||
onChange={() => {
|
||||
setTool({
|
||||
...tool,
|
||||
actions: tool.actions.map((act, index) => {
|
||||
if (index === actionIndex) {
|
||||
return { ...act, active: !act.active };
|
||||
}
|
||||
return act;
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="absolute inset-y-0 start-0 m-[3px] size-[18px] rounded-full bg-white transition-all peer-checked:start-4"></span>
|
||||
</label>
|
||||
<ToggleSwitch
|
||||
checked={action.active}
|
||||
onChange={(checked) => {
|
||||
setTool({
|
||||
...tool,
|
||||
actions: tool.actions.map((act, index) => {
|
||||
if (index === actionIndex) {
|
||||
return { ...act, active: checked };
|
||||
}
|
||||
return act;
|
||||
}),
|
||||
});
|
||||
}}
|
||||
size="small"
|
||||
id={`actionToggle-${actionIndex}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 relative px-5 w-full sm:w-96">
|
||||
<div className="mt-5 relative">
|
||||
<Input
|
||||
type="text"
|
||||
className="w-[97%] ml-5"
|
||||
placeholder="Enter description"
|
||||
value={action.description}
|
||||
onChange={(e) => {
|
||||
@@ -242,7 +235,7 @@ export default function ToolConfig({
|
||||
});
|
||||
}}
|
||||
borderVariant="thin"
|
||||
></Input>
|
||||
/>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<table className="table-default">
|
||||
@@ -437,19 +430,12 @@ function APIToolConfig({
|
||||
<p className="font-semibold text-eerie-black dark:text-bright-gray">
|
||||
{action.name}
|
||||
</p>
|
||||
<label
|
||||
htmlFor={`actionToggle-${actionIndex}`}
|
||||
className="relative inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-300 dark:bg-[#D2D5DA33]/20 transition [-webkit-tap-highlight-color:_transparent] has-[:checked]:bg-[#0C9D35CC] has-[:checked]:dark:bg-[#0C9D35CC]"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`actionToggle-${actionIndex}`}
|
||||
className="peer sr-only"
|
||||
checked={action.active}
|
||||
onChange={() => handleActionToggle(actionName)}
|
||||
/>
|
||||
<span className="absolute inset-y-0 start-0 m-[3px] size-[18px] rounded-full bg-white transition-all peer-checked:start-4"></span>
|
||||
</label>
|
||||
<ToggleSwitch
|
||||
checked={action.active}
|
||||
onChange={() => handleActionToggle(actionName)}
|
||||
size="small"
|
||||
id={`actionToggle-${actionIndex}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8 px-5">
|
||||
<div className="relative w-full">
|
||||
@@ -844,7 +830,7 @@ function APIActionTable({
|
||||
<td colSpan={4} className="text-right">
|
||||
<button
|
||||
onClick={handleAddProperty}
|
||||
className="bg-purple-30 text-white hover:bg-[#6F3FD1] rounded-full px-5 py-[4px] mr-1 text-sm"
|
||||
className="bg-purple-30 text-white hover:bg-violets-are-blue rounded-full px-5 py-[4px] mr-1 text-sm"
|
||||
>
|
||||
{' '}
|
||||
Add{' '}
|
||||
@@ -871,7 +857,7 @@ function APIActionTable({
|
||||
<td colSpan={5}>
|
||||
<button
|
||||
onClick={() => handleAddPropertyStart(section)}
|
||||
className="flex items-start rounded-full px-5 py-[4px] border border-solid border-purple-30 text-purple-30 dark:border-purple-30 dark:text-purple-30 transition-colors hover:bg-[#6F3FD1] hover:text-white dark:hover:bg-purple-30 dark:hover:text-white text-nowrap text-sm"
|
||||
className="flex items-start rounded-full px-5 py-[4px] border border-solid text-violets-are-blue border-violets-are-blue transition-colors hover:bg-violets-are-blue hover:text-white text-nowrap text-sm"
|
||||
>
|
||||
Add New Field
|
||||
</button>
|
||||
|
||||
@@ -4,16 +4,14 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import CogwheelIcon from '../assets/cogwheel.svg';
|
||||
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
||||
import NoFilesIcon from '../assets/no-files.svg';
|
||||
import Input from '../components/Input';
|
||||
import Spinner from '../components/Spinner';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import AddToolModal from '../modals/AddToolModal';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import ToolConfig from './ToolConfig';
|
||||
import { APIToolType, UserToolType } from './types';
|
||||
import ToggleSwitch from '../components/ToggleSwitch';
|
||||
|
||||
export default function Tools() {
|
||||
const { t } = useTranslation();
|
||||
@@ -117,7 +115,7 @@ export default function Tools() {
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-full min-w-[160px] bg-purple-30 px-6 py-3 text-white hover:bg-[#6F3FD1] text-nowrap"
|
||||
className="rounded-full w-[108px] h-[30px] text-sm bg-purple-30 text-white hover:bg-violets-are-blue flex items-center justify-center"
|
||||
onClick={() => {
|
||||
setAddToolModalState('ACTIVE');
|
||||
}}
|
||||
@@ -125,6 +123,7 @@ export default function Tools() {
|
||||
{t('settings.tools.addTool')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="border-b border-light-silver dark:border-dim-gray mb-8 mt-5" />
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="mt-24 h-32 col-span-2 lg:col-span-3 flex items-center justify-center">
|
||||
@@ -132,90 +131,66 @@ export default function Tools() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{userTools.filter((tool) =>
|
||||
tool.displayName
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
).length === 0 ? (
|
||||
<div className="mt-24 col-span-2 lg:col-span-3 text-center text-gray-500 dark:text-gray-400">
|
||||
<img
|
||||
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
|
||||
alt="No tools found"
|
||||
className="h-24 w-24 mx-auto mb-2"
|
||||
/>
|
||||
{t('settings.tools.noToolsFound')}
|
||||
</div>
|
||||
) : (
|
||||
userTools
|
||||
.filter((tool) =>
|
||||
tool.displayName
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
.map((tool, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative h-56 w-full p-6 border rounded-2xl border-silver dark:border-silver/40 flex flex-col justify-between"
|
||||
<div className="flex flex-wrap gap-4 justify-center sm:justify-start">
|
||||
{userTools
|
||||
.filter((tool) =>
|
||||
tool.displayName
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
.map((tool, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="h-52 w-[300px] p-6 border rounded-2xl border-light-gainsboro dark:border-arsenic bg-white-3000 dark:bg-transparent flex flex-col justify-between relative"
|
||||
>
|
||||
<button
|
||||
onClick={() => handleSettingsClick(tool)}
|
||||
aria-label={t('settings.tools.configureToolAria', {
|
||||
toolName: tool.displayName,
|
||||
})}
|
||||
className="absolute top-4 right-4"
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<img
|
||||
src={`/toolIcons/tool_${tool.name}.svg`}
|
||||
alt={`${tool.displayName} icon`}
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
<button
|
||||
className="absolute top-3 right-3 cursor-pointer"
|
||||
onClick={() => handleSettingsClick(tool)}
|
||||
aria-label={t(
|
||||
'settings.tools.configureToolAria',
|
||||
{
|
||||
toolName: tool.displayName,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={CogwheelIcon}
|
||||
alt={t('settings.tools.settingsIconAlt')}
|
||||
className="h-[19px] w-[19px]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-[9px]">
|
||||
<p className="text-sm font-semibold text-eerie-black dark:text-[#EEEEEE] leading-relaxed">
|
||||
{tool.displayName}
|
||||
</p>
|
||||
<p className="mt-1 h-16 overflow-auto text-[13px] text-gray-600 dark:text-gray-400 leading-relaxed pr-1">
|
||||
{tool.description}
|
||||
</p>
|
||||
</div>
|
||||
<img
|
||||
src={CogwheelIcon}
|
||||
alt={t('settings.tools.settingsIconAlt')}
|
||||
className="h-[19px] w-[19px]"
|
||||
/>
|
||||
</button>
|
||||
<div className="w-full">
|
||||
<div className="px-1 w-full flex items-center">
|
||||
<img
|
||||
src={`/toolIcons/tool_${tool.name}.svg`}
|
||||
alt={`${tool.displayName} icon`}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-3 right-3">
|
||||
<label
|
||||
htmlFor={`toolToggle-${index}`}
|
||||
className="relative inline-block h-6 w-10 cursor-pointer rounded-full bg-gray-300 dark:bg-[#D2D5DA33]/20 transition [-webkit-tap-highlight-color:_transparent] has-[:checked]:bg-[#0C9D35CC] has-[:checked]:dark:bg-[#0C9D35CC]"
|
||||
<div className="mt-[9px]">
|
||||
<p
|
||||
title={tool.displayName}
|
||||
className="px-1 text-[13px] font-semibold text-raisin-black-light dark:text-bright-gray leading-relaxed capitalize truncate"
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t('settings.tools.toggleToolAria', {
|
||||
toolName: tool.displayName,
|
||||
})}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`toolToggle-${index}`}
|
||||
className="peer sr-only"
|
||||
checked={tool.status}
|
||||
onChange={() =>
|
||||
updateToolStatus(tool.id, !tool.status)
|
||||
}
|
||||
/>
|
||||
<span className="absolute inset-y-0 start-0 m-[3px] size-[18px] rounded-full bg-white transition-all peer-checked:start-4"></span>
|
||||
</label>
|
||||
{tool.displayName}
|
||||
</p>
|
||||
<p className="mt-1 px-1 h-24 overflow-auto text-[12px] text-old-silver dark:text-sonic-silver-light leading-relaxed">
|
||||
{tool.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div className="absolute bottom-4 right-4">
|
||||
<ToggleSwitch
|
||||
checked={tool.status}
|
||||
onChange={(checked) =>
|
||||
updateToolStatus(tool.id, checked)
|
||||
}
|
||||
size="small"
|
||||
id={`toolToggle-${index}`}
|
||||
ariaLabel={t('settings.tools.toggleToolAria', {
|
||||
toolName: tool.displayName,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user