feat(25-02): add chatApi.uploadFile and useChatFileUpload hook
- Add uploadFile method to chatApi using XHR for progress tracking - Add ChatFileUploadResponse to shared type imports - Create useChatFileUpload hook with PendingFile lifecycle management - Hook manages uploading/done/error states with progress callbacks
This commit is contained in:
parent
5393ba0947
commit
0fe948d1d0
2 changed files with 115 additions and 0 deletions
|
|
@ -7,6 +7,7 @@ import type {
|
|||
ChatMessageSearchResponse,
|
||||
ChatBookmarkToggleResponse,
|
||||
ChatBookmarkListResponse,
|
||||
ChatFileUploadResponse,
|
||||
} from "@paperclipai/shared";
|
||||
|
||||
export const chatApi = {
|
||||
|
|
@ -211,4 +212,40 @@ export const chatApi = {
|
|||
// Returns a download URL — use window.location.href to trigger
|
||||
return `/api/conversations/${conversationId}/export?format=${format}`;
|
||||
},
|
||||
|
||||
async uploadFile(
|
||||
conversationId: string,
|
||||
file: File,
|
||||
opts?: { source?: string; projectId?: string },
|
||||
onProgress?: (percent: number) => void,
|
||||
): Promise<ChatFileUploadResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
if (opts?.source) formData.append("source", opts.source);
|
||||
if (opts?.projectId) formData.append("projectId", opts.projectId);
|
||||
|
||||
// Use XMLHttpRequest for progress tracking
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", `/api/conversations/${conversationId}/files`);
|
||||
xhr.withCredentials = true;
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable && onProgress) {
|
||||
onProgress(Math.round((e.loaded / e.total) * 100));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(JSON.parse(xhr.responseText) as ChatFileUploadResponse);
|
||||
} else {
|
||||
reject(new Error(`Upload failed: ${xhr.status}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => reject(new Error("Upload failed"));
|
||||
xhr.send(formData);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
78
ui/src/hooks/useChatFileUpload.ts
Normal file
78
ui/src/hooks/useChatFileUpload.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { useState, useCallback } from "react";
|
||||
import { chatApi } from "../api/chat";
|
||||
import type { ChatFile } from "@paperclipai/shared";
|
||||
|
||||
export interface PendingFile {
|
||||
id: string; // temp client-side id before upload completes
|
||||
file: File; // raw File object
|
||||
name: string;
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
progress: number; // 0-100
|
||||
status: "uploading" | "done" | "error";
|
||||
uploadedFile?: ChatFile; // set when upload completes
|
||||
contentPath?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function useChatFileUpload(conversationId: string | null) {
|
||||
const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
|
||||
|
||||
const addFile = useCallback(async (file: File) => {
|
||||
if (!conversationId) return;
|
||||
|
||||
const tempId = crypto.randomUUID();
|
||||
const pending: PendingFile = {
|
||||
id: tempId,
|
||||
file,
|
||||
name: file.name,
|
||||
mimeType: file.type || "application/octet-stream",
|
||||
sizeBytes: file.size,
|
||||
progress: 0,
|
||||
status: "uploading",
|
||||
};
|
||||
|
||||
setPendingFiles((prev) => [...prev, pending]);
|
||||
|
||||
try {
|
||||
const result = await chatApi.uploadFile(
|
||||
conversationId,
|
||||
file,
|
||||
{ source: "user_upload" },
|
||||
(percent) => {
|
||||
setPendingFiles((prev) =>
|
||||
prev.map((p) => (p.id === tempId ? { ...p, progress: percent } : p))
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
setPendingFiles((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === tempId
|
||||
? { ...p, status: "done", progress: 100, uploadedFile: result.file, contentPath: result.contentPath }
|
||||
: p
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
setPendingFiles((prev) =>
|
||||
prev.map((p) =>
|
||||
p.id === tempId ? { ...p, status: "error", error: (err as Error).message } : p
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [conversationId]);
|
||||
|
||||
const removeFile = useCallback((tempId: string) => {
|
||||
setPendingFiles((prev) => prev.filter((p) => p.id !== tempId));
|
||||
}, []);
|
||||
|
||||
const clearCompleted = useCallback(() => {
|
||||
setPendingFiles((prev) => prev.filter((p) => p.status !== "done"));
|
||||
}, []);
|
||||
|
||||
const completedFileIds = pendingFiles
|
||||
.filter((p) => p.status === "done" && p.uploadedFile)
|
||||
.map((p) => p.uploadedFile!.id);
|
||||
|
||||
return { pendingFiles, addFile, removeFile, clearCompleted, completedFileIds };
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue