'use client'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { AlertCircle, ArrowLeft, Check, Download, Loader2, Search, Star } from 'lucide-react'; import { getSkillHubFile, getSkillHubDetail, getSkillHubVersion, getSkillHubVersions, installSkillHubSkill, searchSkillHubSkills, } from '@/lib/api'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { SkillDetailView } from '@/components/skills/SkillDetailView'; import type { SkillFileContent, SkillHubSearchItem, SkillHubVersionResponse, SkillVersionRef } from '@/types'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; type SortMode = 'relevance' | 'downloads' | 'newest'; function publishedVersion(skill: SkillHubSearchItem | null): string { return skill?.publishedVersion?.version || skill?.headlineVersion?.version || ''; } function readmeFromVersion(version: SkillHubVersionResponse | null): string { const raw = version?.detail?.parsedMetadataJson; if (!raw) return ''; try { const parsed = JSON.parse(raw); if (parsed && typeof parsed.body === 'string') { return parsed.body; } } catch { // keep empty fallback } return ''; } export default function MarketplacePage() { const { locale } = useAppI18n(); const t = useCallback((zh: string, en: string) => pickAppText(locale, zh, en), [locale]); const [query, setQuery] = useState(''); const [sort, setSort] = useState('newest'); const [starredOnly, setStarredOnly] = useState(false); const [page, setPage] = useState(0); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selected, setSelected] = useState(null); const [versionDetail, setVersionDetail] = useState(null); const [versions, setVersions] = useState([]); const [selectedVersion, setSelectedVersion] = useState(''); const [readmeContent, setReadmeContent] = useState(''); const [selectedFile, setSelectedFile] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [versionLoading, setVersionLoading] = useState(false); const [fileLoading, setFileLoading] = useState(false); const [installing, setInstalling] = useState(false); const load = useCallback(async () => { setLoading(true); setError(null); try { const result = await searchSkillHubSkills({ q: query, sort, page, size: 12 }); const nextItems = Array.isArray(result.items) ? result.items : []; setItems(starredOnly ? nextItems.filter((item) => (item.starCount || 0) > 0) : nextItems); setTotal(result.total || 0); } catch (err: any) { setError(err.message || t('加载 SkillHub 失败', 'Failed to load SkillHub')); } finally { setLoading(false); } }, [page, query, sort, starredOnly, t]); useEffect(() => { void load(); }, [load]); const openDetail = async (item: SkillHubSearchItem) => { setSelected(item); setVersionDetail(null); setVersions([]); setSelectedVersion(''); setReadmeContent(''); setSelectedFile(null); setDetailLoading(true); setError(null); try { const detail = await getSkillHubDetail(item.namespace, item.slug); setSelected(detail); const version = publishedVersion(detail); const versionList = await getSkillHubVersions(detail.namespace, detail.slug).catch(() => ({ items: version ? [{ version, status: detail.publishedVersion?.status || detail.headlineVersion?.status }] : [], total: version ? 1 : 0, page: 0, size: 1, })); setVersions(versionList.items || []); if (version) { await loadVersion(detail, version); } } catch (err: any) { setError(err.message || t('加载技能详情失败', 'Failed to load skill details')); } finally { setDetailLoading(false); } }; const loadVersion = async (skill: SkillHubSearchItem, version: string) => { setVersionLoading(true); setSelectedVersion(version); setSelectedFile(null); try { const nextVersion = await getSkillHubVersion(skill.namespace, skill.slug, version); setVersionDetail(nextVersion); const readme = await getSkillHubFile(skill.namespace, skill.slug, version, 'SKILL.md') .then((file) => file.content || '') .catch(() => readmeFromVersion(nextVersion)); setReadmeContent(readme); } finally { setVersionLoading(false); } }; const openVersion = async (version: string) => { if (!selected || selectedVersion === version) return; setError(null); try { await loadVersion(selected, version); } catch (err: any) { setError(err.message || t('加载技能版本失败', 'Failed to load skill version')); } }; const openFile = async (filePath: string) => { if (!selected || !selectedVersion) return; setFileLoading(true); setError(null); try { setSelectedFile(await getSkillHubFile(selected.namespace, selected.slug, selectedVersion, filePath)); } catch (err: any) { setError(err.message || t('加载文件失败', 'Failed to load file')); } finally { setFileLoading(false); } }; const installSelected = async () => { if (!selected) return; setInstalling(true); setError(null); try { const result = await installSkillHubSkill(selected.namespace, selected.slug, selectedVersion || publishedVersion(selected)); setSelected({ ...selected, installed: true, installed_version: result.version }); await load(); } catch (err: any) { setError(err.message || t('安装技能失败', 'Failed to install skill')); } finally { setInstalling(false); } }; const totalPages = useMemo(() => Math.max(1, Math.ceil(total / 12)), [total]); return (

{t('市场', 'Marketplace')}

{t('搜索、查看并安装 SkillHub 技能。', 'Search, inspect, and install SkillHub skills.')}

{ event.preventDefault(); setPage(0); void load(); }} >
setQuery(event.target.value)} placeholder={t('搜索技能...', 'Search skills...')} className="h-14 rounded-2xl pl-12 text-base" />
{error && ( {error} )} {selected ? (
{detailLoading ? ( ) : ( 0 ? versions : [{ version: selectedVersion || publishedVersion(selected) }]} files={versionDetail?.files || []} content={readmeContent || readmeFromVersion(versionDetail)} selectedFile={selectedFile} loadingFile={fileLoading} loadingVersion={versionLoading} onSelectVersion={(version) => void openVersion(version)} onOpenFile={(filePath) => void openFile(filePath)} badges={ <> @{selected.namespace} {t('下载', 'Downloads')}: {selected.downloadCount || 0} {t('收藏', 'Stars')}: {selected.starCount || 0} {selected.installed && ( {t('已安装', 'Installed')} )} } actions={ } labels={{ overview: t('说明', 'Overview'), files: t('文件', 'Files'), versions: t('版本', 'Versions'), noReadme: t('暂无说明', 'No overview available'), noFiles: t('暂无文件', 'No files'), selectFile: t('选择一个文件查看详情', 'Select a file to view details'), binaryFile: t('二进制文件暂不预览', 'Binary file preview is not available'), current: t('当前', 'Current'), size: t('大小', 'Size'), }} /> )}
) : (
{t('排序:', 'Sort:')} {([ ['relevance', t('相关性', 'Relevance')], ['downloads', t('下载量', 'Downloads')], ['newest', t('最新', 'Newest')], ] as Array<[SortMode, string]>).map(([value, label]) => ( ))} {t('筛选:', 'Filter:')}
{loading ? (
) : (
{items.map((item) => ( ))}
)}
{page + 1} / {totalPages}
)}
); }