mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-30 09:03:15 +00:00
(faet:input) tools pop-up
This commit is contained in:
@@ -4,9 +4,11 @@ import { useDarkTheme } from '../hooks';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PaperPlane from '../assets/paper_plane.svg';
|
||||
import SourceIcon from '../assets/source.svg';
|
||||
import ToolIcon from '../assets/tool.svg';
|
||||
import SpinnerDark from '../assets/spinner-dark.svg';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import SourcesPopup from './SourcesPopup';
|
||||
import ToolsPopup from './ToolsPopup';
|
||||
import { selectSelectedDocs } from '../preferences/preferenceSlice';
|
||||
import { ActiveState } from '../models/misc';
|
||||
import Upload from '../upload/Upload';
|
||||
@@ -29,7 +31,9 @@ export default function MessageInput({
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const sourceButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const toolButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [isSourcesPopupOpen, setIsSourcesPopupOpen] = useState(false);
|
||||
const [isToolsPopupOpen, setIsToolsPopupOpen] = useState(false);
|
||||
const [uploadModalState, setUploadModalState] =
|
||||
useState<ActiveState>('INACTIVE');
|
||||
|
||||
@@ -68,7 +72,7 @@ export default function MessageInput({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full mx-2">
|
||||
<div className="flex flex-col w-full rounded-[23px] border dark:border-grey border-dark-gray bg-lotion dark:bg-charleston-green-3 relative">
|
||||
<div className="flex flex-col w-full rounded-[23px] border dark:border-grey border-dark-gray bg-lotion dark:bg-transparent relative">
|
||||
<div className="w-full">
|
||||
<label htmlFor="message-input" className="sr-only">
|
||||
{t('inputPlaceholder')}
|
||||
@@ -80,7 +84,7 @@ export default function MessageInput({
|
||||
onChange={onChange}
|
||||
tabIndex={1}
|
||||
placeholder={t('inputPlaceholder')}
|
||||
className="inputbox-style w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-t-[23px] bg-lotion dark:bg-charleston-green-3 py-5 text-base leading-tight opacity-100 focus:outline-none dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 px-6 no-scrollbar"
|
||||
className="inputbox-style w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-t-[23px] bg-lotion dark:bg-transparent py-5 text-base leading-tight opacity-100 focus:outline-none dark:text-bright-gray dark:placeholder-bright-gray dark:placeholder-opacity-50 px-6 no-scrollbar"
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label={t('inputPlaceholder')}
|
||||
@@ -91,17 +95,28 @@ export default function MessageInput({
|
||||
<div className="flex-grow flex flex-wrap gap-2">
|
||||
<button
|
||||
ref={sourceButtonRef}
|
||||
className="flex items-center px-3 py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors"
|
||||
className="flex items-center px-3 py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors max-w-[200px]"
|
||||
onClick={() => setIsSourcesPopupOpen(!isSourcesPopupOpen)}
|
||||
>
|
||||
<img src={SourceIcon} alt="Sources" className="w-4 h-4 mr-1.5" />
|
||||
<span className="text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium">
|
||||
<img src={SourceIcon} alt="Sources" className="w-4 h-4 mr-1.5 flex-shrink-0" />
|
||||
<span className="text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium truncate overflow-hidden">
|
||||
{selectedDocs
|
||||
? selectedDocs.name
|
||||
: t('conversation.sources.title')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
ref={toolButtonRef}
|
||||
className="flex items-center px-3 py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors max-w-[200px]"
|
||||
onClick={() => setIsToolsPopupOpen(!isToolsPopupOpen)}
|
||||
>
|
||||
<img src={ToolIcon} alt="Tools" className="w-4 h-4 mr-1.5 flex-shrink-0" />
|
||||
<span className="text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium truncate overflow-hidden">
|
||||
{t('settings.tools.label')}
|
||||
</span>
|
||||
</button>
|
||||
{/*<button
|
||||
className="flex items-center px-3 py-1.5 rounded-[32px] border border-[#AAAAAA] dark:border-purple-taupe hover:bg-gray-100 dark:hover:bg-[#2C2E3C] transition-colors"
|
||||
onClick={() => setUploadModalState('ACTIVE')}
|
||||
>
|
||||
@@ -109,7 +124,7 @@ export default function MessageInput({
|
||||
<span className="text-[14px] text-[#5D5D5D] dark:text-bright-gray font-medium">
|
||||
Attach
|
||||
</span>
|
||||
</button>
|
||||
</button>*/}
|
||||
|
||||
{/* Additional badges can be added here in the future */}
|
||||
</div>
|
||||
@@ -145,6 +160,12 @@ export default function MessageInput({
|
||||
setUploadModalState={setUploadModalState}
|
||||
/>
|
||||
|
||||
<ToolsPopup
|
||||
isOpen={isToolsPopupOpen}
|
||||
onClose={() => setIsToolsPopupOpen(false)}
|
||||
anchorRef={toolButtonRef}
|
||||
/>
|
||||
|
||||
{uploadModalState === 'ACTIVE' && (
|
||||
<Upload
|
||||
receivedFile={[]}
|
||||
|
||||
200
frontend/src/components/ToolsPopup.tsx
Normal file
200
frontend/src/components/ToolsPopup.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import userService from '../api/services/userService';
|
||||
import { UserToolType } from '../settings/types';
|
||||
import Input from './Input';
|
||||
import RedirectIcon from '../assets/redirect.svg';
|
||||
import NoFilesIcon from '../assets/no-files.svg';
|
||||
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
|
||||
import CheckmarkIcon from '../assets/checkmark.svg';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
|
||||
interface ToolsPopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
anchorRef: React.RefObject<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export default function ToolsPopup({
|
||||
isOpen,
|
||||
onClose,
|
||||
anchorRef,
|
||||
}: ToolsPopupProps) {
|
||||
const { t } = useTranslation();
|
||||
const token = useSelector(selectToken);
|
||||
const [userTools, setUserTools] = React.useState<UserToolType[]>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const popupRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
popupRef.current &&
|
||||
!popupRef.current.contains(event.target as Node) &&
|
||||
anchorRef.current &&
|
||||
!anchorRef.current.contains(event.target as Node)
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [onClose, anchorRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
getUserTools();
|
||||
}
|
||||
}, [isOpen, token]);
|
||||
|
||||
const getUserTools = () => {
|
||||
setLoading(true);
|
||||
userService
|
||||
.getUserTools(token)
|
||||
.then((res) => {
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
setUserTools(data.tools);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching tools:', error);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const updateToolStatus = (toolId: string, newStatus: boolean) => {
|
||||
userService
|
||||
.updateToolStatus({ id: toolId, status: newStatus }, token)
|
||||
.then(() => {
|
||||
setUserTools((prevTools) =>
|
||||
prevTools.map((tool) =>
|
||||
tool.id === toolId ? { ...tool, status: newStatus } : tool,
|
||||
),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update tool status:', error);
|
||||
});
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={popupRef}
|
||||
className="absolute z-10 w-[462px] rounded-lg border border-light-silver dark:border-dim-gray bg-lotion dark:bg-charleston-green-2 shadow-lg"
|
||||
style={{
|
||||
bottom: anchorRef.current
|
||||
? window.innerHeight -
|
||||
anchorRef.current.getBoundingClientRect().top +
|
||||
10
|
||||
: 0,
|
||||
left: anchorRef.current
|
||||
? anchorRef.current.getBoundingClientRect().left
|
||||
: 0,
|
||||
position: 'fixed',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
||||
{t('settings.tools.label')}
|
||||
</h3>
|
||||
|
||||
<Input
|
||||
id="tool-search"
|
||||
name="tool-search"
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder={t('settings.tools.searchPlaceholder')}
|
||||
labelBgClassName="bg-lotion dark:bg-charleston-green-2"
|
||||
borderVariant="thin"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 dark:border-white"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-[#D9D9D9] dark:border-dim-gray rounded-md overflow-hidden">
|
||||
<div className="h-[440px] overflow-y-auto">
|
||||
{userTools.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full py-8">
|
||||
<img
|
||||
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
|
||||
alt="No tools found"
|
||||
className="h-24 w-24 mx-auto mb-4"
|
||||
/>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-center">
|
||||
{t('settings.tools.noToolsFound')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
userTools
|
||||
.filter((tool) =>
|
||||
tool.displayName
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
.map((tool) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
onClick={() => updateToolStatus(tool.id, !tool.status)}
|
||||
className="flex items-center justify-between p-3 border-b border-[#D9D9D9] dark:border-dim-gray hover:bg-gray-100 dark:hover:bg-charleston-green-3"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={`/toolIcons/tool_${tool.name}.svg`}
|
||||
alt={`${tool.displayName} icon`}
|
||||
className="h-6 w-6 mr-4"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{tool.displayName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
src={tool.status ? CheckmarkIcon : ''}
|
||||
alt="Tool enabled"
|
||||
width={14}
|
||||
height={14}
|
||||
className={`${!tool.status && 'hidden'}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex justify-start">
|
||||
<a
|
||||
href="/settings/tools"
|
||||
className="text-base text-purple-30 font-medium hover:text-violets-are-blue flex items-center"
|
||||
>
|
||||
{t('settings.tools.manageTools')}
|
||||
<img
|
||||
src={RedirectIcon}
|
||||
alt="Go to tools"
|
||||
className="ml-2 h-[11px] w-[11px]"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user