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:
MyPrototypeWhat 2025-05-14 17:17:24 +08:00
parent 802402e922
commit c799f15fcc
15 changed files with 4320 additions and 3466 deletions

View File

@ -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",

View File

@ -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

View File

@ -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)

View 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()

View File

@ -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)}
/>
</>
)
}

View 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>
)
}

View File

@ -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)}
/>
</>
)
}

View File

@ -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>
)

View File

@ -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,
}
}
*/

View File

@ -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) {

View File

@ -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

View 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 }

View 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,
}

View File

@ -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"