import { useEffect, useMemo, useState } from 'react'; import axios from 'axios'; import { ChevronLeft, ChevronRight, Hammer, RefreshCw, Search, X } from 'lucide-react'; import { APP_ENDPOINTS } from '../../../config/env'; import type { BotSkillMarketItem } from '../../platform/types'; import { LucentIconButton } from '../../../components/lucent/LucentIconButton'; import { normalizePlatformPageSize, readCachedPlatformPageSize, writeCachedPlatformPageSize, } from '../../../utils/platformPageSize'; interface SkillMarketInstallModalProps { isZh: boolean; open: boolean; items: BotSkillMarketItem[]; loading: boolean; installingId: number | null; onClose: () => void; onRefresh: () => Promise | void; onInstall: (item: BotSkillMarketItem) => Promise | void; formatBytes: (bytes: number) => string; } export function SkillMarketInstallModal({ isZh, open, items, loading, installingId, onClose, onRefresh, onInstall, formatBytes, }: SkillMarketInstallModalProps) { const [search, setSearch] = useState(''); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(() => readCachedPlatformPageSize(10)); useEffect(() => { if (!open) return; setSearch(''); setPage(1); void onRefresh(); void (async () => { try { const res = await axios.get<{ page_size?: number }>(`${APP_ENDPOINTS.apiBase}/platform/settings`); const normalized = normalizePlatformPageSize(res.data?.page_size, readCachedPlatformPageSize(10)); writeCachedPlatformPageSize(normalized); setPageSize(normalized); } catch { setPageSize(readCachedPlatformPageSize(10)); } })(); }, [open]); useEffect(() => { setPage(1); }, [search, pageSize]); const filteredItems = useMemo(() => { const keyword = search.trim().toLowerCase(); if (!keyword) return items; return items.filter((item) => [item.display_name, item.skill_key, item.description, item.zip_filename].some((value) => String(value || '').toLowerCase().includes(keyword), ), ); }, [items, search]); const pageCount = Math.max(1, Math.ceil(filteredItems.length / pageSize)); const currentPage = Math.min(page, pageCount); const pagedItems = useMemo( () => filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize), [currentPage, filteredItems, pageSize], ); if (!open) return null; return (
event.stopPropagation()}>

{isZh ? '从市场安装技能' : 'Install From Marketplace'}

void onRefresh()} tooltip={isZh ? '刷新市场技能' : 'Refresh marketplace skills'} aria-label={isZh ? '刷新市场技能' : 'Refresh marketplace skills'} >
setSearch(event.target.value)} placeholder={isZh ? '搜索技能、标识或 ZIP 文件名...' : 'Search skills, keys, or ZIP filenames...'} aria-label={isZh ? '搜索技能市场' : 'Search skill marketplace'} />
{loading ? (
{isZh ? '正在读取技能市场...' : 'Loading marketplace skills...'}
) : pagedItems.length === 0 ? (
{filteredItems.length === 0 ? (isZh ? '没有匹配的技能。' : 'No matching skills found.') : (isZh ? '当前页没有技能。' : 'No skills on this page.')}
) : ( pagedItems.map((skill) => { const isInstalled = Boolean(skill.installed); const isInstalling = installingId === skill.id; return (

{skill.display_name || skill.skill_key}

{skill.skill_key}
{isInstalled ? (isZh ? '已安装' : 'Installed') : (isZh ? '未安装' : 'Not installed')}

{skill.description || (isZh ? '暂无简介。' : 'No description yet.')}

{isZh ? 'ZIP' : 'ZIP'}: {skill.zip_filename} {isZh ? '体积' : 'Size'}: {formatBytes(skill.zip_size_bytes)} {isZh ? '安装次数' : 'Installs'}: {skill.install_count}
{skill.install_error && !isInstalled ? (
{skill.install_error}
) : null}
{skill.zip_exists ? (isZh ? '市场包可用' : 'Package ready') : (isZh ? '市场包缺失' : 'Package missing')}
); }) )}
{isZh ? `第 ${currentPage} / ${pageCount} 页,共 ${filteredItems.length} 个技能` : `Page ${currentPage} / ${pageCount}, ${filteredItems.length} skills`}
setPage((value) => Math.max(1, value - 1))} tooltip={isZh ? '上一页' : 'Previous'} aria-label={isZh ? '上一页' : 'Previous'} > = pageCount} onClick={() => setPage((value) => Math.min(pageCount, value + 1))} tooltip={isZh ? '下一页' : 'Next'} aria-label={isZh ? '下一页' : 'Next'} >
); }