7 Commits

Author SHA1 Message Date
a9cbd1b2ba chore: 更新版本号到 v1.7.0 2026-04-28 00:17:27 +08:00
685582b7e6 feat: 支持子类别配置和关键特性显示
- 类别数据结构新增 subcategories 字段
- 每个子类别可定义 key_features 和 feature_labels
- 前端模型页面添加子类别选择器
- 表格根据子类别动态显示关键特性列
- 后台管理支持编辑子类别配置(JSON格式)
- 预设了各类别的子类别配置(对话、代码、推理、视觉等)
2026-04-28 00:16:55 +08:00
961322f8ba chore: 更新版本号到 v1.6.0 2026-04-27 19:58:02 +08:00
b40e890e2b feat: 后台管理添加大模型接口配置功能
- 网站配置页面新增 LLM 配置区域
- 支持配置 API 地址、API Key、文本模型、视觉模型
- LLM 配置从 config.json 动态读取
- 不再使用硬编码的 LLM_CONFIG 常量
2026-04-27 19:57:22 +08:00
9525d56ffc fix: 修复f-string花括号转义问题导致的API错误 2026-04-27 18:44:37 +08:00
5433605fec fix: 增强剪贴板粘贴的错误提示,说明HTTPS/localhost限制 2026-04-27 18:40:36 +08:00
b981e30f46 fix: 修复版本号显示 2026-04-27 18:39:41 +08:00
5 changed files with 581 additions and 60 deletions

63
app.py
View File

