LocalModelList.tsx 22 KB

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