Skip to content

Commit 3e78973

Browse files
committed
Refactor Chat Header and UploadFileModal components for improved functionality
- Removed unused model selection logic from the Chat Header, simplifying the component. - Enhanced UploadFileModal by introducing a create folder feature and improving folder loading logic with a callback for better state management. - Updated error handling and user feedback for folder selection, ensuring a smoother user experience.
1 parent 5e54dab commit 3e78973

2 files changed

Lines changed: 141 additions & 92 deletions

File tree

client/src/components/Chat/Header.tsx

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,16 @@
11
import { AnimatePresence, motion } from 'framer-motion';
22
import { useState } from 'react';
33
import { useOutletContext } from 'react-router-dom';
4-
import type { TStartupConfig } from 'librechat-data-provider';
54
import type { ContextType } from '~/common';
65
import Settings from '~/components/Nav/Settings';
7-
import { useGetStartupConfig } from '~/data-provider';
86
import { useLocalize } from '~/hooks';
97
import { asset } from '~/utils/assetPath';
10-
import ModelSelector from '~/components/Chat/Menus/Endpoints/ModelSelector';
118
import { HeaderNewChat } from './Menus';
129

13-
/** When model selection is disabled in config, show the configured label only (no dropdown). */
14-
function FixedModelLabel({ startupConfig }: { startupConfig: TStartupConfig | undefined }) {
15-
const list = startupConfig?.modelSpecs?.list ?? [];
16-
const spec = list.find((s) => s.default) ?? list[0];
17-
const label = spec?.label ?? spec?.preset?.model ?? '';
18-
if (!label) {
19-
return null;
20-
}
21-
return (
22-
<div className="border-border-light bg-surface-secondary text-text-primary my-1 flex h-10 max-w-[70vw] items-center justify-center gap-2 rounded-xl border px-3 py-2 text-sm">
23-
<span className="flex-grow truncate text-left">{label}</span>
24-
</div>
25-
);
26-
}
27-
2810
export default function Header() {
2911
const { navVisible } = useOutletContext<ContextType>();
30-
const { data: startupConfig } = useGetStartupConfig();
3112
const localize = useLocalize();
3213
const [showSettings, setShowSettings] = useState(false);
33-
const modelSelectionDisabled = startupConfig?.interface?.modelSelect === false;
3414

3515
return (
3616
<div className="bg-fig-Surface-standard text-fig-Text-heading sticky top-0 z-10 flex h-14 w-full items-center justify-between p-2 font-semibold">
@@ -60,16 +40,9 @@ export default function Header() {
6040
<div className="fy-typography-title">FIA - FYERS Intelligent Assistant</div>
6141
</div>
6242

63-
{/* Right: model row + Settings inside same scale-75 block (gear sits beside model button) */}
43+
{/* Right: Settings */}
6444
<div className="mx-1 flex items-center">
6545
<div className="flex flex-row items-center gap-2 scale-75 origin-right">
66-
<div className="min-w-0 max-w-md flex-1">
67-
{modelSelectionDisabled ? (
68-
<FixedModelLabel startupConfig={startupConfig} />
69-
) : (
70-
<ModelSelector startupConfig={startupConfig} />
71-
)}
72-
</div>
7346
<button
7447
type="button"
7548
onClick={() => setShowSettings(true)}

client/src/components/Resources/Modals/UploadFileModal.tsx

Lines changed: 140 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { useState, useRef, useEffect } from 'react';
1+
import { useState, useRef, useEffect, useCallback } from 'react';
22
import * as Ariakit from '@ariakit/react';
33
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@librechat/client';
44
import { Button } from '@librechat/client';
55
import { useToastContext, DropdownPopup } from '@librechat/client';
66
import { saasApi } from '~/services/saasApi';
7-
import { File as FileIcon, ChevronDown, X } from 'lucide-react';
7+
import { File as FileIcon, ChevronDown, Plus, X } from 'lucide-react';
88
import { cn } from '~/utils';
9+
import { asset } from '~/utils/assetPath';
10+
import CreateFolderModal from '~/components/Resources/Modals/CreateFolderModal';
911
import {
1012
isResearchAllowedUploadFile,
1113
RESEARCH_ALLOWED_ACCEPT,
@@ -57,11 +59,19 @@ interface FlatFolder {
5759

5860
const EMPTY_FOLDER_LIST: any[] = [];
5961

62+
const createFolderButtonClassName = cn(
63+
'flex h-[var(--Size-zero-button)] w-full items-center justify-center gap-0.5 rounded-[2px] border border-fig-Stroke-primary',
64+
'bg-fig-Surface-two-primary px-[var(--Dimensions-Size-xs3)] py-px',
65+
'font-inter text-xs font-normal leading-4 text-fig-Subject-two-primary',
66+
'transition-opacity hover:opacity-90',
67+
'hover:!border-fig-Stroke-primary hover:!bg-fig-Surface-two-primary hover:!text-fig-Subject-two-primary',
68+
);
69+
6070
export default function UploadFileModal({
6171
folderId,
6272
orgId,
6373
folders,
64-
isSuperAdmin: _isSuperAdmin = false,
74+
isSuperAdmin: isSuperAdminProp = false,
6575
isOrgAdmin: isOrgAdminProp,
6676
currentUserId: currentUserIdProp,
6777
onClose,
@@ -79,10 +89,12 @@ export default function UploadFileModal({
7989
const [loading, setLoading] = useState(false);
8090
const [error, setError] = useState<string | null>(null);
8191
const [isFolderMenuOpen, setIsFolderMenuOpen] = useState(false);
92+
const [showCreateFolderModal, setShowCreateFolderModal] = useState(false);
8293
const fileInputRef = useRef<HTMLInputElement>(null);
8394

8495
const currentUserId = currentUserIdProp || (userInfo?.user_id || userInfo?.id)?.toString();
8596
const isOrgAdmin = isOrgAdminProp !== undefined ? isOrgAdminProp : userInfo?.org_role === 'admin';
97+
const isSuperAdmin = isSuperAdminProp || Boolean(userInfo?.is_super_admin);
8698

8799
const resolvedOrgId =
88100
orgId != null && String(orgId).trim() !== ''
@@ -94,6 +106,21 @@ export default function UploadFileModal({
94106
: null;
95107
const hasFoldersFromProps = (folders?.length ?? 0) > 0;
96108

109+
const refreshFolderTree = useCallback(async () => {
110+
setFoldersLoading(true);
111+
setFoldersError(null);
112+
try {
113+
const data = await saasApi.getFolderTree(resolvedOrgId);
114+
setFolderTree(data.folders ?? []);
115+
} catch (err) {
116+
const message = err instanceof Error ? err.message : 'Failed to load folders';
117+
setFoldersError(message);
118+
setFolderTree([]);
119+
} finally {
120+
setFoldersLoading(false);
121+
}
122+
}, [resolvedOrgId]);
123+
97124
useEffect(() => {
98125
if (hasFoldersFromProps && folders) {
99126
setFolderTree(folders);
@@ -139,23 +166,12 @@ export default function UploadFileModal({
139166
let cancelled = false;
140167

141168
const loadFolders = async () => {
142-
setFoldersLoading(true);
143-
setFoldersError(null);
144169
try {
145-
const data = await saasApi.getFolderTree(resolvedOrgId);
146-
if (!cancelled) {
147-
setFolderTree(data.folders ?? []);
148-
}
149-
} catch (err) {
170+
await refreshFolderTree();
171+
} catch {
150172
if (!cancelled) {
151-
const message = err instanceof Error ? err.message : 'Failed to load folders';
152-
setFoldersError(message);
153173
setFolderTree([]);
154174
}
155-
} finally {
156-
if (!cancelled) {
157-
setFoldersLoading(false);
158-
}
159175
}
160176
};
161177

@@ -164,7 +180,7 @@ export default function UploadFileModal({
164180
return () => {
165181
cancelled = true;
166182
};
167-
}, [hasFoldersFromProps, resolvedOrgId, userInfoLoaded]);
183+
}, [hasFoldersFromProps, refreshFolderTree, userInfoLoaded]);
168184

169185
// Flatten folder tree for dropdown
170186
const flattenFolders = (folderNodes: any[], level = 0): FlatFolder[] => {
@@ -210,6 +226,9 @@ export default function UploadFileModal({
210226
return pa - pb;
211227
});
212228

229+
const hasSelectableFolders = sortedFolders.length > 0;
230+
const showEmptyFolderBanner = !foldersLoading && !foldersError && !hasSelectableFolders;
231+
213232
useEffect(() => {
214233
const applyDefaultFolder = () => {
215234
const primary = sortedFolders.find((f) => isResearchDefaultUploadFolder(f));
@@ -296,7 +315,14 @@ export default function UploadFileModal({
296315
}
297316
}
298317

299-
if (sortedFolders.length > 0 && !selectedFolderId) {
318+
if (!hasSelectableFolders) {
319+
const msg = 'Create a folder before uploading documents.';
320+
setError(msg);
321+
showToast({ message: msg, status: 'error' });
322+
return;
323+
}
324+
325+
if (!selectedFolderId) {
300326
const msg = 'Please select a folder.';
301327
setError(msg);
302328
showToast({ message: msg, status: 'error' });
@@ -346,6 +372,7 @@ export default function UploadFileModal({
346372
};
347373

348374
return (
375+
<>
349376
<Dialog
350377
open
351378
onOpenChange={(open) => {
@@ -487,54 +514,88 @@ export default function UploadFileModal({
487514
{foldersError ? (
488515
<p className="fy-typography-body-tiny text-fig-Subject-danger">{foldersError}</p>
489516
) : null}
490-
{!foldersLoading && sortedFolders.length === 0 ? (
491-
<p className="fy-typography-body-tiny text-fig-Subject-soft">
492-
No upload folders are available.
493-
</p>
494-
) : null}
495-
<DropdownPopup
496-
portal={false}
497-
sameWidth={true}
498-
anchor={{ x: 'start', y: 'bottom' }}
499-
menuId="folder-selector-upload"
500-
isOpen={isFolderMenuOpen}
501-
setIsOpen={setIsFolderMenuOpen}
502-
trigger={
503-
<Ariakit.MenuButton
504-
className={cn(
505-
'fy-typography-body flex h-[var(--Size-zero-button)] w-full items-center justify-between',
506-
'rounded-[var(--Corner-moderatelyRounded)] border border-fig-Stroke-soft bg-fig-Surface-standard',
507-
'px-[var(--Padding-zero-spacer)] text-fig-Subject-standard',
508-
'transition-colors hover:border-fig-Stroke-standard',
509-
)}
517+
{showEmptyFolderBanner ? (
518+
<div
519+
className={cn(
520+
'flex flex-col gap-[var(--Gap-parentChild)]',
521+
'rounded-[var(--Corner-moderatelyRounded)] border border-fig-Stroke-soft',
522+
'bg-fig-Surface-one-neutral p-[var(--Padding-spacer)]',
523+
)}
524+
>
525+
<div className="flex items-start gap-[var(--Gap-zero-parentChild)]">
526+
<img
527+
src={asset('Folder.svg')}
528+
alt=""
529+
className="mt-0.5 h-8 w-8 shrink-0 opacity-70 dark:invert"
530+
aria-hidden
531+
/>
532+
<div className="flex min-w-0 flex-1 flex-col gap-[var(--Gap-zero-sibling)]">
533+
<p className="fy-typography-title-tiny text-fig-Subject-standard">
534+
No folders available
535+
</p>
536+
<p className="fy-typography-body-small text-fig-Subject-soft">
537+
Create a folder to upload and organize your documents.
538+
</p>
539+
</div>
540+
</div>
541+
<button
542+
type="button"
543+
onClick={() => setShowCreateFolderModal(true)}
544+
className={createFolderButtonClassName}
510545
>
511-
<span className="min-w-0 flex-1 overflow-hidden text-ellipsis text-left">
512-
{selectedFolderName || 'Select Folder'}
513-
</span>
514-
<ChevronDown
515-
className="h-[var(--Size-zero-icon)] w-[var(--Size-zero-icon)] shrink-0 text-fig-Subject-soft"
546+
<Plus
547+
className="h-[var(--Size-zero-icon)] w-[var(--Size-zero-icon)] shrink-0"
516548
aria-hidden
517549
/>
518-
</Ariakit.MenuButton>
519-
}
520-
items={sortedFolders.map((folder) => ({
521-
label: `${' '.repeat(folder.level)}${folder.level > 0 ? '└─ ' : ''}${folder.name}`,
522-
onClick: () => {
523-
setSelectedFolderId(folder.id);
524-
setSelectedFolderName(folder.name);
525-
setIsFolderMenuOpen(false);
526-
},
527-
}))}
528-
className={cn(
529-
'rounded-[var(--Corner-moderatelyRounded)] border border-fig-Stroke-soft',
530-
'bg-fig-Surface-standard shadow-sm',
531-
)}
532-
itemClassName={cn(
533-
'fy-typography-body px-[var(--Padding-zero-neighbor)] py-[var(--Padding-zero-buddy)]',
534-
'text-fig-Subject-standard hover:bg-fig-Surface-neutral',
535-
'cursor-pointer transition-colors',
536-
)}
537-
/>
550+
Create folder
551+
</button>
552+
</div>
553+
) : null}
554+
{hasSelectableFolders ? (
555+
<DropdownPopup
556+
portal={false}
557+
sameWidth={true}
558+
anchor={{ x: 'start', y: 'bottom' }}
559+
menuId="folder-selector-upload"
560+
isOpen={isFolderMenuOpen}
561+
setIsOpen={setIsFolderMenuOpen}
562+
trigger={
563+
<Ariakit.MenuButton
564+
className={cn(
565+
'fy-typography-body flex h-[var(--Size-zero-button)] w-full items-center justify-between',
566+
'rounded-[var(--Corner-moderatelyRounded)] border border-fig-Stroke-soft bg-fig-Surface-standard',
567+
'px-[var(--Padding-zero-spacer)] text-fig-Subject-standard',
568+
'transition-colors hover:border-fig-Stroke-standard',
569+
)}
570+
>
571+
<span className="min-w-0 flex-1 overflow-hidden text-ellipsis text-left">
572+
{selectedFolderName || 'Select Folder'}
573+
</span>
574+
<ChevronDown
575+
className="h-[var(--Size-zero-icon)] w-[var(--Size-zero-icon)] shrink-0 text-fig-Subject-soft"
576+
aria-hidden
577+
/>
578+
</Ariakit.MenuButton>
579+
}
580+
items={sortedFolders.map((folder) => ({
581+
label: `${' '.repeat(folder.level)}${folder.level > 0 ? '└─ ' : ''}${folder.name}`,
582+
onClick: () => {
583+
setSelectedFolderId(folder.id);
584+
setSelectedFolderName(folder.name);
585+
setIsFolderMenuOpen(false);
586+
},
587+
}))}
588+
className={cn(
589+
'rounded-[var(--Corner-moderatelyRounded)] border border-fig-Stroke-soft',
590+
'bg-fig-Surface-standard shadow-sm',
591+
)}
592+
itemClassName={cn(
593+
'fy-typography-body px-[var(--Padding-zero-neighbor)] py-[var(--Padding-zero-buddy)]',
594+
'text-fig-Subject-standard hover:bg-fig-Surface-neutral',
595+
'cursor-pointer transition-colors',
596+
)}
597+
/>
598+
) : null}
538599
</div>
539600
</div>
540601

@@ -546,7 +607,8 @@ export default function UploadFileModal({
546607
loading ||
547608
foldersLoading ||
548609
!selectedFile ||
549-
(sortedFolders.length > 0 && !selectedFolderId)
610+
!hasSelectableFolders ||
611+
!selectedFolderId
550612
}
551613
className={cn(
552614
'fy-typography-label h-[var(--Size-button)] rounded-[2px]',
@@ -575,5 +637,19 @@ export default function UploadFileModal({
575637
</div>
576638
</DialogContent>
577639
</Dialog>
640+
641+
{showCreateFolderModal ? (
642+
<CreateFolderModal
643+
orgId={resolvedOrgId}
644+
isSuperAdmin={isSuperAdmin}
645+
folders={folderTree}
646+
onClose={() => setShowCreateFolderModal(false)}
647+
onSuccess={async () => {
648+
setShowCreateFolderModal(false);
649+
await refreshFolderTree();
650+
}}
651+
/>
652+
) : null}
653+
</>
578654
);
579655
}

0 commit comments

Comments
 (0)