@@ -1,7 +1,7 @@
"""
ParamHub - 参数百科
AI大模型与硬件参数速查平台
v1.5.0 - 支持多图上传和智能解析产品参数
v1.7.0 - 支持子类别配置和关键特性显示
"""
from flask import Flask, render_template, jsonify, request
@@ -49,7 +49,7 @@ LLM_CONFIG = {
'vision_model': 'gpt-4-vision-preview', # 视觉模型(解析图片)
}
# 默认网站配置
# 默认网站配置包含LLM配置
DEFAULT_CONFIG = {
'site_name': 'ParamHub',
'site_subtitle': '参数百科',
@@ -58,12 +58,31 @@ DEFAULT_CONFIG = {
'copyright_year': '2024',
'contact_email': '',
'github_url': '',
# LLM配置
'llm_base_url': 'http://192.168.2.17:19007/v1',
'llm_api_key': '',
'llm_model': 'auto',
'llm_vision_model': 'gpt-4-vision-preview',
}
def get_llm_config():
"""获取LLM配置从config.json动态读取"""
config = load_config()
return {
'base_url': config.get('llm_base_url', DEFAULT_CONFIG['llm_base_url']),
'api_key': config.get('llm_api_key', DEFAULT_CONFIG['llm_api_key']),
'model': config.get('llm_model', DEFAULT_CONFIG['llm_model']),
'vision_model': config.get('llm_vision_model', DEFAULT_CONFIG['llm_vision_model']),
}
def load_config():
"""加载网站配置"""
if CONFIG_FILE.exists():
return json.loads(CONFIG_FILE.read_text(encoding='utf-8'))
loaded = json.loads(CONFIG_FILE.read_text(encoding='utf-8'))
# 合并默认配置(确保新字段存在)
result = DEFAULT_CONFIG.copy()
result.update(loaded)
return result
return DEFAULT_CONFIG.copy()
def save_config(config):
@@ -140,26 +159,29 @@ def parse_with_llm(text, category_type, images=None):
}
fields = field_templates.get(category_type, field_templates['dynamic'])
fields_json = json.dumps(fields, ensure_ascii=False, indent=2)
# 构建消息内容
content_parts = []
# 如果有图片,添加图片内容
if images and len(images) > 0:
content_parts.append({
"type": "text",
"text": f"""请分析图片中的产品参数信息,提取结构化数据。
prompt_text = """请分析图片中的产品参数信息,提取结构化数据。
需要提取的字段:
{json.dumps(fields, ensure_ascii=False, indent=2)}
""" + fields_json + """
重要要求:
1. 图片中可能包含1个或多个产品请识别所有产品
2. 如果是多张图片,请综合分析所有图片内容
3. 数字字段只返回数字,不带单位
4. 如果某字段没有提及返回null
5. 返回格式:如果识别到多个产品,返回数组 [{"name": ...}, {"name": ...}]; 如果只有一个产品,返回单个对象 {"name": ...}
5. 返回格式:如果识别到多个产品,返回数组 [对象列表]; 如果只有一个产品,返回单个对象
6. 只返回JSON数据不要其他内容"""
content_parts.append({
"type": "text",
"text": prompt_text
})
# 添加每张图片支持URL或base64
@@ -194,15 +216,13 @@ def parse_with_llm(text, category_type, images=None):
print(f"读取图片失败: {e}")
else:
# 纯文本解析
content_parts.append({
"type": "text",
"text": f"""请解析以下文本,提取结构化数据。
prompt_text = """请解析以下文本,提取结构化数据。
文本内容:
{text}
""" + str(text) + """
需要提取的字段:
{json.dumps(fields, ensure_ascii=False, indent=2)}
""" + fields_json + """
要求:
1. 根据文本内容智能提取各个字段的值
@@ -211,17 +231,24 @@ def parse_with_llm(text, category_type, images=None):
4. 返回JSON格式不要包含任何其他内容
请直接返回JSON数据"""
content_parts.append({
"type": "text",
"text": prompt_text
})
try:
# 使用视觉模型解析
model = LLM_CONFIG.get('vision_model', 'gpt-4-vision-preview') if images else LLM_CONFIG['model']
# 动态获取LLM配置
llm_config = get_llm_config()
# 使用视觉模型解析(如果有图片)
model = llm_config.get('vision_model', 'gpt-4-vision-preview') if images else llm_config['model']
response = requests.post(
f"{LLM_CONFIG['base_url']}/chat/completions",
f"{llm_config['base_url']}/chat/completions",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {LLM_CONFIG['api_key']}"
"Authorization": f"Bearer {llm_config['api_key']}"
},
json={
"model": model,
@@ -1375,7 +1402,7 @@ def api_delete_image(filename):
if __name__ == '__main__':
print("=" * 50)
print("ParamHub - 参数百科 v1.4.0")
print("ParamHub - 参数百科 v1.7.0")
print("=" * 50)
print(f"访问地址: http://localhost:19010")
print(f"后台管理: http://localhost:19010/admin")

View File

@@ -5,7 +5,54 @@
"icon": "ri-robot-line",
"color": "blue",
"description": "大语言模型、图像模型等AI模型参数",
"order": 1
"order": 1,
"subcategories": [
{
"id": "chat",
"name": "对话模型",
"icon": "ri-chat-3-line",
"key_features": ["context_length", "mmlu", "input_price", "output_price"],
"feature_labels": {
"context_length": "上下文",
"mmlu": "MMLU",
"input_price": "输入价",
"output_price": "输出价"
}
},
{
"id": "code",
"name": "代码模型",
"icon": "ri-code-line",
"key_features": ["humaneval", "context_length", "input_price"],
"feature_labels": {
"humaneval": "HumanEval",
"context_length": "上下文",
"input_price": "输入价"
}
},
{
"id": "reasoning",
"name": "推理模型",
"icon": "ri-lightbulb-line",
"key_features": ["reasoning_capability", "mmlu", "context_length"],
"feature_labels": {
"reasoning_capability": "推理能力",
"mmlu": "MMLU",
"context_length": "上下文"
}
},
{
"id": "vision",
"name": "视觉模型",
"icon": "ri-image-line",
"key_features": ["vision_capability", "multimodal", "context_length"],
"feature_labels": {
"vision_capability": "视觉能力",
"multimodal": "多模态",
"context_length": "上下文"
}
}
]
},
{
"id": "gpus",
@@ -13,7 +60,45 @@
"icon": "ri-cpu-line",
"color": "green",
"description": "NVIDIA、AMD等GPU显卡规格参数",
"order": 2
"order": 2,
"subcategories": [
{
"id": "gaming",
"name": "游戏显卡",
"icon": "ri-gamepad-line",
"key_features": ["memory_gb", "cuda_cores", "price_usd", "fp16_tflops"],
"feature_labels": {
"memory_gb": "显存",
"cuda_cores": "CUDA核心",
"price_usd": "价格",
"fp16_tflops": "FP16性能"
}
},
{
"id": "professional",
"name": "专业显卡",
"icon": "ri-building-line",
"key_features": ["memory_gb", "tensor_cores", "memory_bandwidth_gbs", "price_usd"],
"feature_labels": {
"memory_gb": "显存",
"tensor_cores": "Tensor核心",
"memory_bandwidth_gbs": "带宽",
"price_usd": "价格"
}
},
{
"id": "datacenter",
"name": "数据中心",
"icon": "ri-server-line",
"key_features": ["memory_gb", "tensor_cores", "memory_bandwidth_gbs", "fp16_tflops"],
"feature_labels": {
"memory_gb": "显存",
"tensor_cores": "Tensor核心",
"memory_bandwidth_gbs": "带宽",
"fp16_tflops": "FP16性能"
}
}
]
},
{
"id": "cpus",
@@ -21,7 +106,45 @@
"icon": "ri-cpu-line",
"color": "purple",
"description": "Intel、AMD等CPU处理器参数",
"order": 3
"order": 3,
"subcategories": [
{
"id": "desktop",
"name": "桌面CPU",
"icon": "ri-computer-line",
"key_features": ["cores", "threads", "boost_clock_ghz", "price_usd"],
"feature_labels": {
"cores": "核心",
"threads": "线程",
"boost_clock_ghz": "加速频率",
"price_usd": "价格"
}
},
{
"id": "server",
"name": "服务器CPU",
"icon": "ri-server-line",
"key_features": ["cores", "threads", "l3_cache_mb", "tdp_watts"],
"feature_labels": {
"cores": "核心",
"threads": "线程",
"l3_cache_mb": "L3缓存",
"tdp_watts": "功耗"
}
},
{
"id": "mobile",
"name": "移动CPU",
"icon": "ri-smartphone-line",
"key_features": ["cores", "threads", "base_clock_ghz", "tdp_watts"],
"feature_labels": {
"cores": "核心",
"threads": "线程",
"base_clock_ghz": "基础频率",
"tdp_watts": "功耗"
}
}
]
},
{
"id": "phones",
@@ -30,7 +153,33 @@
"color": "orange",
"description": "各品牌手机参数规格",
"order": 4,
"visible": false
"visible": true,
"subcategories": [
{
"id": "flagship",
"name": "旗舰手机",
"icon": "ri-star-line",
"key_features": ["processor", "ram_gb", "storage_gb", "price"],
"feature_labels": {
"processor": "处理器",
"ram_gb": "内存",
"storage_gb": "存储",
"price": "价格"
}
},
{
"id": "midrange",
"name": "中端手机",
"icon": "ri-price-tag-3-line",
"key_features": ["processor", "ram_gb", "battery_mah", "price"],
"feature_labels": {
"processor": "处理器",
"ram_gb": "内存",
"battery_mah": "电池",
"price": "价格"
}
}
]
},
{
"id": "laptops",
@@ -38,7 +187,33 @@
"icon": "ri-macbook-line",
"color": "teal",
"description": "笔记本电脑、台式机参数",
"order": 5
"order": 5,
"subcategories": [
{
"id": "gaming-laptop",
"name": "游戏笔记本",
"icon": "ri-gamepad-line",
"key_features": ["processor", "gpu", "ram_gb", "price"],
"feature_labels": {
"processor": "处理器",
"gpu": "显卡",
"ram_gb": "内存",
"price": "价格"
}
},
{
"id": "business-laptop",
"name": "商务笔记本",
"icon": "ri-briefcase-line",
"key_features": ["processor", "ram_gb", "weight_kg", "price"],
"feature_labels": {
"processor": "处理器",
"ram_gb": "内存",
"weight_kg": "重量",
"price": "价格"
}
}
]
},
{
"id": "021dc76d36be",
@@ -47,6 +222,66 @@
"color": "red",
"order": 6,
"description": "汽车方面",
"created_at": "2026-04-09 10:09:01"
"created_at": "2026-04-09 10:09:01",
"subcategories": [
{
"id": "sedan",
"name": "轿车",
"icon": "ri-car-line",
"key_features": ["engine", "power_kw", "price"],
"feature_labels": {
"engine": "发动机",
"power_kw": "功率",
"price": "价格"
}
},
{
"id": "suv",
"name": "SUV",
"icon": "ri-truck-line",
"key_features": ["engine", "seats", "price"],
"feature_labels": {
"engine": "发动机",
"seats": "座位数",
"price": "价格"
}
}
]
},
{
"id": "71fa2b4d818f",
"name": "摄像",
"icon": "ri-camera-line",
"color": "blue",
"order": 0,
"visible": true,
"description": "相机、摄像机等",
"created_at": "2026-04-25 16:38:47",
"subcategories": [
{
"id": "mirrorless",
"name": "无反相机",
"icon": "ri-camera-line",
"key_features": ["sensor", "megapixels", "video_resolution", "price"],
"feature_labels": {
"sensor": "传感器",
"megapixels": "像素",
"video_resolution": "视频",
"price": "价格"
}
},
{
"id": "dslr",
"name": "单反相机",
"icon": "ri-camera-2-line",
"key_features": ["sensor", "megapixels", "lens_mount", "price"],
"feature_labels": {
"sensor": "传感器",
"megapixels": "像素",
"lens_mount": "卡口",
"price": "价格"
}
}
]
}
]

View File

@@ -6,5 +6,9 @@
"copyright_year": "2026",
"contact_email": "wlq@tphai.com",
"github_url": "",
"updated_at": "2026-04-11 02:28:39"
"updated_at": "2026-04-27 19:57:00",
"llm_base_url": "http://192.168.2.17:19007/v1",
"llm_api_key": "",
"llm_model": "auto",
"llm_vision_model": "gpt-4-vision-preview"
}

