mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-03 06:12:14 +08:00
feat: add resource upload support for RAG (#768)
* feat: add resource upload support for RAG - Backend: Added ingest_file method to Retriever and MilvusRetriever - Backend: Added /api/rag/upload endpoint - Frontend: Added RAGTab in settings for uploading resources - Frontend: Updated translations and settings registration * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review * Apply suggestions from code review of src/rag/milvus.py --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -32,6 +32,17 @@
|
||||
"addNewMCPServers": "Add New MCP Servers",
|
||||
"mcpConfigDescription": "DeerFlow uses the standard JSON MCP config to create a new server.",
|
||||
"pasteConfigBelow": "Paste your config below and click \"Add\" to add new servers.",
|
||||
"rag": {
|
||||
"title": "Resources",
|
||||
"description": "Manage your knowledge base resources here. Upload markdown or text files to be indexed for retrieval.",
|
||||
"upload": "Upload",
|
||||
"uploading": "Uploading...",
|
||||
"uploadSuccess": "File uploaded successfully",
|
||||
"uploadFailed": "Failed to upload file",
|
||||
"emptyFile": "Cannot upload an empty file",
|
||||
"loading": "Loading resources...",
|
||||
"noResources": "No resources found. Upload a file to get started."
|
||||
},
|
||||
"add": "Add",
|
||||
"general": {
|
||||
"title": "General",
|
||||
|
||||
@@ -32,6 +32,17 @@
|
||||
"addNewMCPServers": "添加新的 MCP 服务器",
|
||||
"mcpConfigDescription": "DeerFlow 使用标准 JSON MCP 配置来创建新服务器。",
|
||||
"pasteConfigBelow": "将您的配置粘贴到下面,然后点击\"添加\"来添加新服务器。",
|
||||
"rag": {
|
||||
"title": "资源",
|
||||
"description": "在此管理您的知识库资源。上传 Markdown 或文本文件以供检索索引。",
|
||||
"upload": "上传",
|
||||
"uploading": "上传中...",
|
||||
"uploadSuccess": "文件上传成功",
|
||||
"uploadFailed": "文件上传失败",
|
||||
"emptyFile": "无法上传空文件",
|
||||
"loading": "正在加载资源...",
|
||||
"noResources": "未找到资源。上传文件以开始使用。"
|
||||
},
|
||||
"add": "添加",
|
||||
"general": {
|
||||
"title": "通用",
|
||||
|
||||
@@ -6,8 +6,9 @@ import { Settings, type LucideIcon } from "lucide-react";
|
||||
import { AboutTab } from "./about-tab";
|
||||
import { GeneralTab } from "./general-tab";
|
||||
import { MCPTab } from "./mcp-tab";
|
||||
import { RAGTab } from "./rag-tab";
|
||||
|
||||
export const SETTINGS_TABS = [GeneralTab, MCPTab, AboutTab].map((tab) => {
|
||||
export const SETTINGS_TABS = [GeneralTab, RAGTab, MCPTab, AboutTab].map((tab) => {
|
||||
const name = tab.displayName ?? tab.name;
|
||||
return {
|
||||
...tab,
|
||||
|
||||
151
web/src/app/settings/tabs/rag-tab.tsx
Normal file
151
web/src/app/settings/tabs/rag-tab.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Database, FileText, Upload } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { resolveServiceURL } from "~/core/api/resolve-service-url";
|
||||
import type { Resource } from "~/core/messages";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
import type { Tab } from "./types";
|
||||
|
||||
export const RAGTab: Tab = () => {
|
||||
const t = useTranslations("settings.rag");
|
||||
const [resources, setResources] = useState<Resource[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const fetchResources = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(resolveServiceURL("rag/resources"), {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setResources(data.resources ?? []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch resources:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchResources();
|
||||
}, [fetchResources]);
|
||||
|
||||
const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (file.size === 0) {
|
||||
toast.error(t("emptyFile"));
|
||||
event.target.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
try {
|
||||
const response = await fetch(resolveServiceURL("rag/upload"), {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(t("uploadSuccess"));
|
||||
void fetchResources();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
toast.error(error.detail ?? t("uploadFailed"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error);
|
||||
toast.error(t("uploadFailed"));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
// Reset input value to allow uploading same file again
|
||||
event.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<header>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h1 className="text-lg font-medium">{t("title")}</h1>
|
||||
<div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".md,.txt"
|
||||
className="sr-only"
|
||||
onChange={handleUpload}
|
||||
disabled={uploading}
|
||||
aria-label={t("upload")}
|
||||
/>
|
||||
<Button
|
||||
disabled={uploading}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{uploading ? t("uploading") : t("upload")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn("text-muted-foreground text-sm")}>{t("description")}</div>
|
||||
</header>
|
||||
<main>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center p-8 text-sm text-gray-500">
|
||||
{t("loading")}
|
||||
</div>
|
||||
) : resources.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed p-8 text-center text-gray-500">
|
||||
<Database className="h-8 w-8 opacity-50" />
|
||||
<p>{t("noResources")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{resources.map((resource, index) => (
|
||||
<li
|
||||
key={resource.uri}
|
||||
className={cn("bg-card flex items-start gap-3 rounded-lg border p-3")}
|
||||
>
|
||||
<div className={cn("bg-primary/10 rounded p-2")}>
|
||||
<FileText className="text-primary h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="truncate font-medium">{resource.title}</h3>
|
||||
<div className={cn("text-muted-foreground flex items-center gap-2 text-xs")}>
|
||||
<span className="truncate max-w-[300px]" title={resource.uri}>
|
||||
{resource.uri}
|
||||
</span>
|
||||
{resource.description && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="truncate">{resource.description}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RAGTab.icon = Database;
|
||||
RAGTab.displayName = "Resources";
|
||||
@@ -42,4 +42,5 @@ export interface ToolCallRuntime {
|
||||
export interface Resource {
|
||||
uri: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user