mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 20:12:38 +08:00
feat: update store components and enhance assistant functionality
- Refactored store components to improve organization and user experience, including the introduction of new GridView and ListView components. - Implemented a detail dialog for displaying item information and installation options. - Enhanced the store sidebar with collapsible categories for better navigation. - Updated data structures to support dynamic subcategory handling and improved filtering capabilities. - Added utility functions for dialog and collapsible components to streamline UI interactions.
This commit is contained in:
parent
802402e922
commit
c799f15fcc
25
package.json
25
package.json
@ -70,12 +70,6 @@
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@electron/notarize": "^2.5.0",
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.6",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"@tanstack/react-query": "^5.27.0",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
@ -83,8 +77,6 @@
|
||||
"archiver": "^7.0.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bufferutil": "^4.0.9",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"color": "^5.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"docx": "^9.0.2",
|
||||
@ -100,18 +92,14 @@
|
||||
"got-scraping": "^4.1.1",
|
||||
"jsdom": "^26.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"officeparser": "^4.1.1",
|
||||
"opendal": "^0.47.11",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tar": "^7.4.3",
|
||||
"turndown": "^7.2.0",
|
||||
"turndown-plugin-gfm": "^1.0.2",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"undici": "^7.4.0",
|
||||
"webdav": "^5.8.0",
|
||||
"ws": "^8.18.1",
|
||||
@ -137,6 +125,13 @@
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@radix-ui/react-collapsible": "^1.1.10",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.6",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@shikijs/markdown-it": "^3.2.2",
|
||||
"@swc/plugin-styled-components": "^7.1.3",
|
||||
@ -165,6 +160,8 @@
|
||||
"axios": "^1.7.3",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"dexie": "^4.0.8",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
@ -189,6 +186,7 @@
|
||||
"lucide-react": "^0.509.0",
|
||||
"mime": "^4.0.4",
|
||||
"motion": "^12.11.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"npx-scope-finder": "^1.2.0",
|
||||
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"p-queue": "^8.1.0",
|
||||
@ -215,12 +213,15 @@
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.77.2",
|
||||
"shiki": "^3.2.2",
|
||||
"sonner": "^2.0.3",
|
||||
"string-width": "^7.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.5",
|
||||
"tiny-pinyin": "^1.3.2",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"tokenx": "^0.4.1",
|
||||
"tw-animate-css": "^1.2.9",
|
||||
"typescript": "^5.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "6.2.6",
|
||||
|
||||
@ -3,30 +3,192 @@
|
||||
"id": "all",
|
||||
"title": "Categories",
|
||||
"items": [
|
||||
{ "id": "featured", "name": "Featured", "count": 24 },
|
||||
{ "id": "new", "name": "New Releases", "count": 18 },
|
||||
{ "id": "top", "name": "Top Rated", "count": 32 }
|
||||
{
|
||||
"id": "featured",
|
||||
"name": "Featured",
|
||||
"count": 24
|
||||
},
|
||||
{
|
||||
"id": "new",
|
||||
"name": "New Releases",
|
||||
"count": 18
|
||||
},
|
||||
{
|
||||
"id": "top",
|
||||
"name": "Top Rated",
|
||||
"count": 32
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "assistant",
|
||||
"title": "助手",
|
||||
"items": [
|
||||
{ "id": "assistant-job", "name": "职业" },
|
||||
{ "id": "assistant-business", "name": "商业" },
|
||||
{ "id": "assistant-tools", "name": "工具" },
|
||||
{ "id": "assistant-language", "name": "语言" },
|
||||
{ "id": "assistant-office", "name": "办公" },
|
||||
{ "id": "assistant-general", "name": "通用" },
|
||||
{ "id": "assistant-writing", "name": "写作" },
|
||||
{ "id": "assistant-coding", "name": "编程" },
|
||||
{ "id": "assistant-emotion", "name": "情感" },
|
||||
{ "id": "assistant-education", "name": "教育" },
|
||||
{ "id": "assistant-creative", "name": "创意" },
|
||||
{ "id": "assistant-academic", "name": "学术" },
|
||||
{ "id": "assistant-design", "name": "设计" },
|
||||
{ "id": "assistant-art", "name": "艺术" },
|
||||
{ "id": "assistant-entertainment", "name": "娱乐" }
|
||||
{
|
||||
"id": "assistant-job",
|
||||
"name": "职业",
|
||||
"count": 274
|
||||
},
|
||||
{
|
||||
"id": "assistant-business",
|
||||
"name": "商业",
|
||||
"count": 163
|
||||
},
|
||||
{
|
||||
"id": "assistant-tools",
|
||||
"name": "工具",
|
||||
"count": 284
|
||||
},
|
||||
{
|
||||
"id": "assistant-language",
|
||||
"name": "语言",
|
||||
"count": 29
|
||||
},
|
||||
{
|
||||
"id": "assistant-office",
|
||||
"name": "办公",
|
||||
"count": 44
|
||||
},
|
||||
{
|
||||
"id": "assistant-general",
|
||||
"name": "通用",
|
||||
"count": 37
|
||||
},
|
||||
{
|
||||
"id": "assistant-writing",
|
||||
"name": "写作",
|
||||
"count": 128
|
||||
},
|
||||
{
|
||||
"id": "assistant-coding",
|
||||
"name": "编程",
|
||||
"count": 61
|
||||
},
|
||||
{
|
||||
"id": "assistant-emotion",
|
||||
"name": "情感",
|
||||
"count": 57
|
||||
},
|
||||
{
|
||||
"id": "assistant-education",
|
||||
"name": "教育",
|
||||
"count": 275
|
||||
},
|
||||
{
|
||||
"id": "assistant-creative",
|
||||
"name": "创意",
|
||||
"count": 166
|
||||
},
|
||||
{
|
||||
"id": "assistant-academic",
|
||||
"name": "学术",
|
||||
"count": 54
|
||||
},
|
||||
{
|
||||
"id": "assistant-design",
|
||||
"name": "设计",
|
||||
"count": 37
|
||||
},
|
||||
{
|
||||
"id": "assistant-art",
|
||||
"name": "艺术",
|
||||
"count": 42
|
||||
},
|
||||
{
|
||||
"id": "assistant-entertainment",
|
||||
"name": "娱乐",
|
||||
"count": 75
|
||||
},
|
||||
{
|
||||
"id": "assistant-featured",
|
||||
"name": "精选",
|
||||
"count": 4
|
||||
},
|
||||
{
|
||||
"id": "assistant-life",
|
||||
"name": "生活",
|
||||
"count": 83
|
||||
},
|
||||
{
|
||||
"id": "assistant-medical",
|
||||
"name": "医疗",
|
||||
"count": 18
|
||||
},
|
||||
{
|
||||
"id": "assistant-game",
|
||||
"name": "游戏",
|
||||
"count": 34
|
||||
},
|
||||
{
|
||||
"id": "assistant-translation",
|
||||
"name": "翻译",
|
||||
"count": 51
|
||||
},
|
||||
{
|
||||
"id": "assistant-music",
|
||||
"name": "音乐",
|
||||
"count": 5
|
||||
},
|
||||
{
|
||||
"id": "assistant-review",
|
||||
"name": "点评",
|
||||
"count": 10
|
||||
},
|
||||
{
|
||||
"id": "assistant-copywriting",
|
||||
"name": "文案",
|
||||
"count": 78
|
||||
},
|
||||
{
|
||||
"id": "assistant-encyclopedia",
|
||||
"name": "百科",
|
||||
"count": 13
|
||||
},
|
||||
{
|
||||
"id": "assistant-health",
|
||||
"name": "健康",
|
||||
"count": 18
|
||||
},
|
||||
{
|
||||
"id": "assistant-marketing",
|
||||
"name": "营销",
|
||||
"count": 17
|
||||
},
|
||||
{
|
||||
"id": "assistant-science",
|
||||
"name": "科学",
|
||||
"count": 12
|
||||
},
|
||||
{
|
||||
"id": "assistant-analysis",
|
||||
"name": "分析",
|
||||
"count": 32
|
||||
},
|
||||
{
|
||||
"id": "assistant-law",
|
||||
"name": "法律",
|
||||
"count": 11
|
||||
},
|
||||
{
|
||||
"id": "assistant-consulting",
|
||||
"name": "咨询",
|
||||
"count": 18
|
||||
},
|
||||
{
|
||||
"id": "assistant-finance",
|
||||
"name": "金融",
|
||||
"count": 6
|
||||
},
|
||||
{
|
||||
"id": "assistant-travel",
|
||||
"name": "旅游",
|
||||
"count": 5
|
||||
},
|
||||
{
|
||||
"id": "assistant-management",
|
||||
"name": "管理",
|
||||
"count": 21
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -38,43 +200,136 @@
|
||||
"id": "knowledge",
|
||||
"title": "知识库",
|
||||
"items": [
|
||||
{ "id": "knowledge-history", "name": "历史" },
|
||||
{ "id": "knowledge-literature", "name": "文学" },
|
||||
{ "id": "knowledge-education", "name": "教育" },
|
||||
{ "id": "knowledge-law", "name": "法律" },
|
||||
{ "id": "knowledge-science", "name": "科学" },
|
||||
{ "id": "knowledge-medicine", "name": "医学" },
|
||||
{ "id": "knowledge-economics", "name": "经济" },
|
||||
{ "id": "knowledge-art", "name": "艺术" },
|
||||
{ "id": "knowledge-geography", "name": "地理" },
|
||||
{ "id": "knowledge-social", "name": "社会" }
|
||||
{
|
||||
"id": "knowledge-history",
|
||||
"name": "历史"
|
||||
},
|
||||
{
|
||||
"id": "knowledge-literature",
|
||||
"name": "文学"
|
||||
},
|
||||
{
|
||||
"id": "knowledge-education",
|
||||
"name": "教育"
|
||||
},
|
||||
{
|
||||
"id": "knowledge-law",
|
||||
"name": "法律"
|
||||
},
|
||||
{
|
||||
"id": "knowledge-science",
|
||||
"name": "科学"
|
||||
},
|
||||
{
|
||||
"id": "knowledge-medicine",
|
||||
"name": "医学"
|
||||
},
|
||||
{
|
||||
"id": "knowledge-economics",
|
||||
"name": "经济"
|
||||
},
|
||||
{
|
||||
"id": "knowledge-art",
|
||||
"name": "艺术"
|
||||
},
|
||||
{
|
||||
"id": "knowledge-geography",
|
||||
"name": "地理"
|
||||
},
|
||||
{
|
||||
"id": "knowledge-social",
|
||||
"name": "社会"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "mcp-server",
|
||||
"title": "MCP 服务器",
|
||||
"items": [
|
||||
{ "id": "mcp-dev-tools", "name": "Developer Tools" },
|
||||
{ "id": "mcp-research-data", "name": "Research And Data" },
|
||||
{ "id": "mcp-cloud", "name": "Cloud Platforms" },
|
||||
{ "id": "mcp-communication", "name": "Communication" },
|
||||
{ "id": "mcp-browser-auto", "name": "Browser Automation" },
|
||||
{ "id": "mcp-finance", "name": "Finance" },
|
||||
{ "id": "mcp-security", "name": "Security" },
|
||||
{ "id": "mcp-os-auto", "name": "Os Automation" },
|
||||
{ "id": "mcp-databases", "name": "Databases" },
|
||||
{ "id": "mcp-cloud-storage", "name": "Cloud Storage" },
|
||||
{ "id": "mcp-monitoring", "name": "Monitoring" },
|
||||
{ "id": "mcp-media", "name": "Entertainment And Media" },
|
||||
{ "id": "mcp-knowledge-mem", "name": "Knowledge And Memory" },
|
||||
{ "id": "mcp-file-systems", "name": "File Systems" },
|
||||
{ "id": "mcp-location", "name": "Location Services" },
|
||||
{ "id": "mcp-calendar", "name": "Calendar Management" },
|
||||
{ "id": "mcp-customer-data", "name": "Customer Data Platforms" },
|
||||
{ "id": "mcp-ai-chatbot", "name": "AI Chatbot" },
|
||||
{ "id": "mcp-virtualization", "name": "Virtualization" },
|
||||
{ "id": "mcp-official-servers", "name": "Official Servers" },
|
||||
{ "id": "mcp-database", "name": "Database" }
|
||||
{
|
||||
"id": "mcp-dev-tools",
|
||||
"name": "Developer Tools"
|
||||
},
|
||||
{
|
||||
"id": "mcp-research-data",
|
||||
"name": "Research And Data"
|
||||
},
|
||||
{
|
||||
"id": "mcp-cloud",
|
||||
"name": "Cloud Platforms"
|
||||
},
|
||||
{
|
||||
"id": "mcp-communication",
|
||||
"name": "Communication"
|
||||
},
|
||||
{
|
||||
"id": "mcp-browser-auto",
|
||||
"name": "Browser Automation"
|
||||
},
|
||||
{
|
||||
"id": "mcp-finance",
|
||||
"name": "Finance"
|
||||
},
|
||||
{
|
||||
"id": "mcp-security",
|
||||
"name": "Security"
|
||||
},
|
||||
{
|
||||
"id": "mcp-os-auto",
|
||||
"name": "Os Automation"
|
||||
},
|
||||
{
|
||||
"id": "mcp-databases",
|
||||
"name": "Databases"
|
||||
},
|
||||
{
|
||||
"id": "mcp-cloud-storage",
|
||||
"name": "Cloud Storage"
|
||||
},
|
||||
{
|
||||
"id": "mcp-monitoring",
|
||||
"name": "Monitoring"
|
||||
},
|
||||
{
|
||||
"id": "mcp-media",
|
||||
"name": "Entertainment And Media"
|
||||
},
|
||||
{
|
||||
"id": "mcp-knowledge-mem",
|
||||
"name": "Knowledge And Memory"
|
||||
},
|
||||
{
|
||||
"id": "mcp-file-systems",
|
||||
"name": "File Systems"
|
||||
},
|
||||
{
|
||||
"id": "mcp-location",
|
||||
"name": "Location Services"
|
||||
},
|
||||
{
|
||||
"id": "mcp-calendar",
|
||||
"name": "Calendar Management"
|
||||
},
|
||||
{
|
||||
"id": "mcp-customer-data",
|
||||
"name": "Customer Data Platforms"
|
||||
},
|
||||
{
|
||||
"id": "mcp-ai-chatbot",
|
||||
"name": "AI Chatbot"
|
||||
},
|
||||
{
|
||||
"id": "mcp-virtualization",
|
||||
"name": "Virtualization"
|
||||
},
|
||||
{
|
||||
"id": "mcp-official-servers",
|
||||
"name": "Official Servers"
|
||||
},
|
||||
{
|
||||
"id": "mcp-database",
|
||||
"name": "Database"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -7,7 +7,7 @@ const path = require('path')
|
||||
// --- 配置路径 ---
|
||||
const agentsJsonPath = path.resolve(__dirname, '../data/agents.json')
|
||||
const outputDir = path.resolve(__dirname, '../data')
|
||||
const outputJsonPath = path.resolve(outputDir, 'list_assistant.json')
|
||||
const outputJsonPath = path.resolve(outputDir, 'store_list_assistant.json')
|
||||
|
||||
// --- 映射和默认值配置 ---
|
||||
const CATEGORY_ID_ASSISTANT = 'assistant'
|
||||
@ -31,15 +31,24 @@ const groupToSubcategoryMap = {
|
||||
设计: 'assistant-design',
|
||||
艺术: 'assistant-art',
|
||||
娱乐: 'assistant-entertainment',
|
||||
精选: 'assistant-general',
|
||||
生活: 'assistant-general',
|
||||
医疗: 'assistant-health',
|
||||
文案: 'assistant-writing',
|
||||
精选: 'assistant-featured',
|
||||
生活: 'assistant-life',
|
||||
医疗: 'assistant-medical',
|
||||
文案: 'assistant-copywriting',
|
||||
健康: 'assistant-health',
|
||||
点评: 'assistant-review',
|
||||
百科: 'assistant-knowledge',
|
||||
百科: 'assistant-encyclopedia',
|
||||
旅游: 'assistant-travel',
|
||||
翻译: 'assistant-language'
|
||||
翻译: 'assistant-translation',
|
||||
游戏: 'assistant-game',
|
||||
音乐: 'assistant-music',
|
||||
营销: 'assistant-marketing',
|
||||
科学: 'assistant-science',
|
||||
分析: 'assistant-analysis',
|
||||
法律: 'assistant-law',
|
||||
咨询: 'assistant-consulting',
|
||||
金融: 'assistant-finance',
|
||||
管理: 'assistant-management'
|
||||
}
|
||||
|
||||
// 从 agent.group 数组中获取 subcategoryId
|
||||
@ -106,12 +115,13 @@ try {
|
||||
categoryId: CATEGORY_ID_ASSISTANT, // 固定一级分类
|
||||
subcategoryId: subcategoryId, // 从 agent.group 动态获取
|
||||
author: 'Cherry Studio', // agent.author 可能不存在, 提供默认 'Cherry Studio'
|
||||
rating: parseFloat(agent.rating) || 4.0, // agent.rating 可能不存在, 提供默认 4.0
|
||||
downloads: '0',
|
||||
image: agent.emoji || '🤖', // 使用 agent.emoji (顶层), 若无则用默认
|
||||
icon: agent.emoji || '🤖', // 使用 agent.emoji (顶层), 若无则用默认
|
||||
image: '',
|
||||
tags: agentGroups, // 使用 agent.group (顶层) 作为 StoreItem.tags
|
||||
// 如果 group 含 "精选",则 isFeaturedByGroup 为 true。
|
||||
featured: isFeaturedByGroup || (typeof agent.featured === 'boolean' ? agent.featured : false)
|
||||
featured: isFeaturedByGroup,
|
||||
// assistant
|
||||
prompt: agent.prompt || ''
|
||||
}
|
||||
})
|
||||
.filter((item) => item !== null)
|
||||
196
resources/js/update_assistant_count.js
Normal file
196
resources/js/update_assistant_count.js
Normal file
@ -0,0 +1,196 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const assistantListPath = path.join(__dirname, '../data/store_list_assistant.json')
|
||||
const categoriesPath = path.join(__dirname, '../data/store_categories.json')
|
||||
|
||||
// REMOVED: groupToSubcategoryMap loading logic
|
||||
// const converAgentsPath = path.join(__dirname, '../js/conver_agents.json.js')
|
||||
// let groupToSubcategoryMap = {}
|
||||
// try {
|
||||
// // This is a simplified way to get the map.
|
||||
// // NOTE: This uses eval which is generally unsafe if the file content is not trusted.
|
||||
// // For a safer approach, you might need to parse the JS file content
|
||||
// // or export the map from conver_agents.json.js and require() it if it's a module.
|
||||
// const converAgentsContent = fs.readFileSync(converAgentsPath, 'utf-8')
|
||||
// const mapString = converAgentsContent.substring(
|
||||
// converAgentsContent.indexOf('{'),
|
||||
// converAgentsContent.lastIndexOf('}') + 1
|
||||
// )
|
||||
// if (mapString) {
|
||||
// groupToSubcategoryMap = eval('(' + mapString + ')') // Using eval, ensure the content is safe
|
||||
// console.log('Successfully loaded groupToSubcategoryMap.')
|
||||
// } else {
|
||||
// console.warn(
|
||||
// 'Could not extract groupToSubcategoryMap from conver_agents.json.js. ID generation for new items might be affected.'
|
||||
// )
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error('Error loading or parsing groupToSubcategoryMap from conver_agents.json.js:', error)
|
||||
// // Continue without the map if it fails, IDs will be generated as assistant-name
|
||||
// }
|
||||
|
||||
async function updateCounts() {
|
||||
let assistantItems
|
||||
let categoriesData // Declare categoriesData here alongside assistantItems
|
||||
|
||||
// Initialize tagCounts here, it will be populated after loading necessary data
|
||||
const tagCounts = {}
|
||||
|
||||
try {
|
||||
const assistantData = fs.readFileSync(assistantListPath, 'utf-8')
|
||||
assistantItems = JSON.parse(assistantData)
|
||||
console.log(`Successfully read ${path.basename(assistantListPath)}.`)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
console.error(`Error: ${assistantListPath} not found. Please check the path.`)
|
||||
} else if (error instanceof SyntaxError) {
|
||||
console.error(`Error: Could not decode JSON from ${assistantListPath}. Please check its format.`, error)
|
||||
} else {
|
||||
console.error(`An unexpected error occurred while processing ${assistantListPath}:`, error)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
const categoriesFileContent = fs.readFileSync(categoriesPath, 'utf-8')
|
||||
categoriesData = JSON.parse(categoriesFileContent)
|
||||
console.log(`Successfully read ${path.basename(categoriesPath)}.`)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
console.error(`Error: ${categoriesPath} not found. Please check the path.`)
|
||||
} else if (error instanceof SyntaxError) {
|
||||
console.error(`Error: Could not decode JSON from ${categoriesPath}. Please check its format.`, error)
|
||||
} else {
|
||||
console.error(`An unexpected error occurred while processing ${categoriesPath}:`, error)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Determine all potential subcategory names
|
||||
const potentialSubcategoryNames = new Set()
|
||||
|
||||
// Add from existing subcategories in store_categories.json (if assistant category exists)
|
||||
const assistantCatFromData = categoriesData.find((c) => c.id === 'assistant')
|
||||
if (assistantCatFromData && assistantCatFromData.items && Array.isArray(assistantCatFromData.items)) {
|
||||
assistantCatFromData.items.forEach((subItem) => {
|
||||
if (subItem.name) {
|
||||
potentialSubcategoryNames.add(String(subItem.name).trim())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Add from unique tags in store_list_assistant.json
|
||||
// Ensure assistantItems is loaded and is an array before iterating
|
||||
if (assistantItems && Array.isArray(assistantItems)) {
|
||||
for (const item of assistantItems) {
|
||||
if (item.tags && Array.isArray(item.tags)) {
|
||||
for (const tag of item.tags) {
|
||||
const trimmedTag = String(tag).trim()
|
||||
if (trimmedTag) {
|
||||
potentialSubcategoryNames.add(trimmedTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize tagCounts for all potential subcategory names
|
||||
potentialSubcategoryNames.forEach((name) => {
|
||||
tagCounts[name] = 0
|
||||
})
|
||||
|
||||
// Calculate counts based on the new logic
|
||||
// Ensure assistantItems is loaded and is an array
|
||||
if (assistantItems && Array.isArray(assistantItems)) {
|
||||
for (const item of assistantItems) {
|
||||
const itemTitleLower = item.title?.toLowerCase() || ''
|
||||
const itemAuthorLower = item.author?.toLowerCase() || ''
|
||||
// Ensure item.tags is an array and tags are strings and trimmed, and filter out empty strings
|
||||
const currentItemTags =
|
||||
item.tags && Array.isArray(item.tags) ? item.tags.map((t) => String(t).trim()).filter(Boolean) : []
|
||||
|
||||
for (const subcategoryName of potentialSubcategoryNames) {
|
||||
const normalizedSubcategoryName = subcategoryName.toLowerCase() // For case-insensitive matching in title/author
|
||||
|
||||
let foundInItem = false
|
||||
|
||||
// Condition 1: Subcategory name is in item.tags (exact, case-sensitive match using original subcategoryName)
|
||||
if (currentItemTags.includes(subcategoryName)) {
|
||||
foundInItem = true
|
||||
}
|
||||
|
||||
// Condition 2: Subcategory name is in item.title (case-insensitive)
|
||||
if (!foundInItem && itemTitleLower && itemTitleLower.includes(normalizedSubcategoryName)) {
|
||||
foundInItem = true
|
||||
}
|
||||
|
||||
// Condition 3: Subcategory name is in item.author (case-insensitive)
|
||||
if (!foundInItem && itemAuthorLower && itemAuthorLower.includes(normalizedSubcategoryName)) {
|
||||
foundInItem = true
|
||||
}
|
||||
|
||||
if (foundInItem) {
|
||||
tagCounts[subcategoryName]++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('Tag counts calculated based on revised logic (OR condition).')
|
||||
// console.log("Tag counts:", tagCounts); // For debugging
|
||||
|
||||
let assistantCategoryFound = false
|
||||
for (const category of categoriesData) {
|
||||
if (category.id === 'assistant') {
|
||||
assistantCategoryFound = true
|
||||
console.log("Found 'assistant' category. Updating and adding subcategories...")
|
||||
if (!category.items || !Array.isArray(category.items)) {
|
||||
category.items = [] // Initialize if items array is missing or not an array
|
||||
console.warn(" Initialized 'items' array for 'assistant' category as it was missing or invalid.")
|
||||
}
|
||||
|
||||
const existingSubCategoryNames = new Set(category.items.map((subItem) => subItem.name))
|
||||
|
||||
// Update existing subcategories
|
||||
for (const subItem of category.items) {
|
||||
if (subItem.name) {
|
||||
// Match using original case name from categories.json with original case tag from tagCounts
|
||||
const count = tagCounts[subItem.name] || 0
|
||||
subItem.count = count
|
||||
console.log(` Updated count for existing '${subItem.name}': ${count}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Add new subcategories from tagCounts if they don't exist
|
||||
for (const tagName in tagCounts) {
|
||||
if (Object.prototype.hasOwnProperty.call(tagCounts, tagName) && !existingSubCategoryNames.has(tagName)) {
|
||||
const count = tagCounts[tagName]
|
||||
const subcategoryId = `assistant-${tagName.toLowerCase().replace(/\s+/g, '-')}`
|
||||
category.items.push({
|
||||
id: subcategoryId,
|
||||
name: tagName,
|
||||
count: count
|
||||
})
|
||||
console.log(` Added new subcategory '${tagName}' with id '${subcategoryId}' and count ${count}`)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!assistantCategoryFound) {
|
||||
console.warn("Warning: Category with id 'assistant' not found. Cannot update or add subcategories.")
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(categoriesPath, JSON.stringify(categoriesData, null, 2), 'utf-8')
|
||||
console.log(`Successfully updated and wrote back to ${path.basename(categoriesPath)}.`)
|
||||
} catch (error) {
|
||||
console.error(`Error writing updated data to ${path.basename(categoriesPath)}:`, error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('Script finished.')
|
||||
}
|
||||
|
||||
updateCounts()
|
||||
@ -1,48 +1,70 @@
|
||||
import { CherryStoreItem } from '@renderer/types/cherryStore'
|
||||
import { Badge } from '@renderer/ui/badge'
|
||||
import { Button } from '@renderer/ui/button'
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@renderer/ui/card'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@renderer/ui/card'
|
||||
import { BlurFade } from '@renderer/ui/third-party/BlurFade'
|
||||
import { Download, Star } from 'lucide-react'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { ItemDetailDialog } from './ItemDetailDialog'
|
||||
|
||||
export function GridView({ items }: { items: CherryStoreItem[] }) {
|
||||
const [selectedItemForDetail, setSelectedItemForDetail] = useState<CherryStoreItem | null>(null)
|
||||
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false)
|
||||
|
||||
const handleCardClick = (item: CherryStoreItem) => {
|
||||
setSelectedItemForDetail(item)
|
||||
setIsDetailDialogOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-4 ">
|
||||
{items.map((item) => (
|
||||
<BlurFade key={item.id} delay={0.25} inView className="overflow-hidden flex flex-col">
|
||||
<Card className="overflow-hidden p-0 flex flex-col">
|
||||
<CardHeader className="p-0">
|
||||
<div className="aspect-square w-full overflow-hidden bg-muted">
|
||||
<img
|
||||
src={item.image || '/placeholder.svg'} // Use placeholder if image missing
|
||||
alt={item.title}
|
||||
className="h-full w-full object-cover transition-transform hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 flex-grow">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<CardTitle className="line-clamp-1 text-base">{item.title}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{item.author}</p>
|
||||
<>
|
||||
<div className="columns-4 gap-4">
|
||||
{items.map((item) => (
|
||||
<BlurFade key={item.id} delay={0.2} inView className="mb-4 cursor-pointer ">
|
||||
<Card
|
||||
className="overflow-hidden hover:scale-105 transition-transform"
|
||||
onClick={() => handleCardClick(item)}>
|
||||
<CardHeader className="p-0">
|
||||
{item.icon ? (
|
||||
<div
|
||||
className="flex h-full w-full items-center justify-center text-4xl"
|
||||
role="img"
|
||||
aria-label={item.title}>
|
||||
{item.icon}
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn('w-full overflow-hidden bg-muted', 'aspect-square')}>
|
||||
<img
|
||||
src={item.image || '/placeholder.svg'}
|
||||
alt={item.title}
|
||||
className="h-full w-full object-cover transition-transform"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className={cn('px-4', 'min-h-[120px]', 'space-y-1')}>
|
||||
<CardTitle className="line-clamp-2 text-base">{item.title}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{item.author}</p>
|
||||
<div className="space-x-2">
|
||||
{item.tags.map((tag) => (
|
||||
<Badge key={tag} variant="outline">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<Badge variant="outline">{item.type}</Badge>
|
||||
</div>
|
||||
<p className="mt-2 line-clamp-3 text-sm text-muted-foreground">{item.description}</p>
|
||||
</CardContent>
|
||||
<CardFooter className="flex items-center justify-between p-4 pt-0">
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="h-3.5 w-3.5 fill-primary text-primary" />
|
||||
<span className="text-sm">{item.rating}</span>
|
||||
</div>
|
||||
<Button size="sm">
|
||||
<Download className="mr-2 h-3.5 w-3.5 text-white dark:text-black" />
|
||||
Install
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
<p className={cn('mt-2 text-sm text-muted-foreground', 'line-clamp-4 xl:line-clamp-7')}>
|
||||
{item.description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
<ItemDetailDialog
|
||||
item={selectedItemForDetail}
|
||||
isOpen={isDetailDialogOpen}
|
||||
onClose={() => setIsDetailDialogOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
157
src/renderer/src/pages/store/components/ItemDetailDialog.tsx
Normal file
157
src/renderer/src/pages/store/components/ItemDetailDialog.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { CherryStoreItem, CherryStoreType } from '@renderer/types/cherryStore'
|
||||
import { Badge } from '@renderer/ui/badge'
|
||||
import { Button } from '@renderer/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@renderer/ui/dialog'
|
||||
import { Download } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
export function ItemDetailDialog({
|
||||
item,
|
||||
isOpen,
|
||||
onClose
|
||||
}: {
|
||||
item: CherryStoreItem | null
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
if (!item) return null
|
||||
|
||||
const handleInstall = () => {
|
||||
switch (item.type) {
|
||||
case CherryStoreType.ASSISTANT: {
|
||||
const getAgentFromSystemAgent = (agent) => {
|
||||
return {
|
||||
prompt: agent.prompt,
|
||||
description: agent.description,
|
||||
emoji: agent.icon,
|
||||
name: agent.title,
|
||||
id: uuid(),
|
||||
topics: [],
|
||||
type: 'agent'
|
||||
}
|
||||
}
|
||||
|
||||
createAssistantFromAgent(getAgentFromSystemAgent(item) as unknown as Agent)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-h-[90vh] sm:max-w-4xl overflow-y-auto">
|
||||
<DialogHeader className="flex flex-row items-start justify-between">
|
||||
<div>
|
||||
<DialogTitle className="flex items-center text-xl space-x-1">
|
||||
{item.title}
|
||||
<Badge variant="outline" className="ml-2">
|
||||
{item.type}
|
||||
</Badge>
|
||||
{item.icon && <span className="text-2xl">{item.icon}</span>}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="mt-1 text-base">{item.description}</DialogDescription>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div className="md:col-span-2">
|
||||
<div className="rounded-lg border bg-card p-6 text-card-foreground shadow-sm mt-4">
|
||||
<ReactMarkdown>{item.prompt}</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
{/* {item.requirements && (
|
||||
<div className="mt-4 rounded-lg border bg-card p-6 text-card-foreground shadow-sm">
|
||||
<h3 className="mb-4 text-lg font-semibold">功能要求</h3>
|
||||
<ul className="space-y-2">
|
||||
{item.requirements.map((req: string, index: number) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<Check className="mr-2 mt-0.5 h-4 w-4 text-green-500" />
|
||||
<span>{req}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="overflow-hidden">
|
||||
{item.image && (
|
||||
<div className="aspect-square w-full rounded-lg border overflow-hidden bg-muted">
|
||||
<img src={item.image || '/placeholder.svg'} alt={item.title} className="h-full w-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
|
||||
{/* <div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">评分</span>
|
||||
<div className="flex items-center">
|
||||
<Star className="mr-1 h-4 w-4 fill-primary text-primary" />
|
||||
<span className="font-medium">{item.rating}</span>
|
||||
<span className="ml-1 text-xs text-muted-foreground">({item.downloads})</span>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span className="text-sm font-medium">版本</span>
|
||||
{/* <span>{item.version}</span> */}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span className="text-sm font-medium">更新日期</span>
|
||||
{/* <span>{item.lastUpdated}</span> */}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span className="text-sm font-medium">开发者</span>
|
||||
<span>{item.author}</span>
|
||||
</div>
|
||||
|
||||
{/* {item.type !== CherryStoreType.ASSISTANT && ( */}
|
||||
<div className="mt-4">
|
||||
<Button className="w-full" onClick={handleInstall}>
|
||||
<Download className="mr-2 h-4 w-4 text-white! dark:text-black!" />
|
||||
添加到助手
|
||||
</Button>
|
||||
</div>
|
||||
{/* )} */}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm space-y-2">
|
||||
<h3 className="mb-2 text-sm font-medium">标签</h3>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{item.tags.map((tag: string) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<h3 className="mb-2 text-sm font-medium">分类</h3>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{item.type}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <DialogFooter className="mt-6 flex sm:justify-between">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button>
|
||||
<Download className="mr-2 h-4 w-4 text-white! dark:text-black!" />
|
||||
添加到助手
|
||||
</Button>
|
||||
</DialogFooter> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@ -1,54 +1,83 @@
|
||||
import { CherryStoreItem } from '@renderer/types/cherryStore'
|
||||
import { Badge } from '@renderer/ui/badge'
|
||||
import { Button } from '@renderer/ui/button'
|
||||
import { Card } from '@renderer/ui/card'
|
||||
import { Download, Star } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { ItemDetailDialog } from './ItemDetailDialog'
|
||||
|
||||
export function ListView({ items }: { items: CherryStoreItem[] }) {
|
||||
const [selectedItemForDetail, setSelectedItemForDetail] = useState<CherryStoreItem | null>(null)
|
||||
const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false)
|
||||
|
||||
const handleCardClick = (item: CherryStoreItem) => {
|
||||
setSelectedItemForDetail(item)
|
||||
setIsDetailDialogOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{items.map((item) => (
|
||||
<Card key={item.id} className="p-0">
|
||||
<div className="flex flex-col sm:flex-row">
|
||||
<div className="h-24 w-24 shrink-0 overflow-hidden rounded-l-lg bg-muted sm:h-auto">
|
||||
<img
|
||||
src={item.image || '/placeholder.svg'} // Use placeholder if image missing
|
||||
alt={item.title}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col justify-between p-4">
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold">{item.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{item.author}</p>
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{items.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
className="p-0 cursor-pointer hover:shadow-lg transition-shadow"
|
||||
onClick={() => handleCardClick(item)}>
|
||||
<div className="flex flex-col sm:flex-row">
|
||||
<div className="h-24 w-24 shrink-0 overflow-hidden rounded-l-lg bg-muted sm:h-auto flex items-center justify-center">
|
||||
{item.icon ? (
|
||||
<div
|
||||
className="flex h-full w-full items-center justify-center text-3xl"
|
||||
role="img"
|
||||
aria-label={item.title}>
|
||||
{item.icon}
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
src={item.image || '/placeholder.svg'} // Use placeholder if image missing
|
||||
alt={item.title}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col justify-between p-4">
|
||||
<div>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold">{item.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{item.author}</p>
|
||||
</div>
|
||||
<Badge variant="outline">{item.type}</Badge>
|
||||
</div>
|
||||
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">{item.description}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{item.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<Badge variant="outline">{item.type}</Badge>
|
||||
</div>
|
||||
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">{item.description}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{item.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center justify-between border-t p-4 sm:w-48 sm:flex-col sm:items-end sm:justify-center sm:border-l sm:border-t-0 sm:p-4"> */}
|
||||
{/* <div className="flex items-center gap-1">
|
||||
<Star className="h-3.5 w-3.5 fill-primary text-primary" />
|
||||
<span className="text-sm">{item?.rating}</span>
|
||||
<span className="text-xs text-muted-foreground">({item?.downloads})</span>
|
||||
</div> */}
|
||||
{/* <Button size="sm" className="mt-2">
|
||||
<Download className="mr-2 h-3.5 w-3.5 text-white! dark:text-black!" />
|
||||
Install
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t p-4 sm:w-48 sm:flex-col sm:items-end sm:justify-center sm:border-l sm:border-t-0 sm:p-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="h-3.5 w-3.5 fill-primary text-primary" />
|
||||
<span className="text-sm">{item.rating}</span>
|
||||
<span className="text-xs text-muted-foreground">({item.downloads})</span>
|
||||
</div>
|
||||
<Button size="sm" className="mt-2">
|
||||
<Download className="mr-2 h-3.5 w-3.5 text-white dark:text-black" />
|
||||
Install
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<ItemDetailDialog
|
||||
item={selectedItemForDetail}
|
||||
isOpen={isDetailDialogOpen}
|
||||
onClose={() => setIsDetailDialogOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,22 +1,13 @@
|
||||
import { Category, SubCategoryItem } from '@renderer/types/cherryStore'
|
||||
import { Badge } from '@renderer/ui/badge'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem
|
||||
} from '@renderer/ui/sidebar'
|
||||
|
||||
import { Category } from '../data'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@renderer/ui/collapsible'
|
||||
import { Sidebar, SidebarContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@renderer/ui/sidebar'
|
||||
|
||||
interface StoreSidebarProps {
|
||||
categories: Category[]
|
||||
selectedCategory: string
|
||||
selectedSubcategory: string
|
||||
onSelectCategory: (categoryId: string, subcategoryId: string) => void
|
||||
onSelectCategory: (categoryId: string, subcategoryId: string, row: SubCategoryItem) => void
|
||||
}
|
||||
|
||||
export function StoreSidebar({
|
||||
@ -38,32 +29,46 @@ export function StoreSidebar({
|
||||
return (
|
||||
<Sidebar className="absolute left-0 top-0 h-full border-r">
|
||||
<SidebarContent>
|
||||
{categories.map((category) => (
|
||||
<SidebarGroup key={category.id}>
|
||||
{category.id !== 'all' && <SidebarGroupLabel>{category.title}</SidebarGroupLabel>}
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{category.items.map((item) => (
|
||||
<SidebarMenuItem key={item.id}>
|
||||
<SidebarMenuButton
|
||||
isActive={category.id === selectedCategory && item.id === selectedSubcategory}
|
||||
className="justify-between"
|
||||
onClick={() => {
|
||||
onSelectCategory(category.id, item.id)
|
||||
}}>
|
||||
{item.name}
|
||||
{typeof item.count === 'number' && (
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
{item.count}
|
||||
</Badge>
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
))}
|
||||
<SidebarMenu className="gap-0">
|
||||
{categories.map((category, index) => (
|
||||
<Collapsible key={category.id} defaultOpen={index === 0} className="group/collapsible w-full">
|
||||
<SidebarMenuItem className="w-full px-0 py-0">
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
variant="outline"
|
||||
className="rounded-none hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none">
|
||||
<span className="truncate">{category.title}</span>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="overflow-hidden text-sm transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
|
||||
<SidebarMenu className="py-1 pl-4 pr-1">
|
||||
{category.items.map((subItem) => (
|
||||
<SidebarMenuItem key={subItem.id}>
|
||||
<SidebarMenuButton
|
||||
isActive={category.id === selectedCategory && subItem.id === selectedSubcategory}
|
||||
className="w-full justify-between"
|
||||
onClick={() => {
|
||||
onSelectCategory(category.id, subItem.id, subItem)
|
||||
}}
|
||||
size="sm">
|
||||
<span className="truncate">{subItem.name}</span>
|
||||
{typeof subItem.count === 'number' && (
|
||||
<Badge variant="secondary" className="ml-auto shrink-0">
|
||||
{subItem.count}
|
||||
</Badge>
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
{category.items.length === 0 && (
|
||||
<SidebarMenuItem className="px-3 py-1.5 text-xs text-muted-foreground">No items</SidebarMenuItem>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</CollapsibleContent>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
)
|
||||
|
||||
@ -1,20 +1,5 @@
|
||||
import store from '@renderer/store'
|
||||
import { CherryStoreItem } from '@renderer/types/cherryStore'
|
||||
|
||||
// 定义 Category 和 SubCategory 的类型 (基于您 store_categories.json 的结构)
|
||||
// 您可能已经在别处定义了这些类型,如果是,请使用它们。
|
||||
export interface SubCategoryItem {
|
||||
id: string
|
||||
name: string
|
||||
count?: number // count 是可选的,因为并非所有二级分类都有
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: string
|
||||
title: string
|
||||
items: SubCategoryItem[]
|
||||
}
|
||||
import { Category, CherryStoreItem } from '@renderer/types/cherryStore'
|
||||
|
||||
// 移除 LoadedStoreData 和 LoadedStoreDataByType,因为我们将按需加载
|
||||
// export interface LoadedStoreData {
|
||||
@ -126,16 +111,10 @@ export async function loadAndFilterItems(
|
||||
|
||||
let filteredItems = items
|
||||
|
||||
if (subcategoryId) {
|
||||
filteredItems = filteredItems.filter((item) => {
|
||||
return item.subcategoryId === subcategoryId
|
||||
})
|
||||
}
|
||||
|
||||
if (searchQuery) {
|
||||
if (searchQuery || subcategoryId) {
|
||||
const query = searchQuery.toLowerCase()
|
||||
filteredItems = filteredItems.filter((item) => {
|
||||
const searchableText = `${item.title.toLowerCase()} ${item.description?.toLowerCase() || ''} ${item.author?.toLowerCase() || ''} ${item.tags?.join(' ')?.toLowerCase() || ''}`
|
||||
const searchableText = `${item.subcategoryId} ${item.title.toLowerCase()} ${item.author?.toLowerCase() || ''} ${item.tags?.join(' ')?.toLowerCase() || ''}`
|
||||
return searchableText.includes(query)
|
||||
})
|
||||
}
|
||||
@ -144,25 +123,3 @@ export async function loadAndFilterItems(
|
||||
)
|
||||
return filteredItems
|
||||
}
|
||||
|
||||
// 原始的 loadStoreDataSeparately 现在可以移除或重构
|
||||
// 如果还需要一次性加载所有数据(例如用于初始全局搜索或特定情况),可以保留并调整
|
||||
// 但根据需求,我们现在倾向于按需加载
|
||||
/*
|
||||
export async function loadStoreDataSeparately(): Promise<LoadedStoreDataByType> {
|
||||
const categories = (await readFileSafe<Category[]>(getResourcesPath('/data/store_categories.json'))) || []
|
||||
// 示例: 不再默认加载所有类型的 items
|
||||
// const assistantItems = await readFileSafe<CherryStoreItem[]>(getResourcesPath('/data/store_list_assistant.json'))
|
||||
|
||||
console.log('Store data loaded (categories only by default):', {
|
||||
categoriesCount: categories.length,
|
||||
})
|
||||
|
||||
return {
|
||||
categories,
|
||||
// assistantItems: undefined, // Items 会按需加载
|
||||
// knowledgeItems: undefined,
|
||||
// mcpServerItems: undefined,
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { CherryStoreItem } from '@renderer/types/cherryStore'
|
||||
import { Category, CherryStoreItem, SubCategoryItem } from '@renderer/types/cherryStore'
|
||||
import { SidebarProvider } from '@renderer/ui/sidebar'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { StoreContent } from './components/StoreContent'
|
||||
import { StoreSidebar } from './components/StoreSidebar'
|
||||
import { Category, loadAndFilterItems, loadCategories } from './data'
|
||||
import { loadAndFilterItems, loadCategories } from './data'
|
||||
export default function StoreLayout() {
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
@ -31,6 +31,7 @@ export default function StoreLayout() {
|
||||
setCategories(loadedCategories)
|
||||
if (loadedCategories.length > 0 && !selectedCategory) {
|
||||
setSelectedCategory(loadedCategories[0].id)
|
||||
setSelectedSubcategory(loadedCategories[0].items[0].id)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in StoreLayout fetchCategories:', err)
|
||||
@ -39,7 +40,7 @@ export default function StoreLayout() {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
fetchCategories()
|
||||
resourcesPath && fetchCategories()
|
||||
}, [resourcesPath])
|
||||
|
||||
useEffect(() => {
|
||||
@ -66,9 +67,10 @@ export default function StoreLayout() {
|
||||
fetchItems()
|
||||
}, [selectedCategory, selectedSubcategory, searchQuery])
|
||||
|
||||
const handleSelectCategory = (categoryId: string, subcategoryId: string) => {
|
||||
const handleSelectCategory = (categoryId: string, subcategoryId: string, row: SubCategoryItem) => {
|
||||
setSelectedCategory(categoryId)
|
||||
setSelectedSubcategory(subcategoryId)
|
||||
setSearchQuery(row.name)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
@ -1,14 +1,43 @@
|
||||
export interface CherryStoreItem {
|
||||
export enum CherryStoreType {
|
||||
ASSISTANT = 'Assistant',
|
||||
MINI_APP = 'Mini-App',
|
||||
KNOWLEDGE = 'Knowledge',
|
||||
MCP_SERVER = 'MCP-Server',
|
||||
MODEL_PROVIDER = 'Model-Provider',
|
||||
AGENT = 'Agent'
|
||||
}
|
||||
export interface CherryStoreBaseItem {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
type: string
|
||||
categoryId: string
|
||||
subcategoryId: string
|
||||
author: string
|
||||
rating: number
|
||||
downloads: string
|
||||
image: string
|
||||
tags: string[]
|
||||
featured?: boolean
|
||||
// rating: number
|
||||
// downloads: string
|
||||
// featured: boolean
|
||||
// requirements: string[]
|
||||
}
|
||||
|
||||
export interface SubCategoryItem {
|
||||
id: string
|
||||
name: string
|
||||
count?: number // count 是可选的,因为并非所有二级分类都有
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: string
|
||||
title: string
|
||||
items: SubCategoryItem[]
|
||||
}
|
||||
|
||||
export interface AssistantItem extends CherryStoreBaseItem {
|
||||
type: CherryStoreType.ASSISTANT
|
||||
icon?: string
|
||||
prompt?: string
|
||||
}
|
||||
|
||||
export type CherryStoreItem = AssistantItem
|
||||
|
||||
31
src/renderer/src/ui/collapsible.tsx
Normal file
31
src/renderer/src/ui/collapsible.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
133
src/renderer/src/ui/dialog.tsx
Normal file
133
src/renderer/src/ui/dialog.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@renderer/utils/index"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
27
yarn.lock
27
yarn.lock
@ -3076,6 +3076,32 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-collapsible@npm:^1.1.10":
|
||||
version: 1.1.10
|
||||
resolution: "@radix-ui/react-collapsible@npm:1.1.10"
|
||||
dependencies:
|
||||
"@radix-ui/primitive": "npm:1.1.2"
|
||||
"@radix-ui/react-compose-refs": "npm:1.1.2"
|
||||
"@radix-ui/react-context": "npm:1.1.2"
|
||||
"@radix-ui/react-id": "npm:1.1.1"
|
||||
"@radix-ui/react-presence": "npm:1.1.4"
|
||||
"@radix-ui/react-primitive": "npm:2.1.2"
|
||||
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
|
||||
"@radix-ui/react-use-layout-effect": "npm:1.1.1"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
"@types/react-dom": "*"
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
"@types/react-dom":
|
||||
optional: true
|
||||
checksum: 10c0/d806b7da926ba6bf70a42d643b716bcc167502c6b8be115047bd40a1cbd20975869ee824d9406d906ef0b7b069a426a2b57021ff188c3233f592ed9477ad1e48
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-collection@npm:1.1.6":
|
||||
version: 1.1.6
|
||||
resolution: "@radix-ui/react-collection@npm:1.1.6"
|
||||
@ -5231,6 +5257,7 @@ __metadata:
|
||||
"@modelcontextprotocol/sdk": "npm:^1.10.2"
|
||||
"@mozilla/readability": "npm:^0.6.0"
|
||||
"@notionhq/client": "npm:^2.2.15"
|
||||
"@radix-ui/react-collapsible": "npm:^1.1.10"
|
||||
"@radix-ui/react-dialog": "npm:^1.1.13"
|
||||
"@radix-ui/react-dropdown-menu": "npm:^2.1.14"
|
||||
"@radix-ui/react-separator": "npm:^1.1.6"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user