View File

@@ -57,6 +57,8 @@
<h1 class="text-2xl font-bold text-gray-800 mb-6">网站配置</h1>
<div class="bg-white rounded-xl shadow-sm p-6">
<form id="configForm" class="space-y-6">
<!-- 网站基础配置 -->
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2 mb-4"><i class="ri-global-line mr-2"></i>网站基础配置</h3>
<div class="grid grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">网站名称</label>
@@ -87,6 +89,33 @@
<label class="block text-sm font-medium text-gray-700 mb-2">页脚文字</label>
<textarea name="footer_text" id="config_footer_text" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="网站底部的版权信息等"></textarea>
</div>
<!-- 大模型接口配置 -->
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2 mb-4 mt-8"><i class="ri-robot-line mr-2"></i>大模型接口配置(用于智能解析)</h3>
<div class="bg-blue-50 rounded-lg p-4 mb-4">
<p class="text-sm text-blue-700"><i class="ri-information-line mr-1"></i>配置用于智能解析产品参数的大模型API接口。文本解析使用普通模型图片解析使用视觉模型。</p>
</div>
<div class="grid grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">API地址</label>
<input type="url" name="llm_base_url" id="config_llm_base_url" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="http://192.168.2.17:19007/v1">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">API Key</label>
<input type="text" name="llm_api_key" id="config_llm_api_key" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="留空则不验证">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">文本解析模型</label>
<input type="text" name="llm_model" id="config_llm_model" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="auto">
<p class="text-xs text-gray-500 mt-1">用于解析文本数据,如 deepseek-v3、qwen3.5 等</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">图片解析模型(视觉模型)</label>
<input type="text" name="llm_vision_model" id="config_llm_vision_model" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="gpt-4-vision-preview">
<p class="text-xs text-gray-500 mt-1">用于解析图片,如 Qwen/Qwen2-VL-72B-Instruct、gpt-4-vision-preview 等</p>
</div>
</div>
<div class="flex justify-end gap-4">
<button type="button" onclick="loadSiteConfig()" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"><i class="ri-refresh-line mr-1"></i>重置</button>
<button type="button" onclick="saveSiteConfig()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-save-line mr-1"></i>保存配置</button>
@@ -553,6 +582,7 @@
const res = await fetch('/api/config');
const config = await res.json();
// 网站基础配置
document.getElementById('config_site_name').value = config.site_name || '';
document.getElementById('config_site_subtitle').value = config.site_subtitle || '';
document.getElementById('config_icp_number').value = config.icp_number || '';
@@ -560,11 +590,18 @@
document.getElementById('config_github_url').value = config.github_url || '';
document.getElementById('config_copyright_year').value = config.copyright_year || '';
document.getElementById('config_footer_text').value = config.footer_text || '';
// LLM配置
document.getElementById('config_llm_base_url').value = config.llm_base_url || 'http://192.168.2.17:19007/v1';
document.getElementById('config_llm_api_key').value = config.llm_api_key || '';
document.getElementById('config_llm_model').value = config.llm_model || 'auto';
document.getElementById('config_llm_vision_model').value = config.llm_vision_model || 'gpt-4-vision-preview';
}
// 保存网站配置
async function saveSiteConfig() {
const config = {
// 网站基础配置
site_name: document.getElementById('config_site_name').value,
site_subtitle: document.getElementById('config_site_subtitle').value,
icp_number: document.getElementById('config_icp_number').value,
@@ -572,6 +609,11 @@
github_url: document.getElementById('config_github_url').value,
copyright_year: document.getElementById('config_copyright_year').value,
footer_text: document.getElementById('config_footer_text').value,
// LLM配置
llm_base_url: document.getElementById('config_llm_base_url').value,
llm_api_key: document.getElementById('config_llm_api_key').value,
llm_model: document.getElementById('config_llm_model').value,
llm_vision_model: document.getElementById('config_llm_vision_model').value,
};
try {
@@ -880,6 +922,13 @@
else if (key === 'images') {
try { data[key] = JSON.parse(value); } catch { data[key] = []; }
}
else if (key === 'subcategories') {
// 解析子类别JSON
try { data[key] = JSON.parse(value); } catch {
alert('子类别JSON格式错误请检查格式');
return;
}
}
else data[key] = value;
}
});
@@ -979,31 +1028,50 @@
// 从剪贴板粘贴图片
async function pasteImageFromClipboard(type) {
try {
// 检查剪贴板API是否可用需要HTTPS或localhost
if (!navigator.clipboard || !navigator.clipboard.read) {
alert('剪贴板API需要HTTPS或localhost环境。\n当前访问地址不支持请使用文件选择上传。\n\n可改用 localhost:19010 访问来支持粘贴功能。');
return;
}
const clipboardItems = await navigator.clipboard.read();
let found = false;
for (const item of clipboardItems) {
for (const type of item.types) {
if (type.startsWith('image/')) {
const blob = await item.getType(type);
for (const itemType of item.types) {
if (itemType.startsWith('image/')) {
found = true;
const blob = await item.getType(itemType);
const reader = new FileReader();
reader.onload = async (e) => {
const base64 = e.target.result;
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
currentImages.push(data.url);
updateImagePreview();
try {
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
currentImages.push(data.url);
updateImagePreview();
}
} catch (err) {
alert('上传失败: ' + err.message);
}
};
reader.readAsDataURL(blob);
}
}
}
if (!found) {
alert('剪贴板中没有图片,请先复制一张图片');
}
} catch (e) {
alert('无法从剪贴板获取图片,请使用文件选择');
if (e.name === 'NotAllowedError') {
alert('浏览器拒绝访问剪贴板。\n请使用文件选择上传或改用 localhost:19010 访问。');
} else {
alert('无法从剪贴板获取图片: ' + e.message + '\n请使用文件选择上传');
}
}
}
@@ -1035,6 +1103,9 @@
// 表单模板
function getCategoryForm(data = {}) {
const subcategories = data.subcategories || [];
const subcategoriesJson = JSON.stringify(subcategories, null, 2);
return `<form id="itemForm" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div><label class="text-sm text-gray-600 mb-1 block">ID *</label><input type="text" name="id" value="${data.id || ''}" ${data.id ? 'readonly' : ''} required class="w-full px-3 py-2 border rounded-lg ${data.id ? 'bg-gray-100' : ''}"></div>
@@ -1055,6 +1126,24 @@
</select></div>
</div>
<div><label class="text-sm text-gray-600 mb-1 block">描述</label><textarea name="description" rows="2" class="w-full px-3 py-2 border rounded-lg">${data.description || ''}</textarea></div>
<!-- 子类别配置 -->
<div class="border-t pt-4">
<label class="text-sm text-gray-600 mb-2 block"><i class="ri-folder-line mr-1"></i>子类别配置JSON格式</label>
<div class="bg-blue-50 rounded-lg p-3 mb-2 text-xs text-blue-700">
<p class="mb-2">子类别配置示例:</p>
<pre class="bg-blue-100 p-2 rounded overflow-x-auto">[
{
"id": "chat",
"name": "对话模型",
"icon": "ri-chat-3-line",
"key_features": ["context_length", "mmlu"],
"feature_labels": {"context_length": "上下文", "mmlu": "MMLU"}
}
]</pre>
</div>
<textarea name="subcategories" rows="8" class="w-full px-3 py-2 border rounded-lg font-mono text-sm" placeholder='[]'>${subcategoriesJson}</textarea>
</div>
</form>`;
}
@@ -1221,31 +1310,50 @@
// 从剪贴板粘贴图片
async function pasteSmartImageFromClipboard() {
try {
// 检查剪贴板API是否可用需要HTTPS或localhost
if (!navigator.clipboard || !navigator.clipboard.read) {
alert('剪贴板API需要HTTPS或localhost环境。\n当前访问地址不支持请使用文件选择上传。\n\n可改用 localhost:19010 访问来支持粘贴功能。');
return;
}
const clipboardItems = await navigator.clipboard.read();
let found = false;
for (const item of clipboardItems) {
for (const type of item.types) {
if (type.startsWith('image/')) {
found = true;
const blob = await item.getType(type);
const reader = new FileReader();
reader.onload = async (e) => {
const base64 = e.target.result;
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
smartAddImages.push(data.url);
updateSmartImagePreview();
try {
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
smartAddImages.push(data.url);
updateSmartImagePreview();
}
} catch (err) {
alert('上传失败: ' + err.message);
}
};
reader.readAsDataURL(blob);
}
}
}
if (!found) {
alert('剪贴板中没有图片,请先复制一张图片');
}
} catch (e) {
alert('无法从剪贴板获取图片,请使用文件选择');
if (e.name === 'NotAllowedError') {
alert('浏览器拒绝访问剪贴板。\n请使用文件选择上传或改用 localhost:19010 访问。');
} else {
alert('无法从剪贴板获取图片: ' + e.message + '\n请使用文件选择上传');
}
}
}

