diff --git a/backend/onyx/server/user_documents/api.py b/backend/onyx/server/user_documents/api.py index 258e13407..9e01198ef 100644 --- a/backend/onyx/server/user_documents/api.py +++ b/backend/onyx/server/user_documents/api.py @@ -365,7 +365,7 @@ def create_file_from_link( soup = BeautifulSoup(content, "html.parser") parsed_html = web_html_cleanup(soup, mintlify_cleanup_enabled=False) - file_name = f"{parsed_html.title or 'Untitled'}" + file_name = f"{parsed_html.title or 'Untitled'}.txt" file_content = parsed_html.cleaned_text.encode() file = UploadFile(filename=file_name, file=io.BytesIO(file_content)) diff --git a/backend/onyx/server/user_documents/models.py b/backend/onyx/server/user_documents/models.py index 0e64ecd23..4a43c2e53 100644 --- a/backend/onyx/server/user_documents/models.py +++ b/backend/onyx/server/user_documents/models.py @@ -35,7 +35,9 @@ class UserFileSnapshot(BaseModel): def from_model(cls, model: UserFile) -> "UserFileSnapshot": return cls( id=model.id, - name=model.name, + name=model.name[:-4] + if model.link_url and model.name.endswith(".txt") + else model.name, folder_id=model.folder_id, document_id=model.document_id, user_id=model.user_id, diff --git a/web/Dockerfile b/web/Dockerfile index 557ec3957..a4dfb47ed 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -27,6 +27,9 @@ COPY . . # Install dependencies RUN npm ci +# Install Sharp with platform-specific dependencies for Alpine Linux on ARM64 +RUN npm install --platform=linuxmusl --arch=arm64 sharp + # needed to get the `standalone` dir we expect later ENV NEXT_PRIVATE_STANDALONE=true diff --git a/web/src/app/admin/assistants/AssistantEditor.tsx b/web/src/app/admin/assistants/AssistantEditor.tsx index b97373e54..e3f824f4d 100644 --- a/web/src/app/admin/assistants/AssistantEditor.tsx +++ b/web/src/app/admin/assistants/AssistantEditor.tsx @@ -853,10 +853,7 @@ export function AssistantEditor({
-
+

Knowledge

@@ -895,9 +892,6 @@ export function AssistantEditor({
-

- Attach additional unique knowledge to this assistant -

@@ -907,25 +901,51 @@ export function AssistantEditor({
{canShowKnowledgeSource && ( <> - , - }, - { - id: "team_knowledge", - label: "Team Knowledge", - icon: , - }, - ]} - value={values.knowledge_source} - onChange={(value) => { - setFieldValue("knowledge_source", value); - }} - className="mt-2 mb-4 w-full max-w-sm" - /> +
+
+
+ setFieldValue( + "knowledge_source", + "user_files" + ) + } + > +
+ +
+

+ User Knowledge +

+
+ +
+ setFieldValue( + "knowledge_source", + "team_knowledge" + ) + } + > +
+ +
+

+ Team Knowledge +

+
+
+
)} @@ -933,76 +953,22 @@ export function AssistantEditor({ !existingPersona?.is_default_persona && !admin && (
-
- - setFilePickerModalOpen(true)} - > - Attach Files and Groups - -
- - - Select which of your user files and groups this - Assistant should use to inform its responses. If - none are specified, the Assistant will not have - access to any user-specific documents. - - - {(selectedFiles.length > 0 || - selectedFolders.length > 0) && ( -
-

- Selected Files and Folders -

-
- {selectedFiles.map((file: FileResponse) => ( - { - removeSelectedFile(file); - setFieldValue( - "selectedFiles", - values.selectedFiles.filter( - (f: FileResponse) => - f.id !== file.id - ) - ); - }} - title={file.name} - icon={} - /> - ))} - {selectedFolders.map( - (folder: FolderResponse) => ( - { - removeSelectedFolder(folder); - setFieldValue( - "selectedFolders", - values.selectedFolders.filter( - (f: FolderResponse) => - f.id !== folder.id - ) - ); - }} - title={folder.name} - icon={} - /> - ) - )} -
-
- )} +
)} {values.knowledge_source === "team_knowledge" && ccPairs.length > 0 && (
-
<> @@ -1013,7 +979,7 @@ export function AssistantEditor({ className="font-semibold underline hover:underline text-text" target="_blank" > - Team Document Sets + Document Sets ) : ( "Team Document Sets" diff --git a/web/src/app/chat/input/LLMPopover.tsx b/web/src/app/chat/input/LLMPopover.tsx index cbe0582b7..5d3c785e6 100644 --- a/web/src/app/chat/input/LLMPopover.tsx +++ b/web/src/app/chat/input/LLMPopover.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useCallback, useLayoutEffect, + useMemo, } from "react"; import { Popover, @@ -51,51 +52,66 @@ export default function LLMPopover({ const [isOpen, setIsOpen] = useState(false); const { user } = useUser(); - const llmOptionsByProvider: { - [provider: string]: { - name: string; - value: string; - icon: React.FC<{ size?: number; className?: string }>; - }[]; - } = {}; + // Memoize the options to prevent unnecessary recalculations + const { + llmOptionsByProvider, + llmOptions, + defaultProvider, + defaultModelDisplayName, + } = useMemo(() => { + const llmOptionsByProvider: { + [provider: string]: { + name: string; + value: string; + icon: React.FC<{ size?: number; className?: string }>; + }[]; + } = {}; - const uniqueModelNames = new Set(); + const uniqueModelNames = new Set(); - llmProviders.forEach((llmProvider) => { - if (!llmOptionsByProvider[llmProvider.provider]) { - llmOptionsByProvider[llmProvider.provider] = []; - } - - (llmProvider.display_model_names || llmProvider.model_names).forEach( - (modelName) => { - if (!uniqueModelNames.has(modelName)) { - uniqueModelNames.add(modelName); - llmOptionsByProvider[llmProvider.provider].push({ - name: modelName, - value: structureValue( - llmProvider.name, - llmProvider.provider, - modelName - ), - icon: getProviderIcon(llmProvider.provider, modelName), - }); - } + llmProviders.forEach((llmProvider) => { + if (!llmOptionsByProvider[llmProvider.provider]) { + llmOptionsByProvider[llmProvider.provider] = []; } + + (llmProvider.display_model_names || llmProvider.model_names).forEach( + (modelName) => { + if (!uniqueModelNames.has(modelName)) { + uniqueModelNames.add(modelName); + llmOptionsByProvider[llmProvider.provider].push({ + name: modelName, + value: structureValue( + llmProvider.name, + llmProvider.provider, + modelName + ), + icon: getProviderIcon(llmProvider.provider, modelName), + }); + } + } + ); + }); + + const llmOptions = Object.entries(llmOptionsByProvider).flatMap( + ([provider, options]) => [...options] ); - }); - const llmOptions = Object.entries(llmOptionsByProvider).flatMap( - ([provider, options]) => [...options] - ); + const defaultProvider = llmProviders.find( + (llmProvider) => llmProvider.is_default_provider + ); - const defaultProvider = llmProviders.find( - (llmProvider) => llmProvider.is_default_provider - ); + const defaultModelName = defaultProvider?.default_model_name; + const defaultModelDisplayName = defaultModelName + ? getDisplayNameForModel(defaultModelName) + : null; - const defaultModelName = defaultProvider?.default_model_name; - const defaultModelDisplayName = defaultModelName - ? getDisplayNameForModel(defaultModelName) - : null; + return { + llmOptionsByProvider, + llmOptions, + defaultProvider, + defaultModelDisplayName, + }; + }, [llmProviders]); const [localTemperature, setLocalTemperature] = useState( llmManager.temperature ?? 0.5 @@ -105,42 +121,52 @@ export default function LLMPopover({ setLocalTemperature(llmManager.temperature ?? 0.5); }, [llmManager.temperature]); - const handleTemperatureChange = (value: number[]) => { + // Use useCallback to prevent function recreation + const handleTemperatureChange = useCallback((value: number[]) => { setLocalTemperature(value[0]); - }; + }, []); - const handleTemperatureChangeComplete = (value: number[]) => { - llmManager.updateTemperature(value[0]); - }; + const handleTemperatureChangeComplete = useCallback( + (value: number[]) => { + llmManager.updateTemperature(value[0]); + }, + [llmManager] + ); + + // Memoize trigger content to prevent rerendering + const triggerContent = useMemo( + () => ( + + ), + [defaultModelDisplayName, defaultProvider, llmManager?.currentLlm] + ); return ( - - - + {triggerContent} = ({ }; const [uploadingFiles, setUploadingFiles] = useState([]); const [completedFiles, setCompletedFiles] = useState([]); + // Add state for failed uploads + const [failedUploads, setFailedUploads] = useState([]); const [refreshInterval, setRefreshInterval] = useState( null ); @@ -158,9 +180,54 @@ export const DocumentList: React.FC = ({ startRefreshInterval(); } catch (error) { console.error("Error creating file from link:", error); + // Remove from uploading files + setUploadingFiles((prev) => prev.filter((file) => file.name !== url)); + // Add to failed uploads with isPopoverOpen initialized to false + setFailedUploads((prev) => [ + ...prev, + { + name: url, + error: + error instanceof Error ? error.message : "Failed to upload file", + isPopoverOpen: false, + }, + ]); } }; + // Add handler for retrying failed uploads + const handleRetryUpload = async (url: string) => { + // Remove from failed uploads + setFailedUploads((prev) => prev.filter((file) => file.name !== url)); + + // Add back to uploading files + setUploadingFiles((prev) => [...prev, { name: url, progress: 0 }]); + + try { + await createFileFromLink(url, folderId); + startRefreshInterval(); + } catch (error) { + console.error("Error retrying file upload from link:", error); + // Remove from uploading files again + setUploadingFiles((prev) => prev.filter((file) => file.name !== url)); + // Add back to failed uploads with isPopoverOpen initialized to false + setFailedUploads((prev) => [ + ...prev, + { + name: url, + error: + error instanceof Error ? error.message : "Failed to upload file", + isPopoverOpen: false, + }, + ]); + } + }; + + // Add handler for deleting failed uploads + const handleDeleteFailedUpload = (url: string) => { + setFailedUploads((prev) => prev.filter((file) => file.name !== url)); + }; + const handleFileUpload = (files: File[]) => { const fileObjects = files.map((file) => ({ name: file.name, @@ -286,6 +353,8 @@ export const DocumentList: React.FC = ({ // Get the hostname (domain) from the URL const url = new URL(uploadingFile.name); const hostname = url.hostname; + alert("checking for " + hostname); + alert(JSON.stringify(files)); // Look for recently added files that might match this URL const isUploaded = files.some( @@ -304,7 +373,7 @@ export const DocumentList: React.FC = ({ return isUploaded; } catch (e) { console.error("Failed to parse URL:", e); - return false; // Force continued checking + return false; } } @@ -349,9 +418,20 @@ export const DocumentList: React.FC = ({ // For URLs, check if any file contains the hostname const url = new URL(uploadingFile.name); const hostname = url.hostname; + const fullUrl = uploadingFile.name; - return !files.some((file) => - file.name.toLowerCase().includes(hostname.toLowerCase()) + return ( + // !files.some((file) => + // file.name.toLowerCase().includes(hostname.toLowerCase()) + // ) && + !files.some( + (file) => + file.link_url && + // (file.link_url + // .toLowerCase() + // .includes(hostname.toLowerCase()) || + file.link_url.toLowerCase() === fullUrl.toLowerCase() + ) ); } catch (e) { console.error("Failed to parse URL:", e); @@ -394,6 +474,18 @@ export const DocumentList: React.FC = ({ startRefreshInterval(); }; + // Wrap in useCallback to prevent function recreation on each render + const toggleFailedUploadPopover = useCallback( + (index: number, isOpen: boolean) => { + setFailedUploads((prev) => + prev.map((item, i) => + i === index ? { ...item, isPopoverOpen: isOpen } : item + ) + ); + }, + [] + ); + return ( <>
@@ -550,13 +642,108 @@ export const DocumentList: React.FC = ({
))} - {sortedFiles.length === 0 && uploadingFiles.length === 0 && ( -
- {searchQuery - ? "No documents match your search." - : "No documents in this folder yet. Upload files or add URLs to get started."} -
+ {/* Add failed uploads display with popover */} + {useMemo( + () => + failedUploads.map((failedUpload, index) => ( +
+
+
+ + toggleFailedUploadPopover(index, open) + } + > + e.stopPropagation()} + asChild + > +
+ +
+
+ +
+
+

+ Upload failed. +
+ You can retry the upload or remove it from + the list. +

+
+
+ + +
+
+
+
+ + {failedUpload.name.startsWith("http") + ? `${failedUpload.name.substring(0, 30)}${ + failedUpload.name.length > 30 ? "..." : "" + }` + : failedUpload.name} + +
+
+ Upload failed +
+
+ {/* Removed inline buttons as we now use the popover */} +
+
+
+ )), + [ + failedUploads, + toggleFailedUploadPopover, + handleRetryUpload, + handleDeleteFailedUpload, + ] )} + + {sortedFiles.length === 0 && + uploadingFiles.length === 0 && + failedUploads.length === 0 && ( +
+ {searchQuery + ? "No documents match your search." + : "No documents in this folder yet. Upload files or add URLs to get started."} +
+ )} )}
diff --git a/web/src/app/chat/my-documents/components/FileListItem.tsx b/web/src/app/chat/my-documents/components/FileListItem.tsx index 90c48aaf6..fcd67a4b5 100644 --- a/web/src/app/chat/my-documents/components/FileListItem.tsx +++ b/web/src/app/chat/my-documents/components/FileListItem.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { Checkbox } from "@/components/ui/checkbox"; import { File, File as FileIcon, Loader, MoreHorizontal } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -68,6 +68,7 @@ export const FileListItem: React.FC = ({ const { setPopup, popup } = usePopup(); const [showMoveOptions, setShowMoveOptions] = useState(false); const [indexingStatus, setIndexingStatus] = useState(null); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const { getFilesIndexingStatus, refreshFolderDetails } = useDocumentsContext(); @@ -96,9 +97,9 @@ export const FileListItem: React.FC = ({ onMove(file.id, targetFolderId); setShowMoveOptions(false); }; - const FailureWithPopover = () => { + const FailureWithPopover = useCallback(() => { return ( - + e.stopPropagation()} asChild>
@@ -122,6 +123,7 @@ export const FileListItem: React.FC = ({ onClick={(e) => { e.preventDefault(); e.stopPropagation(); + setIsPopoverOpen(false); fetch(`/api/user/file/reindex`, { method: "POST", headers: { @@ -133,7 +135,7 @@ export const FileListItem: React.FC = ({ if (!response.ok) { throw new Error("Failed to reindex file"); } - setIndexingStatus(false); // Set to false to show indexing status + setIndexingStatus(false); refreshFolderDetails(); setPopup({ type: "success", @@ -158,6 +160,7 @@ export const FileListItem: React.FC = ({ className="w-full justify-start text-sm font-medium text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600 transition-colors" onClick={(e) => { e.stopPropagation(); + setIsPopoverOpen(false); handleDelete(); }} > @@ -169,7 +172,15 @@ export const FileListItem: React.FC = ({ ); - }; + }, [ + file.id, + handleDelete, + isPopoverOpen, + refreshFolderDetails, + setIndexingStatus, + setIsPopoverOpen, + setPopup, + ]); return (
= ({ try { const response: FileResponse[] = - await createFileFromLink(url, currentFolder); + await createFileFromLink(url, -1); if (response.length > 0) { // Extract domain from URL to help with detection @@ -1386,9 +1386,32 @@ export const FilePickerModal: React.FC = ({ selectedModel={selectedModel} />
- + + + +
+ +
+
+ {(isUploadingFile || + isCreatingFileFromLink || + uploadingFiles.length > 0) && ( + +

Please wait for all files to finish uploading

+
+ )} +
+