page.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  1. "use client";
  2. import { useState, useEffect } from "react";
  3. import { useRouter } from "next/navigation";
  4. import { ResponsiveLayout } from "@/components/layout";
  5. import { Button } from "@/components/ui/button";
  6. import { Input } from "@/components/ui/input";
  7. import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
  8. import {
  9. fetchAIConfigs,
  10. createAIConfig,
  11. updateAIConfig,
  12. deleteAIConfig,
  13. type AIConfig,
  14. type CreateAIConfigRequest,
  15. type UpdateAIConfigRequest,
  16. } from "@/features/agent/services/aiConfigApi";
  17. import {
  18. fetchEmbeddingConfig,
  19. updateEmbeddingConfig,
  20. type EmbeddingConfig,
  21. type UpdateEmbeddingConfigRequest,
  22. } from "@/features/agent/services/embeddingConfigApi";
  23. import { useProfile } from "@/features/agent/hooks/useProfile";
  24. import { apiUrl } from "@/lib/config";
  25. import { Checkbox } from "@/components/ui/checkbox";
  26. import { Label } from "@/components/ui/label";
  27. import { toast } from "@/hooks/useToast";
  28. import type { I18nKey } from "@/lib/i18n/dict";
  29. import { useI18n } from "@/lib/i18n/provider";
  30. export default function SettingsPage(props: any = {}) {
  31. const { embedded = false } = props;
  32. const router = useRouter();
  33. const { t } = useI18n();
  34. const modelTypeLabel = (mt: string) => {
  35. const map: Record<string, I18nKey> = {
  36. text: "agent.settings.modelType.text",
  37. image: "agent.settings.modelType.image",
  38. audio: "agent.settings.modelType.audio",
  39. video: "agent.settings.modelType.video",
  40. };
  41. const k = map[mt];
  42. return k ? t(k) : mt;
  43. };
  44. const [userId, setUserId] = useState<number | null>(null);
  45. const [configs, setConfigs] = useState<AIConfig[]>([]);
  46. const [loading, setLoading] = useState(true);
  47. const [editingId, setEditingId] = useState<number | null>(null);
  48. const [formData, setFormData] = useState<CreateAIConfigRequest>({
  49. provider: "",
  50. api_url: "",
  51. api_key: "",
  52. model: "",
  53. model_type: "text",
  54. is_active: true,
  55. is_public: false,
  56. description: "",
  57. });
  58. const [submitting, setSubmitting] = useState(false);
  59. const [error, setError] = useState("");
  60. // 知识库向量配置(平台级,仅管理员可修改)
  61. const [embeddingConfig, setEmbeddingConfig] = useState<EmbeddingConfig | null>(null);
  62. const [embeddingForm, setEmbeddingForm] = useState({
  63. embedding_type: "openai",
  64. api_url: "",
  65. api_key: "",
  66. model: "text-embedding-3-small",
  67. customer_can_use_kb: true,
  68. visitor_web_search_enabled: false,
  69. web_search_source: "custom" as "vendor" | "custom",
  70. });
  71. const [embeddingLoading, setEmbeddingLoading] = useState(false);
  72. const [embeddingSubmitting, setEmbeddingSubmitting] = useState(false);
  73. const [embeddingError, setEmbeddingError] = useState("");
  74. // 检查登录状态
  75. useEffect(() => {
  76. const storedUserId = localStorage.getItem("agent_user_id");
  77. if (!storedUserId) {
  78. router.push("/");
  79. return;
  80. }
  81. setUserId(Number.parseInt(storedUserId, 10));
  82. }, [router]);
  83. // 加载个人资料(用于获取和更新 AI 对话接收设置)
  84. const {
  85. profile,
  86. loading: profileLoading,
  87. update: updateProfile,
  88. } = useProfile({
  89. userId: userId ?? null,
  90. enabled: Boolean(userId),
  91. });
  92. // 加载配置列表
  93. const loadConfigs = async () => {
  94. if (!userId) return;
  95. try {
  96. setLoading(true);
  97. const data = await fetchAIConfigs(userId);
  98. setConfigs(data);
  99. } catch (error) {
  100. console.error("加载配置失败:", error);
  101. setError(t("agent.settings.error.loadConfigs"));
  102. } finally {
  103. setLoading(false);
  104. }
  105. };
  106. useEffect(() => {
  107. if (userId) {
  108. loadConfigs();
  109. }
  110. }, [userId]);
  111. // 加载知识库向量配置
  112. const loadEmbeddingConfig = async () => {
  113. if (!userId) return;
  114. try {
  115. setEmbeddingLoading(true);
  116. const data = await fetchEmbeddingConfig(userId);
  117. setEmbeddingConfig(data);
  118. setEmbeddingForm({
  119. embedding_type: data.embedding_type || "openai",
  120. api_url: data.api_url || "",
  121. api_key: "",
  122. model: data.model || "text-embedding-3-small",
  123. customer_can_use_kb: data.customer_can_use_kb ?? true,
  124. visitor_web_search_enabled: data.visitor_web_search_enabled ?? false,
  125. web_search_source: data.web_search_source === "vendor" ? "vendor" : "custom",
  126. });
  127. } catch (e) {
  128. console.error("加载知识库向量配置失败:", e);
  129. setEmbeddingError(t("agent.settings.error.loadEmbedding"));
  130. } finally {
  131. setEmbeddingLoading(false);
  132. }
  133. };
  134. useEffect(() => {
  135. if (userId) {
  136. loadEmbeddingConfig();
  137. }
  138. }, [userId]);
  139. // 保存知识库向量配置(仅管理员;保存后立即生效,无需重启)
  140. const handleSaveEmbeddingConfig = async (e: React.FormEvent) => {
  141. e.preventDefault();
  142. if (!userId) return;
  143. setEmbeddingSubmitting(true);
  144. setEmbeddingError("");
  145. try {
  146. const data: UpdateEmbeddingConfigRequest = {
  147. embedding_type: embeddingForm.embedding_type,
  148. api_url: embeddingForm.api_url || undefined,
  149. model: embeddingForm.model || undefined,
  150. customer_can_use_kb: embeddingForm.customer_can_use_kb,
  151. visitor_web_search_enabled: embeddingForm.visitor_web_search_enabled,
  152. web_search_source: embeddingForm.web_search_source,
  153. };
  154. if (embeddingForm.api_key) {
  155. data.api_key = embeddingForm.api_key;
  156. }
  157. await updateEmbeddingConfig(userId, data);
  158. await loadEmbeddingConfig();
  159. toast.success(t("agent.settings.toast.embeddingSaved"));
  160. } catch (err) {
  161. setEmbeddingError((err as Error).message);
  162. } finally {
  163. setEmbeddingSubmitting(false);
  164. }
  165. };
  166. // 重置表单
  167. const resetForm = () => {
  168. setFormData({
  169. provider: "",
  170. api_url: "",
  171. api_key: "",
  172. model: "",
  173. model_type: "text",
  174. is_active: true,
  175. is_public: false,
  176. description: "",
  177. });
  178. setEditingId(null);
  179. setError("");
  180. };
  181. // 开始编辑
  182. const handleEdit = (config: AIConfig) => {
  183. setFormData({
  184. provider: config.provider,
  185. api_url: config.api_url,
  186. api_key: "", // 不显示 API Key(已加密)
  187. model: config.model,
  188. model_type: config.model_type,
  189. is_active: config.is_active,
  190. is_public: config.is_public,
  191. description: config.description,
  192. });
  193. setEditingId(config.id);
  194. };
  195. // 提交表单
  196. const handleSubmit = async (e: React.FormEvent) => {
  197. e.preventDefault();
  198. if (!userId) return;
  199. setSubmitting(true);
  200. setError("");
  201. try {
  202. if (editingId) {
  203. // 更新配置
  204. const updateData: UpdateAIConfigRequest = {
  205. provider: formData.provider,
  206. api_url: formData.api_url,
  207. model: formData.model,
  208. model_type: formData.model_type,
  209. is_active: formData.is_active,
  210. is_public: formData.is_public,
  211. description: formData.description,
  212. };
  213. // 如果提供了新的 API Key,才更新
  214. if (formData.api_key) {
  215. updateData.api_key = formData.api_key;
  216. }
  217. await updateAIConfig(userId, editingId, updateData);
  218. } else {
  219. // 创建配置
  220. await createAIConfig(userId, formData);
  221. }
  222. resetForm();
  223. await loadConfigs();
  224. } catch (error) {
  225. setError((error as Error).message || t("agent.settings.error.operation"));
  226. } finally {
  227. setSubmitting(false);
  228. }
  229. };
  230. // 删除配置
  231. const handleDelete = async (id: number) => {
  232. if (!userId) return;
  233. if (!confirm(t("agent.settings.confirmDeleteConfig"))) return;
  234. try {
  235. await deleteAIConfig(userId, id);
  236. await loadConfigs();
  237. } catch (error) {
  238. setError((error as Error).message || t("agent.settings.error.delete"));
  239. }
  240. };
  241. // 退出登录
  242. const handleLogout = async () => {
  243. try {
  244. await fetch(apiUrl("/logout"), { method: "POST" });
  245. } catch (error) {
  246. console.error("退出登录失败:", error);
  247. } finally {
  248. localStorage.removeItem("agent_user_id");
  249. localStorage.removeItem("agent_username");
  250. localStorage.removeItem("agent_role");
  251. router.push("/");
  252. }
  253. };
  254. if (!userId) {
  255. return null;
  256. }
  257. // 构建头部内容
  258. const headerContent = (
  259. <div className="border-b bg-card p-3 shadow-sm sm:p-4">
  260. <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
  261. <div>
  262. <h1 className="text-xl font-bold text-foreground">{t("agent.settings.title")}</h1>
  263. <div className="text-sm text-muted-foreground mt-1">{t("agent.settings.subtitle")}</div>
  264. </div>
  265. {!embedded && (
  266. <div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
  267. <Button
  268. onClick={() => router.push("/agent/dashboard")}
  269. variant="outline"
  270. size="sm"
  271. className="w-full sm:w-auto"
  272. >
  273. {t("agent.settings.backDashboard")}
  274. </Button>
  275. <Button
  276. onClick={handleLogout}
  277. variant="outline"
  278. size="sm"
  279. className="w-full sm:w-auto"
  280. >
  281. {t("agent.logout")}
  282. </Button>
  283. </div>
  284. )}
  285. </div>
  286. </div>
  287. );
  288. // 构建主内容区
  289. const mainContent = (
  290. <div className="flex-1 overflow-auto p-3 sm:p-4 md:p-6">
  291. <div className="max-w-6xl mx-auto space-y-6">
  292. {/* 全局设置 */}
  293. <Card>
  294. <CardHeader>
  295. <CardTitle>{t("agent.settings.section.global")}</CardTitle>
  296. </CardHeader>
  297. <CardContent>
  298. <div className="flex items-center space-x-2">
  299. <Checkbox
  300. id="receive_ai_conversations"
  301. checked={!(profile?.receive_ai_conversations ?? false)}
  302. onCheckedChange={async (checked) => {
  303. if (userId) {
  304. try {
  305. await updateProfile({
  306. receive_ai_conversations: !checked,
  307. });
  308. } catch (error) {
  309. console.error("更新设置失败:", error);
  310. toast.error(t("agent.settings.toast.profileUpdateFailed"));
  311. }
  312. }
  313. }}
  314. disabled={profileLoading}
  315. />
  316. <Label
  317. htmlFor="receive_ai_conversations"
  318. className="text-sm font-medium cursor-pointer"
  319. >
  320. {t("agent.settings.global.noReceiveAi")}
  321. </Label>
  322. </div>
  323. <p className="text-xs text-muted-foreground mt-2">
  324. {t("agent.settings.global.noReceiveAiHint")}
  325. </p>
  326. </CardContent>
  327. </Card>
  328. {/* 知识库向量模型(平台级,仅管理员可修改;保存后立即生效) */}
  329. <Card>
  330. <CardHeader>
  331. <CardTitle>{t("agent.settings.embedding.title")}</CardTitle>
  332. <p className="text-sm text-muted-foreground mt-1">
  333. {t("agent.settings.embedding.lead")}
  334. </p>
  335. </CardHeader>
  336. <CardContent>
  337. {embeddingLoading ? (
  338. <div className="text-center py-6 text-muted-foreground">{t("common.loading")}</div>
  339. ) : (
  340. <form onSubmit={handleSaveEmbeddingConfig} className="space-y-4">
  341. {embeddingError && (
  342. <div className="p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
  343. {embeddingError}
  344. </div>
  345. )}
  346. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  347. <div>
  348. <Label className="block text-sm font-medium mb-1">{t("agent.settings.embedding.type")}</Label>
  349. <select
  350. value={embeddingForm.embedding_type}
  351. onChange={(e) =>
  352. setEmbeddingForm({ ...embeddingForm, embedding_type: e.target.value })
  353. }
  354. className="w-full px-3 py-2 border border-input rounded-md text-sm bg-background"
  355. >
  356. <option value="openai">{t("agent.settings.embedding.openaiCompatible")}</option>
  357. <option value="bge">{t("agent.settings.embedding.bgeLocal")}</option>
  358. </select>
  359. </div>
  360. <div>
  361. <Label className="block text-sm font-medium mb-1">{t("agent.settings.embedding.apiUrl")}</Label>
  362. <Input
  363. value={embeddingForm.api_url}
  364. onChange={(e) =>
  365. setEmbeddingForm({ ...embeddingForm, api_url: e.target.value })
  366. }
  367. placeholder={t("agent.settings.embedding.apiUrlPh")}
  368. />
  369. </div>
  370. <div>
  371. <Label className="block text-sm font-medium mb-1">{t("agent.settings.embedding.apiKey")}</Label>
  372. <Input
  373. type="password"
  374. value={embeddingForm.api_key}
  375. onChange={(e) =>
  376. setEmbeddingForm({ ...embeddingForm, api_key: e.target.value })
  377. }
  378. placeholder={
  379. embeddingConfig?.api_key_masked
  380. ? t("agent.settings.embedding.apiKeyKeepEmpty")
  381. : t("agent.settings.embedding.apiKeyInput")
  382. }
  383. />
  384. </div>
  385. <div>
  386. <Label className="block text-sm font-medium mb-1">{t("agent.settings.embedding.model")}</Label>
  387. <Input
  388. value={embeddingForm.model}
  389. onChange={(e) =>
  390. setEmbeddingForm({ ...embeddingForm, model: e.target.value })
  391. }
  392. placeholder={t("agent.settings.embedding.modelPh")}
  393. />
  394. </div>
  395. </div>
  396. <div className="flex items-center gap-2">
  397. <Checkbox
  398. id="customer_can_use_kb"
  399. checked={embeddingForm.customer_can_use_kb}
  400. onCheckedChange={(checked) =>
  401. setEmbeddingForm({
  402. ...embeddingForm,
  403. customer_can_use_kb: checked === true,
  404. })
  405. }
  406. />
  407. <Label htmlFor="customer_can_use_kb" className="text-sm cursor-pointer">
  408. {t("agent.settings.embedding.customerKb")}
  409. </Label>
  410. </div>
  411. <Button type="submit" disabled={embeddingSubmitting}>
  412. {embeddingSubmitting
  413. ? t("common.saving")
  414. : t("agent.settings.embedding.save")}
  415. </Button>
  416. </form>
  417. )}
  418. </CardContent>
  419. </Card>
  420. {/* 联网搜索设置(与知识库向量模型独立;实际仍写入同一配置,仅 UI 分离) */}
  421. <Card>
  422. <CardHeader>
  423. <CardTitle>{t("agent.settings.webSearch.title")}</CardTitle>
  424. <p className="text-sm text-muted-foreground mt-1">
  425. {t("agent.settings.webSearch.lead")}
  426. </p>
  427. </CardHeader>
  428. <CardContent>
  429. {embeddingLoading ? (
  430. <div className="text-center py-6 text-muted-foreground">{t("common.loading")}</div>
  431. ) : (
  432. <form onSubmit={handleSaveEmbeddingConfig} className="space-y-4">
  433. {embeddingError && (
  434. <div className="p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
  435. {embeddingError}
  436. </div>
  437. )}
  438. <div>
  439. <Label className="block text-sm font-medium mb-1">{t("agent.settings.webSearch.mode")}</Label>
  440. <select
  441. value={embeddingForm.web_search_source}
  442. onChange={(e) =>
  443. setEmbeddingForm({
  444. ...embeddingForm,
  445. web_search_source: e.target.value as "vendor" | "custom",
  446. })
  447. }
  448. className="w-full max-w-xs px-3 py-2 border border-input rounded-md text-sm bg-background"
  449. >
  450. <option value="custom">{t("agent.settings.webSearch.modeCustom")}</option>
  451. <option value="vendor">{t("agent.settings.webSearch.modeVendor")}</option>
  452. </select>
  453. <p className="text-xs text-muted-foreground mt-1">
  454. {t("agent.settings.webSearch.modeHint")}
  455. </p>
  456. </div>
  457. <div className="flex items-center gap-2">
  458. <Checkbox
  459. id="visitor_web_search_enabled_standalone"
  460. checked={embeddingForm.visitor_web_search_enabled}
  461. onCheckedChange={(checked) =>
  462. setEmbeddingForm({
  463. ...embeddingForm,
  464. visitor_web_search_enabled: checked === true,
  465. })
  466. }
  467. />
  468. <Label htmlFor="visitor_web_search_enabled_standalone" className="text-sm cursor-pointer">
  469. {t("agent.settings.webSearch.visitorToggle")}
  470. </Label>
  471. </div>
  472. <Button type="submit" disabled={embeddingSubmitting}>
  473. {embeddingSubmitting ? t("common.saving") : t("agent.settings.webSearch.save")}
  474. </Button>
  475. </form>
  476. )}
  477. </CardContent>
  478. </Card>
  479. {/* 配置表单 */}
  480. <Card>
  481. <CardHeader>
  482. <CardTitle>
  483. {editingId
  484. ? t("agent.settings.aiCard.titleEdit")
  485. : t("agent.settings.aiCard.titleAdd")}
  486. </CardTitle>
  487. </CardHeader>
  488. <CardContent>
  489. <form onSubmit={handleSubmit} className="space-y-4">
  490. {error && (
  491. <div className="p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
  492. {error}
  493. </div>
  494. )}
  495. <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
  496. <div>
  497. <label className="block text-sm font-medium mb-1">
  498. {t("agent.settings.aiForm.provider")}{" "}
  499. <span className="text-red-500">*</span>
  500. </label>
  501. <Input
  502. value={formData.provider}
  503. onChange={(e) =>
  504. setFormData({ ...formData, provider: e.target.value })
  505. }
  506. placeholder={t("agent.settings.aiForm.providerPh")}
  507. required
  508. />
  509. </div>
  510. <div>
  511. <label className="block text-sm font-medium mb-1">
  512. {t("agent.settings.aiForm.apiUrl")}{" "}
  513. <span className="text-red-500">*</span>
  514. </label>
  515. <Input
  516. value={formData.api_url}
  517. onChange={(e) =>
  518. setFormData({ ...formData, api_url: e.target.value })
  519. }
  520. placeholder={t("agent.settings.aiForm.apiUrlPh")}
  521. required
  522. />
  523. </div>
  524. <div>
  525. <label className="block text-sm font-medium mb-1">
  526. {t("agent.settings.aiForm.apiKey")}{" "}
  527. <span className="text-red-500">*</span>
  528. </label>
  529. <Input
  530. type="password"
  531. value={formData.api_key}
  532. onChange={(e) =>
  533. setFormData({ ...formData, api_key: e.target.value })
  534. }
  535. placeholder={
  536. editingId
  537. ? t("agent.settings.embedding.apiKeyKeepEmpty")
  538. : t("agent.settings.embedding.apiKeyInput")
  539. }
  540. required={!editingId}
  541. />
  542. </div>
  543. <div>
  544. <label className="block text-sm font-medium mb-1">
  545. {t("agent.settings.aiForm.model")}{" "}
  546. <span className="text-red-500">*</span>
  547. </label>
  548. <Input
  549. value={formData.model}
  550. onChange={(e) =>
  551. setFormData({ ...formData, model: e.target.value })
  552. }
  553. placeholder={t("agent.settings.aiForm.modelPh")}
  554. required
  555. />
  556. </div>
  557. <div>
  558. <label className="block text-sm font-medium mb-1">
  559. {t("agent.settings.aiForm.modelType")}
  560. </label>
  561. <select
  562. value={formData.model_type}
  563. onChange={(e) =>
  564. setFormData({ ...formData, model_type: e.target.value })
  565. }
  566. className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-primary"
  567. >
  568. <option value="text">{t("agent.settings.modelType.text")}</option>
  569. <option value="image">{t("agent.settings.modelType.image")}</option>
  570. <option value="audio">{t("agent.settings.modelType.audio")}</option>
  571. <option value="video">{t("agent.settings.modelType.video")}</option>
  572. </select>
  573. </div>
  574. </div>
  575. <div>
  576. <label className="block text-sm font-medium mb-1">
  577. {t("agent.settings.aiForm.description")}
  578. </label>
  579. <Input
  580. value={formData.description}
  581. onChange={(e) =>
  582. setFormData({ ...formData, description: e.target.value })
  583. }
  584. placeholder={t("agent.settings.aiForm.descPh")}
  585. />
  586. </div>
  587. <div className="flex items-center gap-4">
  588. <label className="flex items-center gap-2">
  589. <input
  590. type="checkbox"
  591. checked={formData.is_active}
  592. onChange={(e) =>
  593. setFormData({ ...formData, is_active: e.target.checked })
  594. }
  595. className="w-4 h-4"
  596. />
  597. <span className="text-sm">{t("agent.settings.aiForm.active")}</span>
  598. </label>
  599. <label className="flex items-center gap-2">
  600. <input
  601. type="checkbox"
  602. checked={formData.is_public}
  603. onChange={(e) =>
  604. setFormData({ ...formData, is_public: e.target.checked })
  605. }
  606. className="w-4 h-4"
  607. />
  608. <span className="text-sm">{t("agent.settings.aiForm.public")}</span>
  609. </label>
  610. </div>
  611. <div className="flex gap-2">
  612. <Button type="submit" disabled={submitting}>
  613. {submitting
  614. ? t("agent.settings.aiForm.submitting")
  615. : editingId
  616. ? t("agent.settings.aiForm.submitUpdate")
  617. : t("agent.settings.aiForm.submitCreate")}
  618. </Button>
  619. {editingId && (
  620. <Button
  621. type="button"
  622. variant="outline"
  623. onClick={resetForm}
  624. >
  625. {t("agent.common.cancel")}
  626. </Button>
  627. )}
  628. </div>
  629. </form>
  630. </CardContent>
  631. </Card>
  632. {/* 配置列表 */}
  633. <Card>
  634. <CardHeader>
  635. <CardTitle>{t("agent.settings.list.title")}</CardTitle>
  636. </CardHeader>
  637. <CardContent>
  638. {loading ? (
  639. <div className="text-center py-8 text-gray-500">
  640. {t("common.loading")}
  641. </div>
  642. ) : configs.length === 0 ? (
  643. <div className="text-center py-8 text-gray-500">
  644. {t("agent.settings.list.empty")}
  645. </div>
  646. ) : (
  647. <div className="space-y-4">
  648. {configs.map((config) => (
  649. <div
  650. key={config.id}
  651. className="p-4 border rounded-lg hover:shadow-md transition-shadow"
  652. >
  653. <div className="flex justify-between items-start">
  654. <div className="flex-1">
  655. <div className="flex items-center gap-2 mb-2">
  656. <h3 className="font-semibold">
  657. {config.provider} - {config.model}
  658. </h3>
  659. {config.is_active && (
  660. <span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded">
  661. {t("agent.settings.badge.active")}
  662. </span>
  663. )}
  664. {config.is_public && (
  665. <span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded">
  666. {t("agent.settings.badge.public")}
  667. </span>
  668. )}
  669. </div>
  670. <div className="text-sm text-gray-600 space-y-1">
  671. <p>
  672. <span className="font-medium">{t("agent.settings.list.apiUrlLabel")}</span>
  673. {config.api_url}
  674. </p>
  675. <p>
  676. <span className="font-medium">{t("agent.settings.list.modelTypeLabel")}</span>
  677. {modelTypeLabel(config.model_type)}
  678. </p>
  679. {config.description && (
  680. <p>
  681. <span className="font-medium">{t("agent.settings.list.descLabel")}</span>
  682. {config.description}
  683. </p>
  684. )}
  685. </div>
  686. </div>
  687. <div className="flex gap-2">
  688. <Button
  689. size="sm"
  690. variant="outline"
  691. onClick={() => handleEdit(config)}
  692. >
  693. {t("agent.common.edit")}
  694. </Button>
  695. <Button
  696. size="sm"
  697. variant="destructive"
  698. onClick={() => handleDelete(config.id)}
  699. >
  700. {t("agent.common.delete")}
  701. </Button>
  702. </div>
  703. </div>
  704. </div>
  705. ))}
  706. </div>
  707. )}
  708. </CardContent>
  709. </Card>
  710. </div>
  711. </div>
  712. );
  713. // 如果是嵌入模式,只返回内容,不包含 ResponsiveLayout
  714. if (embedded) {
  715. return (
  716. <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
  717. {headerContent}
  718. {mainContent}
  719. </div>
  720. );
  721. }
  722. return (
  723. <ResponsiveLayout
  724. main={mainContent}
  725. header={headerContent}
  726. />
  727. );
  728. }