View File

@@ -32,6 +32,19 @@
<p class="text-gray-500 mt-1">AI大模型参数规格一览</p>
</div>
<!-- 子类别选择器 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-4">
<div class="flex items-center gap-2 mb-2">
<span class="text-sm text-gray-600"><i class="ri-folder-line mr-1"></i>子类别:</span>
</div>
<div class="flex gap-2" id="subcategoryTabs">
<button onclick="selectSubcategory('')" class="px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm" id="subcat-all">
<i class="ri-apps-line mr-1"></i>全部
</button>
<!-- 动态加载子类别 -->
</div>
</div>
<!-- 搜索和筛选 -->
<div class="bg-white rounded-xl shadow-sm p-4 mb-6">
<div class="flex gap-4 items-center">
@@ -101,12 +114,38 @@
<script>
let allModels = [];
let categories = [];
let currentCategory = null;
let currentSubcategory = '';
// 子类别默认特性配置
const DEFAULT_KEY_FEATURES = {
'chat': ['context_length', 'mmlu', 'input_price', 'output_price'],
'code': ['humaneval', 'context_length', 'input_price'],
'reasoning': ['mmlu', 'context_length', 'parameters'],
'vision': ['context_length', 'mmlu', 'input_price']
};
const FEATURE_LABELS = {
'context_length': '上下文',
'mmlu': 'MMLU',
'humaneval': 'HumanEval',
'input_price': '输入价',
'output_price': '输出价',
'parameters': '参数量',
'reasoning_capability': '推理',
'vision_capability': '视觉',
'multimodal': '多模态'
};
// 加载导航栏
async function loadNav() {
const res = await fetch('/api/categories');
categories = await res.json();
// 获取当前类别的子类别
currentCategory = categories.find(c => c.id === 'ai-models');
renderSubcategoryTabs();
const builtinPages = [
{name: '首页', href: '/'},
{name: '工具', href: '/tools'},
@@ -134,6 +173,49 @@
document.getElementById('topNav').innerHTML = navHtml;
}
// 渲染子类别选择器
function renderSubcategoryTabs() {
const container = document.getElementById('subcategoryTabs');
if (!currentCategory || !currentCategory.subcategories) {
container.innerHTML = '';
return;
}
let html = `<button onclick="selectSubcategory('')" class="px-4 py-2 ${currentSubcategory === '' ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'} rounded-lg text-sm" id="subcat-all">
<i class="ri-apps-line mr-1"></i>全部
</button>`;
currentCategory.subcategories.forEach(sub => {
const isActive = currentSubcategory === sub.id;
html += `<button onclick="selectSubcategory('${sub.id}')" class="px-4 py-2 ${isActive ? 'bg-indigo-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'} rounded-lg text-sm" id="subcat-${sub.id}">
<i class="${sub.icon || 'ri-folder-line'} mr-1"></i>${sub.name}
</button>`;
});
container.innerHTML = html;
}
// 选择子类别
function selectSubcategory(subId) {
currentSubcategory = subId;
renderSubcategoryTabs();
loadModels();
}
// 获取当前子类别的关键特性
function getKeyFeatures() {
if (!currentSubcategory || !currentCategory || !currentCategory.subcategories) {
return ['parameters', 'context_length', 'mmlu', 'input_price'];
}
const subcat = currentCategory.subcategories.find(s => s.id === currentSubcategory);
if (subcat && subcat.key_features) {
return subcat.key_features;
}
return ['parameters', 'context_length', 'mmlu', 'input_price'];
}
async function loadModels() {
const keyword = document.getElementById('searchInput').value.trim();
@@ -155,6 +237,21 @@
models = models.filter(m => !m.is_open_source);
}
// 子类别过滤(通过模型名称/描述中的关键词判断)
if (currentSubcategory && currentCategory && currentCategory.subcategories) {
const subcat = currentCategory.subcategories.find(s => s.id === currentSubcategory);
if (subcat) {
// 简化过滤:根据子类别关键词匹配
// 实际应该有 subcategory_id 字段,这里暂时用名称匹配
// 用户可以在后台编辑时指定子类别
models = models.filter(m => {
const subcatField = m.subcategory || m.subcategory_id;
if (subcatField) return subcatField === currentSubcategory;
return true; // 暂时显示全部,等后台支持子类别字段后再过滤
});
}
}
renderModels(models);
}
@@ -166,7 +263,31 @@
return;
}
const html = models.map(m => `
// 动态获取关键特性
const keyFeatures = getKeyFeatures();
// 动态表头
let headerHtml = `
<tr>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">模型名称</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">厂商</th>
`;
keyFeatures.forEach(f => {
headerHtml += `<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">${FEATURE_LABELS[f] || f}</th>`;
});
headerHtml += `
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">类型</th>
<th class="px-4 py-3 text-center text-sm font-medium text-gray-600">操作</th>
</tr>
`;
document.querySelector('#modelsTable thead').innerHTML = headerHtml;
// 动态内容
const html = models.map(m => {
let rowHtml = `
<tr class="border-b hover:bg-gray-50 transition ${m.is_pinned ? 'bg-yellow-50' : ''}">
<td class="px-4 py-3">
<div class="flex items-center gap-2">
@@ -178,31 +299,57 @@
</div>
</td>
<td class="px-4 py-3 text-gray-600">${m.organization}</td>
<td class="px-4 py-3">
<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-sm">${m.parameters}B</span>
</td>
<td class="px-4 py-3 text-gray-600">${formatContext(m.context_length)}</td>
<td class="px-4 py-3">
<span class="px-2 py-1 bg-green-100 text-green-700 rounded text-sm">${m.mmlu || '-'}%</span>
</td>
`;
// 关键特性列
keyFeatures.forEach(f => {
const value = formatFeatureValue(f, m);
rowHtml += `<td class="px-4 py-3">${value}</td>`;
});
rowHtml += `
<td class="px-4 py-3">
${m.is_open_source
? '<span class="px-2 py-1 bg-emerald-100 text-emerald-700 rounded text-sm">开源</span>'
: '<span class="px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm">商业</span>'}
</td>
<td class="px-4 py-3 text-sm text-gray-600">
${m.input_price ? `$${m.input_price}/$${m.output_price}` : '免费'}
</td>
<td class="px-4 py-3 text-center">
<button onclick="showDetail('${m.id}')" class="text-indigo-600 hover:text-indigo-800 text-sm">
<i class="ri-eye-line mr-1"></i>详情
</button>
</td>
</tr>
`).join('');
`;
return rowHtml;
}).join('');
document.getElementById('modelsTable').innerHTML = html;
}
// 格式化特性值
function formatFeatureValue(feature, model) {
const value = model[feature];
if (value === null || value === undefined) return '<span class="text-gray-400">-</span>';
switch (feature) {
case 'parameters':
return `<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-sm">${value}B</span>`;
case 'context_length':
return `<span class="text-gray-600">${formatContext(value)}</span>`;
case 'mmlu':
return `<span class="px-2 py-1 bg-green-100 text-green-700 rounded text-sm">${value}%</span>`;
case 'humaneval':
return `<span class="px-2 py-1 bg-purple-100 text-purple-700 rounded text-sm">${value}%</span>`;
case 'input_price':
return `<span class="text-sm text-gray-600">$${value || 0}</span>`;
case 'output_price':
return `<span class="text-sm text-gray-600">$${value || 0}</span>`;
default:
return `<span class="text-gray-600">${value}</span>`;
}
}
function formatContext(len) {
if (!len) return '-';