page.tsx 47 KB

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