mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-17 11:44:44 +08:00
feat: implement file upload feature
This commit is contained in:
@@ -7,6 +7,7 @@ import { useCallback } from "react";
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
|
||||
import { getAPIClient } from "../api";
|
||||
import { uploadFiles } from "../uploads";
|
||||
|
||||
import type {
|
||||
AgentThread,
|
||||
@@ -72,6 +73,44 @@ export function useSubmitThread({
|
||||
const callback = useCallback(
|
||||
async (message: PromptInputMessage) => {
|
||||
const text = message.text.trim();
|
||||
|
||||
// Upload files first if any
|
||||
if (message.files && message.files.length > 0) {
|
||||
try {
|
||||
// Convert FileUIPart to File objects by fetching blob URLs
|
||||
const filePromises = message.files.map(async (fileUIPart) => {
|
||||
if (fileUIPart.url && fileUIPart.filename) {
|
||||
try {
|
||||
// Fetch the blob URL to get the file data
|
||||
const response = await fetch(fileUIPart.url);
|
||||
const blob = await response.blob();
|
||||
|
||||
// Create a File object from the blob
|
||||
return new File([blob], fileUIPart.filename, {
|
||||
type: fileUIPart.mediaType || blob.type,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch file ${fileUIPart.filename}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const files = (await Promise.all(filePromises)).filter(
|
||||
(file): file is File => file !== null,
|
||||
);
|
||||
|
||||
if (files.length > 0 && threadId) {
|
||||
await uploadFiles(threadId, files);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to upload files:", error);
|
||||
// Continue with message submission even if upload fails
|
||||
// You might want to show an error toast here
|
||||
}
|
||||
}
|
||||
|
||||
await thread.submit(
|
||||
{
|
||||
messages: [
|
||||
|
||||
97
frontend/src/core/uploads/api.ts
Normal file
97
frontend/src/core/uploads/api.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* API functions for file uploads
|
||||
*/
|
||||
|
||||
import { getBackendBaseURL } from "../config";
|
||||
|
||||
export interface UploadedFileInfo {
|
||||
filename: string;
|
||||
size: number;
|
||||
path: string;
|
||||
virtual_path: string;
|
||||
artifact_url: string;
|
||||
extension?: string;
|
||||
modified?: number;
|
||||
markdown_file?: string;
|
||||
markdown_path?: string;
|
||||
markdown_virtual_path?: string;
|
||||
markdown_artifact_url?: string;
|
||||
}
|
||||
|
||||
export interface UploadResponse {
|
||||
success: boolean;
|
||||
files: UploadedFileInfo[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ListFilesResponse {
|
||||
files: UploadedFileInfo[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload files to a thread
|
||||
*/
|
||||
export async function uploadFiles(
|
||||
threadId: string,
|
||||
files: File[],
|
||||
): Promise<UploadResponse> {
|
||||
const formData = new FormData();
|
||||
|
||||
files.forEach((file) => {
|
||||
formData.append("files", file);
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${getBackendBaseURL()}/api/threads/${threadId}/uploads`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: "Upload failed" }));
|
||||
throw new Error(error.detail || "Upload failed");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* List all uploaded files for a thread
|
||||
*/
|
||||
export async function listUploadedFiles(
|
||||
threadId: string,
|
||||
): Promise<ListFilesResponse> {
|
||||
const response = await fetch(
|
||||
`${getBackendBaseURL()}/api/threads/${threadId}/uploads/list`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to list uploaded files");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an uploaded file
|
||||
*/
|
||||
export async function deleteUploadedFile(
|
||||
threadId: string,
|
||||
filename: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
const response = await fetch(
|
||||
`${getBackendBaseURL()}/api/threads/${threadId}/uploads/${filename}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete file");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
79
frontend/src/core/uploads/hooks.ts
Normal file
79
frontend/src/core/uploads/hooks.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* React hooks for file uploads
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import {
|
||||
deleteUploadedFile,
|
||||
listUploadedFiles,
|
||||
uploadFiles,
|
||||
type UploadedFileInfo,
|
||||
type UploadResponse,
|
||||
} from "./api";
|
||||
|
||||
/**
|
||||
* Hook to upload files
|
||||
*/
|
||||
export function useUploadFiles(threadId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<UploadResponse, Error, File[]>({
|
||||
mutationFn: (files: File[]) => uploadFiles(threadId, files),
|
||||
onSuccess: () => {
|
||||
// Invalidate the uploaded files list
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["uploads", "list", threadId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to list uploaded files
|
||||
*/
|
||||
export function useUploadedFiles(threadId: string) {
|
||||
return useQuery({
|
||||
queryKey: ["uploads", "list", threadId],
|
||||
queryFn: () => listUploadedFiles(threadId),
|
||||
enabled: !!threadId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to delete an uploaded file
|
||||
*/
|
||||
export function useDeleteUploadedFile(threadId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (filename: string) => deleteUploadedFile(threadId, filename),
|
||||
onSuccess: () => {
|
||||
// Invalidate the uploaded files list
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["uploads", "list", threadId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to handle file uploads in submit flow
|
||||
* Returns a function that uploads files and returns their info
|
||||
*/
|
||||
export function useUploadFilesOnSubmit(threadId: string) {
|
||||
const uploadMutation = useUploadFiles(threadId);
|
||||
|
||||
return useCallback(
|
||||
async (files: File[]): Promise<UploadedFileInfo[]> => {
|
||||
if (files.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await uploadMutation.mutateAsync(files);
|
||||
return result.files;
|
||||
},
|
||||
[uploadMutation],
|
||||
);
|
||||
}
|
||||
6
frontend/src/core/uploads/index.ts
Normal file
6
frontend/src/core/uploads/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* File uploads module
|
||||
*/
|
||||
|
||||
export * from "./api";
|
||||
export * from "./hooks";
|
||||
Reference in New Issue
Block a user