Skip to content

Commit a1fb9c6

Browse files
Implement file deletion and improve file upload UX with instant feedback
Co-authored-by: wenxi <wenxi@onyx.app>
1 parent 3534515 commit a1fb9c6

File tree

6 files changed

+196
-38
lines changed

6 files changed

+196
-38
lines changed

backend/onyx/server/query_and_chat/chat_backend.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
from onyx.db.models import User
6161
from onyx.db.persona import get_persona_by_id
6262
from onyx.db.user_documents import create_user_files
63+
from onyx.db.user_documents import UserFile
6364
from onyx.file_processing.extract_file_text import docx_to_txt_filename
6465
from onyx.file_store.file_store import get_default_file_store
6566
from onyx.file_store.models import FileDescriptor
@@ -804,6 +805,41 @@ def fetch_chat_file(
804805
return StreamingResponse(file_io, media_type=media_type)
805806

806807

808+
@router.delete("/file/{file_id:path}")
809+
def delete_chat_file(
810+
file_id: str,
811+
user: User | None = Depends(current_user),
812+
db_session: Session = Depends(get_session),
813+
) -> dict[str, str]:
814+
"""Delete a chat file and associated user file record"""
815+
user_id = user.id if user else None
816+
817+
# Find the user file record that corresponds to this file_id
818+
user_file = (
819+
db_session.query(UserFile)
820+
.filter(UserFile.file_id == file_id, UserFile.user_id == user_id)
821+
.first()
822+
)
823+
824+
if not user_file:
825+
raise HTTPException(status_code=404, detail="File not found or access denied")
826+
827+
try:
828+
# Delete from file store
829+
file_store = get_default_file_store()
830+
file_store.delete_file(file_id, db_session)
831+
832+
# Delete user file record
833+
db_session.delete(user_file)
834+
db_session.commit()
835+
836+
return {"message": "File deleted successfully"}
837+
except Exception as e:
838+
db_session.rollback()
839+
logger.error(f"Error deleting file {file_id}: {str(e)}")
840+
raise HTTPException(status_code=500, detail="Failed to delete file")
841+
842+
807843
@router.get("/search")
808844
async def search_chats(
809845
query: str | None = Query(None),

web/src/app/chat/ChatPage.tsx

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2139,39 +2139,68 @@ export function ChatPage({
21392139
return;
21402140
}
21412141

2142+
// Immediately add files with uploading state for instant feedback
2143+
const tempFileDescriptors: FileDescriptor[] = acceptedFiles.map((file) => ({
2144+
id: `temp-${Date.now()}-${Math.random()}`,
2145+
name: file.name,
2146+
type: file.type.startsWith("image/") ? ChatFileType.IMAGE : ChatFileType.PLAIN_TEXT,
2147+
isUploading: true,
2148+
}));
2149+
2150+
setCurrentMessageFiles((prev) => [...prev, ...tempFileDescriptors]);
21422151
updateChatState("uploading", currentSessionId());
21432152

2144-
for (let file of acceptedFiles) {
2145-
const formData = new FormData();
2146-
formData.append("files", file);
2147-
const response: FileResponse[] = await uploadFile(formData, null);
2148-
2149-
if (response.length > 0 && response[0] !== undefined) {
2150-
const uploadedFile = response[0];
2151-
2152-
const newFileDescriptor: FileDescriptor = {
2153-
// Use file_id (storage ID) if available, otherwise fallback to DB id
2154-
// Ensure it's a string as FileDescriptor expects
2155-
id: uploadedFile.file_id
2156-
? String(uploadedFile.file_id)
2157-
: String(uploadedFile.id),
2158-
type: uploadedFile.chat_file_type
2159-
? uploadedFile.chat_file_type
2160-
: ChatFileType.PLAIN_TEXT,
2161-
name: uploadedFile.name,
2162-
isUploading: false, // Mark as successfully uploaded
2163-
};
2153+
try {
2154+
const uploadedFileDescriptors: FileDescriptor[] = [];
2155+
2156+
for (let file of acceptedFiles) {
2157+
const formData = new FormData();
2158+
formData.append("files", file);
2159+
const response: FileResponse[] = await uploadFile(formData, null);
2160+
2161+
if (response.length > 0 && response[0] !== undefined) {
2162+
const uploadedFile = response[0];
2163+
2164+
const newFileDescriptor: FileDescriptor = {
2165+
// Use file_id (storage ID) if available, otherwise fallback to DB id
2166+
// Ensure it's a string as FileDescriptor expects
2167+
id: uploadedFile.file_id
2168+
? String(uploadedFile.file_id)
2169+
: String(uploadedFile.id),
2170+
type: uploadedFile.chat_file_type
2171+
? uploadedFile.chat_file_type
2172+
: ChatFileType.PLAIN_TEXT,
2173+
name: uploadedFile.name,
2174+
isUploading: false, // Mark as successfully uploaded
2175+
};
21642176

2165-
setCurrentMessageFiles((prev) => [...prev, newFileDescriptor]);
2166-
} else {
2167-
setPopup({
2168-
type: "error",
2169-
message: "Failed to upload file",
2170-
});
2177+
uploadedFileDescriptors.push(newFileDescriptor);
2178+
} else {
2179+
setPopup({
2180+
type: "error",
2181+
message: `Failed to upload file: ${file.name}`,
2182+
});
2183+
}
21712184
}
2172-
}
21732185

2174-
updateChatState("input", currentSessionId());
2186+
// Replace temp files with actual uploaded files
2187+
setCurrentMessageFiles((prev) => {
2188+
const filtered = prev.filter((f) => !f.id.startsWith("temp-"));
2189+
return [...filtered, ...uploadedFileDescriptors];
2190+
});
2191+
2192+
} catch (error) {
2193+
console.error("Error uploading files:", error);
2194+
setPopup({
2195+
type: "error",
2196+
message: "Failed to upload files",
2197+
});
2198+
2199+
// Remove temp files on error
2200+
setCurrentMessageFiles((prev) => prev.filter((f) => !f.id.startsWith("temp-")));
2201+
} finally {
2202+
updateChatState("input", currentSessionId());
2203+
}
21752204
};
21762205

21772206
// Used to maintain a "time out" for history sidebar so our existing refs can have time to process change

web/src/app/chat/input/ChatInputBar.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { AgenticToggle } from "./AgenticToggle";
3939
import { SettingsContext } from "@/components/settings/SettingsProvider";
4040
import { getProviderIcon } from "@/app/admin/configuration/llm/utils";
4141
import { useDocumentsContext } from "../my-documents/DocumentsContext";
42+
import { deleteChatFile } from "../lib";
4243

4344
const MAX_INPUT_HEIGHT = 200;
4445
export const SourceChip2 = ({
@@ -728,10 +729,20 @@ export function ChatInputBar({
728729
)
729730
}
730731
title={file.name}
731-
onRemove={() => {
732+
onRemove={async () => {
732733
if (file.source === "selected") {
733734
removeSelectedFile(file.originalFile);
734735
} else {
736+
// Delete file from backend before removing from UI
737+
try {
738+
const error = await deleteChatFile(file.id);
739+
if (error) {
740+
console.error("Failed to delete file:", error);
741+
// Still remove from UI even if backend deletion fails
742+
}
743+
} catch (error) {
744+
console.error("Error deleting file:", error);
745+
}
735746
setCurrentMessageFiles(
736747
currentMessageFiles.filter(
737748
(fileInFilter) => fileInFilter.id !== file.id
@@ -752,10 +763,20 @@ export function ChatInputBar({
752763
/>
753764
}
754765
title={file.name}
755-
onRemove={() => {
766+
onRemove={async () => {
756767
if (file.source === "selected") {
757768
removeSelectedFile(file.originalFile);
758769
} else {
770+
// Delete file from backend before removing from UI
771+
try {
772+
const error = await deleteChatFile(file.id);
773+
if (error) {
774+
console.error("Failed to delete file:", error);
775+
// Still remove from UI even if backend deletion fails
776+
}
777+
} catch (error) {
778+
console.error("Error deleting file:", error);
779+
}
759780
setCurrentMessageFiles(
760781
currentMessageFiles.filter(
761782
(fileInFilter) => fileInFilter.id !== file.id

web/src/app/chat/input/SimplifiedChatInputBar.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "../files/InputBarPreview";
1010
import { SendIcon } from "@/components/icons/icons";
1111
import { HorizontalSourceSelector } from "@/components/search/filtering/HorizontalSourceSelector";
12+
import { deleteChatFile } from "../lib";
1213
import { Tag } from "@/lib/types";
1314

1415
const MAX_INPUT_HEIGHT = 200;
@@ -108,7 +109,17 @@ export function SimplifiedChatInputBar({
108109
{file.type === ChatFileType.IMAGE ? (
109110
<InputBarPreviewImageProvider
110111
file={file}
111-
onDelete={() => {
112+
onDelete={async () => {
113+
// Delete file from backend before removing from UI
114+
try {
115+
const error = await deleteChatFile(file.id);
116+
if (error) {
117+
console.error("Failed to delete file:", error);
118+
// Still remove from UI even if backend deletion fails
119+
}
120+
} catch (error) {
121+
console.error("Error deleting file:", error);
122+
}
112123
setFiles(
113124
files.filter(
114125
(fileInFilter) => fileInFilter.id !== file.id
@@ -120,7 +131,17 @@ export function SimplifiedChatInputBar({
120131
) : (
121132
<InputBarPreview
122133
file={file}
123-
onDelete={() => {
134+
onDelete={async () => {
135+
// Delete file from backend before removing from UI
136+
try {
137+
const error = await deleteChatFile(file.id);
138+
if (error) {
139+
console.error("Failed to delete file:", error);
140+
// Still remove from UI even if backend deletion fails
141+
}
142+
} catch (error) {
143+
console.error("Error deleting file:", error);
144+
}
124145
setFiles(
125146
files.filter(
126147
(fileInFilter) => fileInFilter.id !== file.id

web/src/app/chat/lib.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,60 @@ export async function uploadFilesForChat(
739739
return [responseJson.files as FileDescriptor[], null];
740740
}
741741

742+
export async function deleteChatFile(fileId: string): Promise<string | null> {
743+
const response = await fetch(`/api/chat/file/${encodeURIComponent(fileId)}`, {
744+
method: "DELETE",
745+
});
746+
if (!response.ok) {
747+
const errorData = await response.json();
748+
return errorData.detail || "Failed to delete file";
749+
}
750+
return null;
751+
}
752+
753+
export async function uploadFilesImmediately(
754+
files: File[],
755+
onProgress?: (fileName: string, progress: number) => void
756+
): Promise<[FileDescriptor[], string | null]> {
757+
const tempFiles: FileDescriptor[] = files.map((file) => ({
758+
id: `temp-${Date.now()}-${Math.random()}`,
759+
name: file.name,
760+
type: file.type.startsWith("image/") ? "IMAGE" : "PLAIN_TEXT",
761+
isUploading: true,
762+
}));
763+
764+
// Simulate progress for immediate feedback
765+
if (onProgress) {
766+
files.forEach((file) => {
767+
onProgress(file.name, 0);
768+
// Simulate progress steps
769+
let progress = 0;
770+
const progressInterval = setInterval(() => {
771+
progress += 20;
772+
if (progress <= 80) {
773+
onProgress(file.name, progress);
774+
} else {
775+
clearInterval(progressInterval);
776+
}
777+
}, 100);
778+
});
779+
}
780+
781+
try {
782+
const [uploadedFiles, error] = await uploadFilesForChat(files);
783+
784+
if (onProgress) {
785+
files.forEach((file) => {
786+
onProgress(file.name, 100);
787+
});
788+
}
789+
790+
return [uploadedFiles, error];
791+
} catch (error) {
792+
return [[], error instanceof Error ? error.message : "Upload failed"];
793+
}
794+
}
795+
742796
export function useScrollonStream({
743797
chatState,
744798
scrollableDivRef,

web/src/app/chat/nrf/NRFPage.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
import { Modal } from "@/components/Modal";
2323
import { useNightTime } from "@/lib/dateUtils";
2424
import { useFilters } from "@/lib/hooks";
25-
import { uploadFilesForChat } from "../lib";
25+
import { uploadFilesForChat, deleteChatFile } from "../lib";
2626
import { ChatFileType, FileDescriptor } from "../interfaces";
2727
import { useChatContext } from "@/components/context/ChatContext";
2828
import Dropzone from "react-dropzone";
@@ -121,11 +121,8 @@ export default function NRFPage({
121121
isUploading: true,
122122
}));
123123

124-
// only show loading spinner for reasonably large files
125-
const totalSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
126-
if (totalSize > 50 * 1024) {
127-
setCurrentMessageFiles((prev) => [...prev, ...tempFileDescriptors]);
128-
}
124+
// Immediately show files with uploading state for instant feedback
125+
setCurrentMessageFiles((prev) => [...prev, ...tempFileDescriptors]);
129126

130127
const removeTempFiles = (prev: FileDescriptor[]) => {
131128
return prev.filter(

0 commit comments

Comments
 (0)