page.tsx 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326
  1. "use client";
  2. import { useCallback, useEffect, useState } from "react";
  3. import { useRouter } from "next/navigation";
  4. import { useAuth } from "@/features/agent/hooks/useAuth";
  5. import { ResponsiveLayout } from "@/components/layout";
  6. import type { I18nKey } from "@/lib/i18n/dict";
  7. import { useI18n } from "@/lib/i18n/provider";
  8. import {
  9. fetchKnowledgeBases,
  10. createKnowledgeBase,
  11. updateKnowledgeBase,
  12. updateKnowledgeBaseRAGEnabled,
  13. deleteKnowledgeBase,
  14. type KnowledgeBase,
  15. type CreateKnowledgeBaseRequest,
  16. type UpdateKnowledgeBaseRequest,
  17. } from "@/features/agent/services/knowledgeBaseApi";
  18. import {
  19. fetchDocuments,
  20. createDocument,
  21. updateDocument,
  22. deleteDocument,
  23. publishDocument,
  24. unpublishDocument,
  25. type Document,
  26. type CreateDocumentRequest,
  27. type UpdateDocumentRequest,
  28. type DocumentListResult,
  29. } from "@/features/agent/services/documentApi";
  30. import {
  31. importDocuments,
  32. importFromUrls,
  33. type ImportResult,
  34. } from "@/features/agent/services/importApi";
  35. import { Button } from "@/components/ui/button";
  36. import { Input } from "@/components/ui/input";
  37. import {
  38. Dialog,
  39. DialogContent,
  40. DialogHeader,
  41. DialogTitle,
  42. DialogDescription,
  43. } from "@/components/ui/dialog";
  44. import { Card } from "@/components/ui/card";
  45. import { Label } from "@/components/ui/label";
  46. import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
  47. import { Switch } from "@/components/ui/switch";
  48. import {
  49. Plus,
  50. Edit,
  51. Trash2,
  52. Search,
  53. FileText,
  54. Upload,
  55. Link as LinkIcon,
  56. BookOpen,
  57. CheckCircle2,
  58. XCircle,
  59. Loader2,
  60. ChevronLeft,
  61. ChevronRight,
  62. Scissors,
  63. } from "lucide-react";
  64. import { Textarea } from "@/components/ui/textarea";
  65. import { toast } from "@/hooks/useToast";
  66. import DocumentDetailPage from "./[docId]/page";
  67. export default function KnowledgePage(props: any = {}) {
  68. const { embedded = false } = props;
  69. const router = useRouter();
  70. const { agent } = useAuth();
  71. const { t, lang } = useI18n();
  72. const [chunkingDocId, setChunkingDocId] = useState<number | null>(null);
  73. const tr = (key: I18nKey, vars?: Record<string, string>) => {
  74. let s = t(key);
  75. if (!vars) return s;
  76. for (const k of Object.keys(vars)) {
  77. s = s.replaceAll(`{{${k}}}`, vars[k] ?? "");
  78. }
  79. return s;
  80. };
  81. // 知识库状态
  82. const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
  83. const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | null>(null);
  84. const [loadingKBs, setLoadingKBs] = useState(true);
  85. // 文档状态
  86. const [documents, setDocuments] = useState<Document[]>([]);
  87. const [documentResult, setDocumentResult] = useState<DocumentListResult | null>(null);
  88. const [loadingDocs, setLoadingDocs] = useState(false);
  89. const [searchKeyword, setSearchKeyword] = useState("");
  90. const [statusFilter, setStatusFilter] = useState<string>("all");
  91. const [currentPage, setCurrentPage] = useState(1);
  92. const pageSize = 20;
  93. // 对话框状态
  94. const [createKBDialogOpen, setCreateKBDialogOpen] = useState(false);
  95. const [editKBDialogOpen, setEditKBDialogOpen] = useState(false);
  96. const [deleteKBDialogOpen, setDeleteKBDialogOpen] = useState(false);
  97. const [createDocDialogOpen, setCreateDocDialogOpen] = useState(false);
  98. const [editDocDialogOpen, setEditDocDialogOpen] = useState(false);
  99. const [deleteDocDialogOpen, setDeleteDocDialogOpen] = useState(false);
  100. const [importDialogOpen, setImportDialogOpen] = useState(false);
  101. const [importTab, setImportTab] = useState<"file" | "url">("file");
  102. const [selectedDocument, setSelectedDocument] = useState<Document | null>(null);
  103. // 表单状态
  104. const [submitting, setSubmitting] = useState(false);
  105. const [createKBForm, setCreateKBForm] = useState<CreateKnowledgeBaseRequest>({
  106. name: "",
  107. description: "",
  108. });
  109. const [editKBForm, setEditKBForm] = useState<UpdateKnowledgeBaseRequest>({});
  110. const [createDocForm, setCreateDocForm] = useState<CreateDocumentRequest>({
  111. knowledge_base_id: 0,
  112. title: "",
  113. content: "",
  114. summary: "",
  115. type: "document",
  116. status: "draft",
  117. });
  118. const [editDocForm, setEditDocForm] = useState<UpdateDocumentRequest>({});
  119. const [importUrls, setImportUrls] = useState<string>("");
  120. const [importFiles, setImportFiles] = useState<File[]>([]);
  121. // 加载知识库列表(不依赖 selectedKnowledgeBase,避免选中后反复触发 effect 导致疯狂刷新)
  122. const loadKnowledgeBases = useCallback(async () => {
  123. setLoadingKBs(true);
  124. try {
  125. const data = await fetchKnowledgeBases();
  126. setKnowledgeBases(data);
  127. } catch (error) {
  128. console.error("加载知识库列表失败:", error);
  129. toast.error((error as Error).message || t("agent.knowledge.toast.loadKbFailed"));
  130. } finally {
  131. setLoadingKBs(false);
  132. }
  133. }, [t]);
  134. // 加载文档列表(silent:后台轮询向量化状态时不全屏“加载中”、不弹 Toast,避免刷屏)
  135. const loadDocuments = useCallback(
  136. async (opts?: { silent?: boolean }) => {
  137. if (!selectedKnowledgeBase) {
  138. setDocuments([]);
  139. setDocumentResult(null);
  140. return;
  141. }
  142. const silent = opts?.silent === true;
  143. if (!silent) {
  144. setLoadingDocs(true);
  145. }
  146. try {
  147. const status = statusFilter === "all" ? undefined : statusFilter;
  148. const result = await fetchDocuments(
  149. selectedKnowledgeBase.id,
  150. currentPage,
  151. pageSize,
  152. searchKeyword || undefined,
  153. status
  154. );
  155. setDocumentResult(result);
  156. setDocuments(result.documents ?? []);
  157. } catch (error) {
  158. console.error("加载文档列表失败:", error);
  159. if (!silent) {
  160. toast.error((error as Error).message || t("agent.knowledge.toast.loadDocFailed"));
  161. setDocuments([]);
  162. setDocumentResult(null);
  163. }
  164. } finally {
  165. if (!silent) {
  166. setLoadingDocs(false);
  167. }
  168. }
  169. },
  170. [selectedKnowledgeBase, currentPage, searchKeyword, statusFilter, t]
  171. );
  172. // 初始加载
  173. useEffect(() => {
  174. loadKnowledgeBases();
  175. }, [loadKnowledgeBases]);
  176. // 当选择知识库或搜索条件变化时,重新加载文档
  177. useEffect(() => {
  178. setCurrentPage(1); // 切换知识库或搜索时重置页码
  179. void loadDocuments();
  180. }, [loadDocuments]);
  181. // 向量化进行中时自动刷新列表(pending → processing → completed/failed),无需手动整页刷新
  182. useEffect(() => {
  183. if (!selectedKnowledgeBase) return;
  184. const hasEmbeddingInFlight = documents.some(
  185. (d) => d.embedding_status === "pending" || d.embedding_status === "processing"
  186. );
  187. if (!hasEmbeddingInFlight) return;
  188. const intervalMs = 2500;
  189. const id = window.setInterval(() => {
  190. void loadDocuments({ silent: true });
  191. }, intervalMs);
  192. return () => window.clearInterval(id);
  193. }, [selectedKnowledgeBase, documents, loadDocuments]);
  194. // 选择知识库
  195. const handleSelectKnowledgeBase = (kb: KnowledgeBase) => {
  196. setSelectedKnowledgeBase(kb);
  197. setSearchKeyword("");
  198. setStatusFilter("all");
  199. setCurrentPage(1);
  200. };
  201. // 创建知识库
  202. const handleCreateKB = async () => {
  203. if (!createKBForm.name.trim()) {
  204. toast.error(t("agent.knowledge.toast.kbNameRequired"));
  205. return;
  206. }
  207. setSubmitting(true);
  208. try {
  209. await createKnowledgeBase(createKBForm);
  210. setCreateKBDialogOpen(false);
  211. setCreateKBForm({ name: "", description: "" });
  212. await loadKnowledgeBases();
  213. toast.success(t("agent.knowledge.toast.createSuccess"));
  214. } catch (error) {
  215. toast.error((error as Error).message || t("agent.knowledge.toast.createKbFailed"));
  216. } finally {
  217. setSubmitting(false);
  218. }
  219. };
  220. // 打开编辑知识库对话框
  221. const handleOpenEditKB = (kb: KnowledgeBase) => {
  222. setEditKBForm({
  223. name: kb.name,
  224. description: kb.description,
  225. });
  226. setSelectedKnowledgeBase(kb);
  227. setEditKBDialogOpen(true);
  228. };
  229. // 更新知识库
  230. const handleUpdateKB = async () => {
  231. if (!selectedKnowledgeBase) return;
  232. setSubmitting(true);
  233. try {
  234. await updateKnowledgeBase(selectedKnowledgeBase.id, editKBForm);
  235. setEditKBDialogOpen(false);
  236. await loadKnowledgeBases();
  237. toast.success(t("agent.knowledge.toast.updateSuccess"));
  238. } catch (error) {
  239. toast.error((error as Error).message || t("agent.knowledge.toast.updateKbFailed"));
  240. } finally {
  241. setSubmitting(false);
  242. }
  243. };
  244. // 打开删除知识库对话框
  245. const handleOpenDeleteKB = (kb: KnowledgeBase) => {
  246. setSelectedKnowledgeBase(kb);
  247. setDeleteKBDialogOpen(true);
  248. };
  249. // 删除知识库
  250. const handleDeleteKB = async () => {
  251. if (!selectedKnowledgeBase) return;
  252. setSubmitting(true);
  253. try {
  254. await deleteKnowledgeBase(selectedKnowledgeBase.id);
  255. setDeleteKBDialogOpen(false);
  256. setSelectedKnowledgeBase(null);
  257. await loadKnowledgeBases();
  258. toast.success(t("agent.knowledge.toast.deleteSuccess"));
  259. } catch (error) {
  260. toast.error((error as Error).message || t("agent.knowledge.toast.deleteKbFailed"));
  261. } finally {
  262. setSubmitting(false);
  263. }
  264. };
  265. // 打开创建文档对话框
  266. const handleOpenCreateDoc = () => {
  267. if (!selectedKnowledgeBase) {
  268. toast.error(t("agent.knowledge.toast.selectKbFirst"));
  269. return;
  270. }
  271. setCreateDocForm({
  272. knowledge_base_id: selectedKnowledgeBase.id,
  273. title: "",
  274. content: "",
  275. summary: "",
  276. type: "document",
  277. status: "draft",
  278. });
  279. setCreateDocDialogOpen(true);
  280. };
  281. // 创建文档
  282. const handleCreateDoc = async () => {
  283. if (!createDocForm.title.trim() || !createDocForm.content.trim()) {
  284. toast.error(t("agent.knowledge.toast.docTitleContentRequired"));
  285. return;
  286. }
  287. setSubmitting(true);
  288. try {
  289. await createDocument(createDocForm);
  290. setCreateDocDialogOpen(false);
  291. setCreateDocForm({
  292. knowledge_base_id: selectedKnowledgeBase?.id || 0,
  293. title: "",
  294. content: "",
  295. summary: "",
  296. type: "document",
  297. status: "draft",
  298. });
  299. await loadDocuments();
  300. toast.success(t("agent.knowledge.toast.createSuccess"));
  301. } catch (error) {
  302. toast.error((error as Error).message || t("agent.knowledge.toast.createDocFailed"));
  303. } finally {
  304. setSubmitting(false);
  305. }
  306. };
  307. // 打开编辑文档对话框
  308. const handleOpenEditDoc = (doc: Document) => {
  309. setSelectedDocument(doc);
  310. setEditDocForm({
  311. title: doc.title,
  312. content: doc.content,
  313. summary: doc.summary,
  314. type: doc.type,
  315. status: doc.status,
  316. });
  317. setEditDocDialogOpen(true);
  318. };
  319. // 更新文档
  320. const handleUpdateDoc = async (docId: number) => {
  321. setSubmitting(true);
  322. try {
  323. await updateDocument(docId, editDocForm);
  324. setEditDocDialogOpen(false);
  325. await loadDocuments();
  326. toast.success(t("agent.knowledge.toast.updateSuccess"));
  327. } catch (error) {
  328. toast.error((error as Error).message || t("agent.knowledge.toast.updateDocFailed"));
  329. } finally {
  330. setSubmitting(false);
  331. }
  332. };
  333. // 打开删除文档对话框
  334. const handleOpenDeleteDoc = (doc: Document) => {
  335. setSelectedDocument(doc);
  336. setDeleteDocDialogOpen(true);
  337. };
  338. // 删除文档
  339. const handleDeleteDoc = async (docId: number) => {
  340. setSubmitting(true);
  341. try {
  342. await deleteDocument(docId);
  343. setDeleteDocDialogOpen(false);
  344. await loadDocuments();
  345. toast.success(t("agent.knowledge.toast.deleteSuccess"));
  346. } catch (error) {
  347. toast.error((error as Error).message || t("agent.knowledge.toast.deleteDocFailed"));
  348. } finally {
  349. setSubmitting(false);
  350. }
  351. };
  352. // 发布文档
  353. const handlePublishDoc = async (docId: number) => {
  354. try {
  355. await publishDocument(docId);
  356. await loadDocuments();
  357. toast.success(t("agent.knowledge.toast.publishSuccess"));
  358. } catch (error) {
  359. toast.error((error as Error).message || t("agent.knowledge.toast.publishFailed"));
  360. }
  361. };
  362. // 取消发布文档
  363. const handleUnpublishDoc = async (docId: number) => {
  364. try {
  365. await unpublishDocument(docId);
  366. await loadDocuments();
  367. toast.success(t("agent.knowledge.toast.unpublishSuccess"));
  368. } catch (error) {
  369. toast.error((error as Error).message || t("agent.knowledge.toast.unpublishFailed"));
  370. }
  371. };
  372. // 导入文件
  373. const handleImportFiles = async () => {
  374. if (!selectedKnowledgeBase) {
  375. toast.error(t("agent.knowledge.toast.selectKbFirst"));
  376. return;
  377. }
  378. if (importFiles.length === 0) {
  379. toast.error(t("agent.knowledge.toast.selectFiles"));
  380. return;
  381. }
  382. setSubmitting(true);
  383. try {
  384. const result: ImportResult = await importDocuments(selectedKnowledgeBase.id, importFiles);
  385. const errMsg = result.errors?.length ? result.errors[0] : "";
  386. if (result.failed_count > 0 && result.success_count === 0) {
  387. toast.error(
  388. errMsg ||
  389. tr("agent.knowledge.toast.importFailed.files", {
  390. count: String(result.failed_count),
  391. })
  392. );
  393. } else if (result.failed_count > 0) {
  394. toast.success(
  395. tr("agent.knowledge.toast.importDone.partial", {
  396. success: String(result.success_count),
  397. failed: String(result.failed_count),
  398. err: errMsg ? `(${errMsg})` : "",
  399. })
  400. );
  401. } else {
  402. toast.success(
  403. tr("agent.knowledge.toast.importDone.files", {
  404. success: String(result.success_count),
  405. })
  406. );
  407. }
  408. setImportDialogOpen(false);
  409. setImportFiles([]);
  410. try {
  411. await loadDocuments();
  412. await loadKnowledgeBases();
  413. } catch {
  414. toast.error(t("agent.knowledge.toast.importRefreshFailed"));
  415. }
  416. } catch (error) {
  417. toast.error((error as Error).message || t("agent.knowledge.toast.importDocFailed"));
  418. } finally {
  419. setSubmitting(false);
  420. }
  421. };
  422. // 导入 URL
  423. const handleImportUrls = async () => {
  424. if (!selectedKnowledgeBase) {
  425. toast.error(t("agent.knowledge.toast.selectKbFirst"));
  426. return;
  427. }
  428. const urls = importUrls
  429. .split("\n")
  430. .map((url) => url.trim())
  431. .filter((url) => url.length > 0);
  432. if (urls.length === 0) {
  433. toast.error(t("agent.knowledge.toast.urlRequired"));
  434. return;
  435. }
  436. setSubmitting(true);
  437. try {
  438. const result: ImportResult = await importFromUrls({
  439. knowledge_base_id: selectedKnowledgeBase.id,
  440. urls,
  441. });
  442. const errMsg = result.errors?.length ? result.errors[0] : "";
  443. if (result.failed_count > 0 && result.success_count === 0) {
  444. toast.error(
  445. errMsg ||
  446. tr("agent.knowledge.toast.importFailed.urls", {
  447. count: String(result.failed_count),
  448. })
  449. );
  450. } else if (result.failed_count > 0) {
  451. toast.success(
  452. tr("agent.knowledge.toast.importDone.partial", {
  453. success: String(result.success_count),
  454. failed: String(result.failed_count),
  455. err: errMsg ? `(${errMsg})` : "",
  456. })
  457. );
  458. } else {
  459. toast.success(
  460. tr("agent.knowledge.toast.importDone.urls", {
  461. success: String(result.success_count),
  462. })
  463. );
  464. }
  465. setImportDialogOpen(false);
  466. setImportUrls("");
  467. try {
  468. await loadDocuments();
  469. await loadKnowledgeBases();
  470. } catch {
  471. toast.error(t("agent.knowledge.toast.importRefreshFailed"));
  472. }
  473. } catch (error) {
  474. toast.error((error as Error).message || t("agent.knowledge.toast.importUrlFailed"));
  475. } finally {
  476. setSubmitting(false);
  477. }
  478. };
  479. // 格式化时间
  480. const formatTime = (dateStr: string) => {
  481. const date = new Date(dateStr);
  482. return date.toLocaleString(lang === "en" ? "en-US" : "zh-CN", {
  483. year: "numeric",
  484. month: "2-digit",
  485. day: "2-digit",
  486. hour: "2-digit",
  487. minute: "2-digit",
  488. });
  489. };
  490. // 获取状态标签
  491. const getStatusBadge = (status: string) => {
  492. switch (status) {
  493. case "published":
  494. return (
  495. <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800">
  496. <CheckCircle2 className="w-3 h-3 mr-1" />
  497. {t("agent.knowledge.status.published")}
  498. </span>
  499. );
  500. case "draft":
  501. return (
  502. <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800">
  503. {t("agent.knowledge.status.draft")}
  504. </span>
  505. );
  506. default:
  507. return (
  508. <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800">
  509. {status}
  510. </span>
  511. );
  512. }
  513. };
  514. // 获取向量化状态标签
  515. const getEmbeddingStatusBadge = (status: string) => {
  516. switch (status) {
  517. case "completed":
  518. return (
  519. <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800">
  520. <CheckCircle2 className="w-3 h-3 mr-1" />
  521. {t("agent.knowledge.embedding.completed")}
  522. </span>
  523. );
  524. case "processing":
  525. return (
  526. <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-yellow-100 text-yellow-800">
  527. <Loader2 className="w-3 h-3 mr-1 animate-spin" />
  528. {t("agent.knowledge.embedding.processing")}
  529. </span>
  530. );
  531. case "failed":
  532. return (
  533. <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-red-100 text-red-800">
  534. <XCircle className="w-3 h-3 mr-1" />
  535. {t("agent.knowledge.embedding.failed")}
  536. </span>
  537. );
  538. case "pending":
  539. default:
  540. return (
  541. <span className="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800">
  542. {t("agent.knowledge.embedding.pending")}
  543. </span>
  544. );
  545. }
  546. };
  547. // 构建头部内容
  548. const headerContent = (
  549. <div className="bg-card border-b p-3 shadow-sm sm:p-4">
  550. <div className="flex items-center justify-between mb-4">
  551. <h1 className="text-xl font-bold text-foreground">{t("agent.knowledge.title")}</h1>
  552. {!embedded && (
  553. <Button
  554. variant="ghost"
  555. size="sm"
  556. onClick={() => router.push("/agent/dashboard")}
  557. >
  558. {t("agent.common.back")}
  559. </Button>
  560. )}
  561. </div>
  562. </div>
  563. );
  564. // 构建主内容区
  565. const mainContent = (
  566. <div className="flex flex-1 min-h-0 flex-col overflow-hidden md:flex-row">
  567. {/* 左侧:知识库列表(小屏置顶且限高,避免挤掉文档区) */}
  568. <div className="flex h-[min(40vh,320px)] w-full shrink-0 flex-col border-b border-border bg-gray-50 md:h-auto md:max-h-none md:w-64 md:border-b-0 md:border-r">
  569. <div className="p-4 border-b">
  570. <Button
  571. onClick={() => setCreateKBDialogOpen(true)}
  572. className="w-full"
  573. size="sm"
  574. >
  575. <Plus className="w-4 h-4 mr-2" />
  576. {t("agent.knowledge.kb.create")}
  577. </Button>
  578. </div>
  579. <div className="flex-1 overflow-y-auto p-2">
  580. {loadingKBs ? (
  581. <div className="flex items-center justify-center h-full">
  582. <span className="text-muted-foreground">{t("common.loading")}</span>
  583. </div>
  584. ) : knowledgeBases.length === 0 ? (
  585. <div className="flex items-center justify-center h-full">
  586. <span className="text-muted-foreground">{t("agent.knowledge.kb.empty")}</span>
  587. </div>
  588. ) : (
  589. <div className="space-y-2">
  590. {knowledgeBases.map((kb) => (
  591. <Card
  592. key={kb.id}
  593. className={`p-3 cursor-pointer transition-colors ${
  594. selectedKnowledgeBase?.id === kb.id
  595. ? "bg-green-50 border-green-300"
  596. : "hover:bg-gray-100"
  597. }`}
  598. onClick={() => handleSelectKnowledgeBase(kb)}
  599. >
  600. <div className="flex items-start justify-between">
  601. <div className="flex-1 min-w-0">
  602. <div className="flex items-center gap-2 mb-1">
  603. <BookOpen className="w-4 h-4 text-blue-600 flex-shrink-0" />
  604. <h3 className="font-medium text-sm truncate">{kb.name}</h3>
  605. </div>
  606. {kb.description && (
  607. <p className="text-xs text-muted-foreground line-clamp-2 mb-1">
  608. {kb.description}
  609. </p>
  610. )}
  611. <div className="flex items-center gap-2 mt-1">
  612. <span className="text-xs text-muted-foreground">
  613. {tr("agent.knowledge.kb.docCount", {
  614. count: String(kb.document_count),
  615. })}
  616. </span>
  617. </div>
  618. </div>
  619. <div className="flex flex-col gap-1 ml-2">
  620. <Button
  621. variant="ghost"
  622. size="sm"
  623. className="h-6 w-6 p-0"
  624. onClick={(e) => {
  625. e.stopPropagation();
  626. handleOpenEditKB(kb);
  627. }}
  628. >
  629. <Edit className="w-3 h-3" />
  630. </Button>
  631. <Button
  632. variant="ghost"
  633. size="sm"
  634. className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
  635. onClick={(e) => {
  636. e.stopPropagation();
  637. handleOpenDeleteKB(kb);
  638. }}
  639. >
  640. <Trash2 className="w-3 h-3" />
  641. </Button>
  642. </div>
  643. </div>
  644. </Card>
  645. ))}
  646. </div>
  647. )}
  648. </div>
  649. </div>
  650. {/* 右侧:文档列表 */}
  651. <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
  652. {selectedKnowledgeBase ? (
  653. <>
  654. {/* 文档列表头部 */}
  655. <div className="border-b bg-white p-3 sm:p-4">
  656. <div className="mb-4 flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
  657. <h2 className="text-lg font-semibold leading-tight break-words">
  658. {selectedKnowledgeBase.name}
  659. </h2>
  660. <div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
  661. <div className="flex items-center gap-2">
  662. <Label
  663. htmlFor="rag-enabled"
  664. className="whitespace-nowrap text-sm text-muted-foreground"
  665. >
  666. {t("agent.knowledge.rag")}
  667. </Label>
  668. <Switch
  669. id="rag-enabled"
  670. checked={selectedKnowledgeBase.rag_enabled !== false}
  671. onCheckedChange={async (checked) => {
  672. try {
  673. const updated = await updateKnowledgeBaseRAGEnabled(selectedKnowledgeBase.id, checked);
  674. setSelectedKnowledgeBase((prev) => (prev?.id === updated.id ? { ...prev, rag_enabled: updated.rag_enabled } : prev));
  675. setKnowledgeBases((prev) => prev.map((kb) => (kb.id === updated.id ? { ...kb, rag_enabled: updated.rag_enabled } : kb)));
  676. } catch (e) {
  677. toast.error((e as Error).message || t("agent.knowledge.toast.updateFailed"));
  678. }
  679. }}
  680. />
  681. </div>
  682. <div className="flex flex-wrap gap-2">
  683. <Button
  684. variant="outline"
  685. size="sm"
  686. className="flex-1 min-w-[8rem] sm:flex-initial"
  687. onClick={() => {
  688. setImportTab("url");
  689. setImportDialogOpen(true);
  690. }}
  691. >
  692. <LinkIcon className="mr-2 h-4 w-4 shrink-0" />
  693. {t("agent.knowledge.import.url")}
  694. </Button>
  695. <Button
  696. variant="outline"
  697. size="sm"
  698. className="flex-1 min-w-[8rem] sm:flex-initial"
  699. onClick={() => {
  700. setImportTab("file");
  701. setImportDialogOpen(true);
  702. }}
  703. >
  704. <Upload className="mr-2 h-4 w-4 shrink-0" />
  705. {t("agent.knowledge.import.file")}
  706. </Button>
  707. <Button size="sm" className="w-full sm:w-auto" onClick={handleOpenCreateDoc}>
  708. <Plus className="mr-2 h-4 w-4 shrink-0" />
  709. {t("agent.knowledge.doc.create")}
  710. </Button>
  711. </div>
  712. </div>
  713. </div>
  714. {/* 搜索和筛选 */}
  715. <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
  716. <div className="flex-1 relative">
  717. <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
  718. <Input
  719. type="text"
  720. placeholder={t("agent.knowledge.doc.searchPh")}
  721. value={searchKeyword}
  722. onChange={(e) => setSearchKeyword(e.target.value)}
  723. className="pl-10"
  724. />
  725. </div>
  726. <select
  727. value={statusFilter}
  728. onChange={(e) => setStatusFilter(e.target.value)}
  729. className="px-3 py-2 border rounded-md text-sm"
  730. >
  731. <option value="all">{t("agent.knowledge.filter.all")}</option>
  732. <option value="draft">{t("agent.knowledge.status.draft")}</option>
  733. <option value="published">{t("agent.knowledge.status.published")}</option>
  734. </select>
  735. </div>
  736. </div>
  737. {/* 文档列表 */}
  738. <div className="flex-1 overflow-y-auto p-3 sm:p-4">
  739. {loadingDocs ? (
  740. <div className="flex items-center justify-center h-full">
  741. <span className="text-muted-foreground">{t("common.loading")}</span>
  742. </div>
  743. ) : (documents?.length ?? 0) === 0 ? (
  744. <div className="flex items-center justify-center h-full">
  745. <span className="text-muted-foreground">
  746. {searchKeyword || statusFilter !== "all"
  747. ? t("agent.knowledge.doc.empty.filtered")
  748. : t("agent.knowledge.doc.empty")}
  749. </span>
  750. </div>
  751. ) : (
  752. <div className="space-y-4">
  753. {(documents ?? []).map((doc) => (
  754. <Card key={doc.id} className="p-3 sm:p-4">
  755. <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
  756. <div className="min-w-0 flex-1">
  757. <div className="mb-2 flex flex-wrap items-center gap-2">
  758. <FileText className="h-5 w-5 shrink-0 text-blue-600" />
  759. <h3 className="min-w-0 flex-1 font-medium text-foreground break-words sm:truncate">
  760. {doc.title}
  761. </h3>
  762. <span className="flex flex-wrap gap-1">
  763. {getStatusBadge(doc.status)}
  764. {getEmbeddingStatusBadge(doc.embedding_status)}
  765. </span>
  766. </div>
  767. {doc.summary && (
  768. <p className="mb-2 line-clamp-2 text-sm text-muted-foreground">
  769. {doc.summary}
  770. </p>
  771. )}
  772. <div className="flex flex-col gap-1 text-xs text-muted-foreground sm:flex-row sm:flex-wrap sm:gap-4">
  773. <span>
  774. {t("agent.knowledge.doc.type")}: {doc.type}
  775. </span>
  776. <span className="break-words">
  777. {t("agent.knowledge.doc.createdAt")}: {formatTime(doc.created_at)}
  778. </span>
  779. </div>
  780. </div>
  781. <div className="flex w-full shrink-0 flex-col gap-2 sm:w-auto sm:ml-4">
  782. <div className="flex flex-wrap gap-2">
  783. <Button
  784. variant="outline"
  785. size="sm"
  786. className="min-w-0 flex-1 sm:flex-initial"
  787. onClick={() => handleOpenEditDoc(doc)}
  788. >
  789. <Edit className="mr-1 h-4 w-4 shrink-0" />
  790. {t("agent.common.edit")}
  791. </Button>
  792. <Button
  793. variant="outline"
  794. size="sm"
  795. className="shrink-0"
  796. onClick={() => setChunkingDocId(doc.id)}
  797. >
  798. <Scissors className="mr-1 h-4 w-4 shrink-0" />
  799. 分段
  800. </Button>
  801. <Button
  802. variant="destructive"
  803. size="sm"
  804. className="shrink-0"
  805. onClick={() => handleOpenDeleteDoc(doc)}
  806. >
  807. <Trash2 className="h-4 w-4" />
  808. </Button>
  809. </div>
  810. <div className="flex flex-wrap gap-2">
  811. {doc.status === "published" ? (
  812. <Button
  813. variant="outline"
  814. size="sm"
  815. className="w-full sm:w-auto"
  816. onClick={() => handleUnpublishDoc(doc.id)}
  817. >
  818. {t("agent.knowledge.doc.unpublish")}
  819. </Button>
  820. ) : (
  821. <Button
  822. variant="outline"
  823. size="sm"
  824. className="w-full sm:w-auto"
  825. onClick={() => handlePublishDoc(doc.id)}
  826. >
  827. {t("agent.knowledge.doc.publish")}
  828. </Button>
  829. )}
  830. </div>
  831. </div>
  832. </div>
  833. </Card>
  834. ))}
  835. </div>
  836. )}
  837. {/* 分页 */}
  838. {documentResult && documentResult.total_page > 1 && (
  839. <div className="flex items-center justify-center gap-2 mt-4">
  840. <Button
  841. variant="outline"
  842. size="sm"
  843. onClick={() => setCurrentPage(currentPage - 1)}
  844. disabled={currentPage === 1}
  845. >
  846. <ChevronLeft className="w-4 h-4" />
  847. </Button>
  848. <span className="text-sm text-muted-foreground">
  849. {tr("agent.knowledge.pagination", {
  850. page: String(currentPage),
  851. totalPage: String(documentResult.total_page),
  852. total: String(documentResult.total),
  853. })}
  854. </span>
  855. <Button
  856. variant="outline"
  857. size="sm"
  858. onClick={() => setCurrentPage(currentPage + 1)}
  859. disabled={currentPage >= documentResult.total_page}
  860. >
  861. <ChevronRight className="w-4 h-4" />
  862. </Button>
  863. </div>
  864. )}
  865. </div>
  866. </>
  867. ) : (
  868. <div className="flex items-center justify-center h-full">
  869. <span className="text-muted-foreground">{t("agent.knowledge.kb.selectOne")}</span>
  870. </div>
  871. )}
  872. </div>
  873. </div>
  874. );
  875. const dialogs = (
  876. <>
  877. <Dialog open={createKBDialogOpen} onOpenChange={setCreateKBDialogOpen}>
  878. <DialogContent className="max-h-[90dvh] max-w-[min(100vw-2rem,42rem)] overflow-y-auto">
  879. <DialogHeader>
  880. <DialogTitle>{t("agent.knowledge.dialog.kbCreateTitle")}</DialogTitle>
  881. <DialogDescription>{t("agent.knowledge.dialog.kbCreateDesc")}</DialogDescription>
  882. </DialogHeader>
  883. <div className="space-y-4">
  884. <div>
  885. <Label htmlFor="create-kb-name">{t("agent.knowledge.field.name")} *</Label>
  886. <Input
  887. id="create-kb-name"
  888. value={createKBForm.name}
  889. onChange={(e) => setCreateKBForm({ ...createKBForm, name: e.target.value })}
  890. placeholder={t("agent.knowledge.ph.kbName")}
  891. />
  892. </div>
  893. <div>
  894. <Label htmlFor="create-kb-desc">{t("agent.knowledge.field.descOptional")}</Label>
  895. <Textarea
  896. id="create-kb-desc"
  897. value={createKBForm.description || ""}
  898. onChange={(e) =>
  899. setCreateKBForm({ ...createKBForm, description: e.target.value })
  900. }
  901. placeholder={t("agent.knowledge.ph.kbDesc")}
  902. rows={3}
  903. />
  904. </div>
  905. <div className="flex justify-end gap-2">
  906. <Button
  907. variant="outline"
  908. onClick={() => setCreateKBDialogOpen(false)}
  909. disabled={submitting}
  910. >
  911. {t("agent.common.cancel")}
  912. </Button>
  913. <Button onClick={handleCreateKB} disabled={submitting}>
  914. {submitting ? t("agent.knowledge.submitting.creating") : t("agent.common.create")}
  915. </Button>
  916. </div>
  917. </div>
  918. </DialogContent>
  919. </Dialog>
  920. <Dialog open={editKBDialogOpen} onOpenChange={setEditKBDialogOpen}>
  921. <DialogContent className="max-h-[90dvh] max-w-[min(100vw-2rem,42rem)] overflow-y-auto">
  922. <DialogHeader>
  923. <DialogTitle>{t("agent.knowledge.dialog.kbEditTitle")}</DialogTitle>
  924. <DialogDescription>{t("agent.knowledge.dialog.kbEditDesc")}</DialogDescription>
  925. </DialogHeader>
  926. {selectedKnowledgeBase && (
  927. <div className="space-y-4">
  928. <div>
  929. <Label htmlFor="edit-kb-name">{t("agent.knowledge.field.name")} *</Label>
  930. <Input
  931. id="edit-kb-name"
  932. value={editKBForm.name || selectedKnowledgeBase.name}
  933. onChange={(e) => setEditKBForm({ ...editKBForm, name: e.target.value })}
  934. placeholder={t("agent.knowledge.ph.kbName")}
  935. />
  936. </div>
  937. <div>
  938. <Label htmlFor="edit-kb-desc">{t("agent.knowledge.field.descOptional")}</Label>
  939. <Textarea
  940. id="edit-kb-desc"
  941. value={editKBForm.description ?? selectedKnowledgeBase.description ?? ""}
  942. onChange={(e) =>
  943. setEditKBForm({ ...editKBForm, description: e.target.value })
  944. }
  945. placeholder={t("agent.knowledge.ph.kbDesc")}
  946. rows={3}
  947. />
  948. </div>
  949. <div className="flex justify-end gap-2">
  950. <Button
  951. variant="outline"
  952. onClick={() => setEditKBDialogOpen(false)}
  953. disabled={submitting}
  954. >
  955. {t("agent.common.cancel")}
  956. </Button>
  957. <Button onClick={handleUpdateKB} disabled={submitting}>
  958. {submitting ? t("agent.knowledge.submitting.updating") : t("agent.common.update")}
  959. </Button>
  960. </div>
  961. </div>
  962. )}
  963. </DialogContent>
  964. </Dialog>
  965. <Dialog open={deleteKBDialogOpen} onOpenChange={setDeleteKBDialogOpen}>
  966. <DialogContent className="max-w-[min(100vw-2rem,28rem)]">
  967. <DialogHeader>
  968. <DialogTitle>{t("agent.knowledge.dialog.kbDeleteTitle")}</DialogTitle>
  969. </DialogHeader>
  970. {selectedKnowledgeBase && (
  971. <div className="space-y-4">
  972. <p className="text-foreground">
  973. {tr("agent.knowledge.dialog.kbDeleteConfirm", { name: selectedKnowledgeBase.name })}
  974. </p>
  975. <p className="text-sm text-muted-foreground">
  976. {t("agent.knowledge.dialog.kbDeleteHint")}
  977. </p>
  978. <div className="flex justify-end gap-2">
  979. <Button
  980. variant="outline"
  981. onClick={() => setDeleteKBDialogOpen(false)}
  982. disabled={submitting}
  983. >
  984. {t("agent.common.cancel")}
  985. </Button>
  986. <Button variant="destructive" onClick={handleDeleteKB} disabled={submitting}>
  987. {submitting ? t("agent.knowledge.submitting.deleting") : t("agent.common.delete")}
  988. </Button>
  989. </div>
  990. </div>
  991. )}
  992. </DialogContent>
  993. </Dialog>
  994. <Dialog open={createDocDialogOpen} onOpenChange={setCreateDocDialogOpen}>
  995. <DialogContent className="max-h-[90dvh] max-w-[min(100vw-2rem,48rem)] overflow-y-auto">
  996. <DialogHeader>
  997. <DialogTitle>{t("agent.knowledge.dialog.docCreateTitle")}</DialogTitle>
  998. <DialogDescription>{t("agent.knowledge.dialog.docCreateDesc")}</DialogDescription>
  999. </DialogHeader>
  1000. <div className="space-y-4">
  1001. <div>
  1002. <Label htmlFor="create-doc-title">{t("agent.knowledge.field.title")} *</Label>
  1003. <Input
  1004. id="create-doc-title"
  1005. value={createDocForm.title}
  1006. onChange={(e) => setCreateDocForm({ ...createDocForm, title: e.target.value })}
  1007. placeholder={t("agent.knowledge.ph.docTitle")}
  1008. />
  1009. </div>
  1010. <div>
  1011. <Label htmlFor="create-doc-summary">{t("agent.knowledge.field.summaryOptional")}</Label>
  1012. <Textarea
  1013. id="create-doc-summary"
  1014. value={createDocForm.summary || ""}
  1015. onChange={(e) => setCreateDocForm({ ...createDocForm, summary: e.target.value })}
  1016. placeholder={t("agent.knowledge.ph.docSummary")}
  1017. rows={2}
  1018. />
  1019. </div>
  1020. <div>
  1021. <Label htmlFor="create-doc-content">{t("agent.knowledge.field.content")} *</Label>
  1022. <Textarea
  1023. id="create-doc-content"
  1024. value={createDocForm.content}
  1025. onChange={(e) =>
  1026. setCreateDocForm({ ...createDocForm, content: e.target.value })
  1027. }
  1028. placeholder={t("agent.knowledge.ph.docContent")}
  1029. rows={10}
  1030. className="resize-none"
  1031. />
  1032. </div>
  1033. <div className="flex justify-end gap-2">
  1034. <Button
  1035. variant="outline"
  1036. onClick={() => setCreateDocDialogOpen(false)}
  1037. disabled={submitting}
  1038. >
  1039. {t("agent.common.cancel")}
  1040. </Button>
  1041. <Button onClick={handleCreateDoc} disabled={submitting}>
  1042. {submitting ? t("agent.knowledge.submitting.creating") : t("agent.common.create")}
  1043. </Button>
  1044. </div>
  1045. </div>
  1046. </DialogContent>
  1047. </Dialog>
  1048. <Dialog open={editDocDialogOpen} onOpenChange={setEditDocDialogOpen}>
  1049. <DialogContent className="max-h-[90dvh] max-w-[min(100vw-2rem,48rem)] overflow-y-auto">
  1050. <DialogHeader>
  1051. <DialogTitle>{t("agent.knowledge.dialog.docEditTitle")}</DialogTitle>
  1052. <DialogDescription>{t("agent.knowledge.dialog.docEditDesc")}</DialogDescription>
  1053. </DialogHeader>
  1054. <div className="space-y-4">
  1055. <div>
  1056. <Label htmlFor="edit-doc-title">{t("agent.knowledge.field.title")} *</Label>
  1057. <Input
  1058. id="edit-doc-title"
  1059. value={editDocForm.title || ""}
  1060. onChange={(e) => setEditDocForm({ ...editDocForm, title: e.target.value })}
  1061. placeholder={t("agent.knowledge.ph.docTitle")}
  1062. />
  1063. </div>
  1064. <div>
  1065. <Label htmlFor="edit-doc-summary">{t("agent.knowledge.field.summaryOptional")}</Label>
  1066. <Textarea
  1067. id="edit-doc-summary"
  1068. value={editDocForm.summary || ""}
  1069. onChange={(e) => setEditDocForm({ ...editDocForm, summary: e.target.value })}
  1070. placeholder={t("agent.knowledge.ph.docSummary")}
  1071. rows={2}
  1072. />
  1073. </div>
  1074. <div>
  1075. <Label htmlFor="edit-doc-content">{t("agent.knowledge.field.content")} *</Label>
  1076. <Textarea
  1077. id="edit-doc-content"
  1078. value={editDocForm.content || ""}
  1079. onChange={(e) => setEditDocForm({ ...editDocForm, content: e.target.value })}
  1080. placeholder={t("agent.knowledge.ph.docContent")}
  1081. rows={10}
  1082. className="resize-none"
  1083. />
  1084. </div>
  1085. <div className="flex justify-end gap-2">
  1086. <Button
  1087. variant="outline"
  1088. onClick={() => setEditDocDialogOpen(false)}
  1089. disabled={submitting}
  1090. >
  1091. {t("agent.common.cancel")}
  1092. </Button>
  1093. <Button
  1094. onClick={() => {
  1095. if (selectedDocument) {
  1096. handleUpdateDoc(selectedDocument.id);
  1097. }
  1098. }}
  1099. disabled={submitting}
  1100. >
  1101. {submitting ? t("agent.knowledge.submitting.updating") : t("agent.common.update")}
  1102. </Button>
  1103. </div>
  1104. </div>
  1105. </DialogContent>
  1106. </Dialog>
  1107. <Dialog open={deleteDocDialogOpen} onOpenChange={setDeleteDocDialogOpen}>
  1108. <DialogContent className="max-w-[min(100vw-2rem,28rem)]">
  1109. <DialogHeader>
  1110. <DialogTitle>{t("agent.knowledge.dialog.docDeleteTitle")}</DialogTitle>
  1111. </DialogHeader>
  1112. {selectedDocument && (
  1113. <div className="space-y-4">
  1114. <p className="text-foreground">
  1115. {tr("agent.knowledge.dialog.docDeleteConfirm", { title: selectedDocument.title })}
  1116. </p>
  1117. <p className="text-sm text-muted-foreground">{t("common.irreversibleHint")}</p>
  1118. </div>
  1119. )}
  1120. <div className="flex justify-end gap-2">
  1121. <Button
  1122. variant="outline"
  1123. onClick={() => setDeleteDocDialogOpen(false)}
  1124. disabled={submitting}
  1125. >
  1126. {t("agent.common.cancel")}
  1127. </Button>
  1128. <Button
  1129. variant="destructive"
  1130. onClick={() => {
  1131. if (selectedDocument) {
  1132. handleDeleteDoc(selectedDocument.id);
  1133. }
  1134. }}
  1135. disabled={submitting}
  1136. >
  1137. {submitting ? t("agent.knowledge.submitting.deleting") : t("agent.common.delete")}
  1138. </Button>
  1139. </div>
  1140. </DialogContent>
  1141. </Dialog>
  1142. <Dialog
  1143. open={importDialogOpen}
  1144. onOpenChange={(open) => {
  1145. setImportDialogOpen(open);
  1146. if (!open) {
  1147. setImportFiles([]);
  1148. setImportUrls("");
  1149. setImportTab("file");
  1150. }
  1151. }}
  1152. >
  1153. <DialogContent className="max-h-[90dvh] max-w-[min(100vw-2rem,42rem)] overflow-y-auto">
  1154. <DialogHeader>
  1155. <DialogTitle>{t("agent.knowledge.dialog.importTitle")}</DialogTitle>
  1156. <DialogDescription>{t("agent.knowledge.dialog.importDesc")}</DialogDescription>
  1157. </DialogHeader>
  1158. <Tabs
  1159. value={importTab}
  1160. onValueChange={(v) => setImportTab(v as "file" | "url")}
  1161. defaultValue="file"
  1162. >
  1163. <TabsList className="grid w-full grid-cols-2">
  1164. <TabsTrigger value="file">{t("agent.knowledge.import.tabFile")}</TabsTrigger>
  1165. <TabsTrigger value="url">{t("agent.knowledge.import.tabUrl")}</TabsTrigger>
  1166. </TabsList>
  1167. <TabsContent value="file" className="space-y-4 mt-4">
  1168. <div>
  1169. <Label htmlFor="import-files">{t("agent.knowledge.import.pickFiles")}</Label>
  1170. <Input
  1171. id="import-files"
  1172. type="file"
  1173. multiple
  1174. onChange={(e) => {
  1175. const files = Array.from(e.target.files || []);
  1176. setImportFiles(files);
  1177. }}
  1178. />
  1179. {importFiles.length > 0 && (
  1180. <p className="text-sm text-muted-foreground mt-2">
  1181. {tr("agent.knowledge.import.filesSelected", {
  1182. count: String(importFiles.length),
  1183. })}
  1184. </p>
  1185. )}
  1186. </div>
  1187. <div className="flex justify-end gap-2">
  1188. <Button
  1189. variant="outline"
  1190. onClick={() => setImportDialogOpen(false)}
  1191. disabled={submitting}
  1192. >
  1193. {t("agent.common.cancel")}
  1194. </Button>
  1195. <Button onClick={handleImportFiles} disabled={submitting}>
  1196. {submitting ? t("agent.knowledge.submitting.importing") : t("agent.knowledge.import.action")}
  1197. </Button>
  1198. </div>
  1199. </TabsContent>
  1200. <TabsContent value="url" className="space-y-4 mt-4">
  1201. <div>
  1202. <Label htmlFor="import-urls">{t("agent.knowledge.import.urlListLabel")}</Label>
  1203. <Textarea
  1204. id="import-urls"
  1205. value={importUrls}
  1206. onChange={(e) => setImportUrls(e.target.value)}
  1207. placeholder="https://example.com/page1&#10;https://example.com/page2"
  1208. rows={8}
  1209. className="resize-none"
  1210. />
  1211. </div>
  1212. <div className="flex justify-end gap-2">
  1213. <Button
  1214. variant="outline"
  1215. onClick={() => setImportDialogOpen(false)}
  1216. disabled={submitting}
  1217. >
  1218. {t("agent.common.cancel")}
  1219. </Button>
  1220. <Button onClick={handleImportUrls} disabled={submitting}>
  1221. {submitting ? t("agent.knowledge.submitting.importing") : t("agent.knowledge.import.action")}
  1222. </Button>
  1223. </div>
  1224. </TabsContent>
  1225. </Tabs>
  1226. </DialogContent>
  1227. </Dialog>
  1228. </>
  1229. );
  1230. if (embedded) {
  1231. if (chunkingDocId) {
  1232. return (
  1233. <>
  1234. <DocumentDetailPage
  1235. embedded
  1236. docId={chunkingDocId}
  1237. onBack={() => setChunkingDocId(null)}
  1238. />
  1239. {dialogs}
  1240. </>
  1241. );
  1242. }
  1243. return (
  1244. <>
  1245. <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
  1246. {headerContent}
  1247. {mainContent}
  1248. </div>
  1249. {dialogs}
  1250. </>
  1251. );
  1252. }
  1253. if (chunkingDocId) {
  1254. return (
  1255. <>
  1256. <ResponsiveLayout
  1257. main={
  1258. <DocumentDetailPage
  1259. embedded
  1260. docId={chunkingDocId}
  1261. onBack={() => setChunkingDocId(null)}
  1262. />
  1263. }
  1264. />
  1265. {dialogs}
  1266. </>
  1267. );
  1268. }
  1269. return (
  1270. <>
  1271. <ResponsiveLayout main={mainContent} header={headerContent} />
  1272. {dialogs}
  1273. </>
  1274. );
  1275. }