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({
{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 && (
-
- User Knowledge
- 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={ }
- />
- )
- )}
-
-
- )}
+
setFilePickerModalOpen(true)}
+ >
+
+ Select knowledge
+
)}
{values.knowledge_source === "team_knowledge" &&
ccPairs.length > 0 && (
-
Team Knowledge
<>
@@ -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.
+
+
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ toggleFailedUploadPopover(index, false);
+ handleRetryUpload(failedUpload.name);
+ }}
+ >
+
+ Retry Upload
+
+ {
+ e.stopPropagation();
+ toggleFailedUploadPopover(index, false);
+ handleDeleteFailedUpload(
+ failedUpload.name
+ );
+ }}
+ >
+
+ Remove
+
+
+
+
+
+
+ {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}
/>
-
- {buttonContent || "Set Context"}
-
+
+
+
+
+ 0
+ }
+ >
+ {buttonContent || "Set Context"}
+
+
+
+ {(isUploadingFile ||
+ isCreatingFileFromLink ||
+ uploadingFiles.length > 0) && (
+
+ Please wait for all files to finish uploading
+
+ )}
+
+