mirror of
https://github.com/arc53/DocsGPT.git
synced 2026-01-20 05:50:58 +00:00
Fixes: re-blink in converstaion, (refactor) prompts and validate LocalStorage prompts (#2181)
* chore(dependabot): add react-widget npm dependency updates * refactor(prompts): init on load, mv to pref slice * (refactor): searchable dropdowns are separate * (fix/ui) prompts adjust * feat(changelog): dancing stars * (fix)conversation: re-blink bubble past stream * (fix)endless GET sources, esling err --------- Co-authored-by: GH Action - Upstream Sync <action@github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import UploadToast from './components/UploadToast';
|
||||
import Conversation from './conversation/Conversation';
|
||||
import { SharedConversation } from './conversation/SharedConversation';
|
||||
import { useDarkTheme, useMediaQuery } from './hooks';
|
||||
import useDataInitializer from './hooks/useDataInitializer';
|
||||
import useTokenAuth from './hooks/useTokenAuth';
|
||||
import Navigation from './Navigation';
|
||||
import PageNotFound from './PageNotFound';
|
||||
@@ -19,6 +20,7 @@ import Notification from './components/Notification';
|
||||
|
||||
function AuthWrapper({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthLoading } = useTokenAuth();
|
||||
useDataInitializer(isAuthLoading);
|
||||
|
||||
if (isAuthLoading) {
|
||||
return (
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
} from './conversation/conversationSlice';
|
||||
import ConversationTile from './conversation/ConversationTile';
|
||||
import { useDarkTheme, useMediaQuery } from './hooks';
|
||||
import useDefaultDocument from './hooks/useDefaultDocument';
|
||||
import useTokenAuth from './hooks/useTokenAuth';
|
||||
import DeleteConvModal from './modals/DeleteConvModal';
|
||||
import JWTModal from './modals/JWTModal';
|
||||
@@ -155,7 +154,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
}, [agents, sharedAgents, token, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!conversations?.data) fetchConversations();
|
||||
if (queries.length === 0) resetConversation();
|
||||
}, [conversations?.data, dispatch]);
|
||||
|
||||
@@ -290,7 +288,6 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
|
||||
setNavOpen(!(isMobile || isTablet));
|
||||
}, [isMobile, isTablet]);
|
||||
|
||||
useDefaultDocument();
|
||||
return (
|
||||
<>
|
||||
{(isMobile || isTablet) && navOpen && (
|
||||
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
selectSelectedAgent,
|
||||
selectSourceDocs,
|
||||
selectToken,
|
||||
selectPrompts,
|
||||
setSelectedAgent,
|
||||
setPrompts,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import PromptsModal from '../preferences/PromptsModal';
|
||||
import Prompts from '../settings/Prompts';
|
||||
@@ -38,6 +40,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
const token = useSelector(selectToken);
|
||||
const sourceDocs = useSelector(selectSourceDocs);
|
||||
const selectedAgent = useSelector(selectSelectedAgent);
|
||||
const prompts = useSelector(selectPrompts);
|
||||
|
||||
const [effectiveMode, setEffectiveMode] = useState(mode);
|
||||
const [agent, setAgent] = useState<Agent>({
|
||||
@@ -62,9 +65,6 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
default_model_id: '',
|
||||
});
|
||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||
const [prompts, setPrompts] = useState<
|
||||
{ name: string; id: string; type: string }[]
|
||||
>([]);
|
||||
const [userTools, setUserTools] = useState<OptionType[]>([]);
|
||||
const [availableModels, setAvailableModels] = useState<Model[]>([]);
|
||||
const [isSourcePopupOpen, setIsSourcePopupOpen] = useState(false);
|
||||
@@ -401,14 +401,6 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
}));
|
||||
setUserTools(tools);
|
||||
};
|
||||
const getPrompts = async () => {
|
||||
const response = await userService.getPrompts(token);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch prompts');
|
||||
}
|
||||
const data = await response.json();
|
||||
setPrompts(data);
|
||||
};
|
||||
const getModels = async () => {
|
||||
const response = await modelService.getModels(null);
|
||||
if (!response.ok) throw new Error('Failed to fetch models');
|
||||
@@ -417,7 +409,6 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
setAvailableModels(transformed);
|
||||
};
|
||||
getTools();
|
||||
getPrompts();
|
||||
getModels();
|
||||
}, [token]);
|
||||
|
||||
@@ -604,7 +595,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
setHasChanges(isChanged);
|
||||
}, [agent, dispatch, effectiveMode, imageFile, jsonSchemaText]);
|
||||
return (
|
||||
<div className="flex flex-col px-4 pt-4 pb-2 max-[1179px]:min-h-[100dvh] min-[1180px]:h-[100dvh] md:px-12 md:pt-12 md:pb-3">
|
||||
<div className="flex flex-col px-4 pt-4 pb-2 max-[1179px]:min-h-dvh min-[1180px]:h-dvh md:px-12 md:pt-12 md:pb-3">
|
||||
<div className="flex items-center gap-3 px-4">
|
||||
<button
|
||||
className="rounded-full border p-3 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34]"
|
||||
@@ -834,12 +825,16 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
prompts={prompts}
|
||||
selectedPrompt={
|
||||
prompts.find((prompt) => prompt.id === agent.prompt_id) ||
|
||||
prompts[0]
|
||||
prompts[0] || {
|
||||
name: 'default',
|
||||
id: 'default',
|
||||
type: 'public',
|
||||
}
|
||||
}
|
||||
onSelectPrompt={(name, id, type) =>
|
||||
setAgent({ ...agent, prompt_id: id })
|
||||
}
|
||||
setPrompts={setPrompts}
|
||||
setPrompts={(newPrompts) => dispatch(setPrompts(newPrompts))}
|
||||
title={t('agents.form.sections.prompt')}
|
||||
titleClassName="text-lg font-semibold dark:text-[#E0E0E0]"
|
||||
showAddButton={false}
|
||||
@@ -1235,7 +1230,6 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) {
|
||||
/>
|
||||
<AddPromptModal
|
||||
prompts={prompts}
|
||||
setPrompts={setPrompts}
|
||||
isOpen={addPromptModal}
|
||||
onClose={() => setAddPromptModal('INACTIVE')}
|
||||
onSelect={(name: string, id: string, type: string) => {
|
||||
@@ -1269,17 +1263,16 @@ function AgentPreviewArea() {
|
||||
|
||||
function AddPromptModal({
|
||||
prompts,
|
||||
setPrompts,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSelect,
|
||||
}: {
|
||||
prompts: Prompt[];
|
||||
setPrompts?: React.Dispatch<React.SetStateAction<Prompt[]>>;
|
||||
isOpen: ActiveState;
|
||||
onClose: () => void;
|
||||
onSelect?: (name: string, id: string, type: string) => void;
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const token = useSelector(selectToken);
|
||||
|
||||
const [newPromptName, setNewPromptName] = useState('');
|
||||
@@ -1298,12 +1291,13 @@ function AddPromptModal({
|
||||
throw new Error('Failed to add prompt');
|
||||
}
|
||||
const newPrompt = await response.json();
|
||||
if (setPrompts) {
|
||||
// Update Redux store with new prompt
|
||||
dispatch(
|
||||
setPrompts([
|
||||
...prompts,
|
||||
{ name: newPromptName, id: newPrompt.id, type: 'private' },
|
||||
]);
|
||||
}
|
||||
]),
|
||||
);
|
||||
onClose();
|
||||
setNewPromptName('');
|
||||
setNewPromptContent('');
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.4798 10.739C9.27414 11.6748 7.7572 12.116 6.23773 11.9728C4.71826 11.8296 3.31047 11.1127 2.30094 9.96806C1.2914 8.82345 0.756002 7.33714 0.803717 5.81168C0.851432 4.28622 1.47868 2.83628 2.55777 1.75699C3.63706 0.677895 5.087 0.0506505 6.61246 0.00293578C8.13792 -0.044779 9.62423 0.490623 10.7688 1.50016C11.9135 2.50969 12.6303 3.91747 12.7736 5.43694C12.9168 6.95641 12.4756 8.47336 11.5398 9.67899L14.5798 12.719C14.6785 12.8107 14.7507 12.9273 14.7887 13.0565C14.8267 13.1858 14.8291 13.3229 14.7958 13.4534C14.7624 13.5839 14.6944 13.703 14.5991 13.7982C14.5037 13.8933 14.3844 13.961 14.2538 13.994C14.1234 14.0274 13.9864 14.0251 13.8573 13.9872C13.7281 13.9494 13.6115 13.8775 13.5198 13.779L10.4798 10.739ZM11.2998 5.99899C11.3087 5.4026 11.1989 4.81039 10.9768 4.25681C10.7547 3.70323 10.4248 3.19934 10.0062 2.77445C9.58757 2.34955 9.08865 2.01214 8.53844 1.78183C7.98824 1.55152 7.39773 1.43292 6.80127 1.43292C6.20481 1.43292 5.6143 1.55152 5.0641 1.78183C4.5139 2.01214 4.01498 2.34955 3.59637 2.77445C3.17777 3.19934 2.84783 3.70323 2.62575 4.25681C2.40367 4.81039 2.29388 5.4026 2.30277 5.99899C2.32039 7.18045 2.80208 8.30756 3.6438 9.13682C4.48552 9.96608 5.61968 10.4309 6.80127 10.4309C7.98286 10.4309 9.11703 9.96608 9.95874 9.13682C10.8005 8.30756 11.2822 7.18045 11.2998 5.99899Z" fill="#59636E"/>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.57031 0C10.1985 0.000253697 13.1396 2.94204 13.1396 6.57031C13.1396 7.77747 12.8128 8.90773 12.2442 9.87947C12.1066 10.1147 12.0378 10.2323 12.0243 10.3313C12.0115 10.4257 12.0212 10.5011 12.0575 10.5891C12.0956 10.6815 12.1813 10.7673 12.3528 10.9387L13.6914 12.2773C14.0817 12.6679 14.0818 13.301 13.6914 13.6914C13.3009 14.0815 12.6678 14.0816 12.2773 13.6914L10.9387 12.3528C10.7673 12.1813 10.6815 12.0956 10.5891 12.0575C10.5011 12.0212 10.4257 12.0115 10.3313 12.0243C10.2323 12.0378 10.1147 12.1066 9.87947 12.2442C8.90773 12.8128 7.77747 13.1396 6.57031 13.1396C2.94204 13.1396 0.000253694 10.1985 0 6.57031C0 2.94189 2.94189 0 6.57031 0ZM6.57031 2C4.04646 2 2 4.04646 2 6.57031C2.00025 9.09395 4.04661 11.1396 6.57031 11.1396C9.09379 11.1394 11.1394 9.09379 11.1396 6.57031C11.1396 4.04661 9.09395 2.00025 6.57031 2Z" fill="#6F6F6F"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 960 B |
@@ -1,7 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import close from '../assets/cross.svg';
|
||||
import rightArrow from '../assets/arrow-full-right.svg';
|
||||
import bg from '../assets/notification-bg.jpg';
|
||||
|
||||
interface NotificationProps {
|
||||
notificationText: string;
|
||||
@@ -9,6 +7,16 @@ interface NotificationProps {
|
||||
handleCloseNotification: () => void;
|
||||
}
|
||||
|
||||
const stars = Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i,
|
||||
size: Math.random() * 2 + 1, // 1-3px
|
||||
left: Math.random() * 100, // 0-100%
|
||||
top: Math.random() * 100, // 0-100%
|
||||
animationDuration: Math.random() * 3 + 2, // 2-5s
|
||||
animationDelay: Math.random() * 2, // 0-2s
|
||||
opacity: Math.random() * 0.5 + 0.3, // 0.3-0.8
|
||||
}));
|
||||
|
||||
export default function Notification({
|
||||
notificationText,
|
||||
notificationLink,
|
||||
@@ -16,32 +24,112 @@ export default function Notification({
|
||||
}: NotificationProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
className="absolute right-2 bottom-6 z-20 flex w-3/4 items-center justify-center gap-2 rounded-lg bg-cover bg-center bg-no-repeat px-2 py-4 sm:right-4 md:w-2/5 lg:w-1/3 xl:w-1/4 2xl:w-1/5"
|
||||
style={{ backgroundImage: `url(${bg})` }}
|
||||
href={notificationLink}
|
||||
target="_blank"
|
||||
aria-label={t('notification.ariaLabel')}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<p className="text-white-3000 text-xs leading-6 font-semibold xl:text-sm xl:leading-7">
|
||||
{notificationText}
|
||||
</p>
|
||||
<span>
|
||||
<img className="w-full" src={rightArrow} alt="" />
|
||||
</span>
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes twinkle {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2) translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
<button
|
||||
className="absolute top-2 right-2 z-30 h-4 w-4 hover:opacity-70"
|
||||
aria-label={t('notification.closeAriaLabel')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCloseNotification();
|
||||
.star {
|
||||
animation: twinkle var(--duration) ease-in-out infinite;
|
||||
animation-delay: var(--delay);
|
||||
}
|
||||
`}</style>
|
||||
<a
|
||||
className="group absolute right-2 bottom-6 z-20 flex w-3/4 items-center justify-center gap-2 overflow-hidden rounded-lg px-2 py-4 sm:right-4 md:w-2/5 lg:w-1/3 xl:w-1/4 2xl:w-1/5"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(90deg, #390086 0%, #6222B7 100%), linear-gradient(90deg, rgba(57, 0, 134, 0) 0%, #6222B7 53.02%, rgba(57, 0, 134, 0) 100%)',
|
||||
}}
|
||||
href={notificationLink}
|
||||
target="_blank"
|
||||
aria-label={t('notification.ariaLabel')}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<img className="w-full" src={close} alt="Close notification" />
|
||||
</button>
|
||||
</a>
|
||||
{/* Animated stars background */}
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
{stars.map((star) => (
|
||||
<svg
|
||||
key={star.id}
|
||||
className="star absolute"
|
||||
style={
|
||||
{
|
||||
width: `${star.size * 4}px`,
|
||||
height: `${star.size * 4}px`,
|
||||
left: `${star.left}%`,
|
||||
top: `${star.top}%`,
|
||||
opacity: star.opacity,
|
||||
filter: `drop-shadow(0 0 ${star.size}px rgba(255, 255, 255, 0.5))`,
|
||||
'--duration': `${star.animationDuration}s`,
|
||||
'--delay': `${star.animationDelay}s`,
|
||||
} as React.CSSProperties & {
|
||||
'--duration': string;
|
||||
'--delay': string;
|
||||
}
|
||||
}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{/* 4-pointed Christmas star */}
|
||||
<path
|
||||
d="M12 0L13.5 10.5L24 12L13.5 13.5L12 24L10.5 13.5L0 12L10.5 10.5L12 0Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-white-3000 relative z-10 text-xs leading-6 font-semibold xl:text-sm xl:leading-7">
|
||||
{notificationText}
|
||||
</p>
|
||||
<span className="relative z-10 flex items-center">
|
||||
<svg
|
||||
width="18"
|
||||
height="13"
|
||||
viewBox="0 0 18 13"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="overflow-visible"
|
||||
>
|
||||
{/* Arrow tail - grows leftward from arrow head's back point on hover */}
|
||||
<rect
|
||||
x="4"
|
||||
y="5.75"
|
||||
width="8"
|
||||
height="1.5"
|
||||
fill="white"
|
||||
className="scale-x-0 transition-transform duration-300 ease-out group-hover:scale-x-100"
|
||||
style={{ transformOrigin: '12px 6.5px' }}
|
||||
/>
|
||||
{/* Arrow head - pushed forward by the tail on hover */}
|
||||
<path
|
||||
d="M13.0303 7.03033C13.3232 6.73744 13.3232 6.26256 13.0303 5.96967L8.25736 1.1967C7.96447 0.903806 7.48959 0.903806 7.1967 1.1967C6.90381 1.48959 6.90381 1.96447 7.1967 2.25736L11.4393 6.5L7.1967 10.7426C6.90381 11.0355 6.90381 11.5104 7.1967 11.8033C7.48959 12.0962 7.96447 12.0962 8.25736 11.8033L13.0303 7.03033Z"
|
||||
fill="white"
|
||||
className="transition-transform duration-300 ease-out group-hover:translate-x-1"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<button
|
||||
className="absolute top-2 right-2 z-30 h-4 w-4 hover:opacity-70"
|
||||
aria-label={t('notification.closeAriaLabel')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCloseNotification();
|
||||
}}
|
||||
>
|
||||
<img className="w-full" src={close} alt="Close notification" />
|
||||
</button>
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
271
frontend/src/components/SearchableDropdown.tsx
Normal file
271
frontend/src/components/SearchableDropdown.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import React from 'react';
|
||||
|
||||
import Arrow2 from '../assets/dropdown-arrow.svg';
|
||||
import Edit from '../assets/edit.svg';
|
||||
import Search from '../assets/search.svg';
|
||||
import Trash from '../assets/trash.svg';
|
||||
|
||||
/**
|
||||
* SearchableDropdown - A standalone dropdown component with built-in search functionality
|
||||
*/
|
||||
|
||||
type SearchableDropdownOptionBase = {
|
||||
id?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type NameIdOption = { name: string; id: string } & SearchableDropdownOptionBase;
|
||||
|
||||
export type SearchableDropdownOption =
|
||||
| string
|
||||
| NameIdOption
|
||||
| ({ label: string; value: string } & SearchableDropdownOptionBase)
|
||||
| ({ value: number; description: string } & SearchableDropdownOptionBase);
|
||||
|
||||
export type SearchableDropdownSelectedValue = SearchableDropdownOption | null;
|
||||
|
||||
export interface SearchableDropdownProps<
|
||||
T extends SearchableDropdownOption = SearchableDropdownOption,
|
||||
> {
|
||||
options: T[];
|
||||
selectedValue: SearchableDropdownSelectedValue;
|
||||
onSelect: (value: T) => void;
|
||||
size?: string;
|
||||
/** Controls border radius for both button and dropdown menu */
|
||||
rounded?: 'xl' | '3xl';
|
||||
border?: 'border' | 'border-2';
|
||||
showEdit?: boolean;
|
||||
onEdit?: (value: NameIdOption) => void;
|
||||
showDelete?: boolean | ((option: T) => boolean);
|
||||
onDelete?: (id: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function SearchableDropdown<T extends SearchableDropdownOption>({
|
||||
options,
|
||||
selectedValue,
|
||||
onSelect,
|
||||
size = 'w-32',
|
||||
rounded = 'xl',
|
||||
border = 'border-2',
|
||||
showEdit,
|
||||
onEdit,
|
||||
showDelete,
|
||||
onDelete,
|
||||
placeholder,
|
||||
}: SearchableDropdownProps<T>) {
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
|
||||
const borderRadius = rounded === 'xl' ? 'rounded-xl' : 'rounded-3xl';
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen && searchInputRef.current) {
|
||||
searchInputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const getOptionText = (option: SearchableDropdownOption): string => {
|
||||
if (typeof option === 'string') return option;
|
||||
if ('name' in option) return option.name;
|
||||
if ('label' in option) return option.label;
|
||||
if ('description' in option) return option.description;
|
||||
return '';
|
||||
};
|
||||
|
||||
const filteredOptions = React.useMemo(() => {
|
||||
if (!searchQuery.trim()) return options;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return options.filter((option) =>
|
||||
getOptionText(option).toLowerCase().includes(query),
|
||||
);
|
||||
}, [options, searchQuery]);
|
||||
|
||||
const getDisplayValue = (): string => {
|
||||
if (!selectedValue) return placeholder ?? 'From URL';
|
||||
if (typeof selectedValue === 'string') return selectedValue;
|
||||
if ('label' in selectedValue) return selectedValue.label;
|
||||
if ('name' in selectedValue) return selectedValue.name;
|
||||
if ('description' in selectedValue) {
|
||||
return selectedValue.value < 1e9
|
||||
? `${selectedValue.value} (${selectedValue.description})`
|
||||
: selectedValue.description;
|
||||
}
|
||||
return placeholder ?? 'From URL';
|
||||
};
|
||||
|
||||
const isOptionSelected = (option: T): boolean => {
|
||||
if (!selectedValue) return false;
|
||||
if (typeof selectedValue === 'string')
|
||||
return selectedValue === (option as unknown as string);
|
||||
if (typeof option === 'string') return false;
|
||||
|
||||
const optionObj = option as Record<string, unknown>;
|
||||
const selectedObj = selectedValue as Record<string, unknown>;
|
||||
|
||||
if ('name' in optionObj && 'name' in selectedObj)
|
||||
return selectedObj.name === optionObj.name;
|
||||
if ('label' in optionObj && 'label' in selectedObj)
|
||||
return selectedObj.label === optionObj.label;
|
||||
if ('value' in optionObj && 'value' in selectedObj)
|
||||
return selectedObj.value === optionObj.value;
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative ${typeof selectedValue === 'string' ? '' : 'align-middle'} ${size}`}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`flex w-full cursor-pointer items-center justify-between ${border} border-silver dark:border-dim-gray bg-white px-5 py-3 dark:bg-transparent ${borderRadius}`}
|
||||
>
|
||||
<span
|
||||
className={`dark:text-bright-gray truncate ${!selectedValue ? 'text-gray-500 dark:text-gray-400' : ''}`}
|
||||
>
|
||||
{getDisplayValue()}
|
||||
</span>
|
||||
<img
|
||||
src={Arrow2}
|
||||
alt="arrow"
|
||||
className={`h-3 w-3 transform transition-transform ${isOpen ? 'rotate-180' : 'rotate-0'}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`absolute right-0 left-0 z-20 mt-2 ${borderRadius} dark:bg-dark-charcoal bg-[#FBFBFB] shadow-[0px_24px_48px_0px_#00000029]`}
|
||||
>
|
||||
<div
|
||||
className={`border-silver dark:border-dim-gray dark:bg-dark-charcoal sticky top-0 z-10 border-b bg-[#FBFBFB] px-3 py-2 ${rounded === 'xl' ? 'rounded-t-xl' : 'rounded-t-3xl'}`}
|
||||
>
|
||||
<div className="relative flex items-center">
|
||||
<img
|
||||
src={Search}
|
||||
alt="search"
|
||||
width={14}
|
||||
height={14}
|
||||
className="absolute left-3"
|
||||
/>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search..."
|
||||
className="dark:text-bright-gray w-full rounded-lg border-0 bg-transparent py-2 pr-3 pl-10 font-['Inter'] text-[14px] leading-[16.5px] font-normal focus:ring-0 focus:outline-none"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-40 overflow-y-auto">
|
||||
{filteredOptions.length === 0 ? (
|
||||
<div className="px-5 py-3 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
No results found
|
||||
</div>
|
||||
) : (
|
||||
filteredOptions.map((option, index) => {
|
||||
const selected = isOptionSelected(option);
|
||||
const optionObj =
|
||||
typeof option !== 'string'
|
||||
? (option as Record<string, unknown>)
|
||||
: null;
|
||||
const optionType = optionObj?.type as string | undefined;
|
||||
const optionId = optionObj?.id as string | undefined;
|
||||
const optionName = optionObj?.name as string | undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex cursor-pointer items-center justify-between hover:bg-[#ECECEC] dark:hover:bg-[#545561] ${selected ? 'bg-[#ECECEC] dark:bg-[#545561]' : ''}`}
|
||||
>
|
||||
<span
|
||||
onClick={() => {
|
||||
onSelect(option);
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
}}
|
||||
className="dark:text-light-gray ml-5 flex-1 overflow-hidden py-3 font-['Inter'] text-[14px] leading-[16.5px] font-normal text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{getOptionText(option)}
|
||||
</span>
|
||||
{showEdit &&
|
||||
onEdit &&
|
||||
optionObj &&
|
||||
optionType !== 'public' && (
|
||||
<img
|
||||
src={Edit}
|
||||
alt="Edit"
|
||||
className="mr-4 h-4 w-4 cursor-pointer hover:opacity-50"
|
||||
onClick={() => {
|
||||
if (optionName && optionId) {
|
||||
onEdit({
|
||||
id: optionId,
|
||||
name: optionName,
|
||||
type: optionType,
|
||||
});
|
||||
}
|
||||
setIsOpen(false);
|
||||
setSearchQuery('');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showDelete && onDelete && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const id =
|
||||
typeof option === 'string'
|
||||
? option
|
||||
: (optionId ?? '');
|
||||
onDelete(id);
|
||||
}}
|
||||
className={`mr-2 h-4 w-4 cursor-pointer hover:opacity-50 ${
|
||||
typeof showDelete === 'function' &&
|
||||
!showDelete(option)
|
||||
? 'hidden'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={Trash}
|
||||
alt="Delete"
|
||||
className={`mr-2 h-4 w-4 cursor-pointer hover:opacity-50 ${
|
||||
optionType === 'public'
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchableDropdown;
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import ArrowDown from '../assets/arrow-down.svg';
|
||||
import RetryIcon from '../components/RetryIcon';
|
||||
@@ -15,7 +14,6 @@ import Hero from '../Hero';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import ConversationBubble from './ConversationBubble';
|
||||
import { FEEDBACK, Query, Status } from './conversationModels';
|
||||
import { selectConversationId } from '../preferences/preferenceSlice';
|
||||
|
||||
const SCROLL_THRESHOLD = 10;
|
||||
const LAST_BUBBLE_MARGIN = 'mb-32';
|
||||
@@ -52,7 +50,6 @@ export default function ConversationMessages({
|
||||
}: ConversationMessagesProps) {
|
||||
const [isDarkTheme] = useDarkTheme();
|
||||
const { t } = useTranslation();
|
||||
const conversationId = useSelector(selectConversationId);
|
||||
|
||||
const conversationRef = useRef<HTMLDivElement>(null);
|
||||
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
|
||||
@@ -145,7 +142,7 @@ export default function ConversationMessages({
|
||||
return (
|
||||
<ConversationBubble
|
||||
className={bubbleMargin}
|
||||
key={`${conversationId}-${index}-ANSWER`}
|
||||
key={`${index}-ANSWER`}
|
||||
message={query.response}
|
||||
type={'ANSWER'}
|
||||
thought={query.thought}
|
||||
@@ -183,7 +180,7 @@ export default function ConversationMessages({
|
||||
return (
|
||||
<ConversationBubble
|
||||
className={bubbleMargin}
|
||||
key={`${conversationId}-${index}-ERROR`}
|
||||
key={`${index}-ERROR`}
|
||||
message={query.error}
|
||||
type="ERROR"
|
||||
retryBtn={retryButton}
|
||||
@@ -222,10 +219,10 @@ export default function ConversationMessages({
|
||||
|
||||
{queries.length > 0 ? (
|
||||
queries.map((query, index) => (
|
||||
<Fragment key={`${conversationId}-${index}-query-fragment`}>
|
||||
<Fragment key={`${index}-query-fragment`}>
|
||||
<ConversationBubble
|
||||
className={index === 0 ? FIRST_QUESTION_BUBBLE_MARGIN_TOP : ''}
|
||||
key={`${conversationId}-${index}-QUESTION`}
|
||||
key={`${index}-QUESTION`}
|
||||
message={query.prompt}
|
||||
type="QUESTION"
|
||||
handleUpdatedQuestionSubmission={handleQuestionSubmission}
|
||||
|
||||
111
frontend/src/hooks/useDataInitializer.ts
Normal file
111
frontend/src/hooks/useDataInitializer.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { Doc } from '../models/misc';
|
||||
import {
|
||||
getDocs,
|
||||
getConversations,
|
||||
getPrompts,
|
||||
} from '../preferences/preferenceApi';
|
||||
import {
|
||||
selectConversations,
|
||||
selectSelectedDocs,
|
||||
selectToken,
|
||||
setConversations,
|
||||
setPrompts,
|
||||
setSelectedDocs,
|
||||
setSourceDocs,
|
||||
} from '../preferences/preferenceSlice';
|
||||
|
||||
/**
|
||||
* useDataInitializer Hook
|
||||
*
|
||||
* Custom hook responsible for initializing all application data on mount.
|
||||
* This hook handles:
|
||||
* - Fetching and setting up documents (source docs and selected docs)
|
||||
* - Fetching and setting up prompts
|
||||
* - Fetching and setting up conversations
|
||||
*
|
||||
* @param isAuthLoading -
|
||||
*/
|
||||
export default function useDataInitializer(isAuthLoading: boolean) {
|
||||
const dispatch = useDispatch();
|
||||
const token = useSelector(selectToken);
|
||||
const selectedDoc = useSelector(selectSelectedDocs);
|
||||
const conversations = useSelector(selectConversations);
|
||||
|
||||
// Initialize documents
|
||||
useEffect(() => {
|
||||
// Skip if auth is still loading
|
||||
if (isAuthLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchDocs = async () => {
|
||||
try {
|
||||
const data = await getDocs(token);
|
||||
dispatch(setSourceDocs(data));
|
||||
|
||||
// Auto-select default document if none selected
|
||||
if (
|
||||
!selectedDoc ||
|
||||
(Array.isArray(selectedDoc) && selectedDoc.length === 0)
|
||||
) {
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((doc: Doc) => {
|
||||
if (doc.model && doc.name === 'default') {
|
||||
dispatch(setSelectedDocs([doc]));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch documents:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDocs();
|
||||
}, [isAuthLoading, token]);
|
||||
|
||||
// Initialize prompts
|
||||
useEffect(() => {
|
||||
// Skip if auth is still loading
|
||||
if (isAuthLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchPromptsData = async () => {
|
||||
try {
|
||||
const data = await getPrompts(token);
|
||||
dispatch(setPrompts(data));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch prompts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPromptsData();
|
||||
}, [isAuthLoading, token]);
|
||||
|
||||
// Initialize conversations
|
||||
useEffect(() => {
|
||||
// Skip if auth is still loading
|
||||
if (isAuthLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchConversationsData = async () => {
|
||||
if (!conversations?.data) {
|
||||
dispatch(setConversations({ ...conversations, loading: true }));
|
||||
try {
|
||||
const fetchedConversations = await getConversations(token);
|
||||
dispatch(setConversations(fetchedConversations));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch conversations:', error);
|
||||
dispatch(setConversations({ data: null, loading: false }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchConversationsData();
|
||||
}, [isAuthLoading, conversations?.data, token]);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { Doc } from '../models/misc';
|
||||
import { getDocs } from '../preferences/preferenceApi';
|
||||
import {
|
||||
selectSelectedDocs,
|
||||
selectToken,
|
||||
setSelectedDocs,
|
||||
setSourceDocs,
|
||||
} from '../preferences/preferenceSlice';
|
||||
|
||||
export default function useDefaultDocument() {
|
||||
const dispatch = useDispatch();
|
||||
const token = useSelector(selectToken);
|
||||
const selectedDoc = useSelector(selectSelectedDocs);
|
||||
|
||||
const fetchDocs = () => {
|
||||
getDocs(token).then((data) => {
|
||||
dispatch(setSourceDocs(data));
|
||||
if (
|
||||
!selectedDoc ||
|
||||
(Array.isArray(selectedDoc) && selectedDoc.length === 0)
|
||||
)
|
||||
Array.isArray(data) &&
|
||||
data?.forEach((doc: Doc) => {
|
||||
if (doc.model && doc.name === 'default') {
|
||||
dispatch(setSelectedDocs([doc]));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchDocs();
|
||||
}, []);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import conversationService from '../api/services/conversationService';
|
||||
import userService from '../api/services/userService';
|
||||
import { Doc, GetDocsResponse } from '../models/misc';
|
||||
import { Doc, GetDocsResponse, Prompt } from '../models/misc';
|
||||
import { GetConversationsResult, ConversationSummary } from './types';
|
||||
|
||||
//Fetches all JSON objects from the source. We only use the objects with the "model" property in SelectDocsModal.tsx. Hopefully can clean up the source file later.
|
||||
@@ -113,17 +113,40 @@ export function getLocalRecentDocs(sourceDocs?: Doc[] | null): Doc[] | null {
|
||||
return validDocs.length > 0 ? validDocs : null;
|
||||
}
|
||||
|
||||
export function getLocalPrompt(): string | null {
|
||||
const prompt = localStorage.getItem('DocsGPTPrompt');
|
||||
return prompt;
|
||||
export function getLocalPrompt(
|
||||
availablePrompts?: Prompt[] | null,
|
||||
): Prompt | null {
|
||||
const promptString = localStorage.getItem('DocsGPTPrompt');
|
||||
const selectedPrompt = promptString
|
||||
? (JSON.parse(promptString) as Prompt)
|
||||
: null;
|
||||
|
||||
if (!availablePrompts || !selectedPrompt) {
|
||||
return selectedPrompt;
|
||||
}
|
||||
|
||||
const isPromptAvailable = (selected: Prompt) => {
|
||||
return availablePrompts.some((available) => {
|
||||
return available.id === selected.id;
|
||||
});
|
||||
};
|
||||
|
||||
const isValid = isPromptAvailable(selectedPrompt);
|
||||
|
||||
if (!isValid) {
|
||||
localStorage.removeItem('DocsGPTPrompt');
|
||||
return null;
|
||||
}
|
||||
|
||||
return selectedPrompt;
|
||||
}
|
||||
|
||||
export function setLocalApiKey(key: string): void {
|
||||
localStorage.setItem('DocsGPTApiKey', key);
|
||||
}
|
||||
|
||||
export function setLocalPrompt(prompt: string): void {
|
||||
localStorage.setItem('DocsGPTPrompt', prompt);
|
||||
export function setLocalPrompt(prompt: Prompt): void {
|
||||
localStorage.setItem('DocsGPTPrompt', JSON.stringify(prompt));
|
||||
}
|
||||
|
||||
export function setLocalRecentDocs(docs: Doc[] | null): void {
|
||||
@@ -133,3 +156,17 @@ export function setLocalRecentDocs(docs: Doc[] | null): void {
|
||||
localStorage.removeItem('DocsGPTRecentDocs');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPrompts(token: string | null): Promise<Prompt[]> {
|
||||
try {
|
||||
const response = await userService.getPrompts(token);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch prompts');
|
||||
}
|
||||
const data = await response.json();
|
||||
return data as Prompt[];
|
||||
} catch (error) {
|
||||
console.error('Error fetching prompts:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
} from '@reduxjs/toolkit';
|
||||
|
||||
import { Agent } from '../agents/types';
|
||||
import { ActiveState, Doc } from '../models/misc';
|
||||
import { ActiveState, Doc, Prompt } from '../models/misc';
|
||||
import { RootState } from '../store';
|
||||
import {
|
||||
getLocalPrompt,
|
||||
getLocalRecentDocs,
|
||||
setLocalApiKey,
|
||||
setLocalRecentDocs,
|
||||
@@ -18,6 +19,7 @@ import type { Model } from '../models/types';
|
||||
export interface Preference {
|
||||
apiKey: string;
|
||||
prompt: { name: string; id: string; type: string };
|
||||
prompts: Prompt[];
|
||||
chunks: string;
|
||||
token_limit: number;
|
||||
selectedDocs: Doc[];
|
||||
@@ -41,6 +43,11 @@ export interface Preference {
|
||||
const initialState: Preference = {
|
||||
apiKey: 'xxx',
|
||||
prompt: { name: 'default', id: 'default', type: 'public' },
|
||||
prompts: [
|
||||
{ name: 'default', id: 'default', type: 'public' },
|
||||
{ name: 'creative', id: 'creative', type: 'public' },
|
||||
{ name: 'strict', id: 'strict', type: 'public' },
|
||||
],
|
||||
chunks: '2',
|
||||
token_limit: 2000,
|
||||
selectedDocs: [
|
||||
@@ -95,6 +102,9 @@ export const prefSlice = createSlice({
|
||||
setPrompt: (state, action) => {
|
||||
state.prompt = action.payload;
|
||||
},
|
||||
setPrompts: (state, action: PayloadAction<Prompt[]>) => {
|
||||
state.prompts = action.payload;
|
||||
},
|
||||
setChunks: (state, action) => {
|
||||
state.chunks = action.payload;
|
||||
},
|
||||
@@ -135,6 +145,7 @@ export const {
|
||||
setConversations,
|
||||
setToken,
|
||||
setPrompt,
|
||||
setPrompts,
|
||||
setChunks,
|
||||
setTokenLimit,
|
||||
setModalStateDeleteConv,
|
||||
@@ -217,6 +228,27 @@ prefListenerMiddleware.startListening({
|
||||
},
|
||||
});
|
||||
|
||||
prefListenerMiddleware.startListening({
|
||||
matcher: isAnyOf(setPrompts),
|
||||
effect: (_action, listenerApi) => {
|
||||
const state = listenerApi.getState() as RootState;
|
||||
const availablePrompts = state.preference.prompts;
|
||||
if (availablePrompts && availablePrompts.length > 0) {
|
||||
const validatedPrompt = getLocalPrompt(availablePrompts);
|
||||
if (validatedPrompt !== null) {
|
||||
listenerApi.dispatch(setPrompt(validatedPrompt));
|
||||
} else {
|
||||
const defaultPrompt =
|
||||
availablePrompts.find((p) => p.id === 'default') ||
|
||||
availablePrompts[0];
|
||||
if (defaultPrompt) {
|
||||
listenerApi.dispatch(setPrompt(defaultPrompt));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
prefListenerMiddleware.startListening({
|
||||
matcher: isAnyOf(setSelectedModel),
|
||||
effect: (action, listenerApi) => {
|
||||
@@ -247,6 +279,7 @@ export const selectConversationId = (state: RootState) =>
|
||||
state.conversation.conversationId;
|
||||
export const selectToken = (state: RootState) => state.preference.token;
|
||||
export const selectPrompt = (state: RootState) => state.preference.prompt;
|
||||
export const selectPrompts = (state: RootState) => state.preference.prompts;
|
||||
export const selectChunks = (state: RootState) => state.preference.chunks;
|
||||
export const selectTokenLimit = (state: RootState) =>
|
||||
state.preference.token_limit;
|
||||
|
||||
@@ -2,17 +2,17 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import {
|
||||
selectChunks,
|
||||
selectPrompt,
|
||||
selectToken,
|
||||
selectPrompts,
|
||||
selectTokenLimit,
|
||||
setChunks,
|
||||
setModalStateDeleteConv,
|
||||
setPrompt,
|
||||
setPrompts,
|
||||
setTokenLimit,
|
||||
} from '../preferences/preferenceSlice';
|
||||
import Prompts from './Prompts';
|
||||
@@ -22,7 +22,6 @@ export default function General() {
|
||||
t,
|
||||
i18n: { changeLanguage },
|
||||
} = useTranslation();
|
||||
const token = useSelector(selectToken);
|
||||
const themes = [
|
||||
{ value: 'Light', label: t('settings.general.light') },
|
||||
{ value: 'Dark', label: t('settings.general.dark') },
|
||||
@@ -46,9 +45,7 @@ export default function General() {
|
||||
[4000, t('settings.general.high')],
|
||||
[1e9, t('settings.general.unlimited')],
|
||||
]);
|
||||
const [prompts, setPrompts] = React.useState<
|
||||
{ name: string; id: string; type: string }[]
|
||||
>([]);
|
||||
const prompts = useSelector(selectPrompts);
|
||||
const selectedChunks = useSelector(selectChunks);
|
||||
const selectedTokenLimit = useSelector(selectTokenLimit);
|
||||
const [isDarkTheme, toggleTheme] = useDarkTheme();
|
||||
@@ -64,22 +61,6 @@ export default function General() {
|
||||
);
|
||||
const selectedPrompt = useSelector(selectPrompt);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleFetchPrompts = async () => {
|
||||
try {
|
||||
const response = await userService.getPrompts(token);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch prompts');
|
||||
}
|
||||
const promptsData = await response.json();
|
||||
setPrompts(promptsData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
handleFetchPrompts();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
localStorage.setItem('docsgpt-locale', selectedLanguage?.value as string);
|
||||
changeLanguage(selectedLanguage?.value);
|
||||
@@ -169,7 +150,7 @@ export default function General() {
|
||||
onSelectPrompt={(name, id, type) =>
|
||||
dispatch(setPrompt({ name: name, id: id, type: type }))
|
||||
}
|
||||
setPrompts={setPrompts}
|
||||
setPrompts={(newPrompts) => dispatch(setPrompts(newPrompts))}
|
||||
dropdownProps={{ size: 'w-56', rounded: '3xl', border: 'border' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import userService from '../api/services/userService';
|
||||
import Dropdown from '../components/Dropdown';
|
||||
import SearchableDropdown from '../components/SearchableDropdown';
|
||||
import { DropdownProps } from '../components/types/Dropdown.types';
|
||||
import ConfirmationModal from '../modals/ConfirmationModal';
|
||||
import { ActiveState, PromptProps } from '../models/misc';
|
||||
@@ -103,7 +103,12 @@ export default function Prompts({
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete prompt');
|
||||
}
|
||||
if (prompts.length > 0) {
|
||||
// Only change selection if we're deleting the currently selected prompt
|
||||
if (
|
||||
prompts.length > 0 &&
|
||||
selectedPrompt &&
|
||||
selectedPrompt.id === promptToDelete.id
|
||||
) {
|
||||
const firstPrompt = prompts.find((p) => p.id !== promptToDelete.id);
|
||||
if (firstPrompt) {
|
||||
onSelectPrompt(
|
||||
@@ -182,7 +187,7 @@ export default function Prompts({
|
||||
{title ? title : t('settings.general.prompt')}
|
||||
</p>
|
||||
<div className="flex flex-row flex-wrap items-baseline justify-start gap-6">
|
||||
<Dropdown
|
||||
<SearchableDropdown
|
||||
options={prompts.map((prompt: any) =>
|
||||
typeof prompt === 'string'
|
||||
? { name: prompt, id: prompt, type: '' }
|
||||
|
||||
@@ -25,6 +25,11 @@ const preloadedState: { preference: Preference } = {
|
||||
prompt !== null
|
||||
? JSON.parse(prompt)
|
||||
: { name: 'default', id: 'default', type: 'private' },
|
||||
prompts: [
|
||||
{ name: 'default', id: 'default', type: 'public' },
|
||||
{ name: 'creative', id: 'creative', type: 'public' },
|
||||
{ name: 'strict', id: 'strict', type: 'public' },
|
||||
],
|
||||
chunks: JSON.parse(chunks ?? '2').toString(),
|
||||
token_limit: token_limit ? parseInt(token_limit) : 2000,
|
||||
selectedDocs: doc !== null ? JSON.parse(doc) : [],
|
||||
|
||||
Reference in New Issue
Block a user