| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650 |
- /**
- * 本地模型列表组件
- *
- * 显示用户的本地模型列表,仅支持查看和使用
- * 需求: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7
- */
- import React, { useState, useEffect } from 'react';
- import { Server, Loader2, AlertCircle, X, Copy, CheckCircle2, Code } from 'lucide-react';
- import { localModelApi, LocalModel } from '../services/localModelApi';
- import { useToast } from '../contexts/NotificationContext';
- import { copyToClipboard } from '../utils/clipboard';
- interface LocalModelListProps {
- onUseModel?: (modelId: string) => void;
- }
- const LocalModelList: React.FC<LocalModelListProps> = ({ onUseModel }) => {
- const { showToast } = useToast();
- const [models, setModels] = useState<LocalModel[]>([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const [apiExampleModel, setApiExampleModel] = useState<LocalModel | null>(null);
- const [copied, setCopied] = useState<string | null>(null);
- const loadModels = async () => {
- setLoading(true);
- setError(null);
- try {
- const data = await localModelApi.getLocalModels();
- setModels(data);
- } catch (err: any) {
- setError(err.message || '加载失败');
- } finally {
- setLoading(false);
- }
- };
- useEffect(() => {
- loadModels();
- }, []);
- const handleShowApiExample = (model: LocalModel) => {
- setApiExampleModel(model);
- };
- const handleCopy = async (text: string, key: string) => {
- await copyToClipboard(text);
- setCopied(key);
- setTimeout(() => setCopied(null), 2000);
- };
- const getApiBaseUrl = () => {
- const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin;
- return `${baseUrl}/api/v1`;
- };
- const getModelId = (model: LocalModel) => `local:${model.id}`;
- const generateCurlExample = (model: LocalModel) => {
- const modelId = getModelId(model);
- if (model.category === 4) {
- // 文生图
- return `curl ${getApiBaseUrl()}/images/generations \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "${modelId}",
- "prompt": "a beautiful sunset over the ocean",
- "n": 1,
- "size": "1024x1024"
- }'`;
- } else if (model.category === 6) {
- // 图生图
- return `curl ${getApiBaseUrl()}/images/edits \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -F "model=${modelId}" \\
- -F "prompt=make it look like a painting" \\
- -F "image=@/path/to/your/image.png"`;
- } else if (model.category === 2) {
- // TTS
- return `curl ${getApiBaseUrl()}/audio/speech \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "${modelId}",
- "input": "你好,世界",
- "voice": "alloy"
- }' --output speech.mp3`;
- } else if (model.category === 3) {
- // STT
- return `curl ${getApiBaseUrl()}/audio/transcriptions \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -F "model=${modelId}" \\
- -F "file=@/path/to/audio.mp3"`;
- } else if (model.category === 5) {
- // 视频生成
- return `curl ${getApiBaseUrl()}/videos/generations \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "${modelId}",
- "prompt": "a beautiful sunset timelapse",
- "size": "720P"
- }'`;
- } else if (model.category === 7) {
- // 向量嵌入
- return `curl ${getApiBaseUrl()}/embeddings \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "${modelId}",
- "input": "Hello, this is a test."
- }'`;
- } else if (model.category === 8) {
- // 重排序
- return `curl ${getApiBaseUrl()}/rerank \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "${modelId}",
- "query": "什么是机器学习?",
- "documents": [
- "机器学习是人工智能的一个分支。",
- "深度学习使用神经网络。",
- "Python是一种编程语言。"
- ],
- "top_n": 2
- }'`;
- } else {
- // LLM / 多模态
- return `curl ${getApiBaseUrl()}/chat/completions \\
- -H "Content-Type: application/json" \\
- -H "Authorization: Bearer YOUR_API_KEY" \\
- -d '{
- "model": "${modelId}",
- "messages": [
- {"role": "user", "content": "你好"}
- ]
- }'`;
- }
- };
- const generatePythonExample = (model: LocalModel) => {
- const modelId = getModelId(model);
- if (model.category === 4) {
- return `from openai import OpenAI
- client = OpenAI(
- api_key="YOUR_API_KEY",
- base_url="${getApiBaseUrl()}"
- )
- response = client.images.generate(
- model="${modelId}",
- prompt="a beautiful sunset over the ocean",
- n=1,
- size="1024x1024"
- )
- print(response.data[0].url)`;
- } else if (model.category === 6) {
- return `from openai import OpenAI
- client = OpenAI(
- api_key="YOUR_API_KEY",
- base_url="${getApiBaseUrl()}"
- )
- with open("/path/to/your/image.png", "rb") as f:
- response = client.images.edit(
- model="${modelId}",
- image=f,
- prompt="make it look like a painting"
- )
- print(response.data[0].url)`;
- } else if (model.category === 2) {
- return `from openai import OpenAI
- client = OpenAI(
- api_key="YOUR_API_KEY",
- base_url="${getApiBaseUrl()}"
- )
- response = client.audio.speech.create(
- model="${modelId}",
- voice="alloy",
- input="你好,世界"
- )
- response.stream_to_file("speech.mp3")`;
- } else if (model.category === 3) {
- return `from openai import OpenAI
- client = OpenAI(
- api_key="YOUR_API_KEY",
- base_url="${getApiBaseUrl()}"
- )
- with open("/path/to/audio.mp3", "rb") as f:
- transcript = client.audio.transcriptions.create(
- model="${modelId}",
- file=f
- )
- print(transcript.text)`;
- } else if (model.category === 5) {
- return `import requests
- response = requests.post(
- "${getApiBaseUrl()}/videos/generations",
- headers={
- "Authorization": "Bearer YOUR_API_KEY",
- "Content-Type": "application/json"
- },
- json={
- "model": "${modelId}",
- "prompt": "a beautiful sunset timelapse",
- "size": "720P"
- }
- )
- print(response.json()["data"][0]["url"])`;
- } else if (model.category === 7) {
- return `import requests
- response = requests.post(
- "${getApiBaseUrl()}/embeddings",
- headers={
- "Authorization": "Bearer YOUR_API_KEY",
- "Content-Type": "application/json"
- },
- json={
- "model": "${modelId}",
- "input": "Hello, this is a test."
- }
- )
- embeddings = response.json()["data"]
- print(f"向量维度: {len(embeddings[0]['embedding'])}")`;
- } else if (model.category === 8) {
- return `import requests
- response = requests.post(
- "${getApiBaseUrl()}/rerank",
- headers={
- "Authorization": "Bearer YOUR_API_KEY",
- "Content-Type": "application/json"
- },
- json={
- "model": "${modelId}",
- "query": "什么是机器学习?",
- "documents": [
- "机器学习是人工智能的一个分支。",
- "深度学习使用神经网络。",
- "Python是一种编程语言。"
- ],
- "top_n": 2
- }
- )
- result = response.json()
- for item in result["data"]:
- print(f"分数: {item['relevance_score']:.4f} - {item['document']}")`;
- } else {
- return `from openai import OpenAI
- client = OpenAI(
- api_key="YOUR_API_KEY",
- base_url="${getApiBaseUrl()}"
- )
- response = client.chat.completions.create(
- model="${modelId}",
- messages=[
- {"role": "user", "content": "你好"}
- ]
- )
- print(response.choices[0].message.content)`;
- }
- };
- const generateJsExample = (model: LocalModel) => {
- const modelId = getModelId(model);
- if (model.category === 4) {
- return `const response = await fetch("${getApiBaseUrl()}/images/generations", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": "Bearer YOUR_API_KEY"
- },
- body: JSON.stringify({
- model: "${modelId}",
- prompt: "a beautiful sunset over the ocean",
- n: 1,
- size: "1024x1024"
- })
- });
- const data = await response.json();
- console.log(data.data[0].url);`;
- } else if (model.category === 6) {
- return `const formData = new FormData();
- formData.append("model", "${modelId}");
- formData.append("prompt", "make it look like a painting");
- formData.append("image", imageFile); // File object
- const response = await fetch("${getApiBaseUrl()}/images/edits", {
- method: "POST",
- headers: { "Authorization": "Bearer YOUR_API_KEY" },
- body: formData
- });
- const data = await response.json();
- console.log(data.data[0].url);`;
- } else if (model.category === 5) {
- return `const response = await fetch("${getApiBaseUrl()}/videos/generations", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": "Bearer YOUR_API_KEY"
- },
- body: JSON.stringify({
- model: "${modelId}",
- prompt: "a beautiful sunset timelapse",
- size: "720P"
- })
- });
- const data = await response.json();
- console.log(data.data[0].url);`;
- } else if (model.category === 7) {
- return `const response = await fetch("${getApiBaseUrl()}/embeddings", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": "Bearer YOUR_API_KEY"
- },
- body: JSON.stringify({
- model: "${modelId}",
- input: "Hello, this is a test."
- })
- });
- const data = await response.json();
- console.log(\`向量维度: \${data.data[0].embedding.length}\`);`;
- } else if (model.category === 8) {
- return `const response = await fetch("${getApiBaseUrl()}/rerank", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": "Bearer YOUR_API_KEY"
- },
- body: JSON.stringify({
- model: "${modelId}",
- query: "什么是机器学习?",
- documents: [
- "机器学习是人工智能的一个分支。",
- "深度学习使用神经网络。",
- "Python是一种编程语言。"
- ],
- top_n: 2
- })
- });
- const data = await response.json();
- data.data.forEach(item => {
- console.log(\`分数: \${item.relevance_score.toFixed(4)} - \${item.document}\`);
- });`;
- } else {
- return `const response = await fetch("${getApiBaseUrl()}/chat/completions", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": "Bearer YOUR_API_KEY"
- },
- body: JSON.stringify({
- model: "${modelId}",
- messages: [
- { role: "user", content: "你好" }
- ]
- })
- });
- const data = await response.json();
- console.log(data.choices[0].message.content);`;
- }
- };
- if (loading) {
- return (
- <div className="flex items-center justify-center py-20">
- <Loader2 className="w-8 h-8 text-blue-600 animate-spin" />
- <span className="ml-3 text-gray-600">加载中...</span>
- </div>
- );
- }
- if (error) {
- return (
- <div className="bg-red-50 border border-red-200 rounded-xl p-4 text-center">
- <AlertCircle className="w-8 h-8 text-red-500 mx-auto mb-2" />
- <p className="text-red-600">{error}</p>
- <button
- onClick={loadModels}
- className="mt-2 text-sm text-red-600 font-bold hover:underline"
- >
- 重试
- </button>
- </div>
- );
- }
- return (
- <div className="space-y-6">
- {/* 私域模型列表 */}
- {/* 模型列表 */}
- {models.length === 0 ? (
- <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">
- <div className="w-16 h-16 bg-white rounded-full flex items-center justify-center shadow-sm mb-4">
- <Server className="w-8 h-8 text-gray-300" />
- </div>
- <h3 className="text-lg font-bold text-gray-900">暂无私域模型</h3>
- <p className="text-sm text-gray-500 mt-2">请联系管理员添加私域模型</p>
- </div>
- ) : (
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
- {models.map((model) => (
- <div
- key={model.id}
- className="bg-white rounded-2xl border border-gray-100 p-5 hover:shadow-lg hover:border-blue-200 transition-all group"
- >
- <div className="flex items-start justify-between mb-3">
- <div className="flex items-center gap-3">
- <div className="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
- <Server className="w-5 h-5 text-purple-600" />
- </div>
- <div>
- <h3 className="font-semibold text-gray-900">{model.name}</h3>
- <div className="flex items-center gap-2 mt-1">
- <span className="text-xs px-2 py-0.5 bg-purple-100 text-purple-600 rounded-full">
- 私域模型
- </span>
- {model.supplier && (
- <span className="text-xs px-2 py-0.5 bg-blue-100 text-blue-600 rounded-full">
- {model.supplier}
- </span>
- )}
- </div>
- </div>
- </div>
- </div>
- <div className="space-y-2 mb-4">
- <div className="text-xs text-gray-500">
- <span className="font-medium">模型ID:</span>
- <code className="ml-1 px-1.5 py-0.5 bg-gray-100 rounded text-gray-700">local:{model.id}</code>
- </div>
- {model.supplier && (
- <div className="text-xs text-gray-500">
- <span className="font-medium">调用格式:</span>
- <code className="ml-1 px-1.5 py-0.5 bg-gray-100 rounded text-gray-700">{model.supplier}/{model.name}</code>
- </div>
- )}
- <div className="text-xs text-gray-400 truncate" title={model.base_url}>
- {model.base_url}
- </div>
- </div>
- <div className="flex items-center justify-between text-xs text-gray-400">
- <span>
- {model.has_api_key ? '已配置API Key' : '无需认证'}
- </span>
- <span>
- {new Date(model.created_at).toLocaleDateString()}
- </span>
- </div>
- {onUseModel && (
- <button
- onClick={() => handleShowApiExample(model)}
- 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"
- >
- <Code className="w-4 h-4" />
- 使用此模型
- </button>
- )}
- </div>
- ))}
- </div>
- )}
- {/* API调用示例模态框 */}
- {apiExampleModel && (
- <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
- <div className="bg-white rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
- <div className="flex items-center justify-between px-6 py-4 border-b border-gray-100">
- <div>
- <h3 className="text-lg font-semibold text-gray-900">API调用示例</h3>
- <p className="text-sm text-gray-500 mt-0.5">模型: {apiExampleModel.name}</p>
- </div>
- <button
- onClick={() => setApiExampleModel(null)}
- className="p-1 hover:bg-gray-100 rounded-lg transition-colors"
- >
- <X className="w-5 h-5 text-gray-400" />
- </button>
- </div>
- <div className="flex-1 overflow-y-auto p-6 space-y-6">
- {/* 调用方式说明 */}
- <div className="bg-gradient-to-r from-blue-50 to-purple-50 border border-blue-200 rounded-xl p-4">
- <h4 className="text-sm font-semibold text-gray-900 mb-3">📌 模型调用方式</h4>
- <div className="space-y-2 text-sm text-gray-700">
- <div>
- <span className="font-medium">方式1 - 精确ID(推荐):</span>
- <code className="ml-2 px-2 py-0.5 bg-white rounded text-blue-600">local:{apiExampleModel.id}</code>
- </div>
- {apiExampleModel.supplier && (
- <div>
- <span className="font-medium">方式2 - 提供商/名称:</span>
- <code className="ml-2 px-2 py-0.5 bg-white rounded text-blue-600">{apiExampleModel.supplier}/{apiExampleModel.name}</code>
- </div>
- )}
- <div>
- <span className="font-medium">方式3 - 仅名称:</span>
- <code className="ml-2 px-2 py-0.5 bg-white rounded text-blue-600">{apiExampleModel.name}</code>
- <span className="ml-2 text-xs text-gray-500">(如有同名模型,将使用最新创建的)</span>
- </div>
- </div>
- </div>
- {/* 提示信息 */}
- <div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
- <p className="text-sm text-amber-800">
- <strong>提示:</strong>请先在「开放平台」页面创建本地私钥(访问本地模型),然后将下方示例中的 <code className="bg-amber-100 px-1 rounded">YOUR_API_KEY</code> 替换为您的实际API Key。
- </p>
- </div>
- {/* cURL示例 */}
- <div>
- <div className="flex items-center justify-between mb-2">
- <h4 className="text-sm font-medium text-gray-700">cURL</h4>
- <button
- onClick={() => handleCopy(generateCurlExample(apiExampleModel), 'curl')}
- className="flex items-center gap-1 text-xs text-gray-500 hover:text-blue-600 transition-colors"
- >
- {copied === 'curl' ? (
- <>
- <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />
- 已复制
- </>
- ) : (
- <>
- <Copy className="w-3.5 h-3.5" />
- 复制
- </>
- )}
- </button>
- </div>
- <pre className="bg-gray-900 text-gray-100 p-4 rounded-xl text-sm overflow-x-auto">
- <code>{generateCurlExample(apiExampleModel)}</code>
- </pre>
- </div>
- {/* Python示例 */}
- <div>
- <div className="flex items-center justify-between mb-2">
- <h4 className="text-sm font-medium text-gray-700">Python (OpenAI SDK)</h4>
- <button
- onClick={() => handleCopy(generatePythonExample(apiExampleModel), 'python')}
- className="flex items-center gap-1 text-xs text-gray-500 hover:text-blue-600 transition-colors"
- >
- {copied === 'python' ? (
- <>
- <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />
- 已复制
- </>
- ) : (
- <>
- <Copy className="w-3.5 h-3.5" />
- 复制
- </>
- )}
- </button>
- </div>
- <pre className="bg-gray-900 text-gray-100 p-4 rounded-xl text-sm overflow-x-auto">
- <code>{generatePythonExample(apiExampleModel)}</code>
- </pre>
- </div>
- {/* JavaScript示例 */}
- <div>
- <div className="flex items-center justify-between mb-2">
- <h4 className="text-sm font-medium text-gray-700">JavaScript (Fetch)</h4>
- <button
- onClick={() => handleCopy(generateJsExample(apiExampleModel), 'js')}
- className="flex items-center gap-1 text-xs text-gray-500 hover:text-blue-600 transition-colors"
- >
- {copied === 'js' ? (
- <>
- <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />
- 已复制
- </>
- ) : (
- <>
- <Copy className="w-3.5 h-3.5" />
- 复制
- </>
- )}
- </button>
- </div>
- <pre className="bg-gray-900 text-gray-100 p-4 rounded-xl text-sm overflow-x-auto">
- <code>{generateJsExample(apiExampleModel)}</code>
- </pre>
- </div>
- </div>
- <div className="px-6 py-4 border-t border-gray-100 bg-gray-50">
- <div className="flex justify-between items-center">
- <p className="text-xs text-gray-500">
- API端点: <code className="bg-gray-200 px-1.5 py-0.5 rounded">{getApiBaseUrl()}/{
- apiExampleModel.category === 4 ? 'images/generations' :
- apiExampleModel.category === 6 ? 'images/edits' :
- apiExampleModel.category === 2 ? 'audio/speech' :
- apiExampleModel.category === 3 ? 'audio/transcriptions' :
- apiExampleModel.category === 5 ? 'videos/generations' :
- apiExampleModel.category === 7 ? 'embeddings' :
- apiExampleModel.category === 8 ? 'rerank' :
- 'chat/completions'
- }</code>
- </p>
- <button
- onClick={() => setApiExampleModel(null)}
- className="px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-colors text-sm"
- >
- 关闭
- </button>
- </div>
- </div>
- </div>
- </div>
- )}
- </div>
- );
- };
- export default LocalModelList;
|