diff --git a/src/components/BidNavbar.tsx b/src/components/BidNavbar.tsx index d204ef0..4f43d19 100644 --- a/src/components/BidNavbar.tsx +++ b/src/components/BidNavbar.tsx @@ -55,15 +55,11 @@ const BidNavbar: React.FC<{ if (auth && auth.email) { const permission = contributors[auth.email] || "viewer"; setCurrentUserPermission(permission); - console.log("currentUserpermissionnav", permission); + //console.log("currentUserpermissionnav", permission); } }, [location, bidInfo, auth]); - useEffect(() => { - if (auth) { - console.log("auth", auth); - } - }, [auth]); + const getPermissionDetails = (permission) => { switch (permission) { @@ -110,8 +106,6 @@ const BidNavbar: React.FC<{ }, 300); // 300ms matches our CSS transition time }; - console.log(initialBidName); - const baseNavLinkStyles = "mr-6 text-base font-semibold text-gray-hint_text hover:text-orange px-3 py-2.5 cursor-pointer transition-all duration-300 ease-in-out relative"; const activeNavLinkStyles = diff --git a/src/components/SelectFile.tsx b/src/components/SelectFile.tsx new file mode 100644 index 0000000..3eafaae --- /dev/null +++ b/src/components/SelectFile.tsx @@ -0,0 +1,418 @@ +import React, { useEffect, useRef, useState } from "react"; +import { API_URL, HTTP_PREFIX } from "../helper/Constants"; +import axios from "axios"; +import { useAuthUser } from "react-auth-kit"; +import { FolderIcon, FileIcon, ReplyIcon } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Spinner } from "@/components/ui/spinner"; +import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; +import { Checkbox } from "@/components/ui/checkbox"; +import BreadCrumbs from "./BreadCrumbs"; +import { Button } from "@/components/ui/button"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +// Updated interface for SelectFileProps to handle file metadata +interface SelectFileProps { + onFileSelect: ( + files: Array<{ unique_id: string; filename: string; folder: string }> + ) => void; + initialSelectedFiles?: string[]; +} + +const SelectFile: React.FC = ({ + onFileSelect, + initialSelectedFiles = [] +}) => { + const getAuth = useAuthUser(); + const auth = getAuth(); + const tokenRef = useRef(auth?.token || "default"); + + const [availableCollections, setAvailableCollections] = useState([]); + const [folderContents, setFolderContents] = useState({}); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [activeFolder, setActiveFolder] = useState(null); + + // Track selected files as a state + const [selectedFiles, setSelectedFiles] = useState(() => { + console.log("Initializing selectedFiles with:", initialSelectedFiles); + const initialSelection = new Set([...initialSelectedFiles]); + return Array.from(initialSelection); + }); + + const [isLoading, setIsLoading] = useState(true); + const [folderStructure, setFolderStructure] = useState({}); + const [updateTrigger, setUpdateTrigger] = useState(0); + + // Pagination states + const itemsPerPage = 10; + + + const getTopLevelFolders = () => { + const folders = availableCollections.filter( + (collection: string) => + !collection.includes("FORWARDSLASH") && + !collection.startsWith("TenderLibrary_") + ); + + // Sort the folders to put "default" first + return folders.sort((a: string, b: string) => { + if (a === "default") return -1; + if (b === "default") return 1; + return a.localeCompare(b); + }); + }; + + const fetchFolderStructure = async () => { + try { + const response = await axios.post( + `http${HTTP_PREFIX}://${API_URL}/get_collections`, + {}, + { headers: { Authorization: `Bearer ${tokenRef.current}` } } + ); + + setAvailableCollections(response.data.collections); + const structure = {}; + response.data.collections.forEach((collectionName) => { + const parts = collectionName.split("FORWARDSLASH"); + let currentLevel = structure; + parts.forEach((part, index) => { + if (!currentLevel[part]) { + currentLevel[part] = index === parts.length - 1 ? null : {}; + } + currentLevel = currentLevel[part]; + }); + }); + + setFolderStructure(structure); + } catch (error) { + console.error("Error fetching folder structure:", error); + } finally { + setIsLoading(false); + } + }; + + const fetchFolderContents = async (folderPath) => { + try { + const response = await axios.post( + `http${HTTP_PREFIX}://${API_URL}/get_folder_filenames`, + { collection_name: folderPath }, + { headers: { Authorization: `Bearer ${tokenRef.current}` } } + ); + + const filesWithIds = response.data.map((item) => ({ + filename: item.filename, + unique_id: item.unique_id, + isFolder: false + })); + + // Get subfolders + const subfolders = availableCollections + .filter((collection) => + collection.startsWith(folderPath + "FORWARDSLASH") + ) + .map((collection) => { + const parts = collection.split("FORWARDSLASH"); + return { + filename: parts[parts.length - 1], + unique_id: collection, + isFolder: true + }; + }); + + const allContents = [...subfolders, ...filesWithIds]; + + // Log folder contents for debugging + console.log(`Folder contents for ${folderPath}:`, allContents); + + setFolderContents((prevContents) => ({ + ...prevContents, + [folderPath]: allContents + })); + } catch (error) { + console.error("Error fetching folder contents:", error); + } + }; + + const handleFolderClick = (folderPath) => { + setActiveFolder(folderPath); + if (!folderContents[folderPath]) { + fetchFolderContents(folderPath); + } + }; + + const handleBackClick = () => { + if (activeFolder) { + const parts = activeFolder.split("FORWARDSLASH"); + if (parts.length > 1) { + const parentFolder = parts.slice(0, -1).join("FORWARDSLASH"); + setActiveFolder(parentFolder); + if (!folderContents[parentFolder]) { + fetchFolderContents(parentFolder); + } + } else { + setActiveFolder(null); + } + } + }; + + const handleFileSelect = (fileId: string, filename: string) => { + // Get the current folder path - needed for tracking which folder the file belongs to + const currentFolder = activeFolder || "default"; + + console.log(`File selection toggled: ID=${fileId}, Name=${filename}, Folder=${currentFolder}`); + console.log(`Current selectedFiles:`, selectedFiles); + console.log(`Is file already selected? ${selectedFiles.includes(fileId)}`); + + setSelectedFiles((prev) => { + const newSelection = new Set(prev); + if (newSelection.has(fileId)) { + newSelection.delete(fileId); + console.log(`Removing file from selection: ${fileId}`); + } else { + newSelection.add(fileId); + console.log(`Adding file to selection: ${fileId}`); + } + + // Create file metadata objects for each selected file + const selectedFilesWithMetadata = []; + + // Get all selected file IDs + const selectedIds = Array.from(newSelection); + console.log("Updated selection IDs:", selectedIds); + + // For each selected ID, find the corresponding file info across all folders + selectedIds.forEach((id) => { + // Look for the file in all available folder contents + let foundFileInfo = null; + let foundFolder = null; + + // Search in the current folder first + if (folderContents[currentFolder]) { + foundFileInfo = folderContents[currentFolder].find( + (item) => item.unique_id === id && !item.isFolder + ); + if (foundFileInfo) { + foundFolder = currentFolder; + } + } + + // If not found in current folder, search in all folders + if (!foundFileInfo) { + Object.entries(folderContents).forEach(([folderName, contents]) => { + const fileInfo = contents.find( + (item) => item.unique_id === id && !item.isFolder + ); + if (fileInfo && !foundFileInfo) { + foundFileInfo = fileInfo; + foundFolder = folderName; + } + }); + } + + // Add to the selected files metadata if found + if (foundFileInfo && foundFolder) { + selectedFilesWithMetadata.push({ + unique_id: id, + filename: foundFileInfo.filename, + folder: foundFolder + }); + } else { + console.warn(`Could not find file info for ID: ${id}`); + } + }); + + console.log("Selected files with metadata:", selectedFilesWithMetadata); + + // Pass the complete file metadata to the parent + onFileSelect(selectedFilesWithMetadata); + + return selectedIds; + }); + }; + + // Update selectedFiles when initialSelectedFiles changes + useEffect(() => { + console.log("initialSelectedFiles changed:", initialSelectedFiles); + setSelectedFiles((prev) => { + const newSelection = new Set([...initialSelectedFiles]); + return Array.from(newSelection); + }); + }, [initialSelectedFiles]); + + useEffect(() => { + fetchFolderStructure(); + if (activeFolder) { + fetchFolderContents(activeFolder); + } + }, [updateTrigger, activeFolder]); + + useEffect(() => { + if (activeFolder === null) { + const topLevelFolders = getTopLevelFolders(); + const itemsCount = topLevelFolders.length; + const pages = Math.ceil(itemsCount / itemsPerPage); + setTotalPages(pages); + } else { + const itemsCount = folderContents[activeFolder]?.length || 0; + const pages = Math.ceil(itemsCount / itemsPerPage); + setTotalPages(pages); + } + }, [activeFolder, folderContents, availableCollections, itemsPerPage]); + + const formatDisplayName = (name) => { + if (typeof name !== "string") return ""; + return name.replace(/_/g, " "); + }; + + const getCurrentPageItems = () => { + if (activeFolder === null) { + const allFolders = getTopLevelFolders(); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + return allFolders.slice(indexOfFirstItem, indexOfLastItem); + } else { + const contents = folderContents[activeFolder] || []; + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + return contents.slice(indexOfFirstItem, indexOfLastItem); + } + }; + + const renderDirectoryContents = () => { + const currentItems = getCurrentPageItems(); + + if (activeFolder === null) { + // Render top-level folders + return currentItems.map((folder) => ( + + handleFolderClick(folder)} + > + + + {folder === "default" + ? "Whole Content Library" + : formatDisplayName(folder)} + + + + )); + } else { + // Render folder contents (both subfolders and files) + return currentItems.map((item, index) => { + const { filename, unique_id, isFolder } = item; + const isSelected = selectedFiles.includes(unique_id); + + + return ( + + + {isFolder ? ( + // Render folder (no checkbox) +
handleFolderClick(unique_id)} + > + + {formatDisplayName(filename)} +
+ ) : ( + // Render file with checkbox +
+ + handleFileSelect(unique_id, filename) + } + /> +
+ + +
+
+ )} +
+
+ ); + }); + } + }; + + return ( + + +
+
+ + {activeFolder && ( +
handleBackClick()} + > + + Back +
+ )} +
+ {isLoading ? ( +
+ +
+ ) : ( +
+ + {renderDirectoryContents()} +
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+ + + Page {currentPage} of {totalPages} + + +
+ )} + + +
+ )} +
+
+
+ ); +}; + +export default SelectFile; \ No newline at end of file diff --git a/src/views/BidOutline/components/SelectFilePopup.tsx b/src/views/BidOutline/components/SelectFilePopup.tsx new file mode 100644 index 0000000..0eb02eb --- /dev/null +++ b/src/views/BidOutline/components/SelectFilePopup.tsx @@ -0,0 +1,135 @@ +import React, { useState, useEffect, useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle +} from "@/components/ui/dialog"; +import SelectFile from "@/components/SelectFile"; + +const SelectFilePopup = ({ + onSaveSelectedFiles, + initialSelectedFiles = [] +}) => { + console.log( + "SelectFilePopup RENDER - initialSelectedFiles:", + initialSelectedFiles + ); + + const [open, setOpen] = useState(false); + + // Store a reference to the initial files so we can compare changes + const initialFilesRef = useRef(initialSelectedFiles); + + // Track both the file IDs and full metadata + const [selectedFiles, setSelectedFiles] = useState([]); + const [selectedFilesMetadata, setSelectedFilesMetadata] = useState([]); + + // This flag helps prevent circular updates + const processingUpdateRef = useRef(false); + + console.log("initialselectedfiiles"); + console.log(initialSelectedFiles); + + // Log when selected files change + useEffect(() => { + console.log( + "SelectFilePopup EFFECT - selectedFiles changed:", + selectedFiles + ); + }, [selectedFiles]); + + const handleFileSelection = (files) => { + console.log( + "SelectFilePopup - Files selected in SelectFile component:", + files + ); + + // Store the full metadata objects + setSelectedFilesMetadata(files); + + // Extract just the IDs to match SelectFile's expectations + const fileIds = files.map((file) => file.unique_id); + console.log("SelectFilePopup - Extracted file IDs:", fileIds); + + setSelectedFiles(fileIds); + }; + + const handleSave = () => { + console.log( + "SelectFilePopup - Saving selected files:", + selectedFilesMetadata + ); + + // Set the processing flag to prevent circular updates + processingUpdateRef.current = true; + + try { + // Pass the full metadata objects to the parent + onSaveSelectedFiles(selectedFilesMetadata); + } finally { + // Reset the processing flag after a short delay to allow parent updates to complete + setTimeout(() => { + processingUpdateRef.current = false; + }, 500); + } + + // Close the dialog + setOpen(false); + }; + + const handleDialogClose = (isOpen) => { + console.log("SelectFilePopup - Dialog state changing to:", isOpen); + + // If closing without saving, revert to the initial files + if (!isOpen && open) { + console.log( + "Dialog closing without save, reverting to initial state:", + initialFilesRef.current + ); + setSelectedFiles(initialFilesRef.current); + } + + // Update the dialog state + setOpen(isOpen); + }; + + return ( + <> + + + + + + Highlight specific documents you want the answer to focus on + + +
+ +
+ +
+
+
+
+ + ); +}; + +export default SelectFilePopup; diff --git a/src/views/BidOutline/components/SlidingSidepane.tsx b/src/views/BidOutline/components/SlidingSidepane.tsx index 28d0d66..1058e84 100644 --- a/src/views/BidOutline/components/SlidingSidepane.tsx +++ b/src/views/BidOutline/components/SlidingSidepane.tsx @@ -1,9 +1,11 @@ -import React from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faChevronLeft, faChevronRight, - faPlus + faPlus, + faFile, + faXmark } from "@fortawesome/free-solid-svg-icons"; import DebouncedTextArea from "./DeboucedTextArea"; import SubheadingCards from "./SubheadingCards"; @@ -17,7 +19,20 @@ import { Textarea } from "@/components/ui/textarea"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/utils"; -import { ChevronRight } from "lucide-react"; +import { ChevronRight, FileIcon } from "lucide-react"; +import SelectFilePopup from "./SelectFilePopup"; +import { Badge } from "@/components/ui/badge"; +import { API_URL, HTTP_PREFIX } from "@/helper/Constants"; +import axios from "axios"; +import { useAuthUser } from "react-auth-kit"; +import { toast } from "react-toastify"; + +interface HighlightedDocument { + name: string; + folder: string; + unique_id?: string; + rawtext: string; +} interface ProposalSidepaneProps { section: Section; @@ -49,7 +64,6 @@ interface ProposalSidepaneProps { totalSections: number; onNavigate: (direction: "prev" | "next") => void; } - const ProposalSidepane: React.FC = ({ section, contributors, @@ -65,19 +79,121 @@ const ProposalSidepane: React.FC = ({ compliance: false, winThemes: false, painPoints: false, - differentiation: false + differentiation: false, + highlightedDocs: false }); - const toggleSection = (section: keyof typeof openSections) => { + const toggleSection = (sectionName: keyof typeof openSections) => { setOpenSections((prev) => ({ ...prev, - [section]: !prev[section] + [sectionName]: !prev[sectionName] })); }; - if (!section) return null; + // Initialize highlighted documents specifically for this section + const [highlightedDocuments, setHighlightedDocuments] = useState(() => { + // Ensure we're getting the correct highlighted documents for this specific section + return section?.highlightedDocuments || []; + }); + // Whenever the section prop changes, update the local state + useEffect(() => { + setHighlightedDocuments(section?.highlightedDocuments || []); + }, [section]); + + // For tracking loading states of document content fetching + const [loadingDocuments, setLoadingDocuments] = useState<{ + [key: string]: boolean; + }>({}); + + const getAuth = useAuthUser(); + const auth = useMemo(() => getAuth(), [getAuth]); + const tokenRef = useRef(auth?.token || "default"); + + // Function to fetch file content + // Modified to just fetch content and return it + const getFileContent = async (fileName: string, folderName: string) => { + // Set loading state for this specific document + setLoadingDocuments((prev) => ({ ...prev, [fileName]: true })); + + const formData = new FormData(); + formData.append("file_name", fileName); + formData.append("profile_name", folderName); + + try { + const response = await axios.post( + `http${HTTP_PREFIX}://${API_URL}/show_file_content`, + formData, + { + headers: { + Authorization: `Bearer ${tokenRef.current}` + } + } + ); + + console.log(`Content fetched for ${fileName}`); + return response.data; // Just return the content + } catch (error) { + console.error("Error viewing file:", error); + toast.error(`Error retrieving content for ${fileName}`); + return ""; // Return empty string on error + } finally { + setLoadingDocuments((prev) => ({ ...prev, [fileName]: false })); + } + }; + + // This is a partial implementation showing only the changes needed for the highlightedDocuments logic - const hasSubheadings = section.subheadings && section.subheadings.length > 0; + const handleSaveSelectedFiles = async (selectedFilesWithMetadata) => { + console.log("Received files with metadata:", selectedFilesWithMetadata); + + if (!selectedFilesWithMetadata || selectedFilesWithMetadata.length === 0) { + console.log("No files selected, keeping existing documents"); + return; + } + + // Convert to HighlightedDocument objects with the correct rawtext + const documentObjects: HighlightedDocument[] = await Promise.all( + selectedFilesWithMetadata.map(async (file) => { + // Try to find existing document to preserve its rawtext if already fetched + const existingDoc = highlightedDocuments.find( + (doc) => doc.name === file.filename + ); + + // If existing document has rawtext, use it; otherwise, fetch the content + const rawtext = + (await getFileContent(file.filename, file.folder)); + + return { + name: file.filename, + folder: file.folder, + rawtext: rawtext + }; + }) + ); + + // Update the state and section with the new document objects + setHighlightedDocuments(documentObjects); + handleSectionChange(index, "highlightedDocuments", documentObjects); + }; + + const handleRemoveDocument = (document: HighlightedDocument) => { + const updatedDocs = highlightedDocuments.filter( + (doc) => doc.name !== document.name + ); + + // Update local state + setHighlightedDocuments(updatedDocs); + + // Update section through parent's change handler + handleSectionChange(index, "highlightedDocuments", updatedDocs); + }; + + // Get just the file names for the SelectFilePopup component + const selectedFileNames = useMemo(() => { + return highlightedDocuments.map((doc) => doc.name); + }, [highlightedDocuments]); + + if (!section) return null; return ( <> @@ -126,6 +242,7 @@ const ProposalSidepane: React.FC = ({ > + Question {index + 1} of {totalSections} @@ -139,13 +256,70 @@ const ProposalSidepane: React.FC = ({ - - handleSectionChange(index, "status", value) - } - /> +
+ + handleSectionChange(index, "status", value) + } + /> + +
+ + {/* Selected Files Display */} + {highlightedDocuments.length > 0 && ( +
+ toggleSection("highlightedDocs")} + > + + Highlighted Documents ({highlightedDocuments.length}) + + {openSections.highlightedDocs && ( +
+ {highlightedDocuments.map((doc, idx) => ( + + + {doc.name} + {loadingDocuments[doc.name] && ( + (loading...) + )} + + + ))} +
+ )} +
+ )} +
= ({ />
+
Question { appendFormData("submission_deadline", submission_deadline); appendFormData("questions", questions); appendFormData("original_creator", original_creator); - + formData.append("contributors", JSON.stringify(contributors || [])); formData.append( "selectedFolders",