LocalModelList.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. /**
  2. * 本地模型列表组件
  3. *
  4. * 显示用户的本地模型列表,仅支持查看和使用
  5. * 需求: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7
  6. */
  7. import React, { useState, useEffect } from 'react';
  8. import { Server, Loader2, AlertCircle, X, Copy, CheckCircle2, Code } from 'lucide-react';
  9. import { localModelApi, LocalModel } from '../services/localModelApi';
  10. import { useToast } from '../contexts/NotificationContext';
  11. interface LocalModelListProps {
  12. onUseModel?: (modelId: string) => void;
  13. }
  14. const LocalModelList: React.FC<LocalModelListProps> = ({ onUseModel }) => {
  15. const { showToast } = useToast();
  16. const [models, setModels] = useState<LocalModel[]>([]);
  17. const [loading, setLoading] = useState(true);
  18. const [error, setError] = useState<string | null>(null);
  19. const [apiExampleModel, setApiExampleModel] = useState<LocalModel | null>(null);
  20. const [copied, setCopied] = useState<string | null>(null);
  21. const loadModels = async () => {
  22. setLoading(true);
  23. setError(null);
  24. try {
  25. const data = await localModelApi.getLocalModels();
  26. setModels(data);
  27. } catch (err: any) {
  28. setError(err.message || '加载失败');
  29. } finally {
  30. setLoading(false);
  31. }
  32. };
  33. useEffect(() => {
  34. loadModels();
  35. }, []);
  36. const handleShowApiExample = (model: LocalModel) => {
  37. setApiExampleModel(model);
  38. };
  39. const copyToClipboard = async (text: string, key: string) => {
  40. await navigator.clipboard.writeText(text);
  41. setCopied(key);
  42. setTimeout(() => setCopied(null), 2000);
  43. };
  44. const getApiBaseUrl = () => {
  45. const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin;
  46. return `${baseUrl}/api/v1`;
  47. };
  48. const getModelId = (model: LocalModel) => `local:${model.id}`;
  49. const generateCurlExample = (model: LocalModel) => {
  50. const modelId = getModelId(model);
  51. if (model.category === 4) {
  52. // 文生图
  53. return `curl ${getApiBaseUrl()}/images/generations \\
  54. -H "Content-Type: application/json" \\
  55. -H "Authorization: Bearer YOUR_API_KEY" \\
  56. -d '{
  57. "model": "${modelId}",
  58. "prompt": "a beautiful sunset over the ocean",
  59. "n": 1,
  60. "size": "1024x1024"
  61. }'`;
  62. } else if (model.category === 6) {
  63. // 图生图
  64. return `curl ${getApiBaseUrl()}/images/edits \\
  65. -H "Authorization: Bearer YOUR_API_KEY" \\
  66. -F "model=${modelId}" \\
  67. -F "prompt=make it look like a painting" \\
  68. -F "image=@/path/to/your/image.png"`;
  69. } else if (model.category === 2) {
  70. // TTS
  71. return `curl ${getApiBaseUrl()}/audio/speech \\
  72. -H "Content-Type: application/json" \\
  73. -H "Authorization: Bearer YOUR_API_KEY" \\
  74. -d '{
  75. "model": "${modelId}",
  76. "input": "你好,世界",
  77. "voice": "alloy"
  78. }' --output speech.mp3`;
  79. } else if (model.category === 3) {
  80. // STT
  81. return `curl ${getApiBaseUrl()}/audio/transcriptions \\
  82. -H "Authorization: Bearer YOUR_API_KEY" \\
  83. -F "model=${modelId}" \\
  84. -F "file=@/path/to/audio.mp3"`;
  85. } else if (model.category === 5) {
  86. // 视频生成
  87. return `curl ${getApiBaseUrl()}/videos/generations \\
  88. -H "Content-Type: application/json" \\
  89. -H "Authorization: Bearer YOUR_API_KEY" \\
  90. -d '{
  91. "model": "${modelId}",
  92. "prompt": "a beautiful sunset timelapse",
  93. "size": "720P"
  94. }'`;
  95. } else if (model.category === 7) {
  96. // 向量嵌入
  97. return `curl ${getApiBaseUrl()}/embeddings \\
  98. -H "Content-Type: application/json" \\
  99. -H "Authorization: Bearer YOUR_API_KEY" \\
  100. -d '{
  101. "model": "${modelId}",
  102. "input": "Hello, this is a test."
  103. }'`;
  104. } else if (model.category === 8) {
  105. // 重排序
  106. return `curl ${getApiBaseUrl()}/rerank \\
  107. -H "Content-Type: application/json" \\
  108. -H "Authorization: Bearer YOUR_API_KEY" \\
  109. -d '{
  110. "model": "${modelId}",
  111. "query": "什么是机器学习?",
  112. "documents": [
  113. "机器学习是人工智能的一个分支。",
  114. "深度学习使用神经网络。",
  115. "Python是一种编程语言。"
  116. ],
  117. "top_n": 2
  118. }'`;
  119. } else {
  120. // LLM / 多模态
  121. return `curl ${getApiBaseUrl()}/chat/completions \\
  122. -H "Content-Type: application/json" \\
  123. -H "Authorization: Bearer YOUR_API_KEY" \\
  124. -d '{
  125. "model": "${modelId}",
  126. "messages": [
  127. {"role": "user", "content": "你好"}
  128. ]
  129. }'`;
  130. }
  131. };
  132. const generatePythonExample = (model: LocalModel) => {
  133. const modelId = getModelId(model);
  134. if (model.category === 4) {
  135. return `from openai import OpenAI
  136. client = OpenAI(
  137. api_key="YOUR_API_KEY",
  138. base_url="${getApiBaseUrl()}"
  139. )
  140. response = client.images.generate(
  141. model="${modelId}",
  142. prompt="a beautiful sunset over the ocean",
  143. n=1,
  144. size="1024x1024"
  145. )
  146. print(response.data[0].url)`;
  147. } else if (model.category === 6) {
  148. return `from openai import OpenAI
  149. client = OpenAI(
  150. api_key="YOUR_API_KEY",
  151. base_url="${getApiBaseUrl()}"
  152. )
  153. with open("/path/to/your/image.png", "rb") as f:
  154. response = client.images.edit(
  155. model="${modelId}",
  156. image=f,
  157. prompt="make it look like a painting"
  158. )
  159. print(response.data[0].url)`;
  160. } else if (model.category === 2) {
  161. return `from openai import OpenAI
  162. client = OpenAI(
  163. api_key="YOUR_API_KEY",
  164. base_url="${getApiBaseUrl()}"
  165. )
  166. response = client.audio.speech.create(
  167. model="${modelId}",
  168. voice="alloy",
  169. input="你好,世界"
  170. )
  171. response.stream_to_file("speech.mp3")`;
  172. } else if (model.category === 3) {
  173. return `from openai import OpenAI
  174. client = OpenAI(
  175. api_key="YOUR_API_KEY",
  176. base_url="${getApiBaseUrl()}"
  177. )
  178. with open("/path/to/audio.mp3", "rb") as f:
  179. transcript = client.audio.transcriptions.create(
  180. model="${modelId}",
  181. file=f
  182. )
  183. print(transcript.text)`;
  184. } else if (model.category === 5) {
  185. return `import requests
  186. response = requests.post(
  187. "${getApiBaseUrl()}/videos/generations",
  188. headers={
  189. "Authorization": "Bearer YOUR_API_KEY",
  190. "Content-Type": "application/json"
  191. },
  192. json={
  193. "model": "${modelId}",
  194. "prompt": "a beautiful sunset timelapse",
  195. "size": "720P"
  196. }
  197. )
  198. print(response.json()["data"][0]["url"])`;
  199. } else if (model.category === 7) {
  200. return `import requests
  201. response = requests.post(
  202. "${getApiBaseUrl()}/embeddings",
  203. headers={
  204. "Authorization": "Bearer YOUR_API_KEY",
  205. "Content-Type": "application/json"
  206. },
  207. json={
  208. "model": "${modelId}",
  209. "input": "Hello, this is a test."
  210. }
  211. )
  212. embeddings = response.json()["data"]
  213. print(f"向量维度: {len(embeddings[0]['embedding'])}")`;
  214. } else if (model.category === 8) {
  215. return `import requests
  216. response = requests.post(
  217. "${getApiBaseUrl()}/rerank",
  218. headers={
  219. "Authorization": "Bearer YOUR_API_KEY",
  220. "Content-Type": "application/json"
  221. },
  222. json={
  223. "model": "${modelId}",
  224. "query": "什么是机器学习?",
  225. "documents": [
  226. "机器学习是人工智能的一个分支。",
  227. "深度学习使用神经网络。",
  228. "Python是一种编程语言。"
  229. ],
  230. "top_n": 2
  231. }
  232. )
  233. result = response.json()
  234. for item in result["data"]:
  235. print(f"分数: {item['relevance_score']:.4f} - {item['document']}")`;
  236. } else {
  237. return `from openai import OpenAI
  238. client = OpenAI(
  239. api_key="YOUR_API_KEY",
  240. base_url="${getApiBaseUrl()}"
  241. )
  242. response = client.chat.completions.create(
  243. model="${modelId}",
  244. messages=[
  245. {"role": "user", "content": "你好"}
  246. ]
  247. )
  248. print(response.choices[0].message.content)`;
  249. }
  250. };
  251. const generateJsExample = (model: LocalModel) => {
  252. const modelId = getModelId(model);
  253. if (model.category === 4) {
  254. return `const response = await fetch("${getApiBaseUrl()}/images/generations", {
  255. method: "POST",
  256. headers: {
  257. "Content-Type": "application/json",
  258. "Authorization": "Bearer YOUR_API_KEY"
  259. },
  260. body: JSON.stringify({
  261. model: "${modelId}",
  262. prompt: "a beautiful sunset over the ocean",
  263. n: 1,
  264. size: "1024x1024"
  265. })
  266. });
  267. const data = await response.json();
  268. console.log(data.data[0].url);`;
  269. } else if (model.category === 6) {
  270. return `const formData = new FormData();
  271. formData.append("model", "${modelId}");
  272. formData.append("prompt", "make it look like a painting");
  273. formData.append("image", imageFile); // File object
  274. const response = await fetch("${getApiBaseUrl()}/images/edits", {
  275. method: "POST",
  276. headers: { "Authorization": "Bearer YOUR_API_KEY" },
  277. body: formData
  278. });
  279. const data = await response.json();
  280. console.log(data.data[0].url);`;
  281. } else if (model.category === 5) {
  282. return `const response = await fetch("${getApiBaseUrl()}/videos/generations", {
  283. method: "POST",
  284. headers: {
  285. "Content-Type": "application/json",
  286. "Authorization": "Bearer YOUR_API_KEY"
  287. },
  288. body: JSON.stringify({
  289. model: "${modelId}",
  290. prompt: "a beautiful sunset timelapse",
  291. size: "720P"
  292. })
  293. });
  294. const data = await response.json();
  295. console.log(data.data[0].url);`;
  296. } else if (model.category === 7) {
  297. return `const response = await fetch("${getApiBaseUrl()}/embeddings", {
  298. method: "POST",
  299. headers: {
  300. "Content-Type": "application/json",
  301. "Authorization": "Bearer YOUR_API_KEY"
  302. },
  303. body: JSON.stringify({
  304. model: "${modelId}",
  305. input: "Hello, this is a test."
  306. })
  307. });
  308. const data = await response.json();
  309. console.log(\`向量维度: \${data.data[0].embedding.length}\`);`;
  310. } else if (model.category === 8) {
  311. return `const response = await fetch("${getApiBaseUrl()}/rerank", {
  312. method: "POST",
  313. headers: {
  314. "Content-Type": "application/json",
  315. "Authorization": "Bearer YOUR_API_KEY"
  316. },
  317. body: JSON.stringify({
  318. model: "${modelId}",
  319. query: "什么是机器学习?",
  320. documents: [
  321. "机器学习是人工智能的一个分支。",
  322. "深度学习使用神经网络。",
  323. "Python是一种编程语言。"
  324. ],
  325. top_n: 2
  326. })
  327. });
  328. const data = await response.json();
  329. data.data.forEach(item => {
  330. console.log(\`分数: \${item.relevance_score.toFixed(4)} - \${item.document}\`);
  331. });`;
  332. } else {
  333. return `const response = await fetch("${getApiBaseUrl()}/chat/completions", {
  334. method: "POST",
  335. headers: {
  336. "Content-Type": "application/json",
  337. "Authorization": "Bearer YOUR_API_KEY"
  338. },
  339. body: JSON.stringify({
  340. model: "${modelId}",
  341. messages: [
  342. { role: "user", content: "你好" }
  343. ]
  344. })
  345. });
  346. const data = await response.json();
  347. console.log(data.choices[0].message.content);`;
  348. }
  349. };
  350. if (loading) {
  351. return (
  352. <div className="flex items-center justify-center py-20">
  353. <Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
  354. <span className="ml-3 text-gray-600">加载中...</span>
  355. </div>
  356. );
  357. }
  358. if (error) {
  359. return (
  360. <div className="bg-red-50 border border-red-200 rounded-xl p-4 text-center">
  361. <AlertCircle className="w-8 h-8 text-red-500 mx-auto mb-2" />
  362. <p className="text-red-600">{error}</p>
  363. <button
  364. onClick={loadModels}
  365. className="mt-2 text-sm text-red-600 font-bold hover:underline"
  366. >
  367. 重试
  368. </button>
  369. </div>
  370. );
  371. }
  372. return (
  373. <div className="space-y-6">
  374. {/* 私域模型列表 */}
  375. {/* 模型列表 */}
  376. {models.length === 0 ? (
  377. <div className="py-20 flex flex-col items-center justify-center text-center bg-gray-50 rounded-3xl border-2 border-dashed border-gray-200">
  378. <div className="w-16 h-16 bg-white rounded-full flex items-center justify-center shadow-sm mb-4">
  379. <Server className="w-8 h-8 text-gray-300" />
  380. </div>
  381. <h3 className="text-lg font-bold text-gray-900">暂无私域模型</h3>
  382. <p className="text-sm text-gray-500 mt-2">请联系管理员添加私域模型</p>
  383. </div>
  384. ) : (
  385. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  386. {models.map((model) => (
  387. <div
  388. key={model.id}
  389. className="bg-white rounded-2xl border border-gray-100 p-5 hover:shadow-lg hover:border-blue-200 transition-all group"
  390. >
  391. <div className="flex items-start justify-between mb-3">
  392. <div className="flex items-center gap-3">
  393. <div className="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
  394. <Server className="w-5 h-5 text-purple-600" />
  395. </div>
  396. <div>
  397. <h3 className="font-semibold text-gray-900">{model.name}</h3>
  398. <div className="flex items-center gap-2 mt-1">
  399. <span className="text-xs px-2 py-0.5 bg-purple-100 text-purple-600 rounded-full">
  400. 私域模型
  401. </span>
  402. {model.supplier && (
  403. <span className="text-xs px-2 py-0.5 bg-blue-100 text-blue-600 rounded-full">
  404. {model.supplier}
  405. </span>
  406. )}
  407. </div>
  408. </div>
  409. </div>
  410. </div>
  411. <div className="space-y-2 mb-4">
  412. <div className="text-xs text-gray-500">
  413. <span className="font-medium">模型ID:</span>
  414. <code className="ml-1 px-1.5 py-0.5 bg-gray-100 rounded text-gray-700">local:{model.id}</code>
  415. </div>
  416. {model.supplier && (
  417. <div className="text-xs text-gray-500">
  418. <span className="font-medium">调用格式:</span>
  419. <code className="ml-1 px-1.5 py-0.5 bg-gray-100 rounded text-gray-700">{model.supplier}/{model.name}</code>
  420. </div>
  421. )}
  422. <div className="text-xs text-gray-400 truncate" title={model.base_url}>
  423. {model.base_url}
  424. </div>
  425. </div>
  426. <div className="flex items-center justify-between text-xs text-gray-400">
  427. <span>
  428. {model.has_api_key ? '已配置API Key' : '无需认证'}
  429. </span>
  430. <span>
  431. {new Date(model.created_at).toLocaleDateString()}
  432. </span>
  433. </div>
  434. {onUseModel && (
  435. <button
  436. onClick={() => handleShowApiExample(model)}
  437. className="w-full mt-4 py-2 bg-blue-50 text-blue-600 rounded-xl text-sm font-medium hover:bg-blue-100 transition-colors flex items-center justify-center gap-2"
  438. >
  439. <Code className="w-4 h-4" />
  440. 使用此模型
  441. </button>
  442. )}
  443. </div>
  444. ))}
  445. </div>
  446. )}
  447. {/* API调用示例模态框 */}
  448. {apiExampleModel && (
  449. <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
  450. <div className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
  451. <div className="flex items-center justify-between px-6 py-4 border-b border-gray-100">
  452. <div>
  453. <h3 className="text-lg font-semibold text-gray-900">API调用示例</h3>
  454. <p className="text-sm text-gray-500 mt-0.5">模型: {apiExampleModel.name}</p>
  455. </div>
  456. <button
  457. onClick={() => setApiExampleModel(null)}
  458. className="p-1 hover:bg-gray-100 rounded-lg transition-colors"
  459. >
  460. <X className="w-5 h-5 text-gray-400" />
  461. </button>
  462. </div>
  463. <div className="flex-1 overflow-y-auto p-6 space-y-6">
  464. {/* 调用方式说明 */}
  465. <div className="bg-gradient-to-r from-blue-50 to-purple-50 border border-blue-200 rounded-xl p-4">
  466. <h4 className="text-sm font-semibold text-gray-900 mb-3">📌 模型调用方式</h4>
  467. <div className="space-y-2 text-sm text-gray-700">
  468. <div>
  469. <span className="font-medium">方式1 - 精确ID(推荐):</span>
  470. <code className="ml-2 px-2 py-0.5 bg-white rounded text-blue-600">local:{apiExampleModel.id}</code>
  471. </div>
  472. {apiExampleModel.supplier && (
  473. <div>
  474. <span className="font-medium">方式2 - 提供商/名称:</span>
  475. <code className="ml-2 px-2 py-0.5 bg-white rounded text-blue-600">{apiExampleModel.supplier}/{apiExampleModel.name}</code>
  476. </div>
  477. )}
  478. <div>
  479. <span className="font-medium">方式3 - 仅名称:</span>
  480. <code className="ml-2 px-2 py-0.5 bg-white rounded text-blue-600">{apiExampleModel.name}</code>
  481. <span className="ml-2 text-xs text-gray-500">(如有同名模型,将使用最新创建的)</span>
  482. </div>
  483. </div>
  484. </div>
  485. {/* 提示信息 */}
  486. <div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
  487. <p className="text-sm text-amber-800">
  488. <strong>提示:</strong>请先在「开放平台」页面创建本地私钥(访问本地模型),然后将下方示例中的 <code className="bg-amber-100 px-1 rounded">YOUR_API_KEY</code> 替换为您的实际API Key。
  489. </p>
  490. </div>
  491. {/* cURL示例 */}
  492. <div>
  493. <div className="flex items-center justify-between mb-2">
  494. <h4 className="text-sm font-medium text-gray-700">cURL</h4>
  495. <button
  496. onClick={() => copyToClipboard(generateCurlExample(apiExampleModel), 'curl')}
  497. className="flex items-center gap-1 text-xs text-gray-500 hover:text-blue-600 transition-colors"
  498. >
  499. {copied === 'curl' ? (
  500. <>
  501. <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />
  502. 已复制
  503. </>
  504. ) : (
  505. <>
  506. <Copy className="w-3.5 h-3.5" />
  507. 复制
  508. </>
  509. )}
  510. </button>
  511. </div>
  512. <pre className="bg-gray-900 text-gray-100 p-4 rounded-xl text-sm overflow-x-auto">
  513. <code>{generateCurlExample(apiExampleModel)}</code>
  514. </pre>
  515. </div>
  516. {/* Python示例 */}
  517. <div>
  518. <div className="flex items-center justify-between mb-2">
  519. <h4 className="text-sm font-medium text-gray-700">Python (OpenAI SDK)</h4>
  520. <button
  521. onClick={() => copyToClipboard(generatePythonExample(apiExampleModel), 'python')}
  522. className="flex items-center gap-1 text-xs text-gray-500 hover:text-blue-600 transition-colors"
  523. >
  524. {copied === 'python' ? (
  525. <>
  526. <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />
  527. 已复制
  528. </>
  529. ) : (
  530. <>
  531. <Copy className="w-3.5 h-3.5" />
  532. 复制
  533. </>
  534. )}
  535. </button>
  536. </div>
  537. <pre className="bg-gray-900 text-gray-100 p-4 rounded-xl text-sm overflow-x-auto">
  538. <code>{generatePythonExample(apiExampleModel)}</code>
  539. </pre>
  540. </div>
  541. {/* JavaScript示例 */}
  542. <div>
  543. <div className="flex items-center justify-between mb-2">
  544. <h4 className="text-sm font-medium text-gray-700">JavaScript (Fetch)</h4>
  545. <button
  546. onClick={() => copyToClipboard(generateJsExample(apiExampleModel), 'js')}
  547. className="flex items-center gap-1 text-xs text-gray-500 hover:text-blue-600 transition-colors"
  548. >
  549. {copied === 'js' ? (
  550. <>
  551. <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />
  552. 已复制
  553. </>
  554. ) : (
  555. <>
  556. <Copy className="w-3.5 h-3.5" />
  557. 复制
  558. </>
  559. )}
  560. </button>
  561. </div>
  562. <pre className="bg-gray-900 text-gray-100 p-4 rounded-xl text-sm overflow-x-auto">
  563. <code>{generateJsExample(apiExampleModel)}</code>
  564. </pre>
  565. </div>
  566. </div>
  567. <div className="px-6 py-4 border-t border-gray-100 bg-gray-50">
  568. <div className="flex justify-between items-center">
  569. <p className="text-xs text-gray-500">
  570. API端点: <code className="bg-gray-200 px-1.5 py-0.5 rounded">{getApiBaseUrl()}/{
  571. apiExampleModel.category === 4 ? 'images/generations' :
  572. apiExampleModel.category === 6 ? 'images/edits' :
  573. apiExampleModel.category === 2 ? 'audio/speech' :
  574. apiExampleModel.category === 3 ? 'audio/transcriptions' :
  575. apiExampleModel.category === 5 ? 'videos/generations' :
  576. apiExampleModel.category === 7 ? 'embeddings' :
  577. apiExampleModel.category === 8 ? 'rerank' :
  578. 'chat/completions'
  579. }</code>
  580. </p>
  581. <button
  582. onClick={() => setApiExampleModel(null)}
  583. className="px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-colors text-sm"
  584. >
  585. 关闭
  586. </button>
  587. </div>
  588. </div>
  589. </div>
  590. </div>
  591. )}
  592. </div>
  593. );
  594. };
  595. export default LocalModelList;