diff --git a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx index 35041676..e74147c8 100644 --- a/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-market/PluginMarketComponent.tsx @@ -14,7 +14,6 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { Search, Loader2, Wrench, AudioWaveform, Hash } from 'lucide-react'; import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent'; import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO'; -import PluginDetailDialog from './plugin-detail-dialog/PluginDetailDialog'; import { getCloudServiceClientSync } from '@/app/infra/http'; import { useTranslation } from 'react-i18next'; import { PluginV4 } from '@/app/infra/entities/plugin'; @@ -48,15 +47,6 @@ function MarketPageContent({ const [total, setTotal] = useState(0); const [sortOption, setSortOption] = useState('install_count_desc'); - // Plugin detail dialog state - const [selectedPluginAuthor, setSelectedPluginAuthor] = useState< - string | null - >(null); - const [selectedPluginName, setSelectedPluginName] = useState( - null, - ); - const [dialogOpen, setDialogOpen] = useState(false); - const pageSize = 16; // 每页16个,4行x4列 const searchTimeoutRef = useRef(null); const scrollContainerRef = useRef(null); @@ -230,33 +220,46 @@ function MarketPageContent({ fetchPlugins(1, !!searchQuery.trim(), true); }, [sortOption, componentFilter]); - // 处理URL参数,检查是否需要打开插件详情对话框 + // 处理URL参数,重定向到 LangBot Space useEffect(() => { const author = searchParams.get('author'); const pluginName = searchParams.get('plugin'); if (author && pluginName) { - setSelectedPluginAuthor(author); - setSelectedPluginName(pluginName); - setDialogOpen(true); + const detailUrl = `https://space.langbot.app/market/${author}/${pluginName}`; + window.open(detailUrl, '_blank'); } }, [searchParams]); - // 插件详情对话框处理函数 - const handlePluginClick = useCallback( - (author: string, pluginName: string) => { - setSelectedPluginAuthor(author); - setSelectedPluginName(pluginName); - setDialogOpen(true); - }, - [], - ); + // 处理安装插件 + const handleInstallPlugin = useCallback( + async (author: string, pluginName: string) => { + try { + // Find the full plugin object from the list + const pluginVO = plugins.find( + (p) => p.author === author && p.pluginName === pluginName, + ); + if (!pluginVO) { + console.error('Plugin not found:', author, pluginName); + return; + } - const handleDialogClose = useCallback(() => { - setDialogOpen(false); - setSelectedPluginAuthor(null); - setSelectedPluginName(null); - }, []); + // Fetch full plugin details to get PluginV4 object + const response = await getCloudServiceClientSync().getPluginDetail( + author, + pluginName, + ); + const pluginV4: PluginV4 = response.plugin; + + // Call the install function passed from parent + installPlugin(pluginV4); + } catch (error) { + console.error('Failed to install plugin:', error); + toast.error(t('market.installFailed')); + } + }, + [plugins, installPlugin, t], + ); // 清理定时器 useEffect(() => { @@ -459,7 +462,7 @@ function MarketPageContent({ ))} @@ -490,15 +493,6 @@ function MarketPageContent({ )} - - {/* Plugin detail dialog */} - ); } diff --git a/web/src/app/home/plugins/components/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx b/web/src/app/home/plugins/components/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx deleted file mode 100644 index 454b27e8..00000000 --- a/web/src/app/home/plugins/components/plugin-market/plugin-detail-dialog/PluginDetailDialog.tsx +++ /dev/null @@ -1,417 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Loader2, Download, Users } from 'lucide-react'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; -import { PluginV4 } from '@/app/infra/entities/plugin'; -import { getCloudServiceClientSync } from '@/app/infra/http'; -import { extractI18nObject, getAPILanguageCode } from '@/i18n/I18nProvider'; -import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList'; - -interface PluginDetailDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - author: string | null; - pluginName: string | null; - installPlugin: (plugin: PluginV4) => void; -} - -export default function PluginDetailDialog({ - open, - onOpenChange, - author, - pluginName, - installPlugin, -}: PluginDetailDialogProps) { - const { t } = useTranslation(); - const [plugin, setPlugin] = useState(null); - const [readme, setReadme] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [isLoadingReadme, setIsLoadingReadme] = useState(false); - - // 获取插件详情和README - useEffect(() => { - if (open && author && pluginName) { - fetchPluginData(); - } - }, [open, author, pluginName]); - - const fetchPluginData = async () => { - if (!author || !pluginName) return; - - setIsLoading(true); - try { - // 获取插件详情 - const detailResponse = await getCloudServiceClientSync().getPluginDetail( - author, - pluginName, - ); - setPlugin(detailResponse.plugin); - - // 获取README,根据当前语言设置传递language参数 - setIsLoadingReadme(true); - try { - const languageCode = getAPILanguageCode(); - const readmeResponse = - await getCloudServiceClientSync().getPluginREADME( - author, - pluginName, - languageCode, - ); - setReadme(readmeResponse.readme); - } catch (error) { - console.warn('Failed to load README:', error); - setReadme(t('market.noReadme')); - } finally { - setIsLoadingReadme(false); - } - } catch (error) { - console.error('Failed to fetch plugin details:', error); - toast.error(t('market.loadFailed')); - onOpenChange(false); - } finally { - setIsLoading(false); - } - }; - - if (!open) return null; - - const PluginHeader = () => ( -
- {plugin!.name} -
-

- {extractI18nObject(plugin!.label) || plugin!.name} -

-
- - - {plugin!.author} / {plugin!.name} - -
-
- - v{plugin!.latest_version} - - - - {plugin!.install_count.toLocaleString()} {t('market.downloads')} - - {plugin!.components && Object.keys(plugin!.components).length > 0 && ( - - )} - {plugin!.repository && ( - - )} -
-
-
- ); - - const PluginDescription = () => ( -
-

- {extractI18nObject(plugin!.description) || t('market.noDescription')} -

-
- ); - - const PluginOptions = () => ( -
- -
- ); - - const ReadmeContent = () => ( -
- ( -
- - - ), - thead: ({ ...props }) => ( - - ), - tbody: ({ ...props }) => ( - - ), - th: ({ ...props }) => ( - - ), - // 删除线支持 - del: ({ ...props }) => ( - - ), - // Todo 列表支持 - input: ({ type, checked, ...props }) => { - if (type === 'checkbox') { - return ( - - ); - } - return ; - }, - ul: ({ ...props }) => ( -
    - ), - ol: ({ ...props }) => ( -
      - ), - li: ({ ...props }) =>
    1. , - h1: ({ ...props }) => ( -

      - ), - h2: ({ ...props }) => ( -

      - ), - h3: ({ ...props }) => ( -

      - ), - h4: ({ ...props }) => ( -

      - ), - h5: ({ ...props }) => ( -

      - ), - h6: ({ ...props }) => ( -
      - ), - p: ({ ...props }) => ( -

      - ), - code: ({ className, children, ...props }) => { - const match = /language-(\w+)/.exec(className || ''); - const isCodeBlock = match ? true : false; - - // 如果是代码块(有语言标识),由 pre 标签处理样式,淡灰色底,黑色字 - if (isCodeBlock) { - return ( - - {children} - - ); - } - - // 内联代码样式 - 淡灰色底 - return ( - - {children} - - ); - }, - pre: ({ ...props }) => ( -

      -          ),
      -          // 图片组件 - 转换本地路径为API路径
      -          img: ({ src, alt, ...props }) => {
      -            // 处理图片路径
      -            let imageSrc = src || '';
      -
      -            // 确保 src 是字符串类型
      -            if (typeof imageSrc !== 'string') {
      -              return (
      -                {alt
      -              );
      -            }
      -
      -            // 如果是相对路径,转换为API路径
      -            if (
      -              imageSrc &&
      -              !imageSrc.startsWith('http://') &&
      -              !imageSrc.startsWith('https://') &&
      -              !imageSrc.startsWith('data:')
      -            ) {
      -              // 移除开头的 ./ 或 / (支持多个前缀)
      -              imageSrc = imageSrc.replace(/^(\.\/|\/)+/, '');
      -
      -              // 如果路径以 assets/ 开头,直接使用
      -              // 否则假设它在 assets/ 目录下
      -              if (!imageSrc.startsWith('assets/')) {
      -                imageSrc = `assets/${imageSrc}`;
      -              }
      -
      -              // 移除 assets/ 前缀以构建API URL
      -              const assetPath = imageSrc.replace(/^assets\//, '');
      -              imageSrc = getCloudServiceClientSync().getPluginAssetURL(
      -                author!,
      -                pluginName!,
      -                assetPath,
      -              );
      -            }
      -
      -            return (
      -              {alt
      -            );
      -          },
      -        }}
      -      >
      -        {readme}
      -      
      -    
      -  );
      -
      -  return (
      -    
      -      
      -        {isLoading ? (
      -          
      - - {t('market.loading')} -
      - ) : plugin ? ( -
      - {/* 插件信息区域 */} -
      -
      -
      - - -
      -
      - -
      -
      -
      - - {/* README 区域 */} -
      -
      - {isLoadingReadme ? ( -
      - - - {t('market.loading')} - -
      - ) : ( - - )} -
      -
      -
      - ) : null} -
      -
      - ); -} diff --git a/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx b/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx index d6eee159..4e25c437 100644 --- a/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-market/plugin-market-card/PluginMarketCardComponent.tsx @@ -1,23 +1,39 @@ import { PluginMarketCardVO } from './PluginMarketCardVO'; import { useTranslation } from 'react-i18next'; import { Badge } from '@/components/ui/badge'; -import { Wrench, AudioWaveform, Hash } from 'lucide-react'; +import { + Wrench, + AudioWaveform, + Hash, + Download, + ExternalLink, +} from 'lucide-react'; +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; export default function PluginMarketCardComponent({ cardVO, - onPluginClick, + onInstall, }: { cardVO: PluginMarketCardVO; - onPluginClick?: (author: string, pluginName: string) => void; + onInstall?: (author: string, pluginName: string) => void; }) { const { t } = useTranslation(); + const [isHovered, setIsHovered] = useState(false); - function handleCardClick() { - if (onPluginClick) { - onPluginClick(cardVO.author, cardVO.pluginName); + function handleInstallClick(e: React.MouseEvent) { + e.stopPropagation(); + if (onInstall) { + onInstall(cardVO.author, cardVO.pluginName); } } + function handleViewDetailsClick(e: React.MouseEvent) { + e.stopPropagation(); + const detailUrl = `https://space.langbot.app/market/${cardVO.author}/${cardVO.pluginName}`; + window.open(detailUrl, '_blank'); + } + const kindIconMap: Record = { Tool: , EventListener: , @@ -32,8 +48,9 @@ export default function PluginMarketCardComponent({ return (
      setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} >
      {/* 上部分:插件信息 */} @@ -118,6 +135,27 @@ export default function PluginMarketCardComponent({ )}
      + + {/* Hover overlay with action buttons */} + {isHovered && ( +
      + + +
      + )} ); } diff --git a/web/src/app/infra/http/CloudServiceClient.ts b/web/src/app/infra/http/CloudServiceClient.ts index 1c0e9527..b6e8c964 100644 --- a/web/src/app/infra/http/CloudServiceClient.ts +++ b/web/src/app/infra/http/CloudServiceClient.ts @@ -82,6 +82,6 @@ export class CloudServiceClient extends BaseHttpClient { } public getPluginMarketplaceURL(author: string, name: string): string { - return `${this.baseURL}/market?author=${author}&plugin=${name}`; + return `https://space.langbot.app/market/${author}/${name}`; } } diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index 99171157..537fc1aa 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -354,6 +354,7 @@ const enUS = { filterByComponent: 'Component', allComponents: 'All Components', requestPlugin: 'Request Plugin', + viewDetails: 'View Details', }, mcp: { title: 'MCP', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 2c0fa752..8f2da1d3 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -356,6 +356,7 @@ const jaJP = { filterByComponent: 'コンポーネント', allComponents: '全部コンポーネント', requestPlugin: 'プラグインをリクエスト', + viewDetails: '詳細を表示', }, mcp: { title: 'MCP', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index bf6101ab..26508fbd 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -338,6 +338,7 @@ const zhHans = { filterByComponent: '组件', allComponents: '全部组件', requestPlugin: '请求插件', + viewDetails: '查看详情', }, mcp: { title: 'MCP', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 5a9e7819..93f4540f 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -336,6 +336,7 @@ const zhHant = { filterByComponent: '組件', allComponents: '全部組件', requestPlugin: '請求插件', + viewDetails: '查看詳情', }, mcp: { title: 'MCP',
- ), - td: ({ ...props }) => ( - - ), - tr: ({ ...props }) => ( -