From 9d7cdaf5368cee3564f4570008de1e070af26cb7 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Fri, 8 Aug 2025 15:01:04 +0800 Subject: [PATCH 01/28] feat: add testnet faucet button --- src/assets/favicon_io/site.webmanifest | 20 ++- .../wallet/components/balance-card.tsx | 126 +++++++++++++----- .../components/testnet-faucet-dialog.tsx | 51 +++++++ 3 files changed, 164 insertions(+), 33 deletions(-) create mode 100644 src/features/wallet/components/testnet-faucet-dialog.tsx diff --git a/src/assets/favicon_io/site.webmanifest b/src/assets/favicon_io/site.webmanifest index 45dc8a20..fa99de77 100644 --- a/src/assets/favicon_io/site.webmanifest +++ b/src/assets/favicon_io/site.webmanifest @@ -1 +1,19 @@ -{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/src/features/wallet/components/balance-card.tsx b/src/features/wallet/components/balance-card.tsx index 892543e3..72957094 100644 --- a/src/features/wallet/components/balance-card.tsx +++ b/src/features/wallet/components/balance-card.tsx @@ -1,3 +1,5 @@ +import { CircleDollarSign, WalletIcon } from 'lucide-react'; +import { useState } from 'react'; import { Button } from '@/shared/components/ui/button'; import { Card, @@ -8,6 +10,7 @@ import { import { useDevMode } from '@/shared/hooks/use-dev-mode'; import { useNuwaToUsdRate } from '../hooks/use-nuwa-to-usd-rate'; import { useWallet } from '../hooks/use-wallet'; +import { TestnetFaucetDialog } from './testnet-faucet-dialog'; interface BalanceCardProps { onTopUp: () => void; @@ -17,48 +20,107 @@ export function BalanceCard({ onTopUp }: BalanceCardProps) { const { balance } = useWallet(); const nuwaToUsdRate = useNuwaToUsdRate(); const isDevMode = useDevMode(); + const [showFaucetDialog, setShowFaucetDialog] = useState(false); const nuwaValue = balance.toLocaleString(); const usdValue = (balance / nuwaToUsdRate).toFixed(6); return ( - - - Balance - - -
-
-
{`$${usdValue} USD`}
-

- {`${nuwaValue} $NUWA`} -

+ <> + + {/* Decorative background elements */} +
+
+ + +
+
+
+ +
+
+ Balance +

Testnet

+
+
+
+ +
-
- - {isDevMode && ( - <> - + - - - )} + 提现 + +
+ )} */}
-
- -
+ + + + + ); } diff --git a/src/features/wallet/components/testnet-faucet-dialog.tsx b/src/features/wallet/components/testnet-faucet-dialog.tsx new file mode 100644 index 00000000..f4f2caf3 --- /dev/null +++ b/src/features/wallet/components/testnet-faucet-dialog.tsx @@ -0,0 +1,51 @@ +import { MessageCircle } from 'lucide-react'; +import { Button } from '@/shared/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/shared/components/ui/dialog'; + +interface TestnetFaucetDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function TestnetFaucetDialog({ + open, + onOpenChange, +}: TestnetFaucetDialogProps) { + const handleDiscordClick = () => { + // Open Discord link + window.open('https://discord.gg/nuwa', '_blank'); + }; + + return ( + + + + + Need more testnet balance? + + + Join Nuwa AI Discord to get free testnet balance + + + + + + + + + ); +} From c6e2c993b66373539d58b9e691fb6d3c7174dd9b Mon Sep 17 00:00:00 2001 From: Mine77 Date: Fri, 8 Aug 2025 15:01:41 +0800 Subject: [PATCH 02/28] feat: campaign page init --- .../components/campaign-stats-card.tsx | 79 +++++ .../components/campaign-task-detail.tsx | 301 ++++++++++++++++++ .../campaigns/components/campaigns.tsx | 201 ++++++++++++ src/features/campaigns/components/index.ts | 3 + src/features/campaigns/hooks/index.ts | 40 +++ src/features/campaigns/services.ts | 137 ++++++++ src/features/campaigns/stores.ts | 79 +++++ src/features/campaigns/types.ts | 27 ++ .../chat/components/markdown-mermaid.tsx | 3 +- src/features/chat/components/markdown.tsx | 3 +- .../components/app-sidebar-content.tsx | 9 +- src/pages/campaign-task-detail.tsx | 5 + src/pages/campaigns.tsx | 5 + src/router.tsx | 4 + src/shared/components/ui/button.tsx | 2 + src/shared/locales/cn.ts | 1 + src/shared/locales/en.ts | 1 + 17 files changed, 897 insertions(+), 3 deletions(-) create mode 100644 src/features/campaigns/components/campaign-stats-card.tsx create mode 100644 src/features/campaigns/components/campaign-task-detail.tsx create mode 100644 src/features/campaigns/components/campaigns.tsx create mode 100644 src/features/campaigns/components/index.ts create mode 100644 src/features/campaigns/hooks/index.ts create mode 100644 src/features/campaigns/services.ts create mode 100644 src/features/campaigns/stores.ts create mode 100644 src/features/campaigns/types.ts create mode 100644 src/pages/campaign-task-detail.tsx create mode 100644 src/pages/campaigns.tsx diff --git a/src/features/campaigns/components/campaign-stats-card.tsx b/src/features/campaigns/components/campaign-stats-card.tsx new file mode 100644 index 00000000..7636a0d0 --- /dev/null +++ b/src/features/campaigns/components/campaign-stats-card.tsx @@ -0,0 +1,79 @@ +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from '../../../shared/components/ui/card'; +import { Trophy, Target } from 'lucide-react'; +import { useCampaignStats } from '../hooks'; + +export function CampaignStatsCard() { + const { stats } = useCampaignStats(); + + if (!stats) { + return ( +
+ + +
+
+ +
+ {Array.from({ length: 2 }, (_, i) => ( +
+
+
+
+ ))} +
+
+
+
+ ); + } + + const completionPercentage = (stats.tasksCompleted / stats.totalTasks) * 100; + + return ( + + + + + Your Campaign Progress + + + +
+
+
+ +
+
+
+ {stats.totalPoints} +
+
Total Points
+
+
+ +
+
+ +
+
+
+ {stats.tasksCompleted}/{stats.totalTasks} +
+
+ Tasks Completed +
+
+ {Math.round(completionPercentage)}% complete +
+
+
+
+
+
+ ); +} diff --git a/src/features/campaigns/components/campaign-task-detail.tsx b/src/features/campaigns/components/campaign-task-detail.tsx new file mode 100644 index 00000000..de632be0 --- /dev/null +++ b/src/features/campaigns/components/campaign-task-detail.tsx @@ -0,0 +1,301 @@ +import { useEffect, useState } from 'react'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from '../../../shared/components/ui/card'; +import { Button } from '../../../shared/components/ui/button'; +import { Badge } from '../../../shared/components/ui/badge'; +import { ArrowLeft, CheckCircle, Trophy, Users, Lightbulb } from 'lucide-react'; +import { campaignService } from '../services'; +import { useCampaignTaskActions } from '../hooks'; +import type { CampaignTask } from '../types'; + +export function CampaignTaskDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { completeTask } = useCampaignTaskActions(); + const [task, setTask] = useState(null); + const [loading, setLoading] = useState(true); + const [completing, setCompleting] = useState(false); + + useEffect(() => { + const fetchTask = async () => { + if (!id) return; + + setLoading(true); + try { + const taskData = await campaignService.getCampaignTask(id); + setTask(taskData); + } catch (error) { + console.error('Failed to fetch task:', error); + } finally { + setLoading(false); + } + }; + + fetchTask(); + }, [id]); + + const handleCompleteTask = async () => { + if (!task || task.completed) return; + + setCompleting(true); + try { + await completeTask(task.id); + setTask((prev) => + prev + ? { + ...prev, + completed: true, + completedAt: new Date(), + category: 'completed', + } + : null, + ); + } catch (error) { + console.error('Failed to complete task:', error); + } finally { + setCompleting(false); + } + }; + + if (loading) { + return ( +
+
+
+
+
+
+
+
+ ); + } + + if (!task) { + return ( +
+
+
+

Task Not Found

+ + + +
+
+
+ ); + } + + const getDifficultyColor = (difficulty: CampaignTask['difficulty']) => { + switch (difficulty) { + case 'easy': + return 'bg-theme-primary/20 text-theme-primary border-theme-primary/30'; + case 'medium': + return 'bg-theme-primary/30 text-theme-primary border-theme-primary/50'; + case 'hard': + return 'bg-theme-primary/40 text-theme-primary border-theme-primary/70'; + default: + return 'bg-muted text-muted-foreground border-muted'; + } + }; + + const getCategoryColor = (category: CampaignTask['category']) => { + switch (category) { + case 'daily': + return 'bg-theme-primary/20 text-theme-primary border-theme-primary/30'; + case 'ongoing': + return 'bg-theme-primary/30 text-theme-primary border-theme-primary/50'; + case 'completed': + return 'bg-theme-primary/40 text-theme-primary border-theme-primary/70'; + default: + return 'bg-muted text-muted-foreground border-muted'; + } + }; + + return ( +
+
+
+ + + +
+ +
+
+ + +
+
+ {task.icon} +
+ {task.title} +
+ + {task.category} + + + {task.difficulty} + + {task.completed && ( + + + Completed + + )} +
+
+
+
+
+ +
+
+

+ + Description +

+

+ {task.description} +

+
+ + {task.requirements && task.requirements.length > 0 && ( +
+

+ + Requirements +

+
    + {task.requirements.map((requirement, index) => ( +
  • +
    + {requirement} +
  • + ))} +
+
+ )} + + {task.completedAt && ( +
+

Completion Details

+
+
+ + + Completed on{' '} + {task.completedAt.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} + +
+
+
+ )} +
+
+
+
+ +
+ + + Reward + + +
+
+ +
+ {task.points} +
+
Points
+
+
+
+
+ + + + Actions + + +
+ {!task.completed && ( + + )} + + + + +
+
+
+ + {task.unlockConditions && task.unlockConditions.length > 0 && ( + + + Unlock Conditions + + +
    + {task.unlockConditions.map((condition, index) => ( +
  • +
    + {condition} +
  • + ))} +
+
+
+ )} +
+
+
+
+ ); +} diff --git a/src/features/campaigns/components/campaigns.tsx b/src/features/campaigns/components/campaigns.tsx new file mode 100644 index 00000000..02e71d0f --- /dev/null +++ b/src/features/campaigns/components/campaigns.tsx @@ -0,0 +1,201 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from '../../../shared/components/ui/card'; +import { Badge } from '../../../shared/components/ui/badge'; +import { Button } from '../../../shared/components/ui/button'; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '../../../shared/components/ui/tabs'; +import { CheckCircle, Calendar, Target, Trophy } from 'lucide-react'; +import { useCampaignTasks } from '../hooks'; +import type { CampaignTask } from '../types'; +import { CampaignStatsCard } from './campaign-stats-card'; + +export function Campaigns() { + const { tasks, loading } = useCampaignTasks(); + const [selectedCategory, setSelectedCategory] = useState('daily'); + + const filteredTasks = tasks.filter((task) => { + if (selectedCategory === 'daily') return task.category === 'daily'; + if (selectedCategory === 'ongoing') return task.category === 'ongoing'; + if (selectedCategory === 'completed') return task.category === 'completed'; + return true; + }); + + const categories = [ + { id: 'daily', name: 'Daily', icon: Calendar }, + { id: 'ongoing', name: 'On-Going', icon: Target }, + { id: 'completed', name: 'Completed', icon: CheckCircle }, + ]; + + const getDifficultyColor = (difficulty: CampaignTask['difficulty']) => { + switch (difficulty) { + case 'easy': + return 'bg-theme-primary/20 text-theme-primary border-theme-primary/30'; + case 'medium': + return 'bg-theme-primary/30 text-theme-primary border-theme-primary/50'; + case 'hard': + return 'bg-theme-primary/40 text-theme-primary border-theme-primary/70'; + default: + return 'bg-muted text-muted-foreground border-muted'; + } + }; + + if (loading) { + return ( +
+
+
+
+
+ {Array.from({ length: 3 }, (_, i) => ( +
+ ))} +
+
+
+
+ ); + } + + return ( +
+
+
+

+ Campaigns +

+

+ Complete tasks to earn points and unlock achievements +

+
+ + + +
+ + + {categories.map((category) => { + const Icon = category.icon; + return ( + + + {category.name} + + ); + })} + + + +
+ {filteredTasks.map((task) => ( + + +
+
+ {task.icon} +
+ + {task.title} + +
+ + {task.difficulty} + +
+
+
+ {task.completed && ( + + )} +
+
+ +

+ {task.description} +

+ +
+
+ + + {task.points} pts + +
+ + + +
+ + {task.completedAt && ( +
+ + Completed on{' '} + {new Date(task.completedAt).toLocaleDateString()} +
+ )} +
+
+ ))} +
+ + {filteredTasks.length === 0 && ( +
+
+ {selectedCategory === 'daily' && 'No daily tasks available'} + {selectedCategory === 'ongoing' && 'No on-going tasks'} + {selectedCategory === 'completed' && + 'No completed tasks yet'} +
+

+ {selectedCategory === 'daily' && + 'Check back tomorrow for new daily tasks'} + {selectedCategory === 'ongoing' && + 'Start with some daily tasks first'} + {selectedCategory === 'completed' && + 'Complete some tasks to see them here'} +

+
+ )} +
+
+
+
+
+ ); +} diff --git a/src/features/campaigns/components/index.ts b/src/features/campaigns/components/index.ts new file mode 100644 index 00000000..ec301c38 --- /dev/null +++ b/src/features/campaigns/components/index.ts @@ -0,0 +1,3 @@ +export { Campaigns } from './campaigns'; +export { CampaignTaskDetail } from './campaign-task-detail'; +export { CampaignStatsCard } from './campaign-stats-card'; diff --git a/src/features/campaigns/hooks/index.ts b/src/features/campaigns/hooks/index.ts new file mode 100644 index 00000000..26e8b1c2 --- /dev/null +++ b/src/features/campaigns/hooks/index.ts @@ -0,0 +1,40 @@ +import { useEffect } from 'react'; +import { useCampaignStore } from '../stores'; + +export const useCampaignTasks = () => { + const { tasks, loading, error, fetchTasks, clearError } = useCampaignStore(); + + useEffect(() => { + if (tasks.length === 0) { + fetchTasks(); + } + }, [tasks.length, fetchTasks]); + + return { + tasks, + loading, + error, + refresh: fetchTasks, + clearError, + }; +}; + +export const useCampaignStats = () => { + const { stats, fetchStats } = useCampaignStore(); + + useEffect(() => { + if (!stats) { + fetchStats(); + } + }, [stats, fetchStats]); + + return { stats, refresh: fetchStats }; +}; + +export const useCampaignTaskActions = () => { + const { completeTask } = useCampaignStore(); + + return { + completeTask, + }; +}; diff --git a/src/features/campaigns/services.ts b/src/features/campaigns/services.ts new file mode 100644 index 00000000..783d8a53 --- /dev/null +++ b/src/features/campaigns/services.ts @@ -0,0 +1,137 @@ +import type { CampaignTask, CampaignStats, CampaignCategory } from './types'; + +// Mock data for demonstration - in real app this would come from API +const mockTasks: CampaignTask[] = [ + { + id: '1', + title: 'Start Your First Chat', + description: 'Create your first conversation with an AI assistant', + category: 'completed', + points: 100, + difficulty: 'easy', + completed: true, + completedAt: new Date('2024-01-15'), + icon: '💬', + }, + { + id: '2', + title: 'Create Your First CAP', + description: 'Design and create your first Conversational AI Program', + category: 'ongoing', + points: 250, + difficulty: 'medium', + completed: false, + requirements: ['Complete "Start Your First Chat"'], + icon: '🤖', + }, + { + id: '3', + title: 'Daily Check-in', + description: 'Visit the platform and check your dashboard', + category: 'daily', + points: 50, + difficulty: 'easy', + completed: false, + icon: '📅', + }, + { + id: '4', + title: 'Connect Your Wallet', + description: 'Link your Web3 wallet to unlock advanced features', + category: 'completed', + points: 150, + difficulty: 'easy', + completed: true, + completedAt: new Date('2024-01-10'), + icon: '💳', + }, + { + id: '5', + title: 'Power User Achievement', + description: 'Create 10 different CAPs with unique prompts', + category: 'ongoing', + points: 500, + difficulty: 'hard', + completed: false, + requirements: ['Complete "Create Your First CAP"'], + icon: '⚡', + }, + { + id: '6', + title: 'Daily Exploration', + description: 'Try a new AI model or feature today', + category: 'daily', + points: 75, + difficulty: 'medium', + completed: false, + icon: '🔍', + }, +]; + +const mockStats: CampaignStats = { + totalPoints: 250, + tasksCompleted: 2, + totalTasks: 6, +}; + +export class CampaignService { + async getCampaignTasks(): Promise { + // Simulate API delay + await new Promise((resolve) => setTimeout(resolve, 300)); + return mockTasks; + } + + async getCampaignTask(id: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 200)); + return mockTasks.find((task) => task.id === id) || null; + } + + async getCampaignStats(): Promise { + await new Promise((resolve) => setTimeout(resolve, 300)); + return mockStats; + } + + async getCampaignCategories(): Promise { + await new Promise((resolve) => setTimeout(resolve, 300)); + + const categories: CampaignCategory[] = [ + { + id: 'daily', + name: 'Daily Tasks', + description: 'Complete these every day', + icon: '📅', + tasks: mockTasks.filter((task) => task.category === 'daily'), + }, + { + id: 'ongoing', + name: 'On-Going', + description: 'Long-term goals and achievements', + icon: '🎯', + tasks: mockTasks.filter((task) => task.category === 'ongoing'), + }, + { + id: 'completed', + name: 'Completed', + description: 'Tasks you have finished', + icon: '✅', + tasks: mockTasks.filter((task) => task.category === 'completed'), + }, + ]; + + return categories; + } + + async completeTask(taskId: string): Promise { + await new Promise((resolve) => setTimeout(resolve, 500)); + const taskIndex = mockTasks.findIndex((task) => task.id === taskId); + if (taskIndex !== -1) { + mockTasks[taskIndex].completed = true; + mockTasks[taskIndex].completedAt = new Date(); + mockTasks[taskIndex].category = 'completed'; + return true; + } + return false; + } +} + +export const campaignService = new CampaignService(); diff --git a/src/features/campaigns/stores.ts b/src/features/campaigns/stores.ts new file mode 100644 index 00000000..f794ee18 --- /dev/null +++ b/src/features/campaigns/stores.ts @@ -0,0 +1,79 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { CampaignTask, CampaignStats } from './types'; +import { campaignService } from './services'; + +interface CampaignStore { + tasks: CampaignTask[]; + stats: CampaignStats | null; + loading: boolean; + error: string | null; + + // Actions + fetchTasks: () => Promise; + fetchStats: () => Promise; + completeTask: (taskId: string) => Promise; + clearError: () => void; +} + +export const useCampaignStore = create()( + persist( + (set, get) => ({ + tasks: [], + stats: null, + loading: false, + error: null, + + fetchTasks: async () => { + set({ loading: true, error: null }); + try { + const tasks = await campaignService.getCampaignTasks(); + set({ tasks, loading: false }); + } catch (error) { + set({ error: 'Failed to fetch campaign tasks', loading: false }); + } + }, + + fetchStats: async () => { + try { + const stats = await campaignService.getCampaignStats(); + set({ stats }); + } catch (error) { + set({ error: 'Failed to fetch campaign stats' }); + } + }, + + completeTask: async (taskId: string) => { + set({ loading: true, error: null }); + try { + const success = await campaignService.completeTask(taskId); + if (success) { + const { tasks } = get(); + const updatedTasks = tasks.map((task) => + task.id === taskId + ? { ...task, completed: true, completedAt: new Date() } + : task, + ); + set({ tasks: updatedTasks, loading: false }); + + // Refresh stats + get().fetchStats(); + } else { + set({ error: 'Failed to complete task', loading: false }); + } + } catch (error) { + set({ error: 'Failed to complete task', loading: false }); + } + }, + + clearError: () => set({ error: null }), + }), + { + name: 'campaign-storage', + partialize: (state) => ({ + tasks: state.tasks, + stats: state.stats, + }), + }, + ), +); diff --git a/src/features/campaigns/types.ts b/src/features/campaigns/types.ts new file mode 100644 index 00000000..56765ffc --- /dev/null +++ b/src/features/campaigns/types.ts @@ -0,0 +1,27 @@ +export interface CampaignTask { + id: string; + title: string; + description: string; + category: 'daily' | 'ongoing' | 'completed'; + points: number; + difficulty: 'easy' | 'medium' | 'hard'; + requirements?: string[]; + completed: boolean; + completedAt?: Date; + icon?: string; + unlockConditions?: string[]; +} + +export interface CampaignStats { + totalPoints: number; + tasksCompleted: number; + totalTasks: number; +} + +export interface CampaignCategory { + id: string; + name: string; + description: string; + icon: string; + tasks: CampaignTask[]; +} diff --git a/src/features/chat/components/markdown-mermaid.tsx b/src/features/chat/components/markdown-mermaid.tsx index e5a15456..eb7e39a6 100644 --- a/src/features/chat/components/markdown-mermaid.tsx +++ b/src/features/chat/components/markdown-mermaid.tsx @@ -1,5 +1,6 @@ import mermaid from 'mermaid'; -import React, { useEffect, useId, useState } from 'react'; +import type React from 'react'; +import { useEffect, useId, useState } from 'react'; interface MermaidCodeProps { code: string; diff --git a/src/features/chat/components/markdown.tsx b/src/features/chat/components/markdown.tsx index be0f7b69..5d8a9aa0 100644 --- a/src/features/chat/components/markdown.tsx +++ b/src/features/chat/components/markdown.tsx @@ -2,7 +2,8 @@ import MarkdownPreview from '@uiw/react-markdown-preview'; import { memo } from 'react'; import { useTheme } from '@/shared/components/theme-provider'; import './markdown.css'; -import React, { ReactNode } from 'react'; +import type React from 'react'; +import type { ReactNode } from 'react'; import { getCodeString } from 'rehype-rewrite'; import rehypeSanitize from 'rehype-sanitize'; import { MermaidCode } from './markdown-mermaid'; diff --git a/src/features/sidebar/components/app-sidebar-content.tsx b/src/features/sidebar/components/app-sidebar-content.tsx index 13d2cea3..466f0d1f 100644 --- a/src/features/sidebar/components/app-sidebar-content.tsx +++ b/src/features/sidebar/components/app-sidebar-content.tsx @@ -1,4 +1,4 @@ -import { Settings2, Wrench } from 'lucide-react'; +import { Settings2, Trophy, Wrench } from 'lucide-react'; import React from 'react'; import { useNavigate } from 'react-router-dom'; import { useSidebarFloating } from '@/features/sidebar/hooks/use-sidebar-floating'; @@ -109,6 +109,13 @@ export function AppSidebarContent() { + + {isDevMode && ( ; +} diff --git a/src/pages/campaigns.tsx b/src/pages/campaigns.tsx new file mode 100644 index 00000000..2f41c670 --- /dev/null +++ b/src/pages/campaigns.tsx @@ -0,0 +1,5 @@ +import { Campaigns } from '../features/campaigns/components/campaigns'; + +export default function CampaignsPage() { + return ; +} diff --git a/src/router.tsx b/src/router.tsx index e782a3cf..e14ec1a3 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -2,6 +2,8 @@ import { createBrowserRouter, Navigate } from 'react-router-dom'; import MainLayout from './layout/main-layout'; import RootLayout from './layout/root-layout'; import CallbackPage from './pages/callback'; +import CampaignsPage from './pages/campaigns'; +import CampaignTaskDetailPage from './pages/campaign-task-detail'; import CapStudioPage from './pages/cap-studio'; import CapStudioCreatePage from './pages/cap-studio-create'; import CapStudioEditPage from './pages/cap-studio-edit'; @@ -25,6 +27,8 @@ const router = createBrowserRouter([ children: [ { index: true, element: }, { path: 'chat', element: }, + { path: 'campaigns', element: }, + { path: 'campaigns/task/:id', element: }, { path: 'wallet', element: }, { path: 'settings', element: }, { path: 'cap-studio', element: }, diff --git a/src/shared/components/ui/button.tsx b/src/shared/components/ui/button.tsx index 82a61733..6b5addbf 100644 --- a/src/shared/components/ui/button.tsx +++ b/src/shared/components/ui/button.tsx @@ -18,6 +18,8 @@ const buttonVariants = cva( 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', link: 'text-primary underline-offset-4 hover:underline', + primary: + 'bg-gradient-to-br from-theme-primary/80 via-theme-primary to-theme-primary/80 hover:from-theme-primary/90 hover:to-theme-primary/80 text-white shadow-lg shadow-theme-primary/25 border border-theme-primary/20 h-10 px-4 justify-center relative font-medium hover:shadow-xl hover:shadow-theme-primary/30 hover:scale-[1.02] transition-all duration-200 ease-out', }, size: { default: 'h-10 px-4 py-2', diff --git a/src/shared/locales/cn.ts b/src/shared/locales/cn.ts index 49a5d7f8..abe02c5f 100644 --- a/src/shared/locales/cn.ts +++ b/src/shared/locales/cn.ts @@ -18,6 +18,7 @@ export const cn: typeof en = { chat: '聊天', settings: '设置', search: '搜索', + campaigns: '任务活动', capStore: 'Cap 商店', artifact: '文件', togglePin: '固定', diff --git a/src/shared/locales/en.ts b/src/shared/locales/en.ts index 778a466b..504923e0 100644 --- a/src/shared/locales/en.ts +++ b/src/shared/locales/en.ts @@ -17,6 +17,7 @@ export const en = { chat: 'Chat', settings: 'Settings', search: 'Search', + campaigns: 'Campaigns', capStore: 'Cap Store', artifact: 'Files', togglePin: 'Pin', From 4596f84ca57a6357229ff0977067bcf02f1a712c Mon Sep 17 00:00:00 2001 From: Mine77 Date: Fri, 8 Aug 2025 15:13:01 +0800 Subject: [PATCH 03/28] feat: initialize default capability in CapStateStore, enhance CapCard interaction, and update CapStoreModal for active section management --- .../cap-store/components/cap-card.tsx | 31 +++++----- .../cap-store/components/cap-store-modal.tsx | 4 +- src/features/cap-store/stores.ts | 5 +- src/shared/constants/cap.ts | 58 ++++++++++++++++++ src/shared/stores/current-cap-store.ts | 59 +------------------ 5 files changed, 84 insertions(+), 73 deletions(-) diff --git a/src/features/cap-store/components/cap-card.tsx b/src/features/cap-store/components/cap-card.tsx index cf812f30..d5b64631 100644 --- a/src/features/cap-store/components/cap-card.tsx +++ b/src/features/cap-store/components/cap-card.tsx @@ -1,4 +1,4 @@ -import { Loader2, Play, Settings, Trash2 } from 'lucide-react'; +import { Loader2, Settings, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { toast } from 'sonner'; import { @@ -53,8 +53,17 @@ export function CapCard({ cap, onRun }: CapCardProps) { onRun?.(cap); }; + const handleCardClick = () => { + if (isInstalled) { + handleRun(); + } + }; + return ( - +
@@ -70,23 +79,16 @@ export function CapCard({ cap, onRun }: CapCardProps) { {/* Action buttons */}
- {isInstalled ? ( - - ) : ( + {!isInstalled && ( /* Install button */ @@ -274,7 +256,7 @@ export function CampaignTaskDetail() { {task.unlockConditions && task.unlockConditions.length > 0 && ( - + Unlock Conditions @@ -285,7 +267,7 @@ export function CampaignTaskDetail() { key={`condition-${index}-${condition.slice(0, 10)}`} className="text-sm text-muted-foreground flex items-start gap-2" > -
+
{condition} ))} diff --git a/src/features/campaigns/components/campaigns.tsx b/src/features/campaigns/components/campaigns.tsx index 02e71d0f..1227cac5 100644 --- a/src/features/campaigns/components/campaigns.tsx +++ b/src/features/campaigns/components/campaigns.tsx @@ -1,54 +1,30 @@ +import { CheckCircle, Target } from 'lucide-react'; import { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from '../../../shared/components/ui/card'; -import { Badge } from '../../../shared/components/ui/badge'; -import { Button } from '../../../shared/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger, } from '../../../shared/components/ui/tabs'; -import { CheckCircle, Calendar, Target, Trophy } from 'lucide-react'; import { useCampaignTasks } from '../hooks'; -import type { CampaignTask } from '../types'; import { CampaignStatsCard } from './campaign-stats-card'; +import { CampaignTaskCard } from './task-card'; export function Campaigns() { const { tasks, loading } = useCampaignTasks(); - const [selectedCategory, setSelectedCategory] = useState('daily'); + const [selectedCategory, setSelectedCategory] = useState('ongoing'); const filteredTasks = tasks.filter((task) => { - if (selectedCategory === 'daily') return task.category === 'daily'; if (selectedCategory === 'ongoing') return task.category === 'ongoing'; if (selectedCategory === 'completed') return task.category === 'completed'; return true; }); const categories = [ - { id: 'daily', name: 'Daily', icon: Calendar }, { id: 'ongoing', name: 'On-Going', icon: Target }, { id: 'completed', name: 'Completed', icon: CheckCircle }, ]; - const getDifficultyColor = (difficulty: CampaignTask['difficulty']) => { - switch (difficulty) { - case 'easy': - return 'bg-theme-primary/20 text-theme-primary border-theme-primary/30'; - case 'medium': - return 'bg-theme-primary/30 text-theme-primary border-theme-primary/50'; - case 'hard': - return 'bg-theme-primary/40 text-theme-primary border-theme-primary/70'; - default: - return 'bg-muted text-muted-foreground border-muted'; - } - }; - if (loading) { return (
@@ -56,9 +32,9 @@ export function Campaigns() {
- {Array.from({ length: 3 }, (_, i) => ( + {Array.from({ length: 3 }, () => (
))} @@ -71,13 +47,11 @@ export function Campaigns() { return (
-
+
-

- Campaigns -

+

Campaigns

- Complete tasks to earn points and unlock achievements + Complete tasks to earn Nuwa points!

@@ -85,14 +59,14 @@ export function Campaigns() {
- + {categories.map((category) => { const Icon = category.icon; return ( {category.name} @@ -102,91 +76,22 @@ export function Campaigns() { -
+
{filteredTasks.map((task) => ( - - -
-
- {task.icon} -
- - {task.title} - -
- - {task.difficulty} - -
-
-
- {task.completed && ( - - )} -
-
- -

- {task.description} -

- -
-
- - - {task.points} pts - -
- - - -
- - {task.completedAt && ( -
- - Completed on{' '} - {new Date(task.completedAt).toLocaleDateString()} -
- )} -
-
+ ))}
{filteredTasks.length === 0 && (
- {selectedCategory === 'daily' && 'No daily tasks available'} {selectedCategory === 'ongoing' && 'No on-going tasks'} {selectedCategory === 'completed' && 'No completed tasks yet'}

- {selectedCategory === 'daily' && - 'Check back tomorrow for new daily tasks'} {selectedCategory === 'ongoing' && - 'Start with some daily tasks first'} + 'Check back later for new tasks'} {selectedCategory === 'completed' && 'Complete some tasks to see them here'}

diff --git a/src/features/campaigns/components/task-card.tsx b/src/features/campaigns/components/task-card.tsx new file mode 100644 index 00000000..b351e75b --- /dev/null +++ b/src/features/campaigns/components/task-card.tsx @@ -0,0 +1,51 @@ +import { CheckCircle, Trophy } from 'lucide-react'; +import { Link } from 'react-router-dom'; +import { + Card, CardContent, CardHeader, CardTitle +} from '../../../shared/components/ui/card'; +import type { CampaignTask } from '../types'; + +export const CampaignTaskCard = ({ task }: { task: CampaignTask }) => { + return ( + + + +
+
+ {task.icon} +
+ + {task.title} + +

+ {task.description} +

+
+
+
+
+ +
+
+ + + {task.points} pts + +
+
+ + {task.completedAt && ( +
+ + Completed on {new Date(task.completedAt).toLocaleDateString()} +
+ )} +
+
+ + ); +}; diff --git a/src/features/campaigns/hooks/index.ts b/src/features/campaigns/hooks/index.ts index 26e8b1c2..4ed222df 100644 --- a/src/features/campaigns/hooks/index.ts +++ b/src/features/campaigns/hooks/index.ts @@ -5,9 +5,7 @@ export const useCampaignTasks = () => { const { tasks, loading, error, fetchTasks, clearError } = useCampaignStore(); useEffect(() => { - if (tasks.length === 0) { - fetchTasks(); - } + fetchTasks(); }, [tasks.length, fetchTasks]); return { @@ -23,9 +21,7 @@ export const useCampaignStats = () => { const { stats, fetchStats } = useCampaignStore(); useEffect(() => { - if (!stats) { - fetchStats(); - } + fetchStats(); }, [stats, fetchStats]); return { stats, refresh: fetchStats }; diff --git a/src/features/campaigns/services.ts b/src/features/campaigns/services.ts index 783d8a53..888dbfc7 100644 --- a/src/features/campaigns/services.ts +++ b/src/features/campaigns/services.ts @@ -1,4 +1,4 @@ -import type { CampaignTask, CampaignStats, CampaignCategory } from './types'; +import type { CampaignCategory, CampaignStats, CampaignTask } from './types'; // Mock data for demonstration - in real app this would come from API const mockTasks: CampaignTask[] = [ @@ -28,7 +28,7 @@ const mockTasks: CampaignTask[] = [ id: '3', title: 'Daily Check-in', description: 'Visit the platform and check your dashboard', - category: 'daily', + category: 'ongoing', points: 50, difficulty: 'easy', completed: false, @@ -60,7 +60,7 @@ const mockTasks: CampaignTask[] = [ id: '6', title: 'Daily Exploration', description: 'Try a new AI model or feature today', - category: 'daily', + category: 'ongoing', points: 75, difficulty: 'medium', completed: false, From 35d562c8f213a01df44d6055b206149931a1ea91 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Tue, 12 Aug 2025 12:48:10 +0800 Subject: [PATCH 05/28] refactor: remove campaign pages --- .../components/campaign-stats-card.tsx | 88 ------ .../components/campaign-task-detail.tsx | 283 ------------------ .../campaigns/components/campaigns.tsx | 106 ------- src/features/campaigns/components/index.ts | 3 - .../campaigns/components/task-card.tsx | 51 ---- src/features/campaigns/hooks/index.ts | 36 --- src/features/campaigns/services.ts | 137 --------- src/features/campaigns/stores.ts | 79 ----- src/features/campaigns/types.ts | 27 -- .../cap-store/hooks/use-remote-cap.ts | 21 ++ .../components/app-sidebar-content.tsx | 9 +- src/pages/campaign-task-detail.tsx | 5 - src/pages/campaigns.tsx | 5 - src/router.tsx | 4 - 14 files changed, 22 insertions(+), 832 deletions(-) delete mode 100644 src/features/campaigns/components/campaign-stats-card.tsx delete mode 100644 src/features/campaigns/components/campaign-task-detail.tsx delete mode 100644 src/features/campaigns/components/campaigns.tsx delete mode 100644 src/features/campaigns/components/index.ts delete mode 100644 src/features/campaigns/components/task-card.tsx delete mode 100644 src/features/campaigns/hooks/index.ts delete mode 100644 src/features/campaigns/services.ts delete mode 100644 src/features/campaigns/stores.ts delete mode 100644 src/features/campaigns/types.ts delete mode 100644 src/pages/campaign-task-detail.tsx delete mode 100644 src/pages/campaigns.tsx diff --git a/src/features/campaigns/components/campaign-stats-card.tsx b/src/features/campaigns/components/campaign-stats-card.tsx deleted file mode 100644 index 16bb5b8d..00000000 --- a/src/features/campaigns/components/campaign-stats-card.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Target, Trophy } from 'lucide-react'; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from '../../../shared/components/ui/card'; -import { useCampaignStats } from '../hooks'; - -export function CampaignStatsCard() { - const { stats } = useCampaignStats(); - - if (!stats) { - return ( -
- - -
-
- -
- {Array.from({ length: 2 }, () => ( -
-
-
-
- ))} -
-
-
-
- ); - } - - return ( - - {/* Decorative background elements */} -
-
- - -
- -
- - Your Campaign Progress - -

- Track your achievements -

-
-
-
- - -
-
-
- -
-
-
- {stats.totalPoints} -
-
- Total Points -
-
-
- -
-
- -
-
-
- {stats.tasksCompleted} -
-
- Tasks Completed -
-
-
-
-
- - ); -} diff --git a/src/features/campaigns/components/campaign-task-detail.tsx b/src/features/campaigns/components/campaign-task-detail.tsx deleted file mode 100644 index 8c0c9059..00000000 --- a/src/features/campaigns/components/campaign-task-detail.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import { ArrowLeft, CheckCircle, Lightbulb, Trophy, Users } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { Link, useNavigate, useParams } from 'react-router-dom'; -import { Badge } from '../../../shared/components/ui/badge'; -import { Button } from '../../../shared/components/ui/button'; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from '../../../shared/components/ui/card'; -import { useCampaignTaskActions } from '../hooks'; -import { campaignService } from '../services'; -import type { CampaignTask } from '../types'; - -export function CampaignTaskDetail() { - const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); - const { completeTask } = useCampaignTaskActions(); - const [task, setTask] = useState(null); - const [loading, setLoading] = useState(true); - const [completing, setCompleting] = useState(false); - - useEffect(() => { - const fetchTask = async () => { - if (!id) return; - - setLoading(true); - try { - const taskData = await campaignService.getCampaignTask(id); - setTask(taskData); - } catch (error) { - console.error('Failed to fetch task:', error); - } finally { - setLoading(false); - } - }; - - fetchTask(); - }, [id]); - - const handleCompleteTask = async () => { - if (!task || task.completed) return; - - setCompleting(true); - try { - await completeTask(task.id); - setTask((prev) => - prev - ? { - ...prev, - completed: true, - completedAt: new Date(), - category: 'completed', - } - : null, - ); - } catch (error) { - console.error('Failed to complete task:', error); - } finally { - setCompleting(false); - } - }; - - if (loading) { - return ( -
-
-
-
-
-
-
-
- ); - } - - if (!task) { - return ( -
-
-
-

Task Not Found

- - - -
-
-
- ); - } - - - const getCategoryColor = (category: CampaignTask['category']) => { - switch (category) { - case 'daily': - return 'bg-primary/20 text-primary border-primary/30'; - case 'ongoing': - return 'bg-primary/30 text-primary border-primary/50'; - case 'completed': - return 'bg-primary/40 text-primary border-primary/70'; - default: - return 'bg-muted text-muted-foreground border-muted'; - } - }; - - return ( -
-
-
- - - -
- -
-
- - -
-
- {task.icon} -
- {task.title} -
- {task.completed && ( - - - Completed - - )} -
-
-
-
-
- -
-
-

- - Description -

-

- {task.description} -

-
- - {task.requirements && task.requirements.length > 0 && ( -
-

- - Requirements -

-
    - {task.requirements.map((requirement, index) => ( -
  • -
    - {requirement} -
  • - ))} -
-
- )} - - {task.completedAt && ( -
-

Completion Details

-
-
- - - Completed on{' '} - {task.completedAt.toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - })} - -
-
-
- )} -
-
-
-
- -
- - - Reward - - -
-
- -
- {task.points} -
-
Points
-
-
-
-
- - - - Actions - - -
- {!task.completed && ( - - )} - - - - -
-
-
- - {task.unlockConditions && task.unlockConditions.length > 0 && ( - - - Unlock Conditions - - -
    - {task.unlockConditions.map((condition, index) => ( -
  • -
    - {condition} -
  • - ))} -
-
-
- )} -
-
-
-
- ); -} diff --git a/src/features/campaigns/components/campaigns.tsx b/src/features/campaigns/components/campaigns.tsx deleted file mode 100644 index 1227cac5..00000000 --- a/src/features/campaigns/components/campaigns.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { CheckCircle, Target } from 'lucide-react'; -import { useState } from 'react'; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from '../../../shared/components/ui/tabs'; -import { useCampaignTasks } from '../hooks'; -import { CampaignStatsCard } from './campaign-stats-card'; -import { CampaignTaskCard } from './task-card'; - -export function Campaigns() { - const { tasks, loading } = useCampaignTasks(); - const [selectedCategory, setSelectedCategory] = useState('ongoing'); - - const filteredTasks = tasks.filter((task) => { - if (selectedCategory === 'ongoing') return task.category === 'ongoing'; - if (selectedCategory === 'completed') return task.category === 'completed'; - return true; - }); - - const categories = [ - { id: 'ongoing', name: 'On-Going', icon: Target }, - { id: 'completed', name: 'Completed', icon: CheckCircle }, - ]; - - if (loading) { - return ( -
-
-
-
-
- {Array.from({ length: 3 }, () => ( -
- ))} -
-
-
-
- ); - } - - return ( -
-
-
-

Campaigns

-

- Complete tasks to earn Nuwa points! -

-
- - - -
- - - {categories.map((category) => { - const Icon = category.icon; - return ( - - - {category.name} - - ); - })} - - - -
- {filteredTasks.map((task) => ( - - ))} -
- - {filteredTasks.length === 0 && ( -
-
- {selectedCategory === 'ongoing' && 'No on-going tasks'} - {selectedCategory === 'completed' && - 'No completed tasks yet'} -
-

- {selectedCategory === 'ongoing' && - 'Check back later for new tasks'} - {selectedCategory === 'completed' && - 'Complete some tasks to see them here'} -

-
- )} -
-
-
-
-
- ); -} diff --git a/src/features/campaigns/components/index.ts b/src/features/campaigns/components/index.ts deleted file mode 100644 index ec301c38..00000000 --- a/src/features/campaigns/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { Campaigns } from './campaigns'; -export { CampaignTaskDetail } from './campaign-task-detail'; -export { CampaignStatsCard } from './campaign-stats-card'; diff --git a/src/features/campaigns/components/task-card.tsx b/src/features/campaigns/components/task-card.tsx deleted file mode 100644 index b351e75b..00000000 --- a/src/features/campaigns/components/task-card.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { CheckCircle, Trophy } from 'lucide-react'; -import { Link } from 'react-router-dom'; -import { - Card, CardContent, CardHeader, CardTitle -} from '../../../shared/components/ui/card'; -import type { CampaignTask } from '../types'; - -export const CampaignTaskCard = ({ task }: { task: CampaignTask }) => { - return ( - - - -
-
- {task.icon} -
- - {task.title} - -

- {task.description} -

-
-
-
-
- -
-
- - - {task.points} pts - -
-
- - {task.completedAt && ( -
- - Completed on {new Date(task.completedAt).toLocaleDateString()} -
- )} -
-
- - ); -}; diff --git a/src/features/campaigns/hooks/index.ts b/src/features/campaigns/hooks/index.ts deleted file mode 100644 index 4ed222df..00000000 --- a/src/features/campaigns/hooks/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect } from 'react'; -import { useCampaignStore } from '../stores'; - -export const useCampaignTasks = () => { - const { tasks, loading, error, fetchTasks, clearError } = useCampaignStore(); - - useEffect(() => { - fetchTasks(); - }, [tasks.length, fetchTasks]); - - return { - tasks, - loading, - error, - refresh: fetchTasks, - clearError, - }; -}; - -export const useCampaignStats = () => { - const { stats, fetchStats } = useCampaignStore(); - - useEffect(() => { - fetchStats(); - }, [stats, fetchStats]); - - return { stats, refresh: fetchStats }; -}; - -export const useCampaignTaskActions = () => { - const { completeTask } = useCampaignStore(); - - return { - completeTask, - }; -}; diff --git a/src/features/campaigns/services.ts b/src/features/campaigns/services.ts deleted file mode 100644 index 888dbfc7..00000000 --- a/src/features/campaigns/services.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { CampaignCategory, CampaignStats, CampaignTask } from './types'; - -// Mock data for demonstration - in real app this would come from API -const mockTasks: CampaignTask[] = [ - { - id: '1', - title: 'Start Your First Chat', - description: 'Create your first conversation with an AI assistant', - category: 'completed', - points: 100, - difficulty: 'easy', - completed: true, - completedAt: new Date('2024-01-15'), - icon: '💬', - }, - { - id: '2', - title: 'Create Your First CAP', - description: 'Design and create your first Conversational AI Program', - category: 'ongoing', - points: 250, - difficulty: 'medium', - completed: false, - requirements: ['Complete "Start Your First Chat"'], - icon: '🤖', - }, - { - id: '3', - title: 'Daily Check-in', - description: 'Visit the platform and check your dashboard', - category: 'ongoing', - points: 50, - difficulty: 'easy', - completed: false, - icon: '📅', - }, - { - id: '4', - title: 'Connect Your Wallet', - description: 'Link your Web3 wallet to unlock advanced features', - category: 'completed', - points: 150, - difficulty: 'easy', - completed: true, - completedAt: new Date('2024-01-10'), - icon: '💳', - }, - { - id: '5', - title: 'Power User Achievement', - description: 'Create 10 different CAPs with unique prompts', - category: 'ongoing', - points: 500, - difficulty: 'hard', - completed: false, - requirements: ['Complete "Create Your First CAP"'], - icon: '⚡', - }, - { - id: '6', - title: 'Daily Exploration', - description: 'Try a new AI model or feature today', - category: 'ongoing', - points: 75, - difficulty: 'medium', - completed: false, - icon: '🔍', - }, -]; - -const mockStats: CampaignStats = { - totalPoints: 250, - tasksCompleted: 2, - totalTasks: 6, -}; - -export class CampaignService { - async getCampaignTasks(): Promise { - // Simulate API delay - await new Promise((resolve) => setTimeout(resolve, 300)); - return mockTasks; - } - - async getCampaignTask(id: string): Promise { - await new Promise((resolve) => setTimeout(resolve, 200)); - return mockTasks.find((task) => task.id === id) || null; - } - - async getCampaignStats(): Promise { - await new Promise((resolve) => setTimeout(resolve, 300)); - return mockStats; - } - - async getCampaignCategories(): Promise { - await new Promise((resolve) => setTimeout(resolve, 300)); - - const categories: CampaignCategory[] = [ - { - id: 'daily', - name: 'Daily Tasks', - description: 'Complete these every day', - icon: '📅', - tasks: mockTasks.filter((task) => task.category === 'daily'), - }, - { - id: 'ongoing', - name: 'On-Going', - description: 'Long-term goals and achievements', - icon: '🎯', - tasks: mockTasks.filter((task) => task.category === 'ongoing'), - }, - { - id: 'completed', - name: 'Completed', - description: 'Tasks you have finished', - icon: '✅', - tasks: mockTasks.filter((task) => task.category === 'completed'), - }, - ]; - - return categories; - } - - async completeTask(taskId: string): Promise { - await new Promise((resolve) => setTimeout(resolve, 500)); - const taskIndex = mockTasks.findIndex((task) => task.id === taskId); - if (taskIndex !== -1) { - mockTasks[taskIndex].completed = true; - mockTasks[taskIndex].completedAt = new Date(); - mockTasks[taskIndex].category = 'completed'; - return true; - } - return false; - } -} - -export const campaignService = new CampaignService(); diff --git a/src/features/campaigns/stores.ts b/src/features/campaigns/stores.ts deleted file mode 100644 index f794ee18..00000000 --- a/src/features/campaigns/stores.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; -import type { CampaignTask, CampaignStats } from './types'; -import { campaignService } from './services'; - -interface CampaignStore { - tasks: CampaignTask[]; - stats: CampaignStats | null; - loading: boolean; - error: string | null; - - // Actions - fetchTasks: () => Promise; - fetchStats: () => Promise; - completeTask: (taskId: string) => Promise; - clearError: () => void; -} - -export const useCampaignStore = create()( - persist( - (set, get) => ({ - tasks: [], - stats: null, - loading: false, - error: null, - - fetchTasks: async () => { - set({ loading: true, error: null }); - try { - const tasks = await campaignService.getCampaignTasks(); - set({ tasks, loading: false }); - } catch (error) { - set({ error: 'Failed to fetch campaign tasks', loading: false }); - } - }, - - fetchStats: async () => { - try { - const stats = await campaignService.getCampaignStats(); - set({ stats }); - } catch (error) { - set({ error: 'Failed to fetch campaign stats' }); - } - }, - - completeTask: async (taskId: string) => { - set({ loading: true, error: null }); - try { - const success = await campaignService.completeTask(taskId); - if (success) { - const { tasks } = get(); - const updatedTasks = tasks.map((task) => - task.id === taskId - ? { ...task, completed: true, completedAt: new Date() } - : task, - ); - set({ tasks: updatedTasks, loading: false }); - - // Refresh stats - get().fetchStats(); - } else { - set({ error: 'Failed to complete task', loading: false }); - } - } catch (error) { - set({ error: 'Failed to complete task', loading: false }); - } - }, - - clearError: () => set({ error: null }), - }), - { - name: 'campaign-storage', - partialize: (state) => ({ - tasks: state.tasks, - stats: state.stats, - }), - }, - ), -); diff --git a/src/features/campaigns/types.ts b/src/features/campaigns/types.ts deleted file mode 100644 index 56765ffc..00000000 --- a/src/features/campaigns/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -export interface CampaignTask { - id: string; - title: string; - description: string; - category: 'daily' | 'ongoing' | 'completed'; - points: number; - difficulty: 'easy' | 'medium' | 'hard'; - requirements?: string[]; - completed: boolean; - completedAt?: Date; - icon?: string; - unlockConditions?: string[]; -} - -export interface CampaignStats { - totalPoints: number; - tasksCompleted: number; - totalTasks: number; -} - -export interface CampaignCategory { - id: string; - name: string; - description: string; - icon: string; - tasks: CampaignTask[]; -} diff --git a/src/features/cap-store/hooks/use-remote-cap.ts b/src/features/cap-store/hooks/use-remote-cap.ts index c61f85fd..4bb2c2f4 100644 --- a/src/features/cap-store/hooks/use-remote-cap.ts +++ b/src/features/cap-store/hooks/use-remote-cap.ts @@ -153,6 +153,26 @@ export function useRemoteCap() { return Promise.resolve(null); }; + const downloadCap = async (capCid: string) => { + if (!capKit) { + throw new Error('CapKit not initialized'); + } + + const capData = await capKit.downloadCap(capCid, 'utf8'); + const downloadContent: unknown = yaml.load(capData.data.fileData); + + // check if the cap is valid + if (!validateCapContent(downloadContent)) { + console.warn( + `Downloaded cap ${capCid} does not match Cap type specification, skipping...`, + ); + return null; + } + + // parse the cap content + return parseCapContent(downloadContent); + }; + return { remoteCaps: remoteCapState.remoteCaps, isLoading: remoteCapState.isLoading, @@ -166,5 +186,6 @@ export function useRemoteCap() { goToPage, nextPage, previousPage, + downloadCap, }; } diff --git a/src/features/sidebar/components/app-sidebar-content.tsx b/src/features/sidebar/components/app-sidebar-content.tsx index 466f0d1f..13d2cea3 100644 --- a/src/features/sidebar/components/app-sidebar-content.tsx +++ b/src/features/sidebar/components/app-sidebar-content.tsx @@ -1,4 +1,4 @@ -import { Settings2, Trophy, Wrench } from 'lucide-react'; +import { Settings2, Wrench } from 'lucide-react'; import React from 'react'; import { useNavigate } from 'react-router-dom'; import { useSidebarFloating } from '@/features/sidebar/hooks/use-sidebar-floating'; @@ -109,13 +109,6 @@ export function AppSidebarContent() { - - {isDevMode && ( ; -} diff --git a/src/pages/campaigns.tsx b/src/pages/campaigns.tsx deleted file mode 100644 index 2f41c670..00000000 --- a/src/pages/campaigns.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Campaigns } from '../features/campaigns/components/campaigns'; - -export default function CampaignsPage() { - return ; -} diff --git a/src/router.tsx b/src/router.tsx index e14ec1a3..e782a3cf 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -2,8 +2,6 @@ import { createBrowserRouter, Navigate } from 'react-router-dom'; import MainLayout from './layout/main-layout'; import RootLayout from './layout/root-layout'; import CallbackPage from './pages/callback'; -import CampaignsPage from './pages/campaigns'; -import CampaignTaskDetailPage from './pages/campaign-task-detail'; import CapStudioPage from './pages/cap-studio'; import CapStudioCreatePage from './pages/cap-studio-create'; import CapStudioEditPage from './pages/cap-studio-edit'; @@ -27,8 +25,6 @@ const router = createBrowserRouter([ children: [ { index: true, element: }, { path: 'chat', element: }, - { path: 'campaigns', element: }, - { path: 'campaigns/task/:id', element: }, { path: 'wallet', element: }, { path: 'settings', element: }, { path: 'cap-studio', element: }, From 34487f3c95482dbab4d629cc447d5a09ccbddd1d Mon Sep 17 00:00:00 2001 From: Mine77 Date: Tue, 12 Aug 2025 22:55:30 +0800 Subject: [PATCH 06/28] fix: login page logo --- src/pages/callback.tsx | 2 +- src/pages/login.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/callback.tsx b/src/pages/callback.tsx index 58a2b8a9..b840b20a 100644 --- a/src/pages/callback.tsx +++ b/src/pages/callback.tsx @@ -65,7 +65,7 @@ export default function CallbackPage() { {/* Logo */}
- +
diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 96fb7d42..3221129e 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -37,7 +37,7 @@ export default function LoginPage() { {/* Logo */}
- +
{/* Title with icon */} From 4932594b122449e1896d6a0feeb60bc667699840 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Wed, 13 Aug 2025 22:58:14 +0800 Subject: [PATCH 07/28] refactor: update cap-kit version --- package.json | 2 +- pnpm-lock.yaml | 68 +++- .../cap-store/components/cap-avatar.tsx | 44 +++ .../cap-store/components/cap-card.tsx | 157 +++------ .../cap-store/components/cap-selector.tsx | 6 +- .../components/cap-store-content.tsx | 170 +++++++++ .../cap-store/components/cap-store-modal.tsx | 326 +++--------------- .../components/cap-store-sidebar.tsx | 183 ++++++++++ .../cap-store/components/cap-thumbnail.tsx | 37 -- src/features/cap-store/hooks/index.ts | 2 +- src/features/cap-store/hooks/use-cap-store.ts | 124 +++++++ .../cap-store/hooks/use-installed-cap.ts | 29 -- .../cap-store/hooks/use-remote-cap.ts | 203 +++-------- src/features/cap-store/stores.ts | 204 ++++------- src/features/cap-store/types.ts | 13 + .../components/cap-edit/model-details.tsx | 2 +- .../cap-submit/thumbnail-upload.tsx | 26 +- .../cap-studio/hooks/use-submit-cap.ts | 6 +- src/features/cap-studio/services.ts | 2 +- .../cap-studio/stores/cap-studio-stores.ts | 1 - .../cap-studio/stores/model-stores.ts | 2 +- .../chat/components/cap-suggestions.tsx | 185 ---------- src/features/chat/components/index.tsx | 3 - src/shared/constants/cap.ts | 2 +- src/shared/hooks/use-capkit.ts | 2 +- src/shared/locales/cn.ts | 8 + src/shared/locales/en.ts | 8 + src/shared/storage/actions.ts | 4 +- src/shared/storage/db.ts | 4 +- src/shared/types/cap.ts | 28 +- 30 files changed, 867 insertions(+), 984 deletions(-) create mode 100644 src/features/cap-store/components/cap-avatar.tsx create mode 100644 src/features/cap-store/components/cap-store-content.tsx create mode 100644 src/features/cap-store/components/cap-store-sidebar.tsx delete mode 100644 src/features/cap-store/components/cap-thumbnail.tsx create mode 100644 src/features/cap-store/hooks/use-cap-store.ts delete mode 100644 src/features/cap-store/hooks/use-installed-cap.ts create mode 100644 src/features/cap-store/types.ts delete mode 100644 src/features/chat/components/cap-suggestions.tsx diff --git a/package.json b/package.json index 9bd4aff3..f285bdb3 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@hookform/resolvers": "^5.1.1", "@lobehub/icons": "^2.9.0", "@modelcontextprotocol/sdk": "^1.13.2", - "@nuwa-ai/cap-kit": "^0.3.5", + "@nuwa-ai/cap-kit": "^0.3.7", "@nuwa-ai/identity-kit": "^0.3.4", "@nuwa-ai/identity-kit-web": "^0.3.5", "@openrouter/ai-sdk-provider": "^0.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98aad59d..6768fa1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,8 +51,8 @@ importers: specifier: ^1.13.2 version: 1.13.2 '@nuwa-ai/cap-kit': - specifier: ^0.3.5 - version: 0.3.5(react@19.1.0)(typescript@5.8.3)(zod@3.25.67) + specifier: ^0.3.7 + version: 0.3.7(react@19.1.0)(typescript@5.8.3) '@nuwa-ai/identity-kit': specifier: ^0.3.4 version: 0.3.4(typescript@5.8.3) @@ -941,8 +941,8 @@ packages: resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.15.1': - resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.3.1': @@ -957,8 +957,8 @@ packages: resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.4': - resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==} + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ethereumjs/common@3.2.0': @@ -1079,6 +1079,9 @@ packages: '@jridgewell/gen-mapping@0.3.12': resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1094,12 +1097,18 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@lezer/common@1.2.3': resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} @@ -1306,8 +1315,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nuwa-ai/cap-kit@0.3.5': - resolution: {integrity: sha512-2pomn+FWRFmMbEKBemj9WrzVMan/yklM1TLtuCiwkozCj5F4oFTWc4mwVtpDoiUAH/UVB7ngKuXaU9LhT8Mgig==} + '@nuwa-ai/cap-kit@0.3.7': + resolution: {integrity: sha512-H08LZ1sxCvbf9fEiaRnESrTBck0frz8KM96Qmq1eBc/nCtqUzuyakpPK9gGE3YCbEdelHodsM8vLeUspwSZOSA==} '@nuwa-ai/identity-kit-web@0.3.5': resolution: {integrity: sha512-egjSXMiGfDxzPW8l496PRGF2b0MGN+KbTY8m1oRmcnffSWei7A3gvrzHBvKyeHTB93mPbzYck7ca1/09Wclp4A==} @@ -1320,6 +1329,9 @@ packages: '@nuwa-ai/identity-kit@0.3.4': resolution: {integrity: sha512-Lty7wuKmEuriLmGd15EM1IRPvDTuHvm/Px/BXb3i1O/l2sB6ZlYxybL++KU694I0tY2TZznuc3tNJUJfkYyIfA==} + '@nuwa-ai/identity-kit@0.3.6': + resolution: {integrity: sha512-eVhgOQja2YiXVuBnCb16xrdF5uHcmBZWPZdBLKU97aaQ4R5pIvPbE26MbgHy2s4F+ZC2rthpJcd5zthGZGXgqQ==} + '@openrouter/ai-sdk-provider@0.7.2': resolution: {integrity: sha512-Fry2mV7uGGJRmP9JntTZRc8ElESIk7AJNTacLbF6Syoeb5k8d7HPGkcK9rTXDlqBb8HgU1hOKtz23HojesTmnw==} engines: {node: '>=18'} @@ -7254,8 +7266,8 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 '@ant-design/colors@7.2.1': dependencies: @@ -7857,7 +7869,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/core@0.15.1': + '@eslint/core@0.15.2': dependencies: '@types/json-schema': 7.0.15 @@ -7879,9 +7891,9 @@ snapshots: '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.4': + '@eslint/plugin-kit@0.3.5': dependencies: - '@eslint/core': 0.15.1 + '@eslint/core': 0.15.2 levn: 0.4.1 '@ethereumjs/common@3.2.0': @@ -8020,6 +8032,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -8032,6 +8049,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -8042,6 +8061,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping@0.3.30': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@lezer/common@1.2.3': {} '@lezer/highlight@1.2.1': @@ -8449,21 +8473,21 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nuwa-ai/cap-kit@0.3.5(react@19.1.0)(typescript@5.8.3)(zod@3.25.67)': + '@nuwa-ai/cap-kit@0.3.7(react@19.1.0)(typescript@5.8.3)': dependencies: '@modelcontextprotocol/sdk': 1.13.2 '@noble/curves': 1.9.2 '@noble/hashes': 1.8.0 - '@nuwa-ai/identity-kit': 0.3.4(typescript@5.8.3) + '@nuwa-ai/identity-kit': 0.3.6(typescript@5.8.3) '@roochnetwork/rooch-sdk': 0.3.6(typescript@5.8.3) ai: 4.3.16(react@19.1.0)(zod@3.25.67) js-yaml: 4.1.0 multiformats: 9.9.0 + zod: 3.25.67 transitivePeerDependencies: - react - supports-color - typescript - - zod '@nuwa-ai/identity-kit-web@0.3.5(react@19.1.0)(typescript@5.8.3)': dependencies: @@ -8484,6 +8508,16 @@ snapshots: - supports-color - typescript + '@nuwa-ai/identity-kit@0.3.6(typescript@5.8.3)': + dependencies: + '@noble/curves': 1.9.2 + '@noble/hashes': 1.8.0 + '@roochnetwork/rooch-sdk': 0.3.6(typescript@5.8.3) + multiformats: 9.9.0 + transitivePeerDependencies: + - supports-color + - typescript + '@openrouter/ai-sdk-provider@0.7.2(ai@4.3.16(react@19.1.0)(zod@3.25.67))(zod@3.25.67)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -12654,7 +12688,7 @@ snapshots: '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 '@eslint/js': 9.29.0 - '@eslint/plugin-kit': 0.3.4 + '@eslint/plugin-kit': 0.3.5 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 diff --git a/src/features/cap-store/components/cap-avatar.tsx b/src/features/cap-store/components/cap-avatar.tsx new file mode 100644 index 00000000..9cccfd1c --- /dev/null +++ b/src/features/cap-store/components/cap-avatar.tsx @@ -0,0 +1,44 @@ +import { useTheme } from '@/shared/components/theme-provider'; +import { Avatar, AvatarFallback, AvatarImage } from '@/shared/components/ui'; +import type { CapThumbnail } from '@/shared/types/cap'; + +const sizeClasses = { + sm: 'size-6', // 24px + md: 'size-8', // 32px + lg: 'size-10', // 40px + xl: 'size-12', // 48px +} as const; + +export function CapAvatar({ + capName, + capThumbnail, + size = 'md', +}: { + capName: string; + capThumbnail: CapThumbnail; + size?: keyof typeof sizeClasses; +}) { + const sizeClass = sizeClasses[size] || sizeClasses['md']; + + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === 'dark'; + + return ( + + + + {capName.slice(0, 2).toUpperCase()} + + + ); +} diff --git a/src/features/cap-store/components/cap-card.tsx b/src/features/cap-store/components/cap-card.tsx index d5b64631..4c80611c 100644 --- a/src/features/cap-store/components/cap-card.tsx +++ b/src/features/cap-store/components/cap-card.tsx @@ -1,131 +1,66 @@ -import { Loader2, Settings, Trash2 } from 'lucide-react'; -import { useState } from 'react'; -import { toast } from 'sonner'; +import { MoreHorizontal } from 'lucide-react'; import { - Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/shared/components/ui'; -import { useLanguage } from '@/shared/hooks/use-language'; -import type { Cap } from '@/shared/types/cap'; -import { useInstalledCap } from '../hooks/use-installed-cap'; -import { CapThumbnail } from './cap-thumbnail'; +import type { CapMetadata } from '@/shared/types/cap'; +import { CapAvatar } from './cap-avatar'; -export interface CapCardProps { - cap: Cap; - onRun?: (cap: Cap) => void; +interface CapCardActions { + icon: React.ReactNode; + label: string; + onClick: () => void; } -export function CapCard({ cap, onRun }: CapCardProps) { - const { installCap, uninstallCap, updateInstalledCap, isInstalled } = - useInstalledCap(cap); - const [isLoading, setIsLoading] = useState(false); - const { t } = useLanguage(); - - const handleInstall = async () => { - setIsLoading(true); - try { - installCap(cap); - toast.success(`${cap.metadata.displayName} has been installed`); - } catch (error) { - toast.error(t('capStore.card.installFailed')); - } finally { - setIsLoading(false); - } - }; - - const handleUninstall = async () => { - setIsLoading(true); - try { - uninstallCap(cap.id); - toast.success(`${cap.metadata.displayName} has been uninstalled`); - } catch (error) { - toast.error(t('capStore.card.uninstallFailed')); - } finally { - setIsLoading(false); - } - }; - - const handleRun = () => { - onRun?.(cap); - }; - - const handleCardClick = () => { - if (isInstalled) { - handleRun(); - } - }; +export interface CapCardProps { + capMetadata: CapMetadata; + onClick: () => void; + actions?: CapCardActions[]; +} +export function CapCard({ capMetadata, onClick, actions }: CapCardProps) { return ( -
- +
+
-
-

- {cap.metadata.displayName} -

-
-

- {cap.metadata.description} +

+ {capMetadata.displayName} +

+

+ {capMetadata.description}

- - {/* Action buttons */} -
-
- {!isInstalled && ( - /* Install button */ - - )} -
- {isInstalled && ( - - - - - - - - Uninstall - - - - )} -
+ + + + + + {actions?.map((action) => ( + + {action.icon} + {action.label} + + ))} + +
); diff --git a/src/features/cap-store/components/cap-selector.tsx b/src/features/cap-store/components/cap-selector.tsx index 20c1cc7c..c727206f 100644 --- a/src/features/cap-store/components/cap-selector.tsx +++ b/src/features/cap-store/components/cap-selector.tsx @@ -10,12 +10,12 @@ import { import { useCurrentCap } from '@/shared/hooks'; import type { Cap } from '@/shared/types'; import { useRemoteCap } from '../hooks/use-remote-cap'; +import { CapAvatar } from './cap-avatar'; import { CapStoreModal } from './cap-store-modal'; -import { CapThumbnail } from './cap-thumbnail'; const CapInfo = ({ cap }: { cap: Cap }) => ( <> - + {cap.metadata.displayName} ); @@ -41,7 +41,7 @@ export function CapSelector() { event.preventDefault(); setIsModalOpen(true); }} - className="rounded-2xl" + className="rounded-lg" type="button" >
diff --git a/src/features/cap-store/components/cap-store-content.tsx b/src/features/cap-store/components/cap-store-content.tsx new file mode 100644 index 00000000..9ea2dd26 --- /dev/null +++ b/src/features/cap-store/components/cap-store-content.tsx @@ -0,0 +1,170 @@ +import { Clock, Loader2, Package, Star } from 'lucide-react'; +import { Button } from '@/shared/components/ui'; +import { useLanguage } from '@/shared/hooks'; +import type { Cap } from '@/shared/types/cap'; +import { useCapStore } from '../hooks/use-cap-store'; +import type { RemoteCap } from '../types'; +import { CapCard } from './cap-card'; + +export interface CapStoreContentProps { + caps: (Cap | RemoteCap)[]; + activeSection: string; + isLoading?: boolean; + error?: string | null; + onRefresh?: () => void; +} + +export function CapStoreContent({ + caps, + activeSection, + isLoading = false, + error = null, + onRefresh, +}: CapStoreContentProps) { + const { t } = useLanguage(); + const { + runCap, + addCapToFavorite, + removeCapFromFavorite, + removeCapFromRecents, + isCapFavorite, + } = useCapStore(); + + const isShowingInstalled = ['favorites', 'recent'].includes(activeSection); + + // Function to get actions based on cap type and active section + const getCapActions = (cap: Cap | RemoteCap) => { + const actions = []; + + const isRemoteCap = 'cid' in cap; + + if (isCapFavorite(cap.id)) { + actions.push({ + icon: , + label: 'Remove from Favorites', + onClick: () => removeCapFromFavorite(cap.id), + }); + } else { + actions.push({ + icon: , + label: 'Add to Favorites', + onClick: () => addCapToFavorite(cap.id, isRemoteCap ? cap.cid : undefined), + }); + } + + if (activeSection === 'recent') { + actions.push({ + icon: , + label: 'Remove from Recents', + onClick: () => removeCapFromRecents(cap.id), + }); + } + + return actions; + }; + + if (error && !isShowingInstalled) { + return ( +
+ +

+ {t('capStore.status.error')} +

+

+ {t('capStore.status.errorDesc')} +

+ +
+ ); + } + + if (isLoading && !isShowingInstalled) { + return ( +
+ +

+ {t('capStore.status.loading')} +

+
+ ); + } + + if (caps.length === 0) { + return ( +
+ +

+ {getEmptyStateTitle(activeSection, t)} +

+

+ {getEmptyStateDescription(activeSection, t)} +

+
+ ); + } + + return ( +
+ {caps.length > 0 && + caps.map((cap) => { + // Type guard to check if cap is RemoteCap (has cid property) + const isRemoteCap = 'cid' in cap; + + if (isRemoteCap) { + // RemoteCap type - use cid as unique key + return ( + runCap(cap.id, cap.cid)} + actions={getCapActions(cap)} + /> + ); + } else { + // Cap type - use id as unique key + return ( + runCap(cap.id)} + actions={getCapActions(cap)} + /> + ); + } + })} +
+ ); +} + +function getEmptyStateTitle(activeSection: string, t: any): string { + switch (activeSection) { + case 'favorites': + return t('capStore.status.noFavoriteCaps') || 'No Favorite Caps'; + case 'recent': + return t('capStore.status.noRecentCaps') || 'No Recent Caps'; + default: + return t('capStore.status.noCaps') || 'No Caps Found'; + } +} + +function getEmptyStateDescription(activeSection: string, t: any): string { + switch (activeSection) { + case 'favorites': + return ( + t('capStore.status.noFavoriteCapsDesc') || + "You haven't marked any caps as favorites yet. Browse the store and favorite caps you like." + ); + case 'recent': + return ( + t('capStore.status.noRecentCapsDesc') || + "You haven't used any caps recently. Try running a cap to see it here." + ); + default: + return ( + t('capStore.status.noCapsDesc.category') || + 'No caps found in this category. Try searching or browse other categories.' + ); + } +} diff --git a/src/features/cap-store/components/cap-store-modal.tsx b/src/features/cap-store/components/cap-store-modal.tsx index cdd40963..fb56ff80 100644 --- a/src/features/cap-store/components/cap-store-modal.tsx +++ b/src/features/cap-store/components/cap-store-modal.tsx @@ -1,40 +1,27 @@ -import { - BookOpen, - Bot, - Code, - Coins, - Download, - Grid3X3, - Loader2, - MoreHorizontal, - Package, - PenTool, - RefreshCw, - Search, - Wrench, -} from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { toast } from 'sonner'; +import { useState } from 'react'; import * as Dialog from '@/shared/components/ui'; -import { Button, Input } from '@/shared/components/ui'; -import { predefinedTags } from '@/shared/constants/cap'; -import { useCurrentCap, useLanguage } from '@/shared/hooks'; +import { useLanguage } from '@/shared/hooks'; import type { Cap } from '@/shared/types/cap'; +import { useCapStore } from '../hooks/use-cap-store'; import { useRemoteCap } from '../hooks/use-remote-cap'; -import { CapStateStore } from '../stores'; -import { CapCard } from './cap-card'; +import type { RemoteCap } from '../types'; +import { CapStoreContent } from './cap-store-content'; +import { + CapStoreSidebar, + type CapStoreSidebarSection, +} from './cap-store-sidebar'; interface CapStoreModalProps { open?: boolean; onOpenChange?: (open: boolean) => void; children?: React.ReactNode; - initialActiveSection?: string; + initialActiveSection?: CapStoreSidebarSection; } export function CapStoreModal({ open: externalOpen, onOpenChange: externalOnOpenChange, - initialActiveSection = 'installed', + initialActiveSection = { id: 'all', label: 'All Caps', type: 'section' }, children, }: CapStoreModalProps) { const { t } = useLanguage(); @@ -45,188 +32,41 @@ export function CapStoreModal({ const open = isControlled ? externalOpen : internalOpen; const onOpenChange = isControlled ? externalOnOpenChange : setInternalOpen; - const [searchQuery, setSearchQuery] = useState(''); const [activeSection, setActiveSection] = useState(initialActiveSection); - // State for installed caps - const [installedCaps, setInstalledCaps] = useState([]); - - const { remoteCaps, isLoading, error, refetch, lastSearchQuery } = - useRemoteCap(); - - const { setCurrentCap } = useCurrentCap(); - - // Subscribe to installed caps changes - useEffect(() => { - const updateInstalledCaps = () => { - const state = CapStateStore.getState(); - const installed: Cap[] = Object.values(state.installedCaps); - setInstalledCaps(installed); - }; - - // Initial load - updateInstalledCaps(); - - // Subscribe to changes - const unsubscribe = CapStateStore.subscribe(updateInstalledCaps); - - return unsubscribe; - }, []); - - const sidebarSections = [ - { - id: 'installed', - label: t('capStore.sidebar.installed') || 'Installed', - type: 'section', - }, - { - id: 'all', - label: t('capStore.sidebar.all') || 'All Caps', - type: 'section', - }, - { id: 'divider', label: '', type: 'divider' }, - ...predefinedTags.map((tag) => ({ - id: tag.toLowerCase().replace(/\s+/g, '-'), - label: tag, - type: 'tag' as const, - })), - ]; - - const handleRunCap = (cap: Cap) => { - // Set this cap as the current cap - setCurrentCap(cap); - - onOpenChange?.(false); + const { remoteCaps, isLoading, error, fetchCaps, refetch } = useRemoteCap(); + const { getRecentCaps, getFavoriteCaps } = useCapStore(); - toast.success(`${cap.metadata.displayName} has been selected`); - }; - - const handleSearch = () => { - refetch(searchQuery); - }; - - const handleSearchKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleSearch(); + const handleSearchChange = (query: string) => { + if (activeSection.type === 'tag') { + fetchCaps({ searchQuery: query, tags: [activeSection.label] }); + } else if (activeSection.type === 'section') { + fetchCaps({ searchQuery: query }); } }; - const renderContent = () => { - // Determine which caps to display based on active section - const getDisplayCaps = (): Cap[] => { - if (activeSection === 'installed') { - return installedCaps; - } else if (activeSection === 'all') { - return remoteCaps; - } else { - // Filter by tag - const tagName = activeSection.replace(/-/g, ' ').toLowerCase(); - return remoteCaps.filter((cap) => - cap.metadata.tags.some((tag) => tag.toLowerCase() === tagName), - ); - } - }; - - const displayCaps = getDisplayCaps(); - const isShowingInstalled = activeSection === 'installed'; - - if (error && !isShowingInstalled) { - return ( -
- -

- {t('capStore.status.error')} -

-

- {t('capStore.status.errorDesc')} -

- -
- ); + const handleActiveSectionChange = (section: CapStoreSidebarSection) => { + setActiveSection(section); + if (section.type === 'tag') { + fetchCaps({ tags: [section.label] }); + } else if (section.id === 'all') { + fetchCaps({ searchQuery: '' }); } - - if (isLoading && !isShowingInstalled) { - return ( -
- -

- {t('capStore.status.loading')} -

-

- {t('capStore.status.fetching')} -

-
- ); - } - - if (displayCaps.length === 0) { - return ( -
- -

- {isShowingInstalled - ? t('capStore.status.noInstalledCaps') || 'No Installed Caps' - : t('capStore.status.noCaps')} -

-

- {isShowingInstalled - ? t('capStore.status.noInstalledCapsDesc') || - "You haven't installed any caps yet. Browse the store to find caps to install." - : lastSearchQuery.trim() - ? t('capStore.status.noCapsDesc.search') - : t('capStore.status.noCapsDesc.category')} -

-
- ); - } - - return ( -
- {displayCaps.map((cap) => ( - - ))} -
- ); }; - const getSectionIcon = (sectionId: string, type: string) => { - if (type === 'section') { - switch (sectionId) { - case 'installed': - return Download; - case 'all': - return Grid3X3; - default: - return Package; - } + // Determine which caps to display based on active section + const getDisplayCaps = (): (Cap | RemoteCap)[] => { + if (activeSection.id === 'favorites') { + return getFavoriteCaps(); + } else if (activeSection.id === 'recent') { + return getRecentCaps(); + } else { + return remoteCaps; } - - if (type === 'tag') { - switch (sectionId) { - case 'ai-model': - return Bot; - case 'coding': - return Code; - case 'content-writing': - return PenTool; - case 'research': - return BookOpen; - case 'crypto': - return Coins; - case 'tools': - return Wrench; - case 'others': - return MoreHorizontal; - default: - return Package; - } - } - - return Package; }; + const displayCaps: (Cap | RemoteCap)[] = getDisplayCaps(); + return ( {children && ( @@ -247,95 +87,25 @@ export function CapStoreModal({ {t('capStore.title')} - {/* Header */} -
-
- -
-

{t('capStore.title')}

-

- {t('capStore.description')} -

-
- -
- - {/* Search Bar - only show for remote caps */} -
-
- - setSearchQuery(e.target.value)} - onKeyDown={handleSearchKeyDown} - className="pl-10" - /> -
- -
-
- {/* Main Content with Sidebar */}
- {/* Sidebar */} -
-
- -
-
+ {/* Content Area */}
-
{renderContent()}
+
+ refetch()} + /> +
diff --git a/src/features/cap-store/components/cap-store-sidebar.tsx b/src/features/cap-store/components/cap-store-sidebar.tsx new file mode 100644 index 00000000..860b4e29 --- /dev/null +++ b/src/features/cap-store/components/cap-store-sidebar.tsx @@ -0,0 +1,183 @@ +import { + BookOpen, + Bot, + Code, + Coins, + Grid3X3, + Heart, + History, + MoreHorizontal, + Package, + PenTool, + Search, + Wrench, + X, +} from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { Input } from '@/shared/components/ui'; +import { predefinedTags } from '@/shared/constants/cap'; +import { useLanguage } from '@/shared/hooks'; + +export interface CapStoreSidebarProps { + activeSection: CapStoreSidebarSection; + onSectionChange: (section: CapStoreSidebarSection) => void; + onSearchChange: (query: string) => void; +} + +export type CapStoreSidebarSection = { + id: string; + label: string; + type: 'section' | 'tag' | 'divider'; +}; + +export function CapStoreSidebar({ + activeSection, + onSectionChange, + onSearchChange, +}: CapStoreSidebarProps) { + const { t } = useLanguage(); + const [searchValue, setSearchValue] = useState(''); + + const sidebarSections: CapStoreSidebarSection[] = [ + { + id: 'favorites', + label: t('capStore.sidebar.favorites') || 'Favorite Caps', + type: 'section', + }, + { + id: 'recent', + label: t('capStore.sidebar.recent') || 'Recent Caps', + type: 'section', + }, + { id: 'divider1', label: '', type: 'divider' }, + { + id: 'all', + label: t('capStore.sidebar.all') || 'All Caps', + type: 'section', + }, + ...predefinedTags.map((tag) => ({ + id: tag.toLowerCase().replace(/\s+/g, '-'), + label: tag, + type: 'tag' as const, + })), + ]; + + const getSectionIcon = (sectionId: string, type: string) => { + if (type === 'section') { + switch (sectionId) { + case 'favorites': + return Heart; + case 'recent': + return History; + case 'all': + return Grid3X3; + default: + return Package; + } + } + + if (type === 'tag') { + switch (sectionId) { + case 'ai-model': + return Bot; + case 'coding': + return Code; + case 'content-writing': + return PenTool; + case 'research': + return BookOpen; + case 'crypto': + return Coins; + case 'tools': + return Wrench; + case 'others': + return MoreHorizontal; + default: + return Package; + } + } + + return Package; + }; + + useEffect(() => { + setSearchValue(''); + }, [activeSection]); + + const handleSearchChange = (value: string) => { + setSearchValue(value); + onSearchChange(value); + }; + + const handleClearSearch = () => { + setSearchValue(''); + onSearchChange(''); + }; + + return ( +
+ {/* Search Section */} +
+
+
+ + Cap Store +
+ +
+
+ + handleSearchChange(e.target.value)} + className="pl-9 h-8 text-sm" + /> + {searchValue && ( + + )} +
+
+
+
+ + {/* Navigation */} +
+ +
+
+ ); +} diff --git a/src/features/cap-store/components/cap-thumbnail.tsx b/src/features/cap-store/components/cap-thumbnail.tsx deleted file mode 100644 index 7b0fbeca..00000000 --- a/src/features/cap-store/components/cap-thumbnail.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Avatar, AvatarFallback, AvatarImage } from '@/shared/components/ui'; -import type { Cap } from '@/shared/types'; - -const sizeClasses = { - sm: 'size-6', // 24px - md: 'size-8', // 32px - lg: 'size-10', // 40px - xl: 'size-12', // 48px -} as const; - -export function CapThumbnail({ - cap, - size = 'md', -}: { - cap: Cap; - size?: keyof typeof sizeClasses; -}) { - const sizeClass = sizeClasses[size] || sizeClasses['md']; - - return ( - - - - {cap.idName.slice(0, 2).toUpperCase()} - - - ); -} diff --git a/src/features/cap-store/hooks/index.ts b/src/features/cap-store/hooks/index.ts index e2648596..e928ab78 100644 --- a/src/features/cap-store/hooks/index.ts +++ b/src/features/cap-store/hooks/index.ts @@ -1,2 +1,2 @@ -export * from './use-installed-cap.ts'; +export * from './use-cap-store.ts'; export * from './use-remote-cap.ts'; diff --git a/src/features/cap-store/hooks/use-cap-store.ts b/src/features/cap-store/hooks/use-cap-store.ts new file mode 100644 index 00000000..e6f9e0bc --- /dev/null +++ b/src/features/cap-store/hooks/use-cap-store.ts @@ -0,0 +1,124 @@ +import { useCapKit } from '@/shared/hooks'; +import { useCurrentCap } from '@/shared/hooks/use-current-cap'; +import { CapStateStore } from '../stores'; + +/** + * Hook for managing the installed caps + */ +export const useCapStore = () => { + const { installedCaps, addInstalledCap, updateInstalledCap } = + CapStateStore(); + const { capKit } = useCapKit(); + const { setCurrentCap } = useCurrentCap(); + + const downloadCap = async (capCid: string) => { + if (!capKit) { + throw new Error('CapKit not initialized'); + } + + const capData = await capKit.downloadCap(capCid, 'utf8'); + + return capData; + }; + + const addCapToFavorite = async (capId: string, capCid?: string) => { + const installedCap = installedCaps[capId]; + if (!installedCap) { + if (!capCid) { + throw new Error('Cap CID is required for downloading cap'); + } + const capData = await downloadCap(capCid); + console.log(capData); + addInstalledCap({ + capData, + isFavorite: true, + lastUsedAt: null, + }); + } else { + updateInstalledCap(capId, { + isFavorite: true, + }); + } + }; + + const removeCapFromFavorite = (capId: string) => { + const installedCap = installedCaps[capId]; + if (!installedCap) { + throw new Error('Cap is not installed'); + } + updateInstalledCap(capId, { + isFavorite: false, + }); + }; + + const runCap = async (capId: string, capCid?: string) => { + const installedCap = installedCaps[capId]; + if (!installedCap) { + if (!capCid) { + throw new Error('Cap CID is required for downloading cap'); + } + const capData = await downloadCap(capCid); + addInstalledCap({ + capData, + isFavorite: false, + lastUsedAt: Date.now(), + }); + } else { + updateInstalledCap(capId, { + lastUsedAt: Date.now(), + }); + + setCurrentCap(installedCap.capData); + } + }; + + const getRecentCaps = () => { + return Object.values(installedCaps) + .filter((cap) => cap.lastUsedAt !== null) + .sort((a, b) => { + if (a.lastUsedAt && b.lastUsedAt) { + return b.lastUsedAt - a.lastUsedAt; + } + return 0; + }) + .map((cap) => cap.capData); + }; + + const getFavoriteCaps = () => { + return Object.values(installedCaps) + .filter((cap) => cap.isFavorite) + .sort((a, b) => { + if (a.lastUsedAt && b.lastUsedAt) { + return b.lastUsedAt - a.lastUsedAt; + } + return 0; + }) + .map((cap) => cap.capData); + }; + + const removeCapFromRecents = (capId: string) => { + const installedCap = installedCaps[capId]; + if (!installedCap) { + throw new Error('Cap is not installed'); + } + updateInstalledCap(capId, { + ...installedCap, + lastUsedAt: null, + }); + }; + + const isCapFavorite = (capId: string) => { + const installedCap = installedCaps[capId]; + return installedCap?.isFavorite; + }; + + return { + runCap, + getFavoriteCaps, + addCapToFavorite, + removeCapFromFavorite, + getRecentCaps, + removeCapFromRecents, + isCapFavorite, + }; +}; diff --git a/src/features/cap-store/hooks/use-installed-cap.ts b/src/features/cap-store/hooks/use-installed-cap.ts deleted file mode 100644 index c0fc689d..00000000 --- a/src/features/cap-store/hooks/use-installed-cap.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect, useState } from 'react'; -import type { Cap } from '@/shared/types/cap'; -import { CapStateStore } from '../stores'; - -/** - * Hook for managing the installed caps - */ -export const useInstalledCap = (cap: Cap) => { - const [state, setState] = useState(() => CapStateStore.getState()); - - useEffect(() => { - const unsubscribe = CapStateStore.subscribe((newState) => { - setState(newState); - }); - - return unsubscribe; - }, []); - - const { installCap, uninstallCap, updateInstalledCap, installedCaps } = state; - - const isInstalled = !!installedCaps[cap.id]; - - return { - isInstalled, - installCap, - uninstallCap, - updateInstalledCap, - }; -}; diff --git a/src/features/cap-store/hooks/use-remote-cap.ts b/src/features/cap-store/hooks/use-remote-cap.ts index 4bb2c2f4..bfd1f183 100644 --- a/src/features/cap-store/hooks/use-remote-cap.ts +++ b/src/features/cap-store/hooks/use-remote-cap.ts @@ -1,191 +1,102 @@ -import * as yaml from 'js-yaml'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useCapKit } from '@/shared/hooks/use-capkit'; -import type { Cap } from '@/shared/types/cap'; import { CapStateStore } from '../stores'; -import { parseCapContent, validateCapContent } from '../utils'; - -interface CapKitQueryResponse { - code: number; - data: { - items: Array<{ - id: string; - cid: string; - name: string; - }>; - page: number; - pageSize: number; - totalItems: number; - totalPages: number; - }; -} +import type { RemoteCap } from '../types'; interface UseRemoteCapParams { searchQuery?: string; + tags?: string[]; page?: number; + size?: number; } - /** * Hook for accessing the remote caps with advanced filtering, sorting, and pagination */ export function useRemoteCap() { - const [storeState, setStoreState] = useState(() => CapStateStore.getState()); - - // Subscribe to store changes - useEffect(() => { - const unsubscribe = CapStateStore.subscribe((newState) => { - setStoreState(newState); - }); - - return unsubscribe; - }, []); - const { capKit, isLoading: isCapKitLoading } = useCapKit(); + const { remoteCaps, setRemoteCaps } = CapStateStore(); + const [lastSearchParams, setLastSearchParams] = useState( + {}, + ); - const { - setRemoteCaps, - setRemoteCapLoading, - setRemoteCapError, - setRemoteCapPagination, - setLastSearchQuery, - } = storeState; - - const { remoteCapState } = storeState; + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); const fetchCaps = async (params: UseRemoteCapParams = {}) => { - const { searchQuery: queryString = '', page: pageNum = 1 } = params; + const { + searchQuery: queryString = '', + page: pageNum = 0, + size: sizeNum = 5, + tags: tagsArray = [], + } = params; - setRemoteCapLoading(true); - setRemoteCapError(null); + setIsLoading(true); + setError(null); + setLastSearchParams(params); try { if (!capKit) { throw new Error('CapKit not initialized'); } - const response: CapKitQueryResponse = - await capKit.queryWithName(queryString); - - const remoteCapResults: (Cap | null)[] = await Promise.all( - response.data.items.map(async (item) => { - try { - const capData = await capKit.downloadCap(item.cid, 'utf8'); - const downloadContent: unknown = yaml.load(capData.data.fileData); - - // check if the cap is valid - if (!validateCapContent(downloadContent)) { - console.warn( - `Downloaded cap ${item.id} does not match Cap type specification, skipping...`, - ); - return null; - } - - // parse the cap content - return parseCapContent(downloadContent); - } catch (error) { - console.error(`Error processing cap ${item.id}:`, error); - return null; - } - }), + // const response = await capKit.queryWithName(queryString); + const response = await capKit.queryWithName( + queryString, + tagsArray, + pageNum, + sizeNum, ); - // filter out invalid caps - const validRemoteCaps = remoteCapResults.filter( - (cap): cap is Cap => cap !== null, - ); - - setRemoteCaps(validRemoteCaps); - setRemoteCapPagination({ - totalCount: response.data.totalItems, - page: pageNum, - hasMore: response.data.page < response.data.totalPages, - }); - setLastSearchQuery(queryString); - setRemoteCapLoading(false); + const remoteCaps: RemoteCap[] = + response.data?.items?.map((item) => { + return { + cid: item.cid, + version: item.version, + id: item.id, + idName: item.name, + authorDID: item.id.split(':')[0], + metadata: { + displayName: item.displayName, + description: item.description, + tags: item.tags, + repository: item.repository, + homepage: item.homepage, + submittedAt: item.submittedAt, + thumbnail: item.thumbnail, + }, + }; + }) || []; + + setRemoteCaps(remoteCaps); + + setIsLoading(false); return response; } catch (err) { console.error('Error fetching caps:', err); - setRemoteCapError('Failed to fetch caps. Please try again.'); - setRemoteCapLoading(false); + setError('Failed to fetch caps. Please try again.'); + setIsLoading(false); throw err; } }; - useEffect(() => { - if (capKit && !isCapKitLoading) { - fetchCaps({ searchQuery: '', page: 1 }); - } - }, [capKit, isCapKitLoading]); - - const refetch = (newSearchQuery?: string) => { - const queryString = - newSearchQuery !== undefined - ? newSearchQuery - : remoteCapState.lastSearchQuery; - return fetchCaps({ searchQuery: queryString, page: 1 }); + const refetch = () => { + fetchCaps(lastSearchParams); }; const goToPage = (newPage: number) => { return fetchCaps({ - searchQuery: remoteCapState.lastSearchQuery, + searchQuery: '', page: newPage, }); }; - const nextPage = () => { - if (remoteCapState.hasMore) { - return fetchCaps({ - searchQuery: remoteCapState.lastSearchQuery, - page: remoteCapState.page + 1, - }); - } - return Promise.resolve(null); - }; - - const previousPage = () => { - if (remoteCapState.page > 1) { - return fetchCaps({ - searchQuery: remoteCapState.lastSearchQuery, - page: remoteCapState.page - 1, - }); - } - return Promise.resolve(null); - }; - - const downloadCap = async (capCid: string) => { - if (!capKit) { - throw new Error('CapKit not initialized'); - } - - const capData = await capKit.downloadCap(capCid, 'utf8'); - const downloadContent: unknown = yaml.load(capData.data.fileData); - - // check if the cap is valid - if (!validateCapContent(downloadContent)) { - console.warn( - `Downloaded cap ${capCid} does not match Cap type specification, skipping...`, - ); - return null; - } - - // parse the cap content - return parseCapContent(downloadContent); - }; - return { - remoteCaps: remoteCapState.remoteCaps, - isLoading: remoteCapState.isLoading, - error: remoteCapState.error, - totalCount: remoteCapState.totalCount, - page: remoteCapState.page, - hasMore: remoteCapState.hasMore, - lastSearchQuery: remoteCapState.lastSearchQuery, + remoteCaps, + isLoading, + error, fetchCaps, - refetch, goToPage, - nextPage, - previousPage, - downloadCap, + refetch, }; } diff --git a/src/features/cap-store/stores.ts b/src/features/cap-store/stores.ts index 66452d0a..2e3fddb2 100644 --- a/src/features/cap-store/stores.ts +++ b/src/features/cap-store/stores.ts @@ -1,52 +1,33 @@ // cap-store.ts -// Store for managing capability (Cap) installations and their states +// Store for managing capability (Cap) states import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { defaultCap } from '@/shared/constants/cap'; import { NuwaIdentityKit } from '@/shared/services/identity-kit'; import { createPersistConfig, db } from '@/shared/storage'; -import type { Cap } from '@/shared/types/cap'; +import type { InstalledCap, RemoteCap } from './types'; // ================= Interfaces ================= // -// Remote caps state interface -interface RemoteCapState { - remoteCaps: Cap[]; - isLoading: boolean; - error: string | null; - totalCount: number; - page: number; - hasMore: boolean; - lastSearchQuery: string; -} - // Cap store state interface - handles both installed and remote caps interface CapStoreState { - installedCaps: Record; - - // Remote caps state - remoteCapState: RemoteCapState; - - // Installed cap management - installCap: (cap: Cap) => void; - uninstallCap: (id: string) => void; - updateInstalledCap: (id: string, updatedCap: Cap) => void; - - // Remote caps management - setRemoteCaps: (caps: Cap[]) => void; - setRemoteCapLoading: (isLoading: boolean) => void; - setRemoteCapError: (error: string | null) => void; - setRemoteCapPagination: (pagination: { - totalCount: number; - page: number; - hasMore: boolean; - }) => void; - setLastSearchQuery: (query: string) => void; - clearRemoteCaps: () => void; - - // Data management + // Installed cap management use capId as key + installedCaps: Record; + + // Remote cap management + remoteCaps: RemoteCap[]; + + // Installed Cap management + addInstalledCap: (cap: InstalledCap) => void; + updateInstalledCap: ( + id: string, + updates: Partial>, + ) => void; clearAllInstalledCaps: () => void; + // Remote cap management + setRemoteCaps: (caps: RemoteCap[]) => void; + // Data persistence loadFromDB: () => Promise; saveToDB: () => Promise; @@ -85,33 +66,34 @@ export const CapStateStore = create()( (set, get) => ({ // Store state installedCaps: { - [defaultCap.id]: defaultCap, + [defaultCap.id]: { + capData: defaultCap, + isFavorite: false, + lastUsedAt: null, + }, }, - // Remote caps state - remoteCapState: { - remoteCaps: [], - isLoading: false, - error: null, - totalCount: 0, - page: 1, - hasMore: false, - lastSearchQuery: '', - }, + remoteCaps: [], // Installation management - installCap: (cap: Cap) => { + addInstalledCap: (cap: InstalledCap) => { const { installedCaps } = get(); // Don't install if already installed - if (installedCaps[cap.id]) { + if (installedCaps[cap.capData.id]) { return; } + console.log('addInstalledCap', cap); + set((state) => ({ installedCaps: { ...state.installedCaps, - [cap.id]: cap, + [cap.capData.id]: { + capData: cap.capData, + isFavorite: cap.isFavorite, + lastUsedAt: cap.lastUsedAt, + }, }, })); @@ -119,104 +101,29 @@ export const CapStateStore = create()( get().saveToDB(); }, - uninstallCap: (id: string) => { - set((state) => { - const { [id]: removed, ...restCaps } = state.installedCaps; - return { - installedCaps: restCaps, - }; - }); - - // Delete from IndexedDB asynchronously - const deleteFromDB = async () => { - try { - await capDB.caps.delete(id); - } catch (error) { - console.error('Failed to delete cap from DB:', error); - } - }; - deleteFromDB(); - }, - // Data management - updateInstalledCap: (id: string, updatedCap: Cap) => { + updateInstalledCap: ( + id: string, + updates: Partial>, + ) => { const { installedCaps } = get(); - const cap = installedCaps[id]; + const installedCap = installedCaps[id]; - if (!cap) return; + if (!installedCap) return; set((state) => ({ installedCaps: { ...state.installedCaps, - [id]: updatedCap, + [id]: { + ...installedCap, + ...updates, + }, }, })); get().saveToDB(); }, - // Remote caps management - setRemoteCaps: (caps: Cap[]) => { - set((state) => ({ - remoteCapState: { - ...state.remoteCapState, - remoteCaps: caps, - }, - })); - }, - - setRemoteCapLoading: (isLoading: boolean) => { - set((state) => ({ - remoteCapState: { - ...state.remoteCapState, - isLoading, - }, - })); - }, - - setRemoteCapError: (error: string | null) => { - set((state) => ({ - remoteCapState: { - ...state.remoteCapState, - error, - }, - })); - }, - - setRemoteCapPagination: (pagination: { - totalCount: number; - page: number; - hasMore: boolean; - }) => { - set((state) => ({ - remoteCapState: { - ...state.remoteCapState, - ...pagination, - }, - })); - }, - - setLastSearchQuery: (query: string) => { - set((state) => ({ - remoteCapState: { - ...state.remoteCapState, - lastSearchQuery: query, - }, - })); - }, - - clearRemoteCaps: () => { - set((state) => ({ - remoteCapState: { - ...state.remoteCapState, - remoteCaps: [], - totalCount: 0, - page: 1, - hasMore: false, - }, - })); - }, - clearAllInstalledCaps: () => { set({ installedCaps: {}, @@ -228,7 +135,7 @@ export const CapStateStore = create()( const currentDID = await getCurrentDID(); if (!currentDID) return; - await capDB.caps.where('did').equals(currentDID).delete(); + await capDB.capStore.where('did').equals(currentDID).delete(); } catch (error) { console.error('Failed to clear caps from DB:', error); } @@ -236,6 +143,11 @@ export const CapStateStore = create()( clearDB(); }, + // Remote cap management + setRemoteCaps: (caps: RemoteCap[]) => { + set({ remoteCaps: caps }); + }, + // Data persistence methods loadFromDB: async () => { if (typeof window === 'undefined') return; @@ -244,19 +156,24 @@ export const CapStateStore = create()( const currentDID = await getCurrentDID(); if (!currentDID) return; - const caps = await capDB.caps + const installedCaps = await capDB.capStore .where('did') .equals(currentDID) .toArray(); - const capsMap: Record = {}; + const installedCapsMap: Record = {}; - caps.forEach((cap: Cap) => { - capsMap[cap.id] = cap; + installedCaps.forEach((installedCap: any) => { + const { id, ...capData } = installedCap; + installedCapsMap[id] = { + capData: capData.capData, + isFavorite: capData.isFavorite, + lastUsedAt: capData.lastUsedAt, + }; }); set((state) => ({ - installedCaps: { ...state.installedCaps, ...capsMap }, + installedCaps: { ...state.installedCaps, ...installedCapsMap }, })); } catch (error) { console.error('Failed to load caps from DB:', error); @@ -271,11 +188,12 @@ export const CapStateStore = create()( if (!currentDID) return; const { installedCaps } = get(); - const capsToSave = Object.values(installedCaps).map((cap) => ({ + const capsToSave = Object.entries(installedCaps).map(([id, cap]) => ({ + id, ...cap, did: currentDID, })); - await capDB.caps.bulkPut(capsToSave); + await capDB.capStore.bulkPut(capsToSave); } catch (error) { console.error('Failed to save caps to DB:', error); } diff --git a/src/features/cap-store/types.ts b/src/features/cap-store/types.ts new file mode 100644 index 00000000..f6022dd4 --- /dev/null +++ b/src/features/cap-store/types.ts @@ -0,0 +1,13 @@ +import type { Cap, CapID, CapMetadata } from '@/shared/types/cap'; + +export type RemoteCap = CapID & { + cid: string; + version: string; + metadata: CapMetadata; +}; + +export type InstalledCap = { + capData: Cap; + isFavorite: boolean; + lastUsedAt: number | null; +}; diff --git a/src/features/cap-studio/components/cap-edit/model-details.tsx b/src/features/cap-studio/components/cap-edit/model-details.tsx index fd955e1c..79fc792b 100644 --- a/src/features/cap-studio/components/cap-edit/model-details.tsx +++ b/src/features/cap-studio/components/cap-edit/model-details.tsx @@ -19,7 +19,7 @@ export function ModelDetails({ model }: ModelDetailsProps) { {getProviderName(model)}
- Context window: {model.context_length?.toLocaleString() || 'Unknown'}{' '} + Context window: {model.contextLength?.toLocaleString() || 'Unknown'}{' '} tokens
diff --git a/src/features/cap-studio/components/cap-submit/thumbnail-upload.tsx b/src/features/cap-studio/components/cap-submit/thumbnail-upload.tsx index a52f61cd..7e497ff0 100644 --- a/src/features/cap-studio/components/cap-submit/thumbnail-upload.tsx +++ b/src/features/cap-studio/components/cap-submit/thumbnail-upload.tsx @@ -105,10 +105,10 @@ export function ThumbnailUpload({ return ( - + Thumbnail - + Upload a file or enter an image URL to set your Cap thumbnail @@ -117,7 +117,7 @@ export function ThumbnailUpload({
{hasThumbnail ? ( -
+
Thumbnail Preview
) : ( -
- +
+
)}
@@ -153,17 +153,17 @@ export function ThumbnailUpload({ onValueChange={(value) => setActiveTab(value as 'upload' | 'url')} className="w-full" > - + Upload File Image URL @@ -182,12 +182,12 @@ export function ThumbnailUpload({
diff --git a/src/features/cap-store/components/cap-store-sidebar.tsx b/src/features/cap-store/components/cap-store-sidebar.tsx index 860b4e29..d98c47bd 100644 --- a/src/features/cap-store/components/cap-store-sidebar.tsx +++ b/src/features/cap-store/components/cap-store-sidebar.tsx @@ -16,7 +16,7 @@ import { import { useEffect, useState } from 'react'; import { Input } from '@/shared/components/ui'; import { predefinedTags } from '@/shared/constants/cap'; -import { useLanguage } from '@/shared/hooks'; +import { useDebounceValue, useLanguage } from '@/shared/hooks'; export interface CapStoreSidebarProps { activeSection: CapStoreSidebarSection; @@ -37,6 +37,8 @@ export function CapStoreSidebar({ }: CapStoreSidebarProps) { const { t } = useLanguage(); const [searchValue, setSearchValue] = useState(''); + const [debouncedSearchValue, setDebouncedSearchValue] = useDebounceValue('', 500) + const sidebarSections: CapStoreSidebarSection[] = [ { @@ -104,14 +106,19 @@ export function CapStoreSidebar({ setSearchValue(''); }, [activeSection]); + useEffect(() => { + onSearchChange(debouncedSearchValue); + }, [debouncedSearchValue]); + const handleSearchChange = (value: string) => { - setSearchValue(value); - onSearchChange(value); + setSearchValue(value) + setDebouncedSearchValue(value) }; const handleClearSearch = () => { setSearchValue(''); onSearchChange(''); + setDebouncedSearchValue(''); }; return ( diff --git a/src/features/cap-studio/components/cap-edit/cap-edit-form.tsx b/src/features/cap-studio/components/cap-edit/cap-edit-form.tsx index ba485403..e2ce41f4 100644 --- a/src/features/cap-studio/components/cap-edit/cap-edit-form.tsx +++ b/src/features/cap-studio/components/cap-edit/cap-edit-form.tsx @@ -14,13 +14,12 @@ import { FormLabel, FormMessage, Input, - MultiSelect, Textarea, } from '@/shared/components/ui'; -import { predefinedTags } from '@/shared/constants/cap'; import { useEditForm } from '../../hooks/use-edit-form'; import { DashboardGrid } from '../layout/dashboard-layout'; import { ModelSelectorDialog } from '../model-selector'; +import { CapTags } from './cap-tags'; import { McpServersConfig } from './mcp-servers-config'; import { ModelDetails } from './model-details'; import { PromptEditor } from './prompt-editor'; @@ -155,15 +154,10 @@ export function CapEditForm({ editingCap }: CapEditFormProps) { Select one or more tags that describe your cap.

- ({ - label: tag, - value: tag, - }))} - onValueChange={field.onChange} - defaultValue={field.value || []} - placeholder="Select tags..." - className="w-full" + diff --git a/src/features/cap-studio/components/cap-edit/cap-tags.tsx b/src/features/cap-studio/components/cap-edit/cap-tags.tsx new file mode 100644 index 00000000..8d09b93d --- /dev/null +++ b/src/features/cap-studio/components/cap-edit/cap-tags.tsx @@ -0,0 +1,82 @@ +import { + Tags, + TagsContent, + TagsEmpty, + TagsGroup, + TagsInput, + TagsItem, + TagsList, + TagsTrigger, + TagsValue, +} from '@/shared/components/ui/shadcn-io/tags'; +import { predefinedTags } from '@/shared/constants/cap'; + +interface CapTagsProps { + value: string[]; + onChange: (tags: string[]) => void; + placeholder?: string; +} + +export function CapTags({ + value, + onChange, + placeholder = "Search tags..." +}: CapTagsProps) { + const handleTagsChange = (tagsString: string) => { + const tags = tagsString + ? tagsString + .split(',') + .map((tag) => tag.trim()) + .filter(Boolean) + : []; + onChange(tags); + }; + + const handleTagRemove = (tagToRemove: string) => { + const newTags = value?.filter((t) => t !== tagToRemove) || []; + onChange(newTags); + }; + + const handleTagSelect = (tag: string) => { + const currentTags = value || []; + if (!currentTags.includes(tag)) { + onChange([...currentTags, tag]); + } + }; + + return ( + + + {value && value.length > 0 + ? value.map((tag) => ( + handleTagRemove(tag)} + > + {tag} + + )) + : null} + + + + + No tags found. + + {predefinedTags.map((tag) => ( + handleTagSelect(tag)} + > + {tag} + + ))} + + + + + ); +} \ No newline at end of file diff --git a/src/features/cap-studio/hooks/use-edit-form.ts b/src/features/cap-studio/hooks/use-edit-form.ts index c8a2b2f0..c47b26da 100644 --- a/src/features/cap-studio/hooks/use-edit-form.ts +++ b/src/features/cap-studio/hooks/use-edit-form.ts @@ -44,7 +44,7 @@ export const useEditForm = ({ editingCap }: UseEditFormProps) => { const navigate = useNavigate(); const { did } = useAuth(); const { createCap, updateCap } = useLocalCapsHandler(); - const { selectedModel } = useSelectedModel(); + const { selectedModel, setSelectedModel } = useSelectedModel(); const [isSaving, setIsSaving] = useState(false); const [mcpServers, setMcpServers] = useState< Record @@ -74,8 +74,12 @@ export const useEditForm = ({ editingCap }: UseEditFormProps) => { useEffect(() => { if (editingCap) { setMcpServers(editingCap.capData.core.mcpServers || {}); + // Set the selected model to the cap's configured model + if (editingCap.capData.core.model) { + setSelectedModel(editingCap.capData.core.model); + } } - }, [editingCap]); + }, [editingCap, setSelectedModel]); const handleUpdateMcpServers = ( servers: Record, diff --git a/src/features/chat/components/index.tsx b/src/features/chat/components/index.tsx index a44bc06d..cf7e08a5 100644 --- a/src/features/chat/components/index.tsx +++ b/src/features/chat/components/index.tsx @@ -69,7 +69,6 @@ export function Chat({ setMessages={setChatMessages} reload={reload} isReadonly={isReadonly} - isArtifact={false} />
= ({ + src, + alt, + title, + className, + ...props +}) => { + const handleImageError = ( + e: React.SyntheticEvent, + ) => { + // 图片加载失败时的处理 + console.warn('Image failed to load:', src); + // 可以设置默认图片或显示错误状态 + }; + + return ( + + {alt + + ); +}; + const Code: React.FC = ({ inline, children = [], @@ -52,6 +97,7 @@ const NonMemoizedMarkdown = ({ children }: { children: string }) => { rehypePlugins={rehypePlugins} components={{ code: Code, + img: Image, }} /> ); diff --git a/src/features/chat/components/message-actions.tsx b/src/features/chat/components/message-actions.tsx index 435d7013..92152bbd 100644 --- a/src/features/chat/components/message-actions.tsx +++ b/src/features/chat/components/message-actions.tsx @@ -1,8 +1,8 @@ import type { Message } from 'ai'; import { CopyIcon } from 'lucide-react'; import { memo } from 'react'; -import { useCopyToClipboard } from 'usehooks-ts'; import { toast } from 'sonner'; +import { useCopyToClipboard } from 'usehooks-ts'; import { Button } from '@/shared/components/ui/button'; import { Tooltip, @@ -13,14 +13,14 @@ import { export function PureMessageActions({ message, - isLoading, + isStreaming, }: { message: Message; - isLoading: boolean; + isStreaming: boolean; }) { const [_, copyToClipboard] = useCopyToClipboard(); - if (isLoading) return null; + if (isStreaming) return null; if (message.role === 'user') return null; return ( @@ -60,7 +60,7 @@ export function PureMessageActions({ export const MessageActions = memo( PureMessageActions, (prevProps, nextProps) => { - if (prevProps.isLoading !== nextProps.isLoading) return false; + if (prevProps.isStreaming !== nextProps.isStreaming) return false; return true; }, ); diff --git a/src/features/chat/components/message-reasoning.tsx b/src/features/chat/components/message-reasoning.tsx index e8a4d1be..d81b181b 100644 --- a/src/features/chat/components/message-reasoning.tsx +++ b/src/features/chat/components/message-reasoning.tsx @@ -1,76 +1,189 @@ -import { AnimatePresence, motion } from 'framer-motion'; -import { ChevronDownIcon, LoaderIcon } from 'lucide-react'; -import { useState } from 'react'; +import { useControllableState } from '@radix-ui/react-use-controllable-state'; +import { Brain, ChevronDownIcon } from 'lucide-react'; +import type { ComponentProps } from 'react'; +import { createContext, memo, useContext, useEffect, useState } from 'react'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/shared/components/ui'; +import { cn } from '@/shared/utils/index'; import { Markdown } from './markdown'; +type AIReasoningContextValue = { + isStreaming: boolean; + isOpen: boolean; + setIsOpen: (open: boolean) => void; + duration: number; +}; + +const AIReasoningContext = createContext(null); + +const useAIReasoning = () => { + const context = useContext(AIReasoningContext); + if (!context) { + throw new Error('AIReasoning components must be used within AIReasoning'); + } + return context; +}; + +export type AIReasoningProps = ComponentProps & { + isStreaming?: boolean; + open?: boolean; + defaultOpen?: boolean; + onOpenChange?: (open: boolean) => void; + duration?: number; +}; + +export const AIReasoning = memo( + ({ + className, + isStreaming = false, + open, + defaultOpen = false, + onOpenChange, + duration: durationProp, + children, + ...props + }: AIReasoningProps) => { + const [isOpen, setIsOpen] = useControllableState({ + prop: open, + defaultProp: defaultOpen, + onChange: onOpenChange, + }); + const [duration, setDuration] = useControllableState({ + prop: durationProp, + defaultProp: 0, + }); + + const [hasAutoClosedRef, setHasAutoClosedRef] = useState(false); + const [startTime, setStartTime] = useState(null); + + // Track duration when streaming starts and ends + useEffect(() => { + if (isStreaming) { + if (startTime === null) { + setStartTime(Date.now()); + } + } else if (startTime !== null) { + setDuration(Math.round((Date.now() - startTime) / 1000)); + setStartTime(null); + } + }, [isStreaming, startTime, setDuration]); + + // Auto-open when streaming starts, auto-close when streaming ends (once only) + useEffect(() => { + if (isStreaming && !isOpen) { + setIsOpen(true); + } else if (!isStreaming && isOpen && !defaultOpen && !hasAutoClosedRef) { + // Add a small delay before closing to allow user to see the content + const timer = setTimeout(() => { + setIsOpen(false); + setHasAutoClosedRef(true); + }, 1000); + return () => clearTimeout(timer); + } + }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosedRef]); + + const handleOpenChange = (open: boolean) => { + setIsOpen(open); + }; + + return ( + + + {children} + + + ); + }, +); + +export type AIReasoningTriggerProps = ComponentProps< + typeof CollapsibleTrigger +> & { + title?: string; +}; + +export const AIReasoningTrigger = memo( + ({ + className, + title = 'Reasoning', + children, + ...props + }: AIReasoningTriggerProps) => { + const { isStreaming, isOpen, duration } = useAIReasoning(); + + return ( + + {children ?? ( + <> + + {isStreaming && duration === 0 ? ( +

Reasoning...

+ ) : ( +

{duration === 0 ? 'Reasoned for a few seconds' : `Reasoned for ${duration} seconds`}

+ )} + + + )} +
+ ); + }, +); + +export type AIReasoningContentProps = ComponentProps< + typeof CollapsibleContent +> & { + children: string; +}; + +export const AIReasoningContent = memo( + ({ className, children, ...props }: AIReasoningContentProps) => ( + + {children} + + ), +); + +AIReasoning.displayName = 'AIReasoning'; +AIReasoningTrigger.displayName = 'AIReasoningTrigger'; +AIReasoningContent.displayName = 'AIReasoningContent'; + interface MessageReasoningProps { - isLoading: boolean; - reasoning: string; + isStreaming: boolean; + content: string; } -export function MessageReasoning({ - isLoading, - reasoning, -}: MessageReasoningProps) { - const [isExpanded, setIsExpanded] = useState(false); - - const variants = { - collapsed: { - height: 0, - opacity: 0, - marginTop: 0, - marginBottom: 0, - }, - expanded: { - height: 'auto', - opacity: 1, - marginTop: '1rem', - marginBottom: '0.5rem', - }, - }; - +export const MessageReasoning = ({ isStreaming, content }: MessageReasoningProps) => { return ( -
- {isLoading ? ( -
-
Reasoning
-
- -
-
- ) : ( -
-
Reasoned for a few seconds
- -
- )} - - - {(isExpanded || isLoading) && ( - - {reasoning} - - )} - -
+ + + {content} + ); -} +}; \ No newline at end of file diff --git a/src/features/chat/components/message-source-item.tsx b/src/features/chat/components/message-source-item.tsx deleted file mode 100644 index 63761569..00000000 --- a/src/features/chat/components/message-source-item.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { ExternalLinkIcon, Globe } from 'lucide-react'; -import { useState } from 'react'; -import { Skeleton } from '@/shared/components/ui/skeleton'; - -interface MessageSourceItemProps { - source: { - id?: string; - title?: string; - url?: string; - }; - index: number; - onSourceClick: (url: string) => void; -} - -interface UrlMetadata { - title?: string; - description?: string; - image?: string; - logo?: string; - url?: string; - publisher?: string; -} - -export const MessageSourceItem = ({ - source, - index, - onSourceClick, -}: MessageSourceItemProps) => { - const [metadata, setMetadata] = useState(null); - const [loading, setLoading] = useState(false); - - // todo: need to fix the cors issue here - // useEffect(() => { - // const fetchMetadata = async () => { - // setLoading(true); - // const metadata = await urlMetadata(source.url || ''); - // setMetadata(metadata); - // setLoading(false); - // }; - // fetchMetadata(); - // }, [source.url]); - - const title = metadata?.title || source.title || 'Untitled Source'; - const url = source.url || ''; - const id = source.id || 'unknown'; - const isExternalUrl = url.startsWith('http'); - - if (isExternalUrl && url) { - return ( - - ); - } - - return ( -
- - -
- - {index + 1} - - - - {title} - - - {url && ( - - • {url} - - )} -
-
- ); -}; diff --git a/src/features/chat/components/message-source-sidebar.tsx b/src/features/chat/components/message-source-sidebar.tsx new file mode 100644 index 00000000..26960e79 --- /dev/null +++ b/src/features/chat/components/message-source-sidebar.tsx @@ -0,0 +1,119 @@ +import { LinkIcon } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/shared/components/ui/sheet'; +import { useUrlMetadata } from '../hooks/use-url-metadata'; +import type { UrlMetadata } from '../types'; +import { SourceCard } from './source-card'; + +interface MessageSourceSidebarProps { + sources: Array<{ + id?: string; + title?: string; + url?: string; + }>; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +} + +export const MessageSourceSidebar = ({ + sources, + isOpen, + onOpenChange, +}: MessageSourceSidebarProps) => { + const [metadataMap, setMetadataMap] = useState>( + {}, + ); + const [loading, setLoading] = useState(false); + const { getUrlMetadata } = useUrlMetadata(); + + useEffect(() => { + if (!isOpen || sources.length === 0) return; + + const fetchMetadata = async () => { + setLoading(true); + try { + const urls = sources + .map((source) => source.url) + .filter((url): url is string => + Boolean(url && url.startsWith('http')), + ); + + if (urls.length === 0) { + setLoading(false); + return; + } + + const metadata = await getUrlMetadata(urls); + + // Map the metadata to URLs + const metadataByUrl: Record = {}; + if (Array.isArray(metadata)) { + metadata.forEach((meta, index) => { + if (urls[index]) { + metadataByUrl[urls[index]] = meta; + } + }); + } else if (metadata && typeof metadata === 'object') { + // Handle case where metadata is returned as an object with URL keys + Object.entries(metadata).forEach(([url, meta]) => { + metadataByUrl[url] = meta as UrlMetadata; + }); + } + + setMetadataMap(metadataByUrl); + } catch (error) { + console.error('Error fetching URL metadata:', error); + } finally { + setLoading(false); + } + }; + + fetchMetadata(); + }, [isOpen, sources]); + + const handleSourceClick = (url: string) => { + if (url.startsWith('http')) { + window.open(url, '_blank', 'noopener,noreferrer'); + } + }; + + return ( + + + +
+
+ + + Sources ({sources.length}) + +
+
+
+ +
+ {sources.map((source, index) => { + const url = source.url || ''; + const metadata = url ? metadataMap[url] : null; + + return ( + handleSourceClick(url)} + /> + ); + })} +
+
+
+ ); +}; diff --git a/src/features/chat/components/message-source.tsx b/src/features/chat/components/message-source.tsx index c0f044bf..f6895341 100644 --- a/src/features/chat/components/message-source.tsx +++ b/src/features/chat/components/message-source.tsx @@ -1,13 +1,8 @@ -import { ChevronDownIcon, ChevronUpIcon, LinkIcon } from 'lucide-react'; +import { LinkIcon } from 'lucide-react'; import { useState } from 'react'; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from '@/shared/components/ui/collapsible'; import { cn } from '@/shared/utils'; -import { MessageSourceItem } from './message-source-item'; +import { MessageSourceSidebar } from './message-source-sidebar'; interface MessageSourceProps { sources: Array<{ @@ -25,40 +20,30 @@ export const MessageSource = ({ sources, className }: MessageSourceProps) => { return null; } - const handleSourceClick = (url: string) => { - if (url.startsWith('http')) { - window.open(url, '_blank', 'noopener,noreferrer'); - } + const handleButtonClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsOpen(!isOpen); }; return (
- - - - - {sources.length} source{sources.length !== 1 ? 's' : ''} - - {isOpen ? ( - - ) : ( - - )} - + - -
- {sources.map((source, index) => ( - - ))} -
-
-
+
); }; diff --git a/src/features/chat/components/message.tsx b/src/features/chat/components/message.tsx index 58e77493..3ebbb28f 100644 --- a/src/features/chat/components/message.tsx +++ b/src/features/chat/components/message.tsx @@ -15,7 +15,8 @@ import { ToolResult } from './tool-result'; const PurePreviewMessage = ({ chatId, message, - isLoading, + isStreaming, + isStreamingReasoning, setMessages, reload, isReadonly, @@ -23,7 +24,8 @@ const PurePreviewMessage = ({ }: { chatId: string; message: UIMessage; - isLoading: boolean; + isStreaming: boolean; + isStreamingReasoning: boolean; setMessages: UseChatHelpers['setMessages']; reload: UseChatHelpers['reload']; isReadonly: boolean; @@ -57,10 +59,15 @@ const PurePreviewMessage = ({ {message.parts?.map((part, index) => { if (part.type !== 'reasoning') return null; return ( + // ); })} @@ -147,7 +154,7 @@ const PurePreviewMessage = ({ )}
@@ -160,7 +167,7 @@ const PurePreviewMessage = ({ export const PreviewMessage = memo( PurePreviewMessage, (prevProps, nextProps) => { - if (prevProps.isLoading !== nextProps.isLoading) return false; + if (prevProps.isStreaming !== nextProps.isStreaming) return false; if (prevProps.message.id !== nextProps.message.id) return false; if (prevProps.requiresScrollPadding !== nextProps.requiresScrollPadding) return false; diff --git a/src/features/chat/components/messages.tsx b/src/features/chat/components/messages.tsx index bb60ec86..086efb9e 100644 --- a/src/features/chat/components/messages.tsx +++ b/src/features/chat/components/messages.tsx @@ -13,7 +13,6 @@ interface MessagesProps { setMessages: UseChatHelpers['setMessages']; reload: UseChatHelpers['reload']; isReadonly: boolean; - isArtifact: boolean; } function PureMessages({ @@ -23,7 +22,6 @@ function PureMessages({ setMessages, reload, isReadonly, - isArtifact, }: MessagesProps) { const { containerRef: messagesContainerRef, @@ -41,20 +39,25 @@ function PureMessages({ ref={messagesContainerRef} className="flex flex-col min-w-0 gap-6 h-full overflow-y-scroll pt-4 relative" > - {messages.map((message, index) => ( - - ))} + {messages.map((message, index) => { + const isStreaming = status === 'streaming' && messages.length - 1 === index; + const isStreamingReasoning = isStreaming && message.role === 'assistant' && message.parts?.some((part) => part.type === 'reasoning') && !message.parts?.some((part) => part.type === 'text'); + return ( + + ) + })} {status === 'submitted' && messages.length > 0 && diff --git a/src/features/chat/components/multimodal-input.tsx b/src/features/chat/components/multimodal-input.tsx index 177bfa6a..c4ef9cca 100644 --- a/src/features/chat/components/multimodal-input.tsx +++ b/src/features/chat/components/multimodal-input.tsx @@ -27,7 +27,7 @@ import { Button } from '@/shared/components/ui/button'; import { useCurrentCap } from '@/shared/hooks/use-current-cap'; import { useDevMode } from '@/shared/hooks/use-dev-mode'; import type { Cap } from '@/shared/types/cap'; - +import { useUpdateMessages } from '../hooks/use-update-messages'; import { SuggestedActions } from './suggested-actions'; function PureMultimodalInput({ @@ -208,7 +208,11 @@ function PureMultimodalInput({ )} */} {status === 'submitted' || status === 'streaming' ? ( - + ) : ( void; setMessages: UseChatHelpers['setMessages']; + chatId: string; }) { + const updateMessages = useUpdateMessages(); return ( +
+ ); +}; diff --git a/src/features/chat/hooks/use-update-messages.ts b/src/features/chat/hooks/use-update-messages.ts new file mode 100644 index 00000000..4cabc31c --- /dev/null +++ b/src/features/chat/hooks/use-update-messages.ts @@ -0,0 +1,6 @@ +import { ChatStateStore } from '../stores'; + +export const useUpdateMessages = () => { + const { updateMessages } = ChatStateStore.getState(); + return updateMessages; +}; diff --git a/src/features/chat/hooks/use-url-metadata.ts b/src/features/chat/hooks/use-url-metadata.ts new file mode 100644 index 00000000..fa056d12 --- /dev/null +++ b/src/features/chat/hooks/use-url-metadata.ts @@ -0,0 +1,31 @@ +import { createAuthorizedFetch } from '@/shared/services/authorized-fetch'; + +export const useUrlMetadata = () => { + const getUrlMetadata = async (urls: string[]) => { + try { + const authorizedFetch = createAuthorizedFetch(); + const response = await authorizedFetch( + 'https://docs.nuwa.dev/api/url-metadata', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ urls: urls }), + }, + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const metadata = await response.json(); + return metadata; + } catch (error) { + console.error('Error fetching URL metadata:', error); + throw error; + } + }; + + return { getUrlMetadata }; +}; diff --git a/src/features/chat/types.ts b/src/features/chat/types.ts index 2c139088..38db0160 100644 --- a/src/features/chat/types.ts +++ b/src/features/chat/types.ts @@ -10,3 +10,20 @@ export interface ChatSession { pinned?: boolean; did?: string; // Added for IndexedDB storage } + +export interface UrlMetadata { + url?: string; + title?: string; + description?: string; + image?: string; + favicons?: Array<{ + rel: string; + href: string; + sizes?: string; + }>; + 'og:image'?: string; + 'og:site_name'?: string; + 'og:title'?: string; + 'og:description'?: string; + publisher?: string; +} diff --git a/src/features/settings/components/sections/system-section.tsx b/src/features/settings/components/sections/system-section.tsx index 1a6c534b..b611d75c 100644 --- a/src/features/settings/components/sections/system-section.tsx +++ b/src/features/settings/components/sections/system-section.tsx @@ -10,6 +10,7 @@ export function SystemSection() { const [isClearing, setIsClearing] = useState(false); const { clearAllStorage } = useStorage(); const { settings, setSetting } = useSettings(); + const isDevMode = settings.devMode; // Clear all storage logic const handleClearStorage = async () => { @@ -48,7 +49,7 @@ export function SystemSection() { disabled={false} /> - + />}
); } diff --git a/src/features/sidebar/components/search-modal.tsx b/src/features/sidebar/components/search-modal.tsx index 09b76afe..cfac892a 100644 --- a/src/features/sidebar/components/search-modal.tsx +++ b/src/features/sidebar/components/search-modal.tsx @@ -1,15 +1,51 @@ -import { formatDistanceToNow } from 'date-fns'; -import { MessageSquare } from 'lucide-react'; +import { formatDistanceToNow, isSameDay, startOfDay } from 'date-fns'; +import { MessageSquare, X } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import * as Dialog from '@/shared/components/ui'; -import { Input } from '@/shared/components/ui'; +import { Button, Input } from '@/shared/components/ui'; +import { + MiniCalendar, + MiniCalendarDay, + MiniCalendarDays, + MiniCalendarNavigation, +} from '@/shared/components/ui/shadcn-io/mini-calendar'; import { useLanguage } from '@/shared/hooks/use-language'; import { useSearch } from '../hooks/use-search'; export function SearchModal({ children }: { children: React.ReactNode }) { const navigate = useNavigate(); const { t } = useLanguage(); - const { filtered, setQuery, query } = useSearch(); + const { sessionList, setQuery, query } = useSearch(); + const [selectedDate, setSelectedDate] = useState(undefined); + + const filtered = useMemo(() => { + let results = sessionList; + + // Filter by search query + if (query.trim()) { + results = results.filter((s) => + s.title.toLowerCase().includes(query.toLowerCase()), + ); + } + + // Filter by selected date + if (selectedDate) { + results = results.filter((s) => + isSameDay(new Date(s.updatedAt), selectedDate), + ); + } + + return results; + }, [query, sessionList, selectedDate]); + + // Update calendar selection when scrolling through results + useEffect(() => { + if (!query.trim() && filtered.length > 0 && !selectedDate) { + const latestDate = new Date(filtered[0].updatedAt); + setSelectedDate(startOfDay(latestDate)); + } + }, [filtered, query, selectedDate]); return ( @@ -17,8 +53,8 @@ export function SearchModal({ children }: { children: React.ReactNode }) { {t('search.searchHistory')} - setQuery(e.target.value)} - className="w-full rounded-none border-0 border-b focus-visible:ring-0 focus-visible:border-primary" - /> -
+
+
+ setQuery(e.target.value)} + className="w-full rounded-none border-0 border-b focus-visible:ring-0 focus-visible:border-primary pr-8" + /> + {query && ( + + )} +
+ + + + + {(date) => ( + + )} + + + +
+
{filtered.length === 0 ? (
{t('search.noChatsHistory')} diff --git a/src/shared/components/ui/shadcn-io/image-zoom.tsx b/src/shared/components/ui/shadcn-io/image-zoom.tsx new file mode 100644 index 00000000..e088a26c --- /dev/null +++ b/src/shared/components/ui/shadcn-io/image-zoom.tsx @@ -0,0 +1,48 @@ +import Zoom, { + type ControlledProps, + type UncontrolledProps, +} from 'react-medium-image-zoom'; +import { cn } from '../../../utils'; +export type ImageZoomProps = UncontrolledProps & { + isZoomed?: ControlledProps['isZoomed']; + onZoomChange?: ControlledProps['onZoomChange']; + className?: string; + backdropClassName?: string; +}; +export const ImageZoom = ({ + className, + backdropClassName, + ...props +}: ImageZoomProps) => ( +
+ +
+); diff --git a/src/shared/components/ui/shadcn-io/mini-calendar.tsx b/src/shared/components/ui/shadcn-io/mini-calendar.tsx new file mode 100644 index 00000000..c9d1e28c --- /dev/null +++ b/src/shared/components/ui/shadcn-io/mini-calendar.tsx @@ -0,0 +1,228 @@ +import { useControllableState } from '@radix-ui/react-use-controllable-state'; +import { addDays, format, isSameDay, isToday } from 'date-fns'; +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; +import { Slot } from 'radix-ui'; +import { + type ButtonHTMLAttributes, + type ComponentProps, + createContext, + type HTMLAttributes, + type MouseEventHandler, + type ReactNode, + useContext, +} from 'react'; +import { Button } from '@/shared/components/ui/button'; +import { cn } from '@/shared/utils/index'; + +// Context for sharing state between components +type MiniCalendarContextType = { + selectedDate: Date | null | undefined; + onDateSelect: (date: Date) => void; + startDate: Date; + onNavigate: (direction: 'prev' | 'next') => void; + days: number; +}; + +const MiniCalendarContext = createContext(null); + +const useMiniCalendar = () => { + const context = useContext(MiniCalendarContext); + + if (!context) { + throw new Error('MiniCalendar components must be used within MiniCalendar'); + } + + return context; +}; + +// Helper function to get array of consecutive dates +const getDays = (startDate: Date, count: number): Date[] => { + const days: Date[] = []; + for (let i = 0; i < count; i++) { + days.push(addDays(startDate, i)); + } + return days; +}; + +// Helper function to format date +const formatDate = (date: Date) => { + const month = format(date, 'MMM'); + const day = format(date, 'd'); + + return { month, day }; +}; + +export type MiniCalendarProps = HTMLAttributes & { + value?: Date; + defaultValue?: Date; + onValueChange?: (date: Date | undefined) => void; + startDate?: Date; + defaultStartDate?: Date; + onStartDateChange?: (date: Date | undefined) => void; + days?: number; +}; + +export const MiniCalendar = ({ + value, + defaultValue, + onValueChange, + startDate, + defaultStartDate = new Date(), + onStartDateChange, + days = 5, + className, + children, + ...props +}: MiniCalendarProps) => { + const [selectedDate, setSelectedDate] = useControllableState< + Date | undefined + >({ + prop: value, + defaultProp: defaultValue, + onChange: onValueChange, + }); + + const [currentStartDate, setCurrentStartDate] = useControllableState({ + prop: startDate, + defaultProp: defaultStartDate, + onChange: onStartDateChange, + }); + + const handleDateSelect = (date: Date) => { + setSelectedDate(date); + }; + + const handleNavigate = (direction: 'prev' | 'next') => { + const newStartDate = addDays( + currentStartDate || new Date(), + direction === 'next' ? days : -days + ); + setCurrentStartDate(newStartDate); + }; + + const contextValue: MiniCalendarContextType = { + selectedDate: selectedDate || null, + onDateSelect: handleDateSelect, + startDate: currentStartDate || new Date(), + onNavigate: handleNavigate, + days, + }; + + return ( + +
+ {children} +
+
+ ); +}; + +export type MiniCalendarNavigationProps = + ButtonHTMLAttributes & { + direction: 'prev' | 'next'; + asChild?: boolean; + }; + +export const MiniCalendarNavigation = ({ + direction, + asChild = false, + children, + onClick, + ...props +}: MiniCalendarNavigationProps) => { + const { onNavigate } = useMiniCalendar(); + const Icon = direction === 'prev' ? ChevronLeftIcon : ChevronRightIcon; + + const handleClick: MouseEventHandler = (event) => { + onNavigate(direction); + onClick?.(event); + }; + + if (asChild) { + return ( + + {children} + + ); + } + + return ( + + ); +}; + +export type MiniCalendarDaysProps = Omit< + HTMLAttributes, + 'children' +> & { + children: (date: Date) => ReactNode; +}; + +export const MiniCalendarDays = ({ + className, + children, + ...props +}: MiniCalendarDaysProps) => { + const { startDate, days: dayCount } = useMiniCalendar(); + const days = getDays(startDate, dayCount); + + return ( +
+ {days.map((date) => children(date))} +
+ ); +}; + +export type MiniCalendarDayProps = ComponentProps & { + date: Date; +}; + +export const MiniCalendarDay = ({ + date, + className, + ...props +}: MiniCalendarDayProps) => { + const { selectedDate, onDateSelect } = useMiniCalendar(); + const { month, day } = formatDate(date); + const isSelected = selectedDate && isSameDay(date, selectedDate); + const isTodayDate = isToday(date); + + return ( + + ); +}; diff --git a/src/shared/components/ui/shadcn-io/tags.tsx b/src/shared/components/ui/shadcn-io/tags.tsx new file mode 100644 index 00000000..b505db5a --- /dev/null +++ b/src/shared/components/ui/shadcn-io/tags.tsx @@ -0,0 +1,219 @@ +import { XIcon } from 'lucide-react'; +import { + type ComponentProps, + createContext, + type MouseEventHandler, + type ReactNode, + useContext, + useEffect, + useRef, + useState, +} from 'react'; +import { Badge } from '@/shared/components/ui/badge'; +import { Button } from '@/shared/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/shared/components/ui/command'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/shared/components/ui/popover'; +import { cn } from '@/shared/utils'; + +type TagsContextType = { + value?: string; + setValue?: (value: string) => void; + open: boolean; + onOpenChange: (open: boolean) => void; + width?: number; + setWidth?: (width: number) => void; +}; + +const TagsContext = createContext({ + value: undefined, + setValue: undefined, + open: false, + onOpenChange: () => { }, + width: undefined, + setWidth: undefined, +}); + +const useTagsContext = () => { + const context = useContext(TagsContext); + + if (!context) { + throw new Error('useTagsContext must be used within a TagsProvider'); + } + + return context; +}; + +export type TagsProps = { + value?: string; + setValue?: (value: string) => void; + open?: boolean; + onOpenChange?: (open: boolean) => void; + children?: ReactNode; + className?: string; +}; + +export const Tags = ({ + value, + setValue, + open: controlledOpen, + onOpenChange: controlledOnOpenChange, + children, + className, +}: TagsProps) => { + const [uncontrolledOpen, setUncontrolledOpen] = useState(false); + const [width, setWidth] = useState(); + const ref = useRef(null); + + const open = controlledOpen ?? uncontrolledOpen; + const onOpenChange = controlledOnOpenChange ?? setUncontrolledOpen; + + useEffect(() => { + if (!ref.current) { + return; + } + + const resizeObserver = new ResizeObserver((entries) => { + setWidth(entries[0].contentRect.width); + }); + + resizeObserver.observe(ref.current); + + return () => { + resizeObserver.disconnect(); + }; + }, []); + + return ( + + +
+ {children} +
+
+
+ ); +}; + +export type TagsTriggerProps = ComponentProps; + +export const TagsTrigger = ({ + className, + children, + ...props +}: TagsTriggerProps) => ( + + + +); + +export type TagsValueProps = ComponentProps; + +export const TagsValue = ({ + className, + children, + onRemove, + ...props +}: TagsValueProps & { onRemove?: () => void }) => { + const handleRemove: MouseEventHandler = (event) => { + event.preventDefault(); + event.stopPropagation(); + onRemove?.(); + }; + + return ( + + {children} + {onRemove && ( + // biome-ignore lint/a11y/noStaticElementInteractions: "This is a clickable badge" + // biome-ignore lint/a11y/useKeyWithClickEvents: "This is a clickable badge" +
+ +
+ )} +
+ ); +}; + +export type TagsContentProps = ComponentProps; + +export const TagsContent = ({ + className, + children, + ...props +}: TagsContentProps) => { + const { width } = useTagsContext(); + + return ( + + {children} + + ); +}; + +export type TagsInputProps = ComponentProps; + +export const TagsInput = ({ className, ...props }: TagsInputProps) => ( + +); + +export type TagsListProps = ComponentProps; + +export const TagsList = ({ className, ...props }: TagsListProps) => ( + +); + +export type TagsEmptyProps = ComponentProps; + +export const TagsEmpty = ({ + children, + className, + ...props +}: TagsEmptyProps) => ( + {children ?? 'No tags found.'} +); + +export type TagsGroupProps = ComponentProps; + +export const TagsGroup = CommandGroup; + +export type TagsItemProps = ComponentProps; + +export const TagsItem = ({ className, ...props }: TagsItemProps) => ( + +); diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts index 4f224d86..99afd307 100644 --- a/src/shared/hooks/index.ts +++ b/src/shared/hooks/index.ts @@ -1,6 +1,9 @@ export * from './use-capkit'; export * from './use-current-cap'; +export * from './use-debounce-callback'; +export * from './use-debounce-value'; export * from './use-dev-mode'; export * from './use-language'; export * from './use-mobile'; export * from './use-storage'; +export * from './use-unmount'; diff --git a/src/shared/hooks/use-debounce-callback.tsx b/src/shared/hooks/use-debounce-callback.tsx new file mode 100644 index 00000000..903d599a --- /dev/null +++ b/src/shared/hooks/use-debounce-callback.tsx @@ -0,0 +1,63 @@ +import debounce from 'lodash.debounce'; +import * as React from 'react'; + +import { useUnmount } from './use-unmount'; + +type DebounceOptions = { + leading?: boolean; + trailing?: boolean; + maxWait?: number; +}; + +type ControlFunctions = { + cancel: () => void; + flush: () => void; + isPending: () => boolean; +}; + +export type DebouncedState ReturnType> = (( + ...args: Parameters +) => ReturnType | undefined) & + ControlFunctions; + +export function useDebounceCallback ReturnType>( + func: T, + delay = 500, + options?: DebounceOptions, +): DebouncedState { + const debouncedFunc = React.useRef>(null); + + useUnmount(() => { + if (debouncedFunc.current) { + debouncedFunc.current.cancel(); + } + }); + + const debounced = React.useMemo(() => { + const debouncedFuncInstance = debounce(func, delay, options); + + const wrappedFunc: DebouncedState = (...args: Parameters) => { + return debouncedFuncInstance(...args); + }; + + wrappedFunc.cancel = () => { + debouncedFuncInstance.cancel(); + }; + + wrappedFunc.isPending = () => { + return !!debouncedFunc.current; + }; + + wrappedFunc.flush = () => { + return debouncedFuncInstance.flush(); + }; + + return wrappedFunc; + }, [func, delay, options]); + + React.useEffect(() => { + debouncedFunc.current = debounce(func, delay, options); + }, [func, delay, options]); + + return debounced; +} diff --git a/src/shared/hooks/use-debounce-value.tsx b/src/shared/hooks/use-debounce-value.tsx new file mode 100644 index 00000000..f2ffae25 --- /dev/null +++ b/src/shared/hooks/use-debounce-value.tsx @@ -0,0 +1,45 @@ +import { useRef, useState } from 'react'; +import type { DebouncedState } from './use-debounce-callback'; +import { useDebounceCallback } from './use-debounce-callback'; + +type UseDebounceValueOptions = { + leading?: boolean; + trailing?: boolean; + maxWait?: number; + equalityFn?: (left: T, right: T) => boolean; +}; + +/** + * Custom hook that returns a debounced version of the provided value, along with a function to update it. + * @param initialValue The value to be debounced + * @param delay The delay in milliseconds before the value is updated (default is 500ms) + * @param options Optional configurations for the debouncing behavior + * @returns An array containing the debounced value and the function to update it + */ +export function useDebounceValue( + initialValue: T | (() => T), + delay: number, + options?: UseDebounceValueOptions, +): [T, DebouncedState<(value: T) => void>] { + const eq = options?.equalityFn ?? ((left: T, right: T) => left === right); + const unwrappedInitialValue = + initialValue instanceof Function ? initialValue() : initialValue; + const [debouncedValue, setDebouncedValue] = useState( + unwrappedInitialValue, + ); + const previousValueRef = useRef(unwrappedInitialValue); + + const updateDebouncedValue = useDebounceCallback( + setDebouncedValue, + delay, + options, + ); + + // Update the debounced value if the initial value changes + if (!eq(previousValueRef.current as T, unwrappedInitialValue)) { + updateDebouncedValue(unwrappedInitialValue); + previousValueRef.current = unwrappedInitialValue; + } + + return [debouncedValue, updateDebouncedValue]; +} diff --git a/src/shared/hooks/use-unmount.tsx b/src/shared/hooks/use-unmount.tsx new file mode 100644 index 00000000..b138e86a --- /dev/null +++ b/src/shared/hooks/use-unmount.tsx @@ -0,0 +1,36 @@ +import * as React from "react"; + +/** + * A React hook that runs a cleanup function when the component unmounts. + * + * @param fn - The cleanup function to run on unmount + * + * @example + * ```tsx + * function MyComponent() { + * useUnmount(() => { + * // Cleanup logic here + * console.log('Component is unmounting'); + * }); + * + * return
Hello world
; + * } + * ``` + */ +export function useUnmount(fn: () => void): void { + if (typeof fn !== "function") { + throw new Error("useUnmount expects a function as argument"); + } + + const fnRef = React.useRef(fn); + + // Keep the function reference up to date + fnRef.current = fn; + + React.useEffect(() => { + // Return the cleanup function that will be called on unmount + return () => { + fnRef.current(); + }; + }, []); +} \ No newline at end of file From a7f5a6cde2ff4d5692a72c98e5e8f4760fd84064 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Thu, 14 Aug 2025 18:06:27 +0800 Subject: [PATCH 09/28] refactor: cap studio --- docs/mcp-client-design.md | 227 ------- sample-caps-simplified.json | 88 +++ .../cap-studio/components/batch-create.tsx | 561 ++++++++++++++++++ .../components/cap-edit/cap-edit-form.tsx | 69 ++- .../thumbnail-upload.tsx | 0 .../components/cap-submit/author-form.tsx | 83 --- .../components/cap-submit/cap-information.tsx | 78 --- .../components/cap-submit/cap-submit-form.tsx | 299 ++++++---- .../components/cap-submit/index.tsx | 7 +- .../submission-confirmation-dialog.tsx | 132 ----- src/features/cap-studio/components/index.tsx | 3 + .../components/my-caps/cap-card.tsx | 204 +++++-- .../cap-studio/components/my-caps/index.tsx | 259 ++++---- .../cap-studio/hooks/use-edit-form.ts | 41 +- .../cap-studio/hooks/use-submit-form.ts | 28 +- src/pages/cap-studio-batch-create.tsx | 5 + src/router.tsx | 2 + src/shared/types/cap.ts | 77 --- 18 files changed, 1296 insertions(+), 867 deletions(-) delete mode 100644 docs/mcp-client-design.md create mode 100644 sample-caps-simplified.json create mode 100644 src/features/cap-studio/components/batch-create.tsx rename src/features/cap-studio/components/{cap-submit => cap-edit}/thumbnail-upload.tsx (100%) delete mode 100644 src/features/cap-studio/components/cap-submit/author-form.tsx delete mode 100644 src/features/cap-studio/components/cap-submit/cap-information.tsx delete mode 100644 src/features/cap-studio/components/cap-submit/submission-confirmation-dialog.tsx create mode 100644 src/pages/cap-studio-batch-create.tsx diff --git a/docs/mcp-client-design.md b/docs/mcp-client-design.md deleted file mode 100644 index 4c951560..00000000 --- a/docs/mcp-client-design.md +++ /dev/null @@ -1,227 +0,0 @@ -# Nuwa MCPClient 设计方案 - -> 本文档位于 `nuwa-client/docs`,记录了在 Vercel AI SDK 基础上封装专属 **NuwaMCPClient** 的整体思路与 API 设计。文中说明使用中文,代码示例与类型定义使用英文。 - ---- - -## 目标 -1. **补齐功能缺口**:AI-SDK 当前仅暴露 `client.request()`,对 `prompts/list`、`prompts/get`、`resources/list`、`resources/read` 等实验性接口缺乏一层易用封装。我们需要: - * 列表接口:`prompts/list`、`resources/list`。 - * 读取接口:`prompts/get`(替代此前草案中的 `prompts/load`)、`resources/read`(替代 `resources/load`)。 -2. **类型安全**:为 *Prompt* 与 *Resource* 定义 Zod Schema,以便在运行时校验入参、出参,也为 IDE 提供完整类型提示。 -3. **与 AI-SDK 自然融合**:让大语言模型能够通过 **prompts** / **resources** API 发现、调用后端能力;前端/中间件使用体验不逊色于 `openai.functions`。 - -## 总览 -```mermaid -flowchart TD; - subgraph Browser(nuwa-client) - direction TB - FrontEnd -->|getMcpClient()| NuwaMCPClient - NuwaMCPClient -->|HTTP Streaming / SSE| FastMCP - end - subgraph FastMCP Server - direction TB - Prompts & Resources - end -``` - -NuwaMCPClient 在创建时持有底层 `createMCPClient()` 返回的实例,通过 *Proxy* 或 *mixin* 方式注入以下能力: - -* `prompts()` – 获取以 `name` 为键的提示模板元数据映射。 -* `prompts.get(name, args)` – 获取指定 prompt 的 messages 列表(content blocks)。 -* `resources()` – 返回服务器侧全部静态资源 URI;若包含模板则以 `uriTemplate` 字段区分。 -* `resources.read(uri, args?)` – 读取资源内容;若 `uri` 为模板则带参渲染。 - -所有调用最终都会下沉到统一的 `client.request({ method, params, resultSchema })`,并复用现有 DIDAuth 认证与传输协商逻辑。 - -## API 设计(TypeScript) -```ts -// src/features/mcp/types.ts -import { z } from "zod"; - -export const PromptArgumentSchema = z.object({ - name: z.string(), - description: z.string().optional(), - required: z.boolean().default(false), -}); - -export const PromptSchema = z.object({ - name: z.string(), - description: z.string().optional(), - arguments: z.array(PromptArgumentSchema).default([]), - // 其他元信息(模型、系统提示等)后续扩展 -}); - -export type PromptDefinition = z.infer; - -export const ResourceSchema = z.object({ - uri: z.string(), - name: z.string().optional(), - mimeType: z.string().optional(), -}); - -export const ResourceTemplateSchema = z.object({ - uriTemplate: z.string(), - name: z.string().optional(), - mimeType: z.string().optional(), - arguments: z.array(PromptArgumentSchema).default([]), -}); - -export type ResourceDefinition = z.infer; -export type ResourceTemplateDefinition = z.infer; - -/** prompts/get 返回的数据结构 */ -export const PromptMessagesResultSchema = z.object({ - description: z.string().optional(), - messages: z.array( - z.object({ - role: z.enum(["system", "user", "assistant"]), - content: z.unknown(), // 兼容 text / resource / image 等多模态 - }), - ), -}); -export type PromptMessagesResult = z.infer; -``` - -### NuwaMCPClient 接口 -```ts -export interface NuwaMCPClient { - /** 原生 client,必要时可直接访问 `request()` 等底层方法 */ - raw: MCPClient; - - /* -------- prompts -------- */ - prompts(): Promise>; - prompt(name: string): Promise; // sugar - /** - * 获取 prompt 具体内容(messages)。 - * 对应 RPC method: prompts/get - */ - getPrompt(name: string, args?: Record): Promise; - - /* -------- resources -------- */ - /** - * 获取服务器已声明的资源(含模板)。 - * key = 资源 URI 或 URI 模板;value = 元数据对象。 - */ - resources(): Promise>; - /** - * 读取静态资源。 - * 对应 RPC method: resources/read - */ - readResource(uri: string): Promise; - /** - * 读取模板资源并渲染。 - * 对应 RPC method: resources/read - */ - readResourceTemplate(uriTemplate: string, args: Record): Promise; - - /** 透传关闭 */ - close(): Promise; -} -``` - -## 实现要点 -1. **类型补丁**:沿用 `src/features/mcp/services/factory.ts` 中现有逻辑,将 `patchClientWithExtras()` 抽象为可重用 util,并补充 `getPrompt` / `readResource`: - ```ts - const pass = { parse: (v: any) => v } as const; - await client.request({ - request: { method: 'prompts/get', params: { name, arguments: args } }, - resultSchema: pass, - }); - - await client.request({ - request: { method: 'resources/read', params: { uri } }, - resultSchema: pass, - }); - ``` - -2. **Schema 校验**:服务端返回值用上方 Zod Schema 解析;若不符立刻抛错,方便前端/LLM 调试。 - -3. **Proxy 辅助调用**(可选增强): - ```ts - // 允许 client.prompts.shout({ text: "hi" }) 这样链式体验 - Object.defineProperty(client.prompts, name, { - enumerable: true, - value: (args) => loadPrompt(name, args), - }); - ``` - -4. **与 AI-SDK Prompts 集成**: - * 在 `prompts/` 目录下维护 `.prompt` 文件或 JSON 格式描述,利用 SDK 的 `definePrompts()` API(假设未来提供)。 - * 构建阶段(或运行时首次调用)通过 NuwaMCPClient 拉取 prompts,再动态注入到 `ai` 上下文,使得 `useChat()` 等 Hook 可以直接 `callPrompt("shout", { text })`。 - -5. **错误处理策略**: - * 对 4xx / 5xx 统一封装 `MCPError`,内含 `code`、`message`、`detail`(Server stack) - * 对 Capability 缺失做降级提示,保持向后兼容。 - -## 目录结构建议 -``` -nuwa-client/ - src/ - features/ - mcp/ - services/ - factory.ts // 已有:创建底层客户端 - nuwaClient.ts // 新增:实现 NuwaMCPClient 封装 - types.ts // 新增:Prompt / Resource schema - docs/ - mcp-client-design.md // ← 当前文档 -``` - -## 下一步 -* [ ] 根据本文档落地 `types.ts` 与 `nuwaClient.ts`。 -* [ ] 调整现有调用方(如 React hooks)使用新接口。 -* [ ] 完善单元测试:使用 `fastmcp` mock server,覆盖列表、加载、异常流程。 -* [ ] PR 提交并同步文档至团队 Wiki。 - -## Prompt 与 Tool 的集成策略 - -> 参考 [vercel/ai PR #6358](https://github.com/vercel/ai/pull/6358/files) 中的实现,`prompts..execute()` 现在与 `tools..execute()` 在调用方式上一致;但在 **协议** 与 **语义** 上,两者仍存在差异,需要在 Agent 层做出显式选择。 - -### 协议差异 -| 维度 | Tool | Prompt | -| ---- | ---- | ------ | -| RPC method | `tools/list` / `tools/call` | `prompts/list` / `prompts/call` | -| 典型返回值 | 结构化 JSON(供 LLM 函数调用) | content blocks(text/image/embed…) | -| 设计目的 | 让 LLM 执行具备强约束的"函数" | 生成可直投上下文的模板内容 | - -因此,简单把 *prompt* 描述直接塞进 `tools` 数组发给模型并不可行——服务端收到的仍然是 `tools/call` 而非 `prompts/call`,会导致 404/Unsupported method。 - -### 两种通用集成方案 - -| 场景 | 实现思路 | 适用情形 | -| --- | --- | --- | -| **方案 A**:将 Prompt 包装为"虚拟 Tool" | 1. Agent 启动时 `listPrompts()`;
2. 对每个 prompt 生成 wrapper:`execute()` 内部转调 `prompts/call`;
3. 连同真正的 tools 一起暴露给模型。 | 需要让 LLM **自行决定** 何时调用某个 prompt 的场景。 | -| **方案 B**:Prompt 作为上下文生成节点(默认) | 1. Agent 侧逻辑/规则判断触发 prompt;
2. 调用 `prompts/call` 得到 content blocks;
3. 把结果插入对话历史,再继续让模型生成。 | Prompt 主要用于系统提示、总结、补充资料等,不希望 LLM 直接决策时使用。 | - -> **最佳实践**:在 Nuwa Agent 中默认采用 **方案 B**,保证语义清晰;若开发者在配置中设置 `exposePromptsAsTools: true`,则同时启用 **方案 A** 以获得更大的灵活度。 - -### 示例 Wrapper(方案 A) -```ts -import { z } from 'zod'; - -function toolFromPrompt(p: PromptDefinition, client: NuwaMCPClient) { - return { - name: p.name, - description: p.description ?? 'Prompt wrapper', - parameters: p.inputSchema ?? z.object({}).passthrough(), - async execute(args: any) { - const messages = await client.getPrompt(p.name, args); - return { messages }; - }, - }; -} -``` - -集成时: -```ts -const promptMap = await client.prompts(); -const wrappedTools = Object.values(promptMap).map((p) => toolFromPrompt(p, client)); -const toolsForLLM = [...realTools, ...wrappedTools]; -``` - ---- -**更新记录** -* 2024-06-27:初稿,涵盖 API 草案与整合思路 – @AI -* 2024-06-27:补充 Prompt 与 Tool 的集成方案 – @AI -* 2024-06-27:与 MCP 正式文档保持一致,将 *load*/*call* 更换为 *get*/*read* – @AI \ No newline at end of file diff --git a/sample-caps-simplified.json b/sample-caps-simplified.json new file mode 100644 index 00000000..f1e10be1 --- /dev/null +++ b/sample-caps-simplified.json @@ -0,0 +1,88 @@ +[ + { + "idName": "code-assistant", + "metadata": { + "displayName": "Code Assistant", + "description": "A helpful assistant specialized in code review and programming guidance", + "tags": [ + "coding", + "programming", + "assistant" + ], + "thumbnail": { + "type": "url", + "url": "https://example.com/thumbnail.png" + } + }, + "core": { + "prompt": { + "value": "You are a helpful programming assistant. Help users with code review, debugging, and programming best practices. Always provide clear explanations and examples.", + "suggestions": [] + }, + "modelId": "openai/gpt-4", + "mcpServers": {} + } + }, + { + "idName": "writing-assistant", + "metadata": { + "displayName": "Writing Assistant", + "description": "A creative writing assistant that helps with content creation and editing", + "tags": [ + "writing", + "creative", + "content" + ], + "thumbnail": { + "type": "url", + "url": "https://example.com/writing-thumbnail.png" + } + }, + "core": { + "prompt": { + "value": "You are a creative writing assistant. Help users with content creation, editing, proofreading, and improving their writing style. Provide constructive feedback and suggestions.", + "suggestions": [] + }, + "modelId": "openai/gpt-4o-mini", + "mcpServers": { + "writing-tools": { + "url": "npm:@writing-tools/mcp-server", + "transport": "httpStream" + } + } + } + }, + { + "idName": "data-analyst", + "metadata": { + "displayName": "Data Analyst", + "description": "An AI assistant specialized in data analysis and visualization", + "tags": [ + "data", + "analytics", + "visualization" + ], + "thumbnail": { + "type": "url", + "url": "https://example.com/data-thumbnail.png" + } + }, + "core": { + "prompt": { + "value": "You are a data analyst assistant. Help users analyze data, create visualizations, and derive insights. Provide clear explanations of statistical concepts and data interpretation.", + "suggestions": [] + }, + "modelId": "openai/gpt-4", + "mcpServers": { + "pandas-tools": { + "url": "npm:@data-tools/pandas-mcp", + "transport": "httpStream" + }, + "visualization-tools": { + "url": "npm:@viz-tools/mcp-server", + "transport": "httpStream" + } + } + } + } +] \ No newline at end of file diff --git a/src/features/cap-studio/components/batch-create.tsx b/src/features/cap-studio/components/batch-create.tsx new file mode 100644 index 00000000..35de8a45 --- /dev/null +++ b/src/features/cap-studio/components/batch-create.tsx @@ -0,0 +1,561 @@ +import { + ArrowLeft, + CheckCircle, + ChevronDown, + ChevronRight, + Copy, + Upload, + XCircle, +} from 'lucide-react'; +import { useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import { useAuth } from '@/features/auth/hooks'; +import { + Alert, + AlertDescription, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/shared/components/ui'; +import { CapSchema } from '@/shared/types/cap'; +import { useAvailableModels } from '../hooks'; +import { useLocalCapsHandler } from '../hooks/use-local-caps-handler'; +import type { LocalCap } from '../types'; + +interface BatchCreateProps { + onBatchCreate?: (caps: LocalCap[]) => void; +} + +// simplified Cap input format, without id, authorDID, submittedAt +interface SimplifiedCapInput { + idName: string; + metadata: { + displayName: string; + description: string; + tags: string[]; + thumbnail?: { + type: 'url'; + url: string; + }; + homepage?: string; + repository?: string; + }; + core: { + prompt: { + value: string; + suggestions?: string[]; + }; + modelId: string; // only model ID + mcpServers?: Record< + string, + { + url: string; + transport: string; + } + >; + }; +} + +export function BatchCreate({ onBatchCreate }: BatchCreateProps) { + const navigate = useNavigate(); + const { did } = useAuth(); + const { models } = useAvailableModels(); + const { createCap } = useLocalCapsHandler(); + const [uploading, setUploading] = useState(false); + const [showJsonFormat, setShowJsonFormat] = useState(false); + const [parsedData, setParsedData] = useState<{ + validCaps: SimplifiedCapInput[]; + invalidCaps: { cap: any; error: string }[]; + } | null>(null); + const [uploadResults, setUploadResults] = useState<{ + success: LocalCap[]; + errors: { cap: any; error: string }[]; + }>({ success: [], errors: [] }); + const [isCreating, setIsCreating] = useState(false); + const fileInputRef = useRef(null); + + // validate simplified Cap input format + const validateSimplifiedCap = (capData: any): SimplifiedCapInput | null => { + // basic structure validation + if (!capData.idName || typeof capData.idName !== 'string') { + throw new Error('Missing or invalid idName'); + } + + if (!capData.metadata || typeof capData.metadata !== 'object') { + throw new Error('Missing metadata'); + } + + if ( + !capData.metadata.displayName || + typeof capData.metadata.displayName !== 'string' + ) { + throw new Error('Missing displayName in metadata'); + } + + if ( + !capData.metadata.description || + typeof capData.metadata.description !== 'string' + ) { + throw new Error('Missing description in metadata'); + } + + if (!capData.metadata.tags || !Array.isArray(capData.metadata.tags)) { + throw new Error('Missing or invalid tags in metadata'); + } + + if (!capData.core || typeof capData.core !== 'object') { + throw new Error('Missing core'); + } + + if (!capData.core.prompt || typeof capData.core.prompt !== 'object') { + throw new Error('Missing prompt in core'); + } + + if ( + !capData.core.prompt.value || + typeof capData.core.prompt.value !== 'string' + ) { + throw new Error('Missing prompt value'); + } + + if (!capData.core.modelId || typeof capData.core.modelId !== 'string') { + throw new Error('Missing modelId in core'); + } + + // validate model exists + if ( + !models || + !models.find((model) => model.id === capData.core.modelId) + ) { + throw new Error(`Model with ID "${capData.core.modelId}" not found`); + } + + return capData as SimplifiedCapInput; + }; + + // convert simplified input to full Cap object + const convertToFullCap = (simplifiedCap: SimplifiedCapInput): any => { + const model = models?.find((m) => m.id === simplifiedCap.core.modelId); + if (!model) { + throw new Error(`Model not found: ${simplifiedCap.core.modelId}`); + } + + return { + id: `${did}:${simplifiedCap.idName}`, + authorDID: did, + idName: simplifiedCap.idName, + metadata: { + ...simplifiedCap.metadata, + submittedAt: Date.now(), + }, + core: { + prompt: simplifiedCap.core.prompt, + model: model, + mcpServers: simplifiedCap.core.mcpServers || {}, + }, + }; + }; + + const handleBatchUpload = async ( + event: React.ChangeEvent, + ) => { + const file = event.target.files?.[0]; + if (!file) return; + + if (!did) { + toast.error('Please sign in to upload caps'); + return; + } + + setUploading(true); + setParsedData(null); + setUploadResults({ success: [], errors: [] }); + + try { + const text = await file.text(); + const data = JSON.parse(text); + + // Validate that it's an array + if (!Array.isArray(data)) { + throw new Error('File must contain an array of Cap objects'); + } + + const validCaps: SimplifiedCapInput[] = []; + const invalidCaps: { cap: any; error: string }[] = []; + + // Validate each cap without creating them yet + for (let i = 0; i < data.length; i++) { + const capData = data[i]; + try { + const validatedCap = validateSimplifiedCap(capData); + if (validatedCap) { + validCaps.push(validatedCap); + } + } catch (error) { + let errorMessage = 'Invalid cap format'; + + if (error instanceof Error) { + errorMessage = error.message; + } + + const capName = + capData?.metadata?.displayName || + capData?.idName || + `Item ${i + 1}`; + invalidCaps.push({ + cap: capData, + error: `"${capName}": ${errorMessage}`, + }); + } + } + + setParsedData({ validCaps, invalidCaps }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to parse JSON file'; + setParsedData({ + validCaps: [], + invalidCaps: [{ cap: null, error: errorMessage }], + }); + } + + setUploading(false); + + // Reset file input + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const handleConfirmUpload = async () => { + if (!parsedData || parsedData.validCaps.length === 0) return; + + setIsCreating(true); + + try { + const results = { + success: [] as LocalCap[], + errors: [] as { cap: any; error: string }[], + }; + + // Create each validated cap + for (const simplifiedCap of parsedData.validCaps) { + try { + const fullCap = convertToFullCap(simplifiedCap); + + // validate full Cap object + CapSchema.parse(fullCap); + + const newLocalCap = createCap(fullCap); + results.success.push(newLocalCap); + } catch (error) { + const capName = + simplifiedCap.metadata?.displayName || + simplifiedCap.idName || + 'Unknown Cap'; + results.errors.push({ + cap: simplifiedCap, + error: `"${capName}": Failed to create cap - ${error instanceof Error ? error.message : 'Unknown error'}`, + }); + } + } + + setUploadResults(results); + + // If there are successful uploads, call the parent callback + if (results.success.length > 0 && onBatchCreate) { + onBatchCreate(results.success); + } + + // Clear parsed data after creation + setParsedData(null); + } catch (error) { + toast.error('Failed to create caps'); + } + + setIsCreating(false); + }; + + const handleCopyError = async (error: string) => { + try { + await navigator.clipboard.writeText(error); + toast.success('Error message copied to clipboard'); + } catch (err) { + toast.error('Failed to copy to clipboard'); + } + }; + + const hasResults = + uploadResults.success.length > 0 || uploadResults.errors.length > 0; + + return ( +
+ {/* Header */} +
+ +
+ +
+

Batch Create Caps

+

+ Upload multiple caps from a JSON file. Each cap will be validated and + created as a draft in your local workspace. +

+
+ + {/* Upload Section */} + + + Upload JSON File + + Select a JSON file containing an array of simplified cap objects. + The system will automatically fill in required fields like ID, + authorDID, and submission time. + + + +
+ + +
+ + {/* Format Example */} +
+ + {showJsonFormat && ( +
+                {`[
+  {
+    "idName": "my-cap",
+    "metadata": {
+      "displayName": "My Cap",
+      "description": "Description of the cap",
+      "tags": ["tag1", "tag2"],
+      "thumbnail": {
+        "type": "url",
+        "url": "https://example.com/thumbnail.png"
+      },
+      "homepage": "https://example.com" (optional),
+      "repository": "https://github.com/user/repo" (optional)
+    },
+    "core": {
+      "prompt": {
+        "value": "System prompt for the cap",
+        "suggestions": ["suggestion1", "suggestion2"] (optional)
+      },
+      "modelId": "openai/gpt-4o-mini",
+      "mcpServers": {
+        "server-name": {
+          "url": "npm:@example/mcp-server",
+          "transport": "httpStream"
+        }
+      } (optional)
+    }
+  }
+]
+
+Note: The following fields are automatically generated:
+- id: "authorDID:idName"
+- authorDID: from your authentication
+- submittedAt: current timestamp`}
+              
+ )} +
+
+
+ + {/* Preview Section */} + {parsedData && ( + + + Upload Preview + + Review the caps before creating them. Only valid caps will be + created. + + + + {parsedData.validCaps.length > 0 && ( + + + +
+ ✅ {parsedData.validCaps.length} cap(s) ready to create +
+
+ {parsedData.validCaps.map((cap, index) => ( +
+ "{cap.metadata?.displayName || cap.idName}" ( + {cap.idName || `Item ${index + 1}`}) +
+ ))} +
+
+
+ )} + + {parsedData.invalidCaps.length > 0 && ( + + + +
+ ❌ {parsedData.invalidCaps.length} invalid cap(s) found: +
+
+ {parsedData.invalidCaps.map((error, index) => ( +
+
{error.error}
+ +
+ ))} +
+
+
+ )} + + {/* Confirm button */} + {parsedData.validCaps.length > 0 && ( +
+ + +
+ )} +
+
+ )} + + {/* Results Section */} + {hasResults && ( + + + Upload Results + + + {uploadResults.success.length > 0 && ( + + + +
+ ✅ Successfully created {uploadResults.success.length}{' '} + cap(s) as drafts +
+
+ {uploadResults.success.map((cap) => ( +
+ "{cap.capData.metadata.displayName}" ( + {cap.capData.idName}) +
+ ))} +
+
+
+ )} + + {uploadResults.errors.length > 0 && ( + + + +
+ ❌ {uploadResults.errors.length} error(s) occurred: +
+
+ {uploadResults.errors.map((error, index) => ( +
+
{error.error}
+ +
+ ))} +
+
+
+ )} +
+
+ )} +
+ ); +} diff --git a/src/features/cap-studio/components/cap-edit/cap-edit-form.tsx b/src/features/cap-studio/components/cap-edit/cap-edit-form.tsx index e2ce41f4..719d8c55 100644 --- a/src/features/cap-studio/components/cap-edit/cap-edit-form.tsx +++ b/src/features/cap-studio/components/cap-edit/cap-edit-form.tsx @@ -1,4 +1,5 @@ import { AlertCircle, CheckCircle2, Loader2, Save } from 'lucide-react'; +import { useState } from 'react'; import type { LocalCap } from '@/features/cap-studio/types'; import { Button, @@ -9,6 +10,7 @@ import { CardTitle, Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -16,6 +18,7 @@ import { Input, Textarea, } from '@/shared/components/ui'; +import type { CapThumbnail } from '@/shared/types/cap'; import { useEditForm } from '../../hooks/use-edit-form'; import { DashboardGrid } from '../layout/dashboard-layout'; import { ModelSelectorDialog } from '../model-selector'; @@ -23,12 +26,16 @@ import { CapTags } from './cap-tags'; import { McpServersConfig } from './mcp-servers-config'; import { ModelDetails } from './model-details'; import { PromptEditor } from './prompt-editor'; +import { ThumbnailUpload } from './thumbnail-upload'; interface CapEditFormProps { editingCap?: LocalCap; } export function CapEditForm({ editingCap }: CapEditFormProps) { + const [thumbnail, setThumbnail] = useState( + editingCap?.capData.metadata.thumbnail || null, + ); const { form, handleFormSave, @@ -60,7 +67,9 @@ export function CapEditForm({ editingCap }: CapEditFormProps) {
- - { - e.preventDefault(); - handleFormSubmit(); - }} - className="space-y-6" - > - {/* Cap Information - Read Only */} - - - {/* Author */} - - - {/* Thumbnail Upload */} - + {/* Cap Information - Read Only */} + + + Cap Information + Basic information about your cap + + +
+
+
+ Name +
+

{cap.capData.idName}

+
+
+
+ Display Name +
+

{cap.capData.metadata.displayName}

+
+
+
+
+ Description +
+

{cap.capData.metadata.description}

+
+
+
+ Tags +
+
+ {cap.capData.metadata.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+
+
+ Model +
+
+ +
+

{getModelName(cap.capData.core.model)}

+

{getProviderName(cap.capData.core.model)}

+
+
+
+
+
+ MCP Servers +
+

+ {Object.keys(cap.capData.core.mcpServers).length > 0 + ? Object.keys(cap.capData.core.mcpServers).join(', ') + : 'None'} +

+
+
+
+
- {/* Submit */} - - -
-
-
- {form.formState.isValid ? ( - <> - - Ready to submit - - ) : ( - <> - - Please complete all required fields - - )} -
+ {/* Author Information - Read Only */} + + + Author Information + Author and licensing information + + +
+
+
+ Homepage +
+

{homepage || 'Not provided'}

+
+
+
+ Repository +
+

{repository || 'Not provided'}

+
+
+
+
-
- - -
+ {/* Thumbnail - Read Only */} + + + Thumbnail + Cap thumbnail image + + +
+
+ {thumbnailSrc ? ( + Thumbnail Preview + ) : ( +
+ + No thumbnail
-
- - - - + )} +
+
+ {thumbnailSrc + ? 'Thumbnail ready for submission' + : 'No thumbnail provided'} +
+
+ + + + {/* Submit */} + + +
+
+ Ready to publish your cap to the store +
- setShowConfirmDialog(false)} - onConfirm={handleConfirmedSubmit} - /> +
+ + +
+
+
+
); } diff --git a/src/features/cap-studio/components/cap-submit/index.tsx b/src/features/cap-studio/components/cap-submit/index.tsx index 22105e7f..c4db2aff 100644 --- a/src/features/cap-studio/components/cap-submit/index.tsx +++ b/src/features/cap-studio/components/cap-submit/index.tsx @@ -25,7 +25,12 @@ export function Submit() { return ( - + ); } diff --git a/src/features/cap-studio/components/cap-submit/submission-confirmation-dialog.tsx b/src/features/cap-studio/components/cap-submit/submission-confirmation-dialog.tsx deleted file mode 100644 index 5c9b7bc7..00000000 --- a/src/features/cap-studio/components/cap-submit/submission-confirmation-dialog.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { FileText, Loader2, Upload } from 'lucide-react'; -import { - Button, - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from '@/shared/components/ui'; -import type { CapThumbnail } from '@/shared/types/cap'; -import type { SubmitFormData } from '../../hooks/use-submit-form'; -import type { LocalCap } from '../../types'; - -interface SubmissionConfirmationDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - data: SubmitFormData; - cap: LocalCap; - thumbnail: CapThumbnail; - isSubmitting: boolean; - onCancel: () => void; - onConfirm: () => Promise; -} - -export function SubmissionConfirmationDialog({ - open, - onOpenChange, - data, - cap, - thumbnail, - isSubmitting, - onCancel, - onConfirm, -}: SubmissionConfirmationDialogProps) { - return ( - - - - Confirm Submission - - Please review your cap information before submitting to the store - - - -
- - -
-
-
- ); -} - -interface CapStorePreviewProps { - data: SubmitFormData; - cap: LocalCap; - thumbnail?: CapThumbnail; -} - -function CapStorePreview({ data, cap, thumbnail }: CapStorePreviewProps) { - return ( -
- {/* Header */} -
- {thumbnail ? ( - Thumbnail - ) : ( -
- -
- )} -
-

- {cap.capData.metadata.displayName || 'Untitled Cap'} -

-

- {cap.capData.metadata.description || 'No description provided'} -

-
-
- - {/* Links */} - {(data.homepage || data.repository) && ( -
-

Links

-
- {data.homepage && ( -
- Homepage: - - {data.homepage} - -
- )} - {data.repository && ( -
- Repository: - - {data.repository} - -
- )} -
-
- )} -
- ); -} diff --git a/src/features/cap-studio/components/index.tsx b/src/features/cap-studio/components/index.tsx index 3caeaf71..71c5cd10 100644 --- a/src/features/cap-studio/components/index.tsx +++ b/src/features/cap-studio/components/index.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { Button } from '@/shared/components/ui/button'; import { useCurrentCap } from '@/shared/hooks'; import type { LocalCap } from '../types'; +import { BatchCreate } from './batch-create'; import { DashboardHeader, DashboardLayout } from './layout/dashboard-layout'; import { MyCaps } from './my-caps'; @@ -53,3 +54,5 @@ export function CapStudio() { ); } + +export { BatchCreate }; diff --git a/src/features/cap-studio/components/my-caps/cap-card.tsx b/src/features/cap-studio/components/my-caps/cap-card.tsx index 9eea821f..82a30d09 100644 --- a/src/features/cap-studio/components/my-caps/cap-card.tsx +++ b/src/features/cap-studio/components/my-caps/cap-card.tsx @@ -2,6 +2,7 @@ import { formatDistanceToNow } from 'date-fns'; import { Bot, Bug, + Check, Clock, Copy, Edit, @@ -24,6 +25,7 @@ import { Avatar, AvatarFallback, AvatarImage, + Badge, Button, Card, CardContent, @@ -42,6 +44,10 @@ interface CapCardProps { onTest?: () => void; onSubmit?: () => void; onUpdate?: () => void; + isMultiSelectMode?: boolean; + isSelected?: boolean; + onToggleSelect?: () => void; + onEnterMultiSelectMode?: () => void; } export function CapCard({ @@ -50,9 +56,14 @@ export function CapCard({ onTest, onSubmit, onUpdate, + isMultiSelectMode, + isSelected, + onToggleSelect, + onEnterMultiSelectMode, }: CapCardProps) { const { deleteCap } = useLocalCapsHandler(); const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [isHovered, setIsHovered] = useState(false); const handleDelete = () => { deleteCap(cap.id); @@ -66,15 +77,76 @@ export function CapCard({ } }; + const handleSelectClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (onToggleSelect) { + onToggleSelect(); + } + }; + + const handleCardClick = () => { + if (!isMultiSelectMode && onEnterMultiSelectMode) { + onEnterMultiSelectMode(); + } else if (isMultiSelectMode && onToggleSelect) { + onToggleSelect(); + } + }; + const mcpServerCount = Object.keys(cap.capData.core.mcpServers).length; const lastUpdated = formatDistanceToNow(new Date(cap.updatedAt), { addSuffix: true, }); + // Determine border color based on published status + const borderColor = + cap.status === 'submitted' + ? 'border-l-theme-primary/50' + : 'border-l-primary/20'; + return ( - + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {cap.status === 'submitted' && ( + { + e.stopPropagation(); + handleCopyCid(); + }} + > + + Published + + )}
+ {(isMultiSelectMode || isHovered) && ( +
+ +
+ )}
@@ -119,69 +191,91 @@ export function CapCard({
-
-
- - - - - {cap.status === 'draft' ? ( - - ) : ( + {!isMultiSelectMode && ( +
+
- )} -
- - - - - - {cap.status === 'submitted' && cap.cid && ( - <> - - - Copy Published CID - - - + {cap.status === 'draft' ? ( + + ) : ( + )} +
- setShowDeleteDialog(true)} - className="text-destructive" - > - - Delete Cap - - - -
+ + + + + + {cap.status === 'submitted' && cap.cid && ( + <> + + + Copy Published CID + + + + )} + + + + Test Cap + + + + + setShowDeleteDialog(true)} + className="text-destructive" + > + + Delete Cap + + + +
+ )}
+
diff --git a/src/features/cap-studio/components/my-caps/index.tsx b/src/features/cap-studio/components/my-caps/index.tsx index e86b2c93..acd15400 100644 --- a/src/features/cap-studio/components/my-caps/index.tsx +++ b/src/features/cap-studio/components/my-caps/index.tsx @@ -1,5 +1,6 @@ -import { Plus, Search } from 'lucide-react'; +import { CheckSquare, Plus, Search, Send, Trash2, Upload } from 'lucide-react'; import { useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { Button, Card, @@ -7,12 +8,9 @@ import { CardHeader, CardTitle, Input, - Tabs, - TabsContent, - TabsList, - TabsTrigger, } from '@/shared/components/ui'; import { useLocalCaps } from '../../hooks'; +import { useLocalCapsHandler } from '../../hooks/use-local-caps-handler'; import type { LocalCap } from '../../types'; import { DashboardGrid } from '../layout/dashboard-layout'; import { CapCard } from './cap-card'; @@ -22,6 +20,9 @@ interface MyCapsProps { onTestCap?: (cap: LocalCap) => void; onSubmitCap?: (cap: LocalCap) => void; onCreateNew?: () => void; + onBatchCreate?: (caps: LocalCap[]) => void; + onBulkDelete?: (caps: LocalCap[]) => void; + onBulkPublish?: (caps: LocalCap[]) => void; } export function MyCaps({ @@ -29,35 +30,73 @@ export function MyCaps({ onTestCap, onSubmitCap, onCreateNew, + onBatchCreate, + onBulkDelete, + onBulkPublish, }: MyCapsProps) { + const navigate = useNavigate(); const localCaps = useLocalCaps(); const [searchQuery, setSearchQuery] = useState(''); + const { createCap, deleteCap } = useLocalCapsHandler(); - // Get caps array - const capsArray = localCaps; + // Multi-select state + const [selectedCapIds, setSelectedCapIds] = useState>(new Set()); + const [isMultiSelectMode, setIsMultiSelectMode] = useState(false); - // Separate caps by status - const draftCaps = useMemo(() => { - const drafts = capsArray.filter((cap) => cap.status === 'draft'); - if (!searchQuery) return drafts; + // Multi-select handlers + const toggleCapSelection = (capId: string) => { + setSelectedCapIds((prev) => { + const newSet = new Set(prev); + if (newSet.has(capId)) { + newSet.delete(capId); + } else { + newSet.add(capId); + } + return newSet; + }); + }; - const query = searchQuery.toLowerCase(); - return drafts.filter( - (cap) => - cap.capData.metadata.displayName.toLowerCase().includes(query) || - cap.capData.metadata.description.toLowerCase().includes(query) || - cap.capData.metadata.tags.some((tag) => - tag.toLowerCase().includes(query), - ), - ); - }, [capsArray, searchQuery]); + const enterMultiSelectMode = () => { + setIsMultiSelectMode(true); + }; + + const selectAllCaps = () => { + setSelectedCapIds(new Set(allCaps.map((cap) => cap.id))); + }; + + const clearSelection = () => { + setSelectedCapIds(new Set()); + setIsMultiSelectMode(false); + }; - const publishedCaps = useMemo(() => { - const published = capsArray.filter((cap) => cap.status === 'submitted'); - if (!searchQuery) return published; + const handleBulkDelete = () => { + const selectedCaps = allCaps.filter((cap) => selectedCapIds.has(cap.id)); + selectedCaps.forEach((cap) => deleteCap(cap.id)); + if (onBulkDelete) { + onBulkDelete(selectedCaps); + } + clearSelection(); + }; + + const handleBulkPublish = () => { + const selectedCaps = allCaps.filter((cap) => selectedCapIds.has(cap.id)); + selectedCaps.forEach((cap) => { + if (onSubmitCap) { + onSubmitCap(cap); + } + }); + if (onBulkPublish) { + onBulkPublish(selectedCaps); + } + clearSelection(); + }; + + // Get and filter all caps + const allCaps = useMemo(() => { + if (!searchQuery) return localCaps; const query = searchQuery.toLowerCase(); - return published.filter( + return localCaps.filter( (cap) => cap.capData.metadata.displayName.toLowerCase().includes(query) || cap.capData.metadata.description.toLowerCase().includes(query) || @@ -65,9 +104,12 @@ export function MyCaps({ tag.toLowerCase().includes(query), ), ); - }, [capsArray, searchQuery]); + }, [localCaps, searchQuery]); + + const selectedCaps = allCaps.filter((cap) => selectedCapIds.has(cap.id)); + const hasSelectedCaps = selectedCaps.length > 0; - if (capsArray.length === 0) { + if (localCaps.length === 0) { return ( @@ -97,13 +139,62 @@ export function MyCaps({ {/* Header with controls */}
- + {!isMultiSelectMode ? ( + <> + + + + ) : ( + <> + + + {selectedCapIds.size > 0 && ( + <> + + {hasSelectedCaps && ( + + )} + + )} + + )}
-
+
- {/* Tabs for draft and published */} - - - Drafts ({draftCaps.length}) - - Published ({publishedCaps.length}) - - - - - {draftCaps.length === 0 ? ( - - - - {searchQuery ? 'No matching draft caps' : 'No draft caps'} - - - {searchQuery - ? 'Try adjusting your search criteria' - : 'Create your first cap to get started'} - - - - ) : ( - - {draftCaps.map((cap) => ( - onEditCap?.(cap)} - onTest={() => onTestCap?.(cap)} - onSubmit={() => onSubmitCap?.(cap)} - /> - ))} - - )} - - - - {publishedCaps.length === 0 ? ( - - - - {searchQuery - ? 'No matching published caps' - : 'No published caps'} - - - {searchQuery - ? 'Try adjusting your search criteria' - : 'Submit your drafts to see them here'} - - - - ) : ( - - {publishedCaps.map((cap) => ( - onEditCap?.(cap)} - onTest={() => onTestCap?.(cap)} - onUpdate={() => onSubmitCap?.(cap)} - /> - ))} - - )} - - + {/* All Caps */} + {allCaps.length === 0 ? ( + + + + {searchQuery ? 'No matching caps found' : 'No caps yet'} + + + {searchQuery + ? 'Try adjusting your search criteria' + : 'Create your first cap to get started'} + + + + ) : ( + + {allCaps.map((cap) => ( + onEditCap?.(cap)} + onTest={() => onTestCap?.(cap)} + onSubmit={() => onSubmitCap?.(cap)} + onUpdate={() => onSubmitCap?.(cap)} + isMultiSelectMode={isMultiSelectMode} + isSelected={selectedCapIds.has(cap.id)} + onToggleSelect={() => toggleCapSelection(cap.id)} + onEnterMultiSelectMode={() => { + enterMultiSelectMode(); + toggleCapSelection(cap.id); + }} + /> + ))} + + )}
); } diff --git a/src/features/cap-studio/hooks/use-edit-form.ts b/src/features/cap-studio/hooks/use-edit-form.ts index c47b26da..1d9b1e10 100644 --- a/src/features/cap-studio/hooks/use-edit-form.ts +++ b/src/features/cap-studio/hooks/use-edit-form.ts @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { z } from 'zod'; import { useAuth } from '@/features/auth/hooks'; -import type { CapMcpServerConfig } from '@/shared/types/cap'; +import type { CapMcpServerConfig, CapThumbnail } from '@/shared/types/cap'; import type { LocalCap } from '../types'; import { useLocalCapsHandler } from './use-local-caps-handler'; import { useSelectedModel } from './use-selected-model'; @@ -32,6 +32,12 @@ const capSchema = z.object({ value: z.string(), suggestions: z.array(z.string()).optional(), }), + homepage: z.string().url('Must be a valid URL').optional().or(z.literal('')), + repository: z + .string() + .url('Must be a valid URL') + .optional() + .or(z.literal('')), }); type CapFormData = z.infer; @@ -68,6 +74,8 @@ export const useEditForm = ({ editingCap }: UseEditFormProps) => { ? editingCap.capData.core.prompt.suggestions : [], }, + homepage: editingCap?.capData.metadata.homepage || '', + repository: editingCap?.capData.metadata.repository || '', }, }); @@ -87,7 +95,11 @@ export const useEditForm = ({ editingCap }: UseEditFormProps) => { setMcpServers(servers); }; - const handleUpdateCap = async (editingCap: LocalCap, data: CapFormData) => { + const handleUpdateCap = async ( + editingCap: LocalCap, + data: CapFormData, + thumbnail?: CapThumbnail, + ) => { // Update existing cap updateCap(editingCap.id, { capData: { @@ -99,7 +111,12 @@ export const useEditForm = ({ editingCap }: UseEditFormProps) => { description: data.description, tags: data.tags, submittedAt: 0, - thumbnail: null, + thumbnail: + thumbnail !== undefined + ? thumbnail + : editingCap.capData.metadata.thumbnail || null, + homepage: data.homepage || undefined, + repository: data.repository || undefined, }, core: { prompt: data.prompt, @@ -114,7 +131,10 @@ export const useEditForm = ({ editingCap }: UseEditFormProps) => { navigate('/cap-studio'); }; - const handleCreateCap = async (data: CapFormData) => { + const handleCreateCap = async ( + data: CapFormData, + thumbnail?: CapThumbnail, + ) => { // Create new cap createCap({ id: `${did}:${data.idName}`, @@ -125,7 +145,9 @@ export const useEditForm = ({ editingCap }: UseEditFormProps) => { description: data.description, tags: data.tags, submittedAt: 0, - thumbnail: null, + thumbnail: thumbnail || null, + homepage: data.homepage || undefined, + repository: data.repository || undefined, }, core: { prompt: data.prompt, @@ -139,7 +161,10 @@ export const useEditForm = ({ editingCap }: UseEditFormProps) => { navigate('/cap-studio'); }; - const handleFormSave = async (data: CapFormData) => { + const handleFormSave = async ( + data: CapFormData, + thumbnail?: CapThumbnail, + ) => { // Trigger validation for all fields const isValid = await form.trigger(); @@ -157,9 +182,9 @@ export const useEditForm = ({ editingCap }: UseEditFormProps) => { try { if (editingCap) { // Update existing cap - handleUpdateCap(editingCap, data); + handleUpdateCap(editingCap, data, thumbnail); } else { - handleCreateCap(data); + handleCreateCap(data, thumbnail); } } catch (error) { toast.error('Failed to save cap. Please try again.'); diff --git a/src/features/cap-studio/hooks/use-submit-form.ts b/src/features/cap-studio/hooks/use-submit-form.ts index 65a44565..bc04b883 100644 --- a/src/features/cap-studio/hooks/use-submit-form.ts +++ b/src/features/cap-studio/hooks/use-submit-form.ts @@ -46,7 +46,7 @@ export const useSubmitForm = ({ cap }: UseSubmitFormProps) => { navigate('/cap-studio'); }; - const processConfirmedSubmit = async (submitFormData: SubmitFormData) => { + const processConfirmedSubmit = async (submitFormData: SubmitFormData, thumbnailOverride?: CapThumbnail) => { try { const capWithSubmitFormData = { ...cap.capData, @@ -55,7 +55,7 @@ export const useSubmitForm = ({ cap }: UseSubmitFormProps) => { homepage: submitFormData.homepage || undefined, repository: submitFormData.repository || undefined, submittedAt: Date.now(), - thumbnail, + thumbnail: thumbnailOverride !== undefined ? thumbnailOverride : thumbnail, }, }; @@ -120,6 +120,29 @@ export const useSubmitForm = ({ cap }: UseSubmitFormProps) => { } }; + const handleDirectSubmit = async (thumbnail?: CapThumbnail, homepage?: string, repository?: string) => { + setIsSubmitting(true); + + const submitFormData = { + homepage: homepage || '', + repository: repository || '', + }; + + try { + await processConfirmedSubmit(submitFormData, thumbnail); + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'Failed to submit cap. Please try again.'; + toast.error(errorMessage); + + navigate('/cap-studio'); + } finally { + setIsSubmitting(false); + } + }; + const handleFieldChange = (fieldName: keyof SubmitFormData) => { form.trigger(fieldName); }; @@ -129,6 +152,7 @@ export const useSubmitForm = ({ cap }: UseSubmitFormProps) => { handleCancel, handleFormSubmit, handleConfirmedSubmit, + handleDirectSubmit, handleFieldChange, isSubmitting, showConfirmDialog, diff --git a/src/pages/cap-studio-batch-create.tsx b/src/pages/cap-studio-batch-create.tsx new file mode 100644 index 00000000..9fcb0b0e --- /dev/null +++ b/src/pages/cap-studio-batch-create.tsx @@ -0,0 +1,5 @@ +import { BatchCreate } from '@/features/cap-studio/components/batch-create'; + +export default function CapStudioBatchCreatePage() { + return ; +} \ No newline at end of file diff --git a/src/router.tsx b/src/router.tsx index e782a3cf..5d28ac59 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -3,6 +3,7 @@ import MainLayout from './layout/main-layout'; import RootLayout from './layout/root-layout'; import CallbackPage from './pages/callback'; import CapStudioPage from './pages/cap-studio'; +import CapStudioBatchCreatePage from './pages/cap-studio-batch-create'; import CapStudioCreatePage from './pages/cap-studio-create'; import CapStudioEditPage from './pages/cap-studio-edit'; import CapStudioMcpPage from './pages/cap-studio-mcp'; @@ -29,6 +30,7 @@ const router = createBrowserRouter([ { path: 'settings', element: }, { path: 'cap-studio', element: }, { path: 'cap-studio/create', element: }, + { path: 'cap-studio/batch-create', element: }, { path: 'cap-studio/edit/:id', element: }, { path: 'cap-studio/submit/:id', element: }, { path: 'cap-studio/mcp', element: }, diff --git a/src/shared/types/cap.ts b/src/shared/types/cap.ts index 06bbb45c..37925b52 100644 --- a/src/shared/types/cap.ts +++ b/src/shared/types/cap.ts @@ -21,80 +21,3 @@ export { CapSchema, CapThumbnailSchema, } from '@nuwa-ai/cap-kit'; - -/* -// Zod schemas as single source of truth -export const CapMcpServerConfigSchema = z.object({ - url: z.string(), - transport: z.enum(['httpStream', 'sse']), -}); - -export const CapModelSchema = z.object({ - id: z.string(), - name: z.string(), - slug: z.string(), - providerName: z.string(), - providerSlug: z.string(), - description: z.string(), - contextLength: z.number(), - pricing: z.object({ - input_per_million_tokens: z.number(), - output_per_million_tokens: z.number(), - request_per_k_requests: z.number(), - image_per_k_images: z.number(), - web_search_per_k_searches: z.number(), - }), - supported_inputs: z.array(z.string()), - supported_parameters: z.array(z.string()), -}); - -export const CapPromptSchema = z.object({ - value: z.string(), - suggestions: z.array(z.string()).optional(), -}); - -export const CapIDSchema = z.object({ - id: z.string(), - authorDID: z.string(), - idName: z.string(), -}); - -export const CapCoreSchema = z.object({ - prompt: CapPromptSchema, - model: CapModelSchema, - mcpServers: z.record(z.string(), CapMcpServerConfigSchema), -}); - -export const CapThumbnailSchema = z - .object({ - type: z.enum(['file', 'url']), - file: z.string().optional(), - url: z.string().optional(), - }) - .nullable(); - -export const CapMetadataSchema = z.object({ - displayName: z.string(), - description: z.string(), - tags: z.array(z.string()), - submittedAt: z.number(), - homepage: z.string().optional(), - repository: z.string().optional(), - thumbnail: CapThumbnailSchema, -}); - -export const CapSchema = CapIDSchema.extend({ - core: CapCoreSchema, - metadata: CapMetadataSchema, -}); - -// Inferred TypeScript types from Zod schemas -export type CapMcpServerConfig = z.infer; -export type CapModel = z.infer; -export type CapPrompt = z.infer; -export type CapID = z.infer; -export type CapCore = z.infer; -export type CapThumbnail = z.infer; -export type CapMetadata = z.infer; -export type Cap = z.infer; -*/ From c991d4863644b4316df757151f07d321e4306261 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Thu, 14 Aug 2025 20:40:02 +0800 Subject: [PATCH 10/28] fix: cap store data compatibility issues --- .../cap-store/components/cap-card.tsx | 88 ++++++++----- .../cap-store/components/cap-selector.tsx | 30 +++-- .../components/cap-store-content.tsx | 80 ++++++------ .../components/cap-store-modal-context.tsx | 81 ++++++++++++ .../cap-store/components/cap-store-modal.tsx | 52 ++------ .../components/cap-store-sidebar.tsx | 7 +- src/features/cap-store/hooks/use-cap-store.ts | 6 +- .../cap-store/hooks/use-remote-cap.ts | 5 +- src/features/cap-store/types.ts | 6 + src/features/cap-store/utils.ts | 60 +++++++++ .../components/my-caps/cap-card.tsx | 26 +--- .../cap-studio/hooks/use-submit-form.ts | 116 ++++-------------- .../chat/components/suggested-actions.tsx | 2 +- 13 files changed, 312 insertions(+), 247 deletions(-) create mode 100644 src/features/cap-store/components/cap-store-modal-context.tsx diff --git a/src/features/cap-store/components/cap-card.tsx b/src/features/cap-store/components/cap-card.tsx index 4c80611c..17a12d20 100644 --- a/src/features/cap-store/components/cap-card.tsx +++ b/src/features/cap-store/components/cap-card.tsx @@ -1,4 +1,5 @@ -import { MoreHorizontal } from 'lucide-react'; +import { Loader2, MoreHorizontal } from 'lucide-react'; +import { useState } from 'react'; import { Card, DropdownMenu, @@ -6,8 +7,11 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/shared/components/ui'; -import type { CapMetadata } from '@/shared/types/cap'; +import type { Cap } from '@/shared/types/cap'; +import { useCapStore } from '../hooks/use-cap-store'; +import type { RemoteCap } from '../types'; import { CapAvatar } from './cap-avatar'; +import { useCapStoreModal } from './cap-store-modal-context'; interface CapCardActions { icon: React.ReactNode; @@ -16,22 +20,41 @@ interface CapCardActions { } export interface CapCardProps { - capMetadata: CapMetadata; - onClick: () => void; + cap: Cap | RemoteCap; actions?: CapCardActions[]; } -export function CapCard({ capMetadata, onClick, actions }: CapCardProps) { +export function CapCard({ cap, actions }: CapCardProps) { + const { runCap } = useCapStore(); + const { closeModal } = useCapStoreModal(); + const [isLoading, setIsLoading] = useState(false); + + const handleCapClick = async (cap: Cap | RemoteCap) => { + setIsLoading(true); + try { + const isRemoteCap = 'cid' in cap; + if (isRemoteCap) { + await runCap(cap.id, cap.cid); + } else { + await runCap(cap.id); + } + closeModal(); + } finally { + setIsLoading(false); + } + }; + + const capMetadata = cap.metadata; return ( handleCapClick(cap)} >

@@ -41,26 +64,35 @@ export function CapCard({ capMetadata, onClick, actions }: CapCardProps) { {capMetadata.description}

- - - - - - {actions?.map((action) => ( - - {action.icon} - {action.label} - - ))} - - + {isLoading ? ( + + ) : ( + + + + + + {actions?.map((action) => ( + e.preventDefault()} + onClick={(e) => e.stopPropagation()} + onSelect={() => action.onClick()} + > + {action.icon} + {action.label} + + ))} + + + )}
); diff --git a/src/features/cap-store/components/cap-selector.tsx b/src/features/cap-store/components/cap-selector.tsx index c727206f..384cc0fa 100644 --- a/src/features/cap-store/components/cap-selector.tsx +++ b/src/features/cap-store/components/cap-selector.tsx @@ -1,5 +1,4 @@ import { AlertCircle, Loader2 } from 'lucide-react'; -import { useState } from 'react'; import { Button, Tooltip, @@ -9,29 +8,29 @@ import { } from '@/shared/components/ui'; import { useCurrentCap } from '@/shared/hooks'; import type { Cap } from '@/shared/types'; -import { useRemoteCap } from '../hooks/use-remote-cap'; import { CapAvatar } from './cap-avatar'; import { CapStoreModal } from './cap-store-modal'; +import { CapStoreModalProvider, useCapStoreModal } from './cap-store-modal-context'; const CapInfo = ({ cap }: { cap: Cap }) => ( <> - + {cap.metadata.displayName} ); -export function CapSelector() { +function CapSelectorButton() { const { currentCap, isCurrentCapMCPInitialized, isCurrentCapMCPError, errorMessage, } = useCurrentCap(); - const [isModalOpen, setIsModalOpen] = useState(false); - - // Prefetch remote caps when component loads - useRemoteCap(); - + const { openModal } = useCapStoreModal(); return (
diff --git a/src/features/cap-store/components/cap-store-sidebar.tsx b/src/features/cap-store/components/cap-store-sidebar.tsx index d98c47bd..df50d32f 100644 --- a/src/features/cap-store/components/cap-store-sidebar.tsx +++ b/src/features/cap-store/components/cap-store-sidebar.tsx @@ -17,6 +17,7 @@ import { useEffect, useState } from 'react'; import { Input } from '@/shared/components/ui'; import { predefinedTags } from '@/shared/constants/cap'; import { useDebounceValue, useLanguage } from '@/shared/hooks'; +import type { CapStoreSidebarSection } from '../types'; export interface CapStoreSidebarProps { activeSection: CapStoreSidebarSection; @@ -24,11 +25,7 @@ export interface CapStoreSidebarProps { onSearchChange: (query: string) => void; } -export type CapStoreSidebarSection = { - id: string; - label: string; - type: 'section' | 'tag' | 'divider'; -}; + export function CapStoreSidebar({ activeSection, diff --git a/src/features/cap-store/hooks/use-cap-store.ts b/src/features/cap-store/hooks/use-cap-store.ts index e6f9e0bc..406fee95 100644 --- a/src/features/cap-store/hooks/use-cap-store.ts +++ b/src/features/cap-store/hooks/use-cap-store.ts @@ -28,7 +28,6 @@ export const useCapStore = () => { throw new Error('Cap CID is required for downloading cap'); } const capData = await downloadCap(capCid); - console.log(capData); addInstalledCap({ capData, isFavorite: true, @@ -52,7 +51,11 @@ export const useCapStore = () => { }; const runCap = async (capId: string, capCid?: string) => { + console.log(capId); const installedCap = installedCaps[capId]; + + console.log(installedCap); + if (!installedCap) { if (!capCid) { throw new Error('Cap CID is required for downloading cap'); @@ -63,6 +66,7 @@ export const useCapStore = () => { isFavorite: false, lastUsedAt: Date.now(), }); + setCurrentCap(capData); } else { updateInstalledCap(capId, { lastUsedAt: Date.now(), diff --git a/src/features/cap-store/hooks/use-remote-cap.ts b/src/features/cap-store/hooks/use-remote-cap.ts index bfd1f183..3d444ef9 100644 --- a/src/features/cap-store/hooks/use-remote-cap.ts +++ b/src/features/cap-store/hooks/use-remote-cap.ts @@ -13,7 +13,7 @@ interface UseRemoteCapParams { * Hook for accessing the remote caps with advanced filtering, sorting, and pagination */ export function useRemoteCap() { - const { capKit, isLoading: isCapKitLoading } = useCapKit(); + const { capKit } = useCapKit(); const { remoteCaps, setRemoteCaps } = CapStateStore(); const [lastSearchParams, setLastSearchParams] = useState( {}, @@ -26,7 +26,7 @@ export function useRemoteCap() { const { searchQuery: queryString = '', page: pageNum = 0, - size: sizeNum = 5, + size: sizeNum = 50, tags: tagsArray = [], } = params; @@ -39,7 +39,6 @@ export function useRemoteCap() { throw new Error('CapKit not initialized'); } - // const response = await capKit.queryWithName(queryString); const response = await capKit.queryWithName( queryString, tagsArray, diff --git a/src/features/cap-store/types.ts b/src/features/cap-store/types.ts index f6022dd4..25e4a89b 100644 --- a/src/features/cap-store/types.ts +++ b/src/features/cap-store/types.ts @@ -11,3 +11,9 @@ export type InstalledCap = { isFavorite: boolean; lastUsedAt: number | null; }; + +export type CapStoreSidebarSection = { + id: string; + label: string; + type: 'section' | 'tag' | 'divider'; +}; diff --git a/src/features/cap-store/utils.ts b/src/features/cap-store/utils.ts index 74742a1b..3642ffc7 100644 --- a/src/features/cap-store/utils.ts +++ b/src/features/cap-store/utils.ts @@ -1,4 +1,5 @@ import { type Cap, CapSchema } from '@/shared/types/cap'; +import type { RemoteCap } from './types'; // Cap type guards and utility functions export function validateCapContent(content: unknown): content is Cap { @@ -13,3 +14,62 @@ export function validateCapContent(content: unknown): content is Cap { export function parseCapContent(content: unknown): Cap { return CapSchema.parse(content); } + +/** + * sort the caps by metadata, prioritize the caps with complete metadata, then sort by the newness + */ +export function sortCapsByMetadata( + caps: (Cap | RemoteCap)[], +): (Cap | RemoteCap)[] { + return [...caps].sort((a, b) => { + // 1. prioritize the caps with complete metadata + const aCompleteness = getCapCompleteness(a); + const bCompleteness = getCapCompleteness(b); + + if (aCompleteness !== bCompleteness) { + return bCompleteness - aCompleteness; // the caps with complete metadata are sorted first + } + + // 2. if the completeness is the same, sort by the newness (newer caps are sorted first) + const aTimestamp = getCapTimestamp(a); + const bTimestamp = getCapTimestamp(b); + + return bTimestamp - aTimestamp; // the caps with newer timestamp are sorted first + }); +} + +/** + * calculate the completeness score of the cap + * the higher the score, the more complete the cap is + */ +function getCapCompleteness(cap: Cap | RemoteCap): number { + let score = 0; + + // the basic information completeness + if (cap.metadata.description) score += cap.metadata.displayName.length / 20; + if (cap.metadata.thumbnail) score += 20; + if (cap.metadata.tags && cap.metadata.tags.length > 0) score += 10; + if (cap.metadata.displayName.includes('test')) score -= 20; + if (cap.metadata.description.includes('test')) score -= 20; + + return score; +} + +/** + * get the timestamp of the cap for sorting + * prioritize the last used time, then the submitted time + */ +function getCapTimestamp(cap: Cap | RemoteCap): number { + // check if the cap is an InstalledCap (has lastUsedAt property) + if ('lastUsedAt' in cap && typeof cap.lastUsedAt === 'number') { + return cap.lastUsedAt; + } + + // use the submitted time (all caps have this property) + if (cap.metadata.submittedAt) { + return cap.metadata.submittedAt; + } + + // return 0 by default (to ensure stable sorting) + return 0; +} diff --git a/src/features/cap-studio/components/my-caps/cap-card.tsx b/src/features/cap-studio/components/my-caps/cap-card.tsx index 82a30d09..fca6b5c5 100644 --- a/src/features/cap-studio/components/my-caps/cap-card.tsx +++ b/src/features/cap-studio/components/my-caps/cap-card.tsx @@ -25,7 +25,6 @@ import { Avatar, AvatarFallback, AvatarImage, - Badge, Button, Card, CardContent, @@ -116,19 +115,7 @@ export function CapCard({ onMouseLeave={() => setIsHovered(false)} > - {cap.status === 'submitted' && ( - { - e.stopPropagation(); - handleCopyCid(); - }} - > - - Published - - )} +
{(isMultiSelectMode || isHovered) && (
@@ -247,13 +234,10 @@ export function CapCard({ {cap.status === 'submitted' && cap.cid && ( - <> - - - Copy Published CID - - - + + + Copy Published CID + )} diff --git a/src/features/cap-studio/hooks/use-submit-form.ts b/src/features/cap-studio/hooks/use-submit-form.ts index bc04b883..58445663 100644 --- a/src/features/cap-studio/hooks/use-submit-form.ts +++ b/src/features/cap-studio/hooks/use-submit-form.ts @@ -1,6 +1,4 @@ -import { zodResolver } from '@hookform/resolvers/zod'; import { useState } from 'react'; -import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { z } from 'zod'; @@ -26,27 +24,26 @@ interface UseSubmitFormProps { export const useSubmitForm = ({ cap }: UseSubmitFormProps) => { const [isSubmitting, setIsSubmitting] = useState(false); - const [showConfirmDialog, setShowConfirmDialog] = useState(false); - const [thumbnail, setThumbnail] = useState(null); const navigate = useNavigate(); const { updateCap } = useLocalCapsHandler(); const { submitCap } = useSubmitCap(); - const form = useForm({ - resolver: zodResolver(submitSchema), - defaultValues: { - homepage: '', - repository: '', - }, - }); - - const watchedData = form.watch(); - const handleCancel = () => { navigate('/cap-studio'); }; - const processConfirmedSubmit = async (submitFormData: SubmitFormData, thumbnailOverride?: CapThumbnail) => { + const handleDirectSubmit = async ( + thumbnail?: CapThumbnail, + homepage?: string, + repository?: string, + ) => { + setIsSubmitting(true); + + const submitFormData = { + homepage: homepage || '', + repository: repository || '', + }; + try { const capWithSubmitFormData = { ...cap.capData, @@ -55,81 +52,25 @@ export const useSubmitForm = ({ cap }: UseSubmitFormProps) => { homepage: submitFormData.homepage || undefined, repository: submitFormData.repository || undefined, submittedAt: Date.now(), - thumbnail: thumbnailOverride !== undefined ? thumbnailOverride : thumbnail, + thumbnail: thumbnail || null, }, }; + console.log(capWithSubmitFormData); + // make the submission const result = await submitCap(capWithSubmitFormData); - if (result.success) { - // update cap status to submitted - updateCap(cap.id, { - status: 'submitted', - cid: result.capId, - capData: capWithSubmitFormData, - }); - - toast.success(result.message); - - navigate('/cap-studio'); - } else { - throw new Error(result.message); - } - } catch (error) { - const errorMessage = - error instanceof Error - ? error.message - : 'Failed to submit cap. Please try again.'; - toast.error(errorMessage); - - navigate('/cap-studio'); - } - }; - - const handleFormSubmit = async () => { - // Trigger validation and show errors - const isValid = await form.trigger(); - - if (!isValid) { - // Form will show validation errors automatically - return; - } - - // Show confirmation dialog - setShowConfirmDialog(true); - }; + // update cap status to submitted + updateCap(cap.id, { + status: 'submitted', + cid: result.capId, + capData: capWithSubmitFormData, + }); - const handleConfirmedSubmit = async () => { - const data = form.getValues(); - setIsSubmitting(true); - setShowConfirmDialog(false); - - try { - await processConfirmedSubmit(data); - } catch (error) { - const errorMessage = - error instanceof Error - ? error.message - : 'Failed to submit cap. Please try again.'; - toast.error(errorMessage); + toast.success(result.message); navigate('/cap-studio'); - } finally { - setIsSubmitting(false); - } - }; - - const handleDirectSubmit = async (thumbnail?: CapThumbnail, homepage?: string, repository?: string) => { - setIsSubmitting(true); - - const submitFormData = { - homepage: homepage || '', - repository: repository || '', - }; - - try { - await processConfirmedSubmit(submitFormData, thumbnail); } catch (error) { const errorMessage = error instanceof Error @@ -143,22 +84,9 @@ export const useSubmitForm = ({ cap }: UseSubmitFormProps) => { } }; - const handleFieldChange = (fieldName: keyof SubmitFormData) => { - form.trigger(fieldName); - }; - return { - form, handleCancel, - handleFormSubmit, - handleConfirmedSubmit, handleDirectSubmit, - handleFieldChange, isSubmitting, - showConfirmDialog, - thumbnail, - setThumbnail, - setShowConfirmDialog, - watchedData, }; }; diff --git a/src/features/chat/components/suggested-actions.tsx b/src/features/chat/components/suggested-actions.tsx index a63843a9..c962778f 100644 --- a/src/features/chat/components/suggested-actions.tsx +++ b/src/features/chat/components/suggested-actions.tsx @@ -14,7 +14,7 @@ function PureSuggestedActions({ append }: SuggestedActionsProps) { return (
{suggestedActions.map((suggestedAction, index) => ( Date: Fri, 15 Aug 2025 10:06:16 +0800 Subject: [PATCH 11/28] bump cap-kit to 0.3.8 --- package.json | 2 +- pnpm-lock.yaml | 75 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index b8688b97..47156b65 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@icons-pack/react-simple-icons": "^13.7.0", "@lobehub/icons": "^2.9.0", "@modelcontextprotocol/sdk": "^1.13.2", - "@nuwa-ai/cap-kit": "^0.3.7", + "@nuwa-ai/cap-kit": "^0.3.8", "@nuwa-ai/identity-kit": "^0.4.0", "@nuwa-ai/identity-kit-web": "^0.3.5", "@openrouter/ai-sdk-provider": "^0.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4914a52b..8f7fd71a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,8 +54,8 @@ importers: specifier: ^1.13.2 version: 1.13.2 '@nuwa-ai/cap-kit': - specifier: ^0.3.7 - version: 0.3.7(react@19.1.0)(typescript@5.8.3) + specifier: ^0.3.8 + version: 0.3.8(react@19.1.0)(typescript@5.8.3) '@nuwa-ai/identity-kit': specifier: ^0.4.0 version: 0.4.0(typescript@5.8.3) @@ -513,6 +513,10 @@ packages: resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} @@ -530,8 +534,8 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.27.3': - resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -552,8 +556,8 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.2': - resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==} + '@babel/helpers@7.28.3': + resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} engines: {node: '>=6.9.0'} '@babel/parser@7.28.0': @@ -561,6 +565,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.3': + resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-transform-runtime@7.28.0': resolution: {integrity: sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==} engines: {node: '>=6.9.0'} @@ -579,6 +588,10 @@ packages: resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.3': + resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.0': resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} engines: {node: '>=6.9.0'} @@ -1351,8 +1364,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nuwa-ai/cap-kit@0.3.7': - resolution: {integrity: sha512-H08LZ1sxCvbf9fEiaRnESrTBck0frz8KM96Qmq1eBc/nCtqUzuyakpPK9gGE3YCbEdelHodsM8vLeUspwSZOSA==} + '@nuwa-ai/cap-kit@0.3.8': + resolution: {integrity: sha512-moaBuYJr0j71t6kjF74I0jRuAkbqicIK4oulDcYkv4YI2b2DKtVpm4+EGzpdvxoVGFw+dO23GhIPv19pGG5itw==} '@nuwa-ai/identity-kit-web@0.3.5': resolution: {integrity: sha512-egjSXMiGfDxzPW8l496PRGF2b0MGN+KbTY8m1oRmcnffSWei7A3gvrzHBvKyeHTB93mPbzYck7ca1/09Wclp4A==} @@ -7833,13 +7846,13 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 + '@babel/generator': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.28.2 - '@babel/parser': 7.28.0 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.0) + '@babel/helpers': 7.28.3 + '@babel/parser': 7.28.3 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 @@ -7857,6 +7870,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.28.0 @@ -7885,12 +7906,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color @@ -7902,7 +7923,7 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.28.2': + '@babel/helpers@7.28.3': dependencies: '@babel/template': 7.27.2 '@babel/types': 7.28.2 @@ -7911,6 +7932,10 @@ snapshots: dependencies: '@babel/types': 7.28.0 + '@babel/parser@7.28.3': + dependencies: + '@babel/types': 7.28.2 + '@babel/plugin-transform-runtime@7.28.0(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 @@ -7943,6 +7968,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.28.3': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -8965,10 +9002,10 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nuwa-ai/cap-kit@0.3.7(react@19.1.0)(typescript@5.8.3)': + '@nuwa-ai/cap-kit@0.3.8(react@19.1.0)(typescript@5.8.3)': dependencies: '@modelcontextprotocol/sdk': 1.13.2 - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.6 '@noble/hashes': 1.8.0 '@nuwa-ai/identity-kit': 0.3.6(typescript@5.8.3) '@roochnetwork/rooch-sdk': 0.3.6(typescript@5.8.3) @@ -9002,7 +9039,7 @@ snapshots: '@nuwa-ai/identity-kit@0.3.6(typescript@5.8.3)': dependencies: - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.6 '@noble/hashes': 1.8.0 '@roochnetwork/rooch-sdk': 0.3.6(typescript@5.8.3) multiformats: 9.9.0 From 73bbb2e7a92cdc24ef99e2d7a5e29d4ca9a807df Mon Sep 17 00:00:00 2001 From: jolestar Date: Thu, 14 Aug 2025 01:08:27 +0900 Subject: [PATCH 12/28] Integrate with payment-kit --- package.json | 5 +- pnpm-lock.yaml | 210 ++++++++++-------- src/features/chat/services/providers/index.ts | 8 +- src/shared/services/payment-fetch.ts | 38 ++++ 4 files changed, 165 insertions(+), 96 deletions(-) create mode 100644 src/shared/services/payment-fetch.ts diff --git a/package.json b/package.json index 47156b65..087d9b05 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,10 @@ "@icons-pack/react-simple-icons": "^13.7.0", "@lobehub/icons": "^2.9.0", "@modelcontextprotocol/sdk": "^1.13.2", - "@nuwa-ai/cap-kit": "^0.3.8", + "@nuwa-ai/cap-kit": "^0.4.4", "@nuwa-ai/identity-kit": "^0.4.0", - "@nuwa-ai/identity-kit-web": "^0.3.5", + "@nuwa-ai/identity-kit-web": "^0.4.0", + "@nuwa-ai/payment-kit": "^0.4.4", "@openrouter/ai-sdk-provider": "^0.7.2", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f7fd71a..a72c96da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,14 +54,17 @@ importers: specifier: ^1.13.2 version: 1.13.2 '@nuwa-ai/cap-kit': - specifier: ^0.3.8 - version: 0.3.8(react@19.1.0)(typescript@5.8.3) + specifier: ^0.4.4 + version: 0.4.4(react@19.1.0)(typescript@5.8.3) '@nuwa-ai/identity-kit': specifier: ^0.4.0 version: 0.4.0(typescript@5.8.3) '@nuwa-ai/identity-kit-web': - specifier: ^0.3.5 - version: 0.3.5(react@19.1.0)(typescript@5.8.3) + specifier: ^0.4.0 + version: 0.4.0(react@19.1.0)(typescript@5.8.3) + '@nuwa-ai/payment-kit': + specifier: ^0.4.4 + version: 0.4.4(express@5.1.0)(typescript@5.8.3) '@openrouter/ai-sdk-provider': specifier: ^0.7.2 version: 0.7.2(ai@4.3.16(react@19.1.0)(zod@3.25.67))(zod@3.25.67) @@ -513,10 +516,6 @@ packages: resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} - engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} @@ -534,8 +533,8 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.3': - resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -556,8 +555,8 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.3': - resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} + '@babel/helpers@7.28.2': + resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==} engines: {node: '>=6.9.0'} '@babel/parser@7.28.0': @@ -565,11 +564,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.28.3': - resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/plugin-transform-runtime@7.28.0': resolution: {integrity: sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==} engines: {node: '>=6.9.0'} @@ -588,10 +582,6 @@ packages: resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.3': - resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} - engines: {node: '>=6.9.0'} - '@babel/types@7.28.0': resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} engines: {node: '>=6.9.0'} @@ -1328,10 +1318,6 @@ packages: resolution: {integrity: sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==} engines: {node: ^14.21.3 || >=16} - '@noble/curves@1.9.6': - resolution: {integrity: sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==} - engines: {node: ^14.21.3 || >=16} - '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -1364,26 +1350,31 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nuwa-ai/cap-kit@0.3.8': - resolution: {integrity: sha512-moaBuYJr0j71t6kjF74I0jRuAkbqicIK4oulDcYkv4YI2b2DKtVpm4+EGzpdvxoVGFw+dO23GhIPv19pGG5itw==} + '@nuwa-ai/cap-kit@0.4.4': + resolution: {integrity: sha512-oQhMhsMklFlDafYxqsjoZmlGrMWXb9EXUgYkHho9MO0OYmRyKH1nz2O64zTtE2jC1l03zEj5PfKV1HydJA27eA==} - '@nuwa-ai/identity-kit-web@0.3.5': - resolution: {integrity: sha512-egjSXMiGfDxzPW8l496PRGF2b0MGN+KbTY8m1oRmcnffSWei7A3gvrzHBvKyeHTB93mPbzYck7ca1/09Wclp4A==} + '@nuwa-ai/identity-kit-web@0.4.0': + resolution: {integrity: sha512-8v95z8BN6duFGsiULgFdTsRATrUkBQ8mPNBQMZTW2DSiaZzoY7HKe4xn7xOe1kBV+DzJGAXHtsFWAJDhYpKfdg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: react: optional: true - '@nuwa-ai/identity-kit@0.3.4': - resolution: {integrity: sha512-Lty7wuKmEuriLmGd15EM1IRPvDTuHvm/Px/BXb3i1O/l2sB6ZlYxybL++KU694I0tY2TZznuc3tNJUJfkYyIfA==} - - '@nuwa-ai/identity-kit@0.3.6': - resolution: {integrity: sha512-eVhgOQja2YiXVuBnCb16xrdF5uHcmBZWPZdBLKU97aaQ4R5pIvPbE26MbgHy2s4F+ZC2rthpJcd5zthGZGXgqQ==} - '@nuwa-ai/identity-kit@0.4.0': resolution: {integrity: sha512-LEULnr4ptnB7DV0+mHdqEM10fzK/DC1jNPpHukB0/kBameQ7p2JnKf8l3JRQ8nVV/x9ObPzyexDfzA6+T4IacQ==} + '@nuwa-ai/payment-kit@0.4.4': + resolution: {integrity: sha512-GgaRaFfMVrHVwggrvJrWvyCX3ISAj5jV6JUzVBeTnNNP9/aJBkTHnK4aRkB8QFvJDMoD8xpOdJrd9Qkri3wkIg==} + peerDependencies: + express: '>=4' + pg: '>=8' + peerDependenciesMeta: + express: + optional: true + pg: + optional: true + '@openrouter/ai-sdk-provider@0.7.2': resolution: {integrity: sha512-Fry2mV7uGGJRmP9JntTZRc8ElESIk7AJNTacLbF6Syoeb5k8d7HPGkcK9rTXDlqBb8HgU1hOKtz23HojesTmnw==} engines: {node: '>=18'} @@ -3161,6 +3152,9 @@ packages: '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/pg@8.15.5': + resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} + '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} @@ -5230,6 +5224,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lossless-json@4.1.1: + resolution: {integrity: sha512-HusN80C0ohtT9kOHQH7EuUaqzRQsnekpa+2ot8OzvW0iC08dq/YtM/7uKwwajldQsCrHyC8q9fz3t3L+TmDltA==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -5643,6 +5640,10 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -5797,6 +5798,17 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -5914,6 +5926,22 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + preact@10.24.2: resolution: {integrity: sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==} @@ -7846,13 +7874,13 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.0 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.0) - '@babel/helpers': 7.28.3 - '@babel/parser': 7.28.3 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.28.2 + '@babel/parser': 7.28.0 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.0 '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 @@ -7870,14 +7898,6 @@ snapshots: '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 - '@babel/generator@7.28.3': - dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.2 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.30 - jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.28.0 @@ -7906,12 +7926,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.0)': + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.3 + '@babel/traverse': 7.28.0 transitivePeerDependencies: - supports-color @@ -7923,7 +7943,7 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.28.3': + '@babel/helpers@7.28.2': dependencies: '@babel/template': 7.27.2 '@babel/types': 7.28.2 @@ -7932,10 +7952,6 @@ snapshots: dependencies: '@babel/types': 7.28.0 - '@babel/parser@7.28.3': - dependencies: - '@babel/types': 7.28.2 - '@babel/plugin-transform-runtime@7.28.0(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 @@ -7968,18 +7984,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.28.3': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.3 - '@babel/template': 7.27.2 - '@babel/types': 7.28.2 - debug: 4.4.1 - transitivePeerDependencies: - - supports-color - '@babel/types@7.28.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -8976,10 +8980,6 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 - '@noble/curves@1.9.6': - dependencies: - '@noble/hashes': 1.8.0 - '@noble/hashes@1.4.0': {} '@noble/hashes@1.5.0': {} @@ -9002,12 +9002,12 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nuwa-ai/cap-kit@0.3.8(react@19.1.0)(typescript@5.8.3)': + '@nuwa-ai/cap-kit@0.4.4(react@19.1.0)(typescript@5.8.3)': dependencies: '@modelcontextprotocol/sdk': 1.13.2 - '@noble/curves': 1.9.6 + '@noble/curves': 1.9.2 '@noble/hashes': 1.8.0 - '@nuwa-ai/identity-kit': 0.3.6(typescript@5.8.3) + '@nuwa-ai/identity-kit': 0.4.0(typescript@5.8.3) '@roochnetwork/rooch-sdk': 0.3.6(typescript@5.8.3) ai: 4.3.16(react@19.1.0)(zod@3.25.67) js-yaml: 4.1.0 @@ -9018,16 +9018,16 @@ snapshots: - supports-color - typescript - '@nuwa-ai/identity-kit-web@0.3.5(react@19.1.0)(typescript@5.8.3)': + '@nuwa-ai/identity-kit-web@0.4.0(react@19.1.0)(typescript@5.8.3)': dependencies: - '@nuwa-ai/identity-kit': 0.3.4(typescript@5.8.3) + '@nuwa-ai/identity-kit': 0.4.0(typescript@5.8.3) optionalDependencies: react: 19.1.0 transitivePeerDependencies: - supports-color - typescript - '@nuwa-ai/identity-kit@0.3.4(typescript@5.8.3)': + '@nuwa-ai/identity-kit@0.4.0(typescript@5.8.3)': dependencies: '@noble/curves': 1.9.2 '@noble/hashes': 1.8.0 @@ -9037,22 +9037,18 @@ snapshots: - supports-color - typescript - '@nuwa-ai/identity-kit@0.3.6(typescript@5.8.3)': + '@nuwa-ai/payment-kit@0.4.4(express@5.1.0)(typescript@5.8.3)': dependencies: - '@noble/curves': 1.9.6 - '@noble/hashes': 1.8.0 + '@nuwa-ai/identity-kit': 0.4.0(typescript@5.8.3) '@roochnetwork/rooch-sdk': 0.3.6(typescript@5.8.3) - multiformats: 9.9.0 - transitivePeerDependencies: - - supports-color - - typescript - - '@nuwa-ai/identity-kit@0.4.0(typescript@5.8.3)': - dependencies: - '@noble/curves': 1.9.6 - '@noble/hashes': 1.8.0 - '@roochnetwork/rooch-sdk': 0.3.6(typescript@5.8.3) - multiformats: 9.9.0 + '@types/js-yaml': 4.0.9 + '@types/pg': 8.15.5 + js-yaml: 4.1.0 + lossless-json: 4.1.1 + on-headers: 1.1.0 + zod: 3.25.67 + optionalDependencies: + express: 5.1.0 transitivePeerDependencies: - supports-color - typescript @@ -11606,6 +11602,12 @@ snapshots: '@types/parse-json@4.0.2': {} + '@types/pg@8.15.5': + dependencies: + '@types/node': 24.0.3 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + '@types/prismjs@1.26.5': {} '@types/react-dom@19.1.6(@types/react@19.1.8)': @@ -14723,6 +14725,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lossless-json@4.1.1: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -15407,6 +15411,8 @@ snapshots: dependencies: ee-first: 1.1.1 + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -15620,6 +15626,18 @@ snapshots: pathe@2.0.3: {} + pg-int8@1.0.1: {} + + pg-protocol@1.10.3: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -15730,6 +15748,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + preact@10.24.2: {} preact@10.27.0: {} diff --git a/src/features/chat/services/providers/index.ts b/src/features/chat/services/providers/index.ts index 6611d07c..d39b735a 100644 --- a/src/features/chat/services/providers/index.ts +++ b/src/features/chat/services/providers/index.ts @@ -1,12 +1,14 @@ import { createOpenAI } from '@ai-sdk/openai'; -import { createAuthorizedFetch } from '@/shared/services/authorized-fetch'; import { createOpenRouter } from './openrouter-provider'; +import { createPaymentFetch } from '@/shared/services/payment-fetch'; // Settings of Nuwa LLM Gateway +// const baseURL = 'https://test-llm.nuwa.dev/api/v1'; +const baseURL = 'https://llm-gateway-payment-test.up.railway.app/api/v1'; const providerSettings = { apiKey: 'NOT-USED', // specify a fake api key to avoid provider errors - baseURL: 'https://test-llm.nuwa.dev/api/v1', - fetch: createAuthorizedFetch(), + baseURL, + fetch: createPaymentFetch(baseURL), }; const openrouter = createOpenRouter(providerSettings); diff --git a/src/shared/services/payment-fetch.ts b/src/shared/services/payment-fetch.ts new file mode 100644 index 00000000..3f610854 --- /dev/null +++ b/src/shared/services/payment-fetch.ts @@ -0,0 +1,38 @@ +import { createHttpClient } from '@nuwa-ai/payment-kit'; +import type { PaymentChannelHttpClient } from '@nuwa-ai/payment-kit'; +import { IdentityKitWeb } from '@nuwa-ai/identity-kit-web'; + +/** + * Create a fetch-compatible function backed by Payment Kit. + * It automatically handles payment-channel headers and streaming settlement. + */ +export function createPaymentFetch(baseUrl: string, options?: { maxAmount?: bigint }) { + let clientPromise: Promise | null = null; + + async function ensureClient(): Promise { + if (!clientPromise) { + const sdk = await IdentityKitWeb.init({ storage: 'local' }); + const env = sdk.getIdentityEnv(); + clientPromise = createHttpClient({ + baseUrl, + env, + maxAmount: options?.maxAmount, + debug: false, + }); + } + return clientPromise; + } + + return async function paymentFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const targetUrl = new URL(typeof input === 'string' ? input : (input as any).url ?? input.toString()); + const methodFromInit = (init?.method ?? 'POST').toUpperCase() as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + + const client = await ensureClient(); + // Important: do NOT wait for payment resolution here, otherwise + // streaming responses may time out before emitting the in-band payment header. + const handle = await client.createRequestHandle(methodFromInit, targetUrl.toString(), init); + return handle.response; + }; +} + + From 4dfd320f4419f26c27275dc04886e2f38a822cf3 Mon Sep 17 00:00:00 2001 From: jolestar Date: Thu, 14 Aug 2025 23:28:44 +0900 Subject: [PATCH 13/28] Balance and Transactions --- src/features/chat/services/providers/index.ts | 4 +- .../wallet/components/balance-card.tsx | 15 ++- .../wallet/components/sidebar-wallet-card.tsx | 8 +- .../wallet/components/transaction-history.tsx | 108 +++++++++--------- src/shared/config/llm-gateway.ts | 6 + src/shared/hooks/usePaymentHub.ts | 43 +++++++ src/shared/hooks/useTransactions.ts | 50 ++++++++ src/shared/services/payment-clients.ts | 42 +++++++ src/shared/services/payment-fetch.ts | 28 +---- 9 files changed, 210 insertions(+), 94 deletions(-) create mode 100644 src/shared/config/llm-gateway.ts create mode 100644 src/shared/hooks/usePaymentHub.ts create mode 100644 src/shared/hooks/useTransactions.ts create mode 100644 src/shared/services/payment-clients.ts diff --git a/src/features/chat/services/providers/index.ts b/src/features/chat/services/providers/index.ts index d39b735a..7670f3a1 100644 --- a/src/features/chat/services/providers/index.ts +++ b/src/features/chat/services/providers/index.ts @@ -1,10 +1,10 @@ import { createOpenAI } from '@ai-sdk/openai'; import { createOpenRouter } from './openrouter-provider'; import { createPaymentFetch } from '@/shared/services/payment-fetch'; +import { LLM_GATEWAY_BASE_URL } from '@/shared/config/llm-gateway'; // Settings of Nuwa LLM Gateway -// const baseURL = 'https://test-llm.nuwa.dev/api/v1'; -const baseURL = 'https://llm-gateway-payment-test.up.railway.app/api/v1'; +const baseURL = LLM_GATEWAY_BASE_URL; const providerSettings = { apiKey: 'NOT-USED', // specify a fake api key to avoid provider errors baseURL, diff --git a/src/features/wallet/components/balance-card.tsx b/src/features/wallet/components/balance-card.tsx index 72957094..51a2d178 100644 --- a/src/features/wallet/components/balance-card.tsx +++ b/src/features/wallet/components/balance-card.tsx @@ -8,8 +8,7 @@ import { CardTitle, } from '@/shared/components/ui/card'; import { useDevMode } from '@/shared/hooks/use-dev-mode'; -import { useNuwaToUsdRate } from '../hooks/use-nuwa-to-usd-rate'; -import { useWallet } from '../hooks/use-wallet'; +import { usePaymentHubRgas } from '@/shared/hooks/usePaymentHub'; import { TestnetFaucetDialog } from './testnet-faucet-dialog'; interface BalanceCardProps { @@ -17,13 +16,12 @@ interface BalanceCardProps { } export function BalanceCard({ onTopUp }: BalanceCardProps) { - const { balance } = useWallet(); - const nuwaToUsdRate = useNuwaToUsdRate(); + const { amount, usd } = usePaymentHubRgas(); const isDevMode = useDevMode(); const [showFaucetDialog, setShowFaucetDialog] = useState(false); - const nuwaValue = balance.toLocaleString(); - const usdValue = (balance / nuwaToUsdRate).toFixed(6); + const rgasValue = amount; + const usdValue = usd; return ( <> @@ -39,8 +37,8 @@ export function BalanceCard({ onTopUp }: BalanceCardProps) {
- Balance -

Testnet

+ PaymentHub Balance +

RGas on Testnet

@@ -68,6 +66,7 @@ export function BalanceCard({ onTopUp }: BalanceCardProps) { USD
+
{rgasValue} RGas
{/*
diff --git a/src/features/wallet/components/sidebar-wallet-card.tsx b/src/features/wallet/components/sidebar-wallet-card.tsx index bbd3581b..305917d7 100644 --- a/src/features/wallet/components/sidebar-wallet-card.tsx +++ b/src/features/wallet/components/sidebar-wallet-card.tsx @@ -5,8 +5,7 @@ import { useCopyToClipboard } from 'usehooks-ts'; import { useAuth } from '@/features/auth/hooks/use-auth'; import { Card, CardContent } from '@/shared/components/ui/card'; import { cn } from '@/shared/utils'; -import { useNuwaToUsdRate } from '../hooks/use-nuwa-to-usd-rate'; -import { useWallet } from '../hooks/use-wallet'; +import { usePaymentHubRgas } from '@/shared/hooks/usePaymentHub'; interface SidebarWalletCardProps { className?: string; @@ -15,11 +14,10 @@ interface SidebarWalletCardProps { export function SidebarWalletCard({ className }: SidebarWalletCardProps) { const navigate = useNavigate(); const { did, isConnected } = useAuth(); - const { balance } = useWallet(); - const nuwaToUsdRate = useNuwaToUsdRate(); + const { usd, loading, error } = usePaymentHubRgas(); const [_, copyToClipboard] = useCopyToClipboard(); - const usdValue = (balance / nuwaToUsdRate).toFixed(2); + const usdValue = loading ? '…' : error ? '-' : usd; const handleClick = () => { navigate('/wallet'); diff --git a/src/features/wallet/components/transaction-history.tsx b/src/features/wallet/components/transaction-history.tsx index 4351e879..1726cc60 100644 --- a/src/features/wallet/components/transaction-history.tsx +++ b/src/features/wallet/components/transaction-history.tsx @@ -4,80 +4,78 @@ import { CardHeader, CardTitle, } from '@/shared/components/ui/card'; -import { useNuwaToUsdRate } from '../hooks/use-nuwa-to-usd-rate'; -import { useWallet } from '../hooks/use-wallet'; -import type { Transaction } from '../types'; - -function TransactionRow({ transaction }: { transaction: Transaction }) { - const nuwaToUsdRate = useNuwaToUsdRate(); - const amountColor = - transaction.type === 'deposit' ? 'text-green-600' : 'text-red-600'; - const amountPrefix = transaction.type === 'deposit' ? '+' : '-'; - - const formatDate = (timestamp: string) => { - return new Date(timestamp).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }); - }; - - const getStatusBadge = (status: Transaction['status']) => { - const baseClasses = - 'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium'; - - switch (status) { - case 'completed': - return `${baseClasses} bg-green-100 text-green-800`; - case 'confirming': - return `${baseClasses} bg-yellow-100 text-yellow-800`; - default: - return `${baseClasses} bg-gray-100 text-gray-800`; - } - }; - - const usdValue = transaction.amount.toFixed(6).toLocaleString(); - const nuwaValue = (transaction.amount * nuwaToUsdRate).toFixed(6); +import { useTransactions } from '@/shared/hooks/useTransactions'; +import { formatAmount } from '@nuwa-ai/payment-kit'; +function TransactionRow({ + operation, + status, + statusCode, + cost, + costUsd, + paidAt, + stream, +}: { + operation: string; + status: string; + statusCode?: number; + cost?: string; + costUsd?: string; + paidAt?: string; + stream?: boolean; +}) { return (
- {transaction.label} - - {transaction.status} - + {operation} + {stream && stream}
-

- {formatDate(transaction.timestamp.toString())} -

+

{paidAt ? new Date(paidAt).toLocaleString() : ''}

-
-
{`${amountPrefix}$${usdValue} USD`}
-
- {`${amountPrefix}${nuwaValue} $NUWA`} -
+
+
{status}{statusCode ? ` (${statusCode})` : ''}
+ {cost && ( +
{costUsd ? `${cost} (${costUsd} USD)` : cost}
+ )}
); } export function TransactionHistory() { - const { transactions } = useWallet(); + const { items, loading, error, reload } = useTransactions(50); return ( - - Recent Transactions + + Payment Transactions + - {transactions.length === 0 ? ( -

- No transactions found -

+ {loading ?

Loading...

: error ? ( +

{error}

+ ) : items.length === 0 ? ( +

No payment transactions

) : (
- {transactions.map((transaction) => ( - + {items.map((tx) => ( + { + const v = tx.payment?.costUsd as unknown; + if (typeof v === 'bigint') return `$${formatAmount(v, 12)}`; + if (v !== undefined && v !== null) { + return `$${formatAmount(BigInt(String(v)), 12)}`; + } + return undefined; + })()} + paidAt={tx.payment?.paidAt} + stream={tx.stream} + /> ))}
)} diff --git a/src/shared/config/llm-gateway.ts b/src/shared/config/llm-gateway.ts new file mode 100644 index 00000000..8eac61fd --- /dev/null +++ b/src/shared/config/llm-gateway.ts @@ -0,0 +1,6 @@ +// Centralized LLM Gateway configuration for reuse across features + +// TODO: consider moving to environment-configurable source +export const LLM_GATEWAY_BASE_URL = 'https://llm-gateway-payment-test.up.railway.app/api/v1'; + + diff --git a/src/shared/hooks/usePaymentHub.ts b/src/shared/hooks/usePaymentHub.ts new file mode 100644 index 00000000..f94c6ae5 --- /dev/null +++ b/src/shared/hooks/usePaymentHub.ts @@ -0,0 +1,43 @@ +import { useCallback, useEffect, useState } from 'react'; +import { getPaymentHubClient } from '@/shared/services/payment-clients'; + +function formatBigIntWithDecimals(value: bigint, decimals: number, fractionDigits: number): string { + const negative = value < 0n; + const v = negative ? -value : value; + const base = 10n ** BigInt(decimals); + const integer = v / base; + let fraction = (v % base).toString().padStart(decimals, '0'); + if (fractionDigits >= 0) fraction = fraction.slice(0, Math.min(decimals, fractionDigits)); + const fracPart = fractionDigits > 0 ? `.${fraction}` : ''; + return `${negative ? '-' : ''}${integer.toString()}${fracPart}`; +} + +export function usePaymentHubRgas(defaultAssetId = '0x3::gas_coin::RGas') { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [amount, setAmount] = useState('0'); + const [usd, setUsd] = useState('0'); + + const refetch = useCallback(async () => { + setLoading(true); + try { + const hub = await getPaymentHubClient(defaultAssetId); + const res = await hub.getBalanceWithUsd({ assetId: defaultAssetId }); + setAmount(formatBigIntWithDecimals(res.balance, 8, 8)); + setUsd(formatBigIntWithDecimals(res.balancePicoUSD, 12, 2)); + setError(null); + } catch (e: any) { + setError(e?.message || String(e)); + } finally { + setLoading(false); + } + }, [defaultAssetId]); + + useEffect(() => { + refetch(); + }, [refetch]); + + return { loading, error, amount, usd, refetch }; +} + + diff --git a/src/shared/hooks/useTransactions.ts b/src/shared/hooks/useTransactions.ts new file mode 100644 index 00000000..53e883d3 --- /dev/null +++ b/src/shared/hooks/useTransactions.ts @@ -0,0 +1,50 @@ +import { useCallback, useEffect, useState } from 'react'; +import { getHttpClient } from '@/shared/services/payment-clients'; +import type { TransactionRecord, TransactionStore } from '@nuwa-ai/payment-kit'; + +export function useTransactions(limit = 50) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [items, setItems] = useState([]); + const [store, setStore] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + try { + const client = await getHttpClient(); + const txStore = client.getTransactionStore(); + setStore(txStore); + const res = await txStore.list({}, { limit }); + setItems(res.items); + setError(null); + } catch (e: any) { + setError(e?.message || String(e)); + } finally { + setLoading(false); + } + }, [limit]); + + useEffect(() => { + let unsub: (() => void) | undefined; + (async () => { + await load(); + try { + if (store && store.subscribe) { + unsub = store.subscribe(() => { + // naive: just reload on any change + load(); + }); + } + } catch {} + })(); + return () => { + if (unsub) { + try { unsub(); } catch {} + } + }; + }, [load, store]); + + return { loading, error, items, reload: load }; +} + + diff --git a/src/shared/services/payment-clients.ts b/src/shared/services/payment-clients.ts new file mode 100644 index 00000000..6cea4e56 --- /dev/null +++ b/src/shared/services/payment-clients.ts @@ -0,0 +1,42 @@ +import { IdentityKitWeb } from '@nuwa-ai/identity-kit-web'; +import { createHttpClient, RoochPaymentChannelContract, PaymentHubClient } from '@nuwa-ai/payment-kit'; +import type { PaymentChannelHttpClient } from '@nuwa-ai/payment-kit'; +import { LLM_GATEWAY_BASE_URL } from '@/shared/config/llm-gateway'; + +let httpClientPromise: Promise | null = null; +let hubClientPromise: Promise | null = null; + +async function getIdentityEnvAndSigner() { + const sdk = await IdentityKitWeb.init({ storage: 'local' }); + const env = sdk.getIdentityEnv(); + const signer = env.keyManager; + return { sdk, env, signer }; +} + +export async function getHttpClient(): Promise { + if (!httpClientPromise) { + httpClientPromise = (async () => { + const { env } = await getIdentityEnvAndSigner(); + return createHttpClient({ baseUrl: LLM_GATEWAY_BASE_URL, env }); + })(); + } + return httpClientPromise; +} + +export async function getPaymentHubClient(defaultAssetId?: string): Promise { + if (!hubClientPromise) { + hubClientPromise = (async () => { + const { env, signer } = await getIdentityEnvAndSigner(); + const chain = (env as any).chainConfig || undefined; + const contract = new RoochPaymentChannelContract({ + rpcUrl: chain?.rpcUrl, + network: chain?.network, + debug: !!chain?.debug, + }); + return new PaymentHubClient({ contract, signer, defaultAssetId: defaultAssetId || '0x3::gas_coin::RGas' }); + })(); + } + return hubClientPromise; +} + + diff --git a/src/shared/services/payment-fetch.ts b/src/shared/services/payment-fetch.ts index 3f610854..7c3612c3 100644 --- a/src/shared/services/payment-fetch.ts +++ b/src/shared/services/payment-fetch.ts @@ -1,36 +1,16 @@ -import { createHttpClient } from '@nuwa-ai/payment-kit'; -import type { PaymentChannelHttpClient } from '@nuwa-ai/payment-kit'; -import { IdentityKitWeb } from '@nuwa-ai/identity-kit-web'; +import { getHttpClient } from '@/shared/services/payment-clients'; /** * Create a fetch-compatible function backed by Payment Kit. * It automatically handles payment-channel headers and streaming settlement. */ -export function createPaymentFetch(baseUrl: string, options?: { maxAmount?: bigint }) { - let clientPromise: Promise | null = null; - - async function ensureClient(): Promise { - if (!clientPromise) { - const sdk = await IdentityKitWeb.init({ storage: 'local' }); - const env = sdk.getIdentityEnv(); - clientPromise = createHttpClient({ - baseUrl, - env, - maxAmount: options?.maxAmount, - debug: false, - }); - } - return clientPromise; - } - +export function createPaymentFetch(baseUrl: string, _options?: { maxAmount?: bigint }) { return async function paymentFetch(input: RequestInfo | URL, init?: RequestInit): Promise { const targetUrl = new URL(typeof input === 'string' ? input : (input as any).url ?? input.toString()); const methodFromInit = (init?.method ?? 'POST').toUpperCase() as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; - const client = await ensureClient(); - // Important: do NOT wait for payment resolution here, otherwise - // streaming responses may time out before emitting the in-band payment header. - const handle = await client.createRequestHandle(methodFromInit, targetUrl.toString(), init); + const client = await getHttpClient(); + const handle = await client.requestWithPayment(methodFromInit, targetUrl.toString(), init); return handle.response; }; } From 16f62443853eba24c729c752a571ed1c46425ef0 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Fri, 15 Aug 2025 10:23:40 +0800 Subject: [PATCH 14/28] remove claude workflows --- .github/workflows/claude-code-review.yml | 75 ------------------------ .github/workflows/claude.yml | 59 ------------------- 2 files changed, 134 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml delete mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index ecd27d0a..00000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@beta - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" - - # Direct prompt for automated review (no @claude mention needed) - direct_prompt: | - Please review this pull request and provide feedback on: - - Code quality and best practices - - Potential bugs or issues - - Performance considerations - - Security concerns - - Test coverage - - Be constructive and helpful in your feedback. - - # Optional: Customize review based on file types - # direct_prompt: | - # Review this PR focusing on: - # - For TypeScript files: Type safety and proper interface usage - # - For API endpoints: Security, input validation, and error handling - # - For React components: Performance, accessibility, and best practices - # - For tests: Coverage, edge cases, and test quality - - # Optional: Different prompts for different authors - # direct_prompt: | - # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && - # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || - # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} - - # Optional: Add specific tools for running tests or linting - # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" - - # Optional: Skip review for certain conditions - # if: | - # !contains(github.event.pull_request.title, '[skip-review]') && - # !contains(github.event.pull_request.title, '[WIP]') - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index 58d0fa2e..00000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@beta - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" - - # Optional: Customize the trigger phrase (default: @claude) - # trigger_phrase: "/claude" - - # Optional: Trigger when specific user is assigned to an issue - # assignee_trigger: "claude-bot" - - # Optional: Allow Claude to run specific commands - # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" - - # Optional: Add custom instructions for Claude to customize its behavior for your project - # custom_instructions: | - # Follow our coding standards - # Ensure all new code has tests - # Use TypeScript for new files - - # Optional: Custom environment variables for Claude - # claude_env: | - # NODE_ENV: test - From b5f3eaee6536e7e331b1ceb38fe460dc3cf07d23 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Fri, 15 Aug 2025 10:42:00 +0800 Subject: [PATCH 15/28] fix: only try to update title once --- src/features/chat/hooks/use-update-chat-title.ts | 1 + src/features/chat/services/providers/index.ts | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/features/chat/hooks/use-update-chat-title.ts b/src/features/chat/hooks/use-update-chat-title.ts index 818ad640..e7694dac 100644 --- a/src/features/chat/hooks/use-update-chat-title.ts +++ b/src/features/chat/hooks/use-update-chat-title.ts @@ -13,6 +13,7 @@ export const useUpdateChatTitle = (sessionId: string) => { const firstMessage = session.messages[0]; if (!firstMessage) return; + if (session.title !== 'New Chat') return; const title = await generateTitleFromUserMessage({ message: firstMessage }); diff --git a/src/features/chat/services/providers/index.ts b/src/features/chat/services/providers/index.ts index 7670f3a1..ce18b4e7 100644 --- a/src/features/chat/services/providers/index.ts +++ b/src/features/chat/services/providers/index.ts @@ -1,7 +1,6 @@ -import { createOpenAI } from '@ai-sdk/openai'; -import { createOpenRouter } from './openrouter-provider'; -import { createPaymentFetch } from '@/shared/services/payment-fetch'; import { LLM_GATEWAY_BASE_URL } from '@/shared/config/llm-gateway'; +import { createPaymentFetch } from '@/shared/services/payment-fetch'; +import { createOpenRouter } from './openrouter-provider'; // Settings of Nuwa LLM Gateway const baseURL = LLM_GATEWAY_BASE_URL; @@ -12,7 +11,6 @@ const providerSettings = { }; const openrouter = createOpenRouter(providerSettings); -const openai = createOpenAI(providerSettings); // Export a provider that dynamically resolves models export const llmProvider = { @@ -22,5 +20,4 @@ export const llmProvider = { utility: () => { return openrouter.chat('openai/gpt-4o-mini'); }, - image: (modelId: string) => openai.image(modelId), }; From 10fd2095bc8613c6a0c4adbe5e5a8babee8a1939 Mon Sep 17 00:00:00 2001 From: jolestar Date: Fri, 15 Aug 2025 14:11:18 +0900 Subject: [PATCH 16/28] upgrade payment-kit to 0.4.6 --- package.json | 4 ++-- pnpm-lock.yaml | 20 +++++++++---------- .../wallet/components/transaction-history.tsx | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 087d9b05..bec3ed87 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,10 @@ "@icons-pack/react-simple-icons": "^13.7.0", "@lobehub/icons": "^2.9.0", "@modelcontextprotocol/sdk": "^1.13.2", - "@nuwa-ai/cap-kit": "^0.4.4", + "@nuwa-ai/cap-kit": "^0.4.6", "@nuwa-ai/identity-kit": "^0.4.0", "@nuwa-ai/identity-kit-web": "^0.4.0", - "@nuwa-ai/payment-kit": "^0.4.4", + "@nuwa-ai/payment-kit": "^0.4.6", "@openrouter/ai-sdk-provider": "^0.7.2", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a72c96da..3ff647fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,8 +54,8 @@ importers: specifier: ^1.13.2 version: 1.13.2 '@nuwa-ai/cap-kit': - specifier: ^0.4.4 - version: 0.4.4(react@19.1.0)(typescript@5.8.3) + specifier: ^0.4.6 + version: 0.4.6(react@19.1.0)(typescript@5.8.3) '@nuwa-ai/identity-kit': specifier: ^0.4.0 version: 0.4.0(typescript@5.8.3) @@ -63,8 +63,8 @@ importers: specifier: ^0.4.0 version: 0.4.0(react@19.1.0)(typescript@5.8.3) '@nuwa-ai/payment-kit': - specifier: ^0.4.4 - version: 0.4.4(express@5.1.0)(typescript@5.8.3) + specifier: ^0.4.6 + version: 0.4.6(express@5.1.0)(typescript@5.8.3) '@openrouter/ai-sdk-provider': specifier: ^0.7.2 version: 0.7.2(ai@4.3.16(react@19.1.0)(zod@3.25.67))(zod@3.25.67) @@ -1350,8 +1350,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nuwa-ai/cap-kit@0.4.4': - resolution: {integrity: sha512-oQhMhsMklFlDafYxqsjoZmlGrMWXb9EXUgYkHho9MO0OYmRyKH1nz2O64zTtE2jC1l03zEj5PfKV1HydJA27eA==} + '@nuwa-ai/cap-kit@0.4.6': + resolution: {integrity: sha512-kyb5ZC4nkvgJcsQlHbpkR6ldOQ0Rr+KwExm5+Cnqh18XMaqQjrgPJZkatOryp90lYseFj42PfAa/J95FJpYsbg==} '@nuwa-ai/identity-kit-web@0.4.0': resolution: {integrity: sha512-8v95z8BN6duFGsiULgFdTsRATrUkBQ8mPNBQMZTW2DSiaZzoY7HKe4xn7xOe1kBV+DzJGAXHtsFWAJDhYpKfdg==} @@ -1364,8 +1364,8 @@ packages: '@nuwa-ai/identity-kit@0.4.0': resolution: {integrity: sha512-LEULnr4ptnB7DV0+mHdqEM10fzK/DC1jNPpHukB0/kBameQ7p2JnKf8l3JRQ8nVV/x9ObPzyexDfzA6+T4IacQ==} - '@nuwa-ai/payment-kit@0.4.4': - resolution: {integrity: sha512-GgaRaFfMVrHVwggrvJrWvyCX3ISAj5jV6JUzVBeTnNNP9/aJBkTHnK4aRkB8QFvJDMoD8xpOdJrd9Qkri3wkIg==} + '@nuwa-ai/payment-kit@0.4.6': + resolution: {integrity: sha512-ICIWEPSmNuy34KQlX+eEbQYHNHkeS4S8a28/kuCH60sXCkwiH1nmLFFGF8J9Xxge5rkHbZsHXSlstofv+PNo/Q==} peerDependencies: express: '>=4' pg: '>=8' @@ -9002,7 +9002,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nuwa-ai/cap-kit@0.4.4(react@19.1.0)(typescript@5.8.3)': + '@nuwa-ai/cap-kit@0.4.6(react@19.1.0)(typescript@5.8.3)': dependencies: '@modelcontextprotocol/sdk': 1.13.2 '@noble/curves': 1.9.2 @@ -9037,7 +9037,7 @@ snapshots: - supports-color - typescript - '@nuwa-ai/payment-kit@0.4.4(express@5.1.0)(typescript@5.8.3)': + '@nuwa-ai/payment-kit@0.4.6(express@5.1.0)(typescript@5.8.3)': dependencies: '@nuwa-ai/identity-kit': 0.4.0(typescript@5.8.3) '@roochnetwork/rooch-sdk': 0.3.6(typescript@5.8.3) diff --git a/src/features/wallet/components/transaction-history.tsx b/src/features/wallet/components/transaction-history.tsx index 1726cc60..3f9405bc 100644 --- a/src/features/wallet/components/transaction-history.tsx +++ b/src/features/wallet/components/transaction-history.tsx @@ -73,7 +73,7 @@ export function TransactionHistory() { } return undefined; })()} - paidAt={tx.payment?.paidAt} + paidAt={new Date(tx.timestamp).toISOString()} stream={tx.stream} /> ))} From 78e4dba00ef9683f1c1f467b04e03292941dd257 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Fri, 15 Aug 2025 18:26:52 +0800 Subject: [PATCH 17/28] feat: integrate payment kit --- package.json | 37 +- pnpm-lock.yaml | 1296 +---------------- src/features/cap-store/hooks/use-cap-store.ts | 3 - src/features/cap-store/stores.ts | 2 - src/features/chat/components/header.tsx | 4 +- .../chat/components/multimodal-input.tsx | 8 +- src/features/chat/hooks/use-chat-sessions.ts | 12 +- .../chat/hooks/use-update-chat-title.ts | 19 +- src/features/chat/services/handler.ts | 28 +- src/features/chat/services/index.ts | 4 +- src/features/chat/services/utility-ai.ts | 20 + src/features/chat/stores.ts | 116 +- src/features/chat/types.ts | 10 + .../wallet/components/balance-card.tsx | 8 +- src/features/wallet/components/chat-item.tsx | 87 ++ .../wallet/components/sidebar-wallet-card.tsx | 2 +- .../components/transaction-details-modal.tsx | 301 ++++ .../wallet/components/transaction-history.tsx | 132 +- .../wallet/components/transaction-item.tsx | 61 + src/features/wallet/hooks/use-account-data.ts | 47 - .../wallet/hooks/use-chat-transaction-info.ts | 68 + src/features/wallet/services/account-api.ts | 84 -- src/features/wallet/types.ts | 19 +- src/pages/wallet.tsx | 6 - .../{usePaymentHub.ts => use-payment-hub.ts} | 11 +- src/shared/hooks/useTransactions.ts | 50 - src/shared/services/mcp-transport.ts | 1 - 27 files changed, 828 insertions(+), 1608 deletions(-) create mode 100644 src/features/wallet/components/chat-item.tsx create mode 100644 src/features/wallet/components/transaction-details-modal.tsx create mode 100644 src/features/wallet/components/transaction-item.tsx delete mode 100644 src/features/wallet/hooks/use-account-data.ts create mode 100644 src/features/wallet/hooks/use-chat-transaction-info.ts delete mode 100644 src/features/wallet/services/account-api.ts rename src/shared/hooks/{usePaymentHub.ts => use-payment-hub.ts} (86%) delete mode 100644 src/shared/hooks/useTransactions.ts diff --git a/package.json b/package.json index 087d9b05..6c1413de 100644 --- a/package.json +++ b/package.json @@ -15,26 +15,18 @@ "check:fix": "biome check --apply src/" }, "dependencies": { - "@ai-sdk/openai": "^1.3.22", "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8", "@ai-sdk/react": "^1.2.12", - "@babycommando/entity-db": "^1.0.11", - "@codemirror/lang-python": "^6.2.1", - "@codemirror/state": "^6.5.2", - "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.37.2", "@fontsource/geist-mono": "^5.2.6", "@fontsource/geist-sans": "^5.2.5", "@hookform/resolvers": "^5.1.1", - "@icons-pack/react-simple-icons": "^13.7.0", "@lobehub/icons": "^2.9.0", "@modelcontextprotocol/sdk": "^1.13.2", - "@nuwa-ai/cap-kit": "^0.4.4", + "@nuwa-ai/cap-kit": "^0.4.6", "@nuwa-ai/identity-kit": "^0.4.0", "@nuwa-ai/identity-kit-web": "^0.4.0", - "@nuwa-ai/payment-kit": "^0.4.4", - "@openrouter/ai-sdk-provider": "^0.7.2", + "@nuwa-ai/payment-kit": "^0.4.6", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-aspect-ratio": "^1.1.7", @@ -70,61 +62,36 @@ "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.84.1", "@uiw/react-markdown-preview": "^5.1.4", - "@vercel/functions": "^2.2.2", "@web3icons/react": "^4.0.19", - "@xenova/transformers": "^2.17.2", "ai": "^4.3.16", "class-variance-authority": "^0.7.1", "classnames": "^2.5.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "codemirror": "^6.0.2", "date-fns": "^4.1.0", "dexie": "^4.0.11", - "diff-match-patch": "^1.0.5", "embla-carousel-react": "^8.6.0", "fast-deep-equal": "^3.1.3", "framer-motion": "^12.19.1", - "idb": "^8.0.3", "input-otp": "^1.4.2", - "js-yaml": "^4.1.0", "lodash.debounce": "^4.0.8", "lucide-react": "^0.522.0", "mermaid": "^11.8.0", - "nanoid": "^5.1.5", - "next-themes": "^0.4.6", - "orderedmap": "^2.1.1", - "papaparse": "^5.5.3", - "prosemirror-example-setup": "^1.2.3", - "prosemirror-inputrules": "^1.5.0", - "prosemirror-markdown": "^1.13.2", - "prosemirror-model": "^1.25.1", - "prosemirror-schema-basic": "^1.2.4", - "prosemirror-schema-list": "^1.5.1", - "prosemirror-state": "^1.4.3", - "prosemirror-view": "^1.40.0", "radix-ui": "^1.4.3", "react": "^19.1.0", - "react-data-grid": "7.0.0-beta.56", "react-day-picker": "^9.7.0", "react-dom": "^19.1.0", "react-hook-form": "^7.58.1", - "react-image": "^4.1.0", - "react-markdown": "^10.1.0", "react-medium-image-zoom": "^5.3.0", "react-resizable-panels": "^3.0.3", - "react-router": "^7.6.2", "react-router-dom": "^7.6.2", "recharts": "^3.0.0", "rehype-rewrite": "^4.0.2", "rehype-sanitize": "^6.0.0", - "remark-gfm": "^4.0.1", "sonner": "^2.0.5", "swr": "^2.3.3", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", - "url-metadata": "^5.2.1", - "use-stick-to-bottom": "^1.1.1", "usehooks-ts": "^3.1.1", "vaul": "^1.1.2", "viem": "^2.33.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a72c96da..f2fece84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@ai-sdk/openai': - specifier: ^1.3.22 - version: 1.3.22(zod@3.25.67) '@ai-sdk/provider': specifier: ^1.1.3 version: 1.1.3 @@ -20,21 +17,6 @@ importers: '@ai-sdk/react': specifier: ^1.2.12 version: 1.2.12(react@19.1.0)(zod@3.25.67) - '@babycommando/entity-db': - specifier: ^1.0.11 - version: 1.0.11 - '@codemirror/lang-python': - specifier: ^6.2.1 - version: 6.2.1 - '@codemirror/state': - specifier: ^6.5.2 - version: 6.5.2 - '@codemirror/theme-one-dark': - specifier: ^6.1.3 - version: 6.1.3 - '@codemirror/view': - specifier: ^6.37.2 - version: 6.37.2 '@fontsource/geist-mono': specifier: ^5.2.6 version: 5.2.6 @@ -44,9 +26,6 @@ importers: '@hookform/resolvers': specifier: ^5.1.1 version: 5.1.1(react-hook-form@7.58.1(react@19.1.0)) - '@icons-pack/react-simple-icons': - specifier: ^13.7.0 - version: 13.7.0(react@19.1.0) '@lobehub/icons': specifier: ^2.9.0 version: 2.9.0(@babel/core@7.28.0)(@types/react@19.1.8)(acorn@8.15.0)(antd@5.26.3(date-fns@4.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(framer-motion@12.19.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -54,8 +33,8 @@ importers: specifier: ^1.13.2 version: 1.13.2 '@nuwa-ai/cap-kit': - specifier: ^0.4.4 - version: 0.4.4(react@19.1.0)(typescript@5.8.3) + specifier: ^0.4.6 + version: 0.4.6(react@19.1.0)(typescript@5.8.3) '@nuwa-ai/identity-kit': specifier: ^0.4.0 version: 0.4.0(typescript@5.8.3) @@ -63,11 +42,8 @@ importers: specifier: ^0.4.0 version: 0.4.0(react@19.1.0)(typescript@5.8.3) '@nuwa-ai/payment-kit': - specifier: ^0.4.4 - version: 0.4.4(express@5.1.0)(typescript@5.8.3) - '@openrouter/ai-sdk-provider': - specifier: ^0.7.2 - version: 0.7.2(ai@4.3.16(react@19.1.0)(zod@3.25.67))(zod@3.25.67) + specifier: ^0.4.6 + version: 0.4.6(express@5.1.0)(typescript@5.8.3) '@radix-ui/react-accordion': specifier: ^1.2.11 version: 1.2.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -173,15 +149,9 @@ importers: '@uiw/react-markdown-preview': specifier: ^5.1.4 version: 5.1.4(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@vercel/functions': - specifier: ^2.2.2 - version: 2.2.2 '@web3icons/react': specifier: ^4.0.19 version: 4.0.19(react@19.1.0)(typescript@5.8.3) - '@xenova/transformers': - specifier: ^2.17.2 - version: 2.17.2 ai: specifier: ^4.3.16 version: 4.3.16(react@19.1.0)(zod@3.25.67) @@ -197,18 +167,12 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - codemirror: - specifier: ^6.0.2 - version: 6.0.2 date-fns: specifier: ^4.1.0 version: 4.1.0 dexie: specifier: ^4.0.11 version: 4.0.11 - diff-match-patch: - specifier: ^1.0.5 - version: 1.0.5 embla-carousel-react: specifier: ^8.6.0 version: 8.6.0(react@19.1.0) @@ -218,15 +182,9 @@ importers: framer-motion: specifier: ^12.19.1 version: 12.19.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - idb: - specifier: ^8.0.3 - version: 8.0.3 input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - js-yaml: - specifier: ^4.1.0 - version: 4.1.0 lodash.debounce: specifier: ^4.0.8 version: 4.0.8 @@ -236,51 +194,12 @@ importers: mermaid: specifier: ^11.8.0 version: 11.8.0 - nanoid: - specifier: ^5.1.5 - version: 5.1.5 - next-themes: - specifier: ^0.4.6 - version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - orderedmap: - specifier: ^2.1.1 - version: 2.1.1 - papaparse: - specifier: ^5.5.3 - version: 5.5.3 - prosemirror-example-setup: - specifier: ^1.2.3 - version: 1.2.3 - prosemirror-inputrules: - specifier: ^1.5.0 - version: 1.5.0 - prosemirror-markdown: - specifier: ^1.13.2 - version: 1.13.2 - prosemirror-model: - specifier: ^1.25.1 - version: 1.25.1 - prosemirror-schema-basic: - specifier: ^1.2.4 - version: 1.2.4 - prosemirror-schema-list: - specifier: ^1.5.1 - version: 1.5.1 - prosemirror-state: - specifier: ^1.4.3 - version: 1.4.3 - prosemirror-view: - specifier: ^1.40.0 - version: 1.40.0 radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.1.0 version: 19.1.0 - react-data-grid: - specifier: 7.0.0-beta.56 - version: 7.0.0-beta.56(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-day-picker: specifier: ^9.7.0 version: 9.7.0(react@19.1.0) @@ -290,21 +209,12 @@ importers: react-hook-form: specifier: ^7.58.1 version: 7.58.1(react@19.1.0) - react-image: - specifier: ^4.1.0 - version: 4.1.0(@babel/runtime@7.27.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-markdown: - specifier: ^10.1.0 - version: 10.1.0(@types/react@19.1.8)(react@19.1.0) react-medium-image-zoom: specifier: ^5.3.0 version: 5.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-resizable-panels: specifier: ^3.0.3 version: 3.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-router: - specifier: ^7.6.2 - version: 7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-router-dom: specifier: ^7.6.2 version: 7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -317,9 +227,6 @@ importers: rehype-sanitize: specifier: ^6.0.0 version: 6.0.0 - remark-gfm: - specifier: ^4.0.1 - version: 4.0.1 sonner: specifier: ^2.0.5 version: 2.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -332,12 +239,6 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17) - url-metadata: - specifier: ^5.2.1 - version: 5.2.1 - use-stick-to-bottom: - specifier: ^1.1.1 - version: 1.1.1(react@19.1.0) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -414,12 +315,6 @@ packages: '@adraffy/ens-normalize@1.11.0': resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} - '@ai-sdk/openai@1.3.22': - resolution: {integrity: sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.0.0 - '@ai-sdk/provider-utils@2.2.8': resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} engines: {node: '>=18'} @@ -516,6 +411,10 @@ packages: resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.27.2': resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} @@ -533,8 +432,8 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.27.3': - resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -555,8 +454,8 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.2': - resolution: {integrity: sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==} + '@babel/helpers@7.28.3': + resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} engines: {node: '>=6.9.0'} '@babel/parser@7.28.0': @@ -564,6 +463,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.3': + resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-transform-runtime@7.28.0': resolution: {integrity: sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==} engines: {node: '>=6.9.0'} @@ -582,6 +486,10 @@ packages: resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.3': + resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.0': resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} engines: {node: '>=6.9.0'} @@ -590,9 +498,6 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} - '@babycommando/entity-db@1.0.11': - resolution: {integrity: sha512-7516YByMjoR6x/b6/fJx+w+GfNTE5T9X68Be+OtWYJPsRn7ELPWOs+4L8GbljW01WITr5anxuRYbFLWpdKLs1A==} - '@base-org/account@1.1.1': resolution: {integrity: sha512-IfVJPrDPhHfqXRDb89472hXkpvJuQQR7FDI9isLPHEqSYt/45whIoBxSPgZ0ssTt379VhQo4+87PWI1DoLSfAQ==} @@ -670,33 +575,6 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} - '@codemirror/autocomplete@6.18.6': - resolution: {integrity: sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==} - - '@codemirror/commands@6.8.1': - resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==} - - '@codemirror/lang-python@6.2.1': - resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} - - '@codemirror/language@6.11.1': - resolution: {integrity: sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==} - - '@codemirror/lint@6.8.5': - resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==} - - '@codemirror/search@6.5.11': - resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} - - '@codemirror/state@6.5.2': - resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==} - - '@codemirror/theme-one-dark@6.1.3': - resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} - - '@codemirror/view@6.37.2': - resolution: {integrity: sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==} - '@coinbase/wallet-sdk@3.9.3': resolution: {integrity: sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==} @@ -1072,10 +950,6 @@ packages: peerDependencies: react-hook-form: ^7.55.0 - '@huggingface/jinja@0.2.2': - resolution: {integrity: sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==} - engines: {node: '>=18'} - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1102,11 +976,6 @@ packages: '@iconify/utils@2.3.0': resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} - '@icons-pack/react-simple-icons@13.7.0': - resolution: {integrity: sha512-Vx5mnIm/3gD/9dpCfw/EdCXwzCswmvWnvMjL6zUJTbpk2PuyCdx5zSfiX8KQKYszD/1Z2mfaiBtqCxlHuDcpuA==} - peerDependencies: - react: ^16.13 || ^17 || ^18 || ^19 - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1144,18 +1013,6 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} - '@lezer/common@1.2.3': - resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} - - '@lezer/highlight@1.2.1': - resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} - - '@lezer/lr@1.4.2': - resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} - - '@lezer/python@1.1.18': - resolution: {integrity: sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==} - '@lit-labs/ssr-dom-shim@1.3.0': resolution: {integrity: sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==} @@ -1192,9 +1049,6 @@ packages: react: ^19.0.0 react-dom: ^19.0.0 - '@marijn/find-cluster-break@1.0.2': - resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} - '@mdx-js/mdx@3.1.0': resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} @@ -1318,6 +1172,10 @@ packages: resolution: {integrity: sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.9.6': + resolution: {integrity: sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==} + engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -1350,8 +1208,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nuwa-ai/cap-kit@0.4.4': - resolution: {integrity: sha512-oQhMhsMklFlDafYxqsjoZmlGrMWXb9EXUgYkHho9MO0OYmRyKH1nz2O64zTtE2jC1l03zEj5PfKV1HydJA27eA==} + '@nuwa-ai/cap-kit@0.4.6': + resolution: {integrity: sha512-kyb5ZC4nkvgJcsQlHbpkR6ldOQ0Rr+KwExm5+Cnqh18XMaqQjrgPJZkatOryp90lYseFj42PfAa/J95FJpYsbg==} '@nuwa-ai/identity-kit-web@0.4.0': resolution: {integrity: sha512-8v95z8BN6duFGsiULgFdTsRATrUkBQ8mPNBQMZTW2DSiaZzoY7HKe4xn7xOe1kBV+DzJGAXHtsFWAJDhYpKfdg==} @@ -1364,8 +1222,8 @@ packages: '@nuwa-ai/identity-kit@0.4.0': resolution: {integrity: sha512-LEULnr4ptnB7DV0+mHdqEM10fzK/DC1jNPpHukB0/kBameQ7p2JnKf8l3JRQ8nVV/x9ObPzyexDfzA6+T4IacQ==} - '@nuwa-ai/payment-kit@0.4.4': - resolution: {integrity: sha512-GgaRaFfMVrHVwggrvJrWvyCX3ISAj5jV6JUzVBeTnNNP9/aJBkTHnK4aRkB8QFvJDMoD8xpOdJrd9Qkri3wkIg==} + '@nuwa-ai/payment-kit@0.4.6': + resolution: {integrity: sha512-ICIWEPSmNuy34KQlX+eEbQYHNHkeS4S8a28/kuCH60sXCkwiH1nmLFFGF8J9Xxge5rkHbZsHXSlstofv+PNo/Q==} peerDependencies: express: '>=4' pg: '>=8' @@ -1375,13 +1233,6 @@ packages: pg: optional: true - '@openrouter/ai-sdk-provider@0.7.2': - resolution: {integrity: sha512-Fry2mV7uGGJRmP9JntTZRc8ElESIk7AJNTacLbF6Syoeb5k8d7HPGkcK9rTXDlqBb8HgU1hOKtz23HojesTmnw==} - engines: {node: '>=18'} - peerDependencies: - ai: ^4.3.16 - zod: ^3.25.34 - '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -1397,36 +1248,6 @@ packages: '@primer/octicons@19.15.3': resolution: {integrity: sha512-SVD0JbTzabLqLx4d5Cl3cyY+i0u3j5/lI6P3FyNkkJb9Z4VAQ+mBvl7WgK0ZFEXBsLc2gxnoYXvC3TkNSEz1NQ==} - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.4': - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} - - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/inquire@1.1.0': - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.0': - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -3116,27 +2937,15 @@ packages: '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - '@types/lodash.debounce@4.0.9': resolution: {integrity: sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==} '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} - '@types/long@4.0.2': - resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -3257,15 +3066,6 @@ packages: peerDependencies: react: '>= 16.8.0' - '@vercel/functions@2.2.2': - resolution: {integrity: sha512-wgtgFwhYAUzS7UOvw44q14FaoRPONEnyoM0JlrjcDkI+kzSu7M8J5Tk5T3Kvv52//8is+HGxVQtaBSiO3G0wbw==} - engines: {node: '>= 18'} - peerDependencies: - '@aws-sdk/credential-provider-web-identity': '*' - peerDependenciesMeta: - '@aws-sdk/credential-provider-web-identity': - optional: true - '@vitejs/plugin-react-swc@3.10.2': resolution: {integrity: sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==} peerDependencies: @@ -3415,9 +3215,6 @@ packages: peerDependencies: react: ^18.2.0 - '@xenova/transformers@2.17.2': - resolution: {integrity: sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==} - abitype@1.0.8: resolution: {integrity: sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==} peerDependencies: @@ -3549,9 +3346,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - b4a@1.6.7: - resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} - babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} @@ -3577,36 +3371,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - bare-events@2.5.4: - resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} - - bare-fs@4.1.6: - resolution: {integrity: sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==} - engines: {bare: '>=1.16.0'} - peerDependencies: - bare-buffer: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - - bare-os@3.6.1: - resolution: {integrity: sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==} - engines: {bare: '>=1.14.0'} - - bare-path@3.0.0: - resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - - bare-stream@2.6.5: - resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==} - peerDependencies: - bare-buffer: '*' - bare-events: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - bare-events: - optional: true - base-x@3.0.11: resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} @@ -3656,9 +3420,6 @@ packages: resolution: {integrity: sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==} engines: {node: '>=0.10'} - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - blakejs@1.2.1: resolution: {integrity: sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==} @@ -3724,9 +3485,6 @@ packages: buffer-xor@1.0.3: resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -3788,13 +3546,6 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - - cheerio@1.1.0: - resolution: {integrity: sha512-+0hMx9eYhJvWbgpKV9hN7jg0JcwydpopZE4hgi+KvQtByZXPp04NiCWU0LzcAbP63abZckIHkTQaXVF52mX3xQ==} - engines: {node: '>=18.17'} - chevrotain-allstar@0.3.1: resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} peerDependencies: @@ -3811,9 +3562,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chroma-js@3.1.2: resolution: {integrity: sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg==} @@ -3844,9 +3592,6 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc - codemirror@6.0.2: - resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} - collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -3857,13 +3602,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -3963,9 +3701,6 @@ packages: create-hmac@1.1.7: resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} - crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - cross-fetch@3.2.0: resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} @@ -3979,16 +3714,9 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - css-selector-parser@3.1.3: resolution: {integrity: sha512-gJMigczVZqYAk0hPVzx/M4Hm1D9QOtqkdQk9005TNzDIUGzo5cnHEDiKUT7jGPximL/oYb+LIitcHFQ4aKupxg==} - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -4202,14 +3930,6 @@ packages: resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} engines: {node: '>=14.16'} - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -4242,10 +3962,6 @@ packages: detect-browser@5.3.0: resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} - engines: {node: '>=8'} - detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -4271,22 +3987,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - dompurify@3.2.6: resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - drbg.js@1.0.1: resolution: {integrity: sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==} engines: {node: '>=0.10'} @@ -4350,9 +4053,6 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - encoding-sniffer@0.2.1: - resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} - end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -4363,10 +4063,6 @@ packages: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -4526,10 +4222,6 @@ packages: evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - express-rate-limit@7.5.1: resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} engines: {node: '>= 16'} @@ -4561,9 +4253,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -4640,9 +4329,6 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatbuffers@1.12.0: - resolution: {integrity: sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==} - flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -4683,9 +4369,6 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4724,9 +4407,6 @@ packages: giscus@1.6.0: resolution: {integrity: sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ==} - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -4757,9 +4437,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - guid-typescript@1.0.9: - resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} - h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} @@ -4867,9 +4544,6 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - htmlparser2@10.0.0: - resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} - http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -4884,9 +4558,6 @@ packages: idb-keyval@6.2.2: resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} - idb@8.0.3: - resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} - ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -4912,9 +4583,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} @@ -4938,10 +4606,6 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - ipaddr.js@2.2.0: - resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} - engines: {node: '>= 10'} - iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -4958,9 +4622,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -5172,9 +4833,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - lit-element@4.2.0: resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==} @@ -5214,9 +4872,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - long@4.0.0: - resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -5252,10 +4907,6 @@ packages: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -5331,9 +4982,6 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -5476,10 +5124,6 @@ packages: resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} engines: {node: '>= 0.6'} - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} @@ -5493,9 +5137,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -5512,9 +5153,6 @@ packages: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} engines: {node: '>=0.10.0'} - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} @@ -5541,14 +5179,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.5: - resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} - engines: {node: ^18 || >=20} - hasBin: true - - napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -5556,25 +5186,12 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - next-themes@0.4.6: - resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} - peerDependencies: - react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - - node-abi@3.75.0: - resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} - engines: {node: '>=10'} - node-addon-api@2.0.2: resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} node-addon-api@5.1.0: resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} - node-addon-api@6.1.0: - resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} - node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} @@ -5653,26 +5270,10 @@ packages: oniguruma-to-es@4.3.3: resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} - onnx-proto@4.0.4: - resolution: {integrity: sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==} - - onnxruntime-common@1.14.0: - resolution: {integrity: sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==} - - onnxruntime-node@1.14.0: - resolution: {integrity: sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==} - os: [win32, darwin, linux] - - onnxruntime-web@1.14.0: - resolution: {integrity: sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - orderedmap@2.1.1: - resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} - ox@0.6.7: resolution: {integrity: sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==} peerDependencies: @@ -5739,9 +5340,6 @@ packages: package-manager-detector@1.3.0: resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} - papaparse@5.5.3: - resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -5756,12 +5354,6 @@ packages: parse-numeric-range@1.3.0: resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} - parse5-htmlparser2-tree-adapter@7.1.0: - resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} - - parse5-parser-stream@7.1.2: - resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} - parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -5856,9 +5448,6 @@ packages: pkg-types@2.2.0: resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} - platform@1.3.6: - resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} - pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -5948,11 +5537,6 @@ packages: preact@10.27.0: resolution: {integrity: sha512-/DTYoB6mwwgPytiqQTh/7SFRL98ZdiD8Sk8zIUVOxtwq4oWcwrcd1uno9fE/zZmUaUrFNYzbH14CPebOz9tZQw==} - prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - hasBin: true - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -5972,55 +5556,6 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - prosemirror-commands@1.7.1: - resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} - - prosemirror-dropcursor@1.8.2: - resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} - - prosemirror-example-setup@1.2.3: - resolution: {integrity: sha512-+hXZi8+xbFvYM465zZH3rdZ9w7EguVKmUYwYLZjIJIjPK+I0nPTwn8j0ByW2avchVczRwZmOJGNvehblyIerSQ==} - - prosemirror-gapcursor@1.3.2: - resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} - - prosemirror-history@1.4.1: - resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} - - prosemirror-inputrules@1.5.0: - resolution: {integrity: sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==} - - prosemirror-keymap@1.2.3: - resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} - - prosemirror-markdown@1.13.2: - resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==} - - prosemirror-menu@1.2.5: - resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} - - prosemirror-model@1.25.1: - resolution: {integrity: sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==} - - prosemirror-schema-basic@1.2.4: - resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} - - prosemirror-schema-list@1.5.1: - resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} - - prosemirror-state@1.4.3: - resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} - - prosemirror-transform@1.10.4: - resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==} - - prosemirror-view@1.40.0: - resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==} - - protobufjs@6.11.4: - resolution: {integrity: sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==} - hasBin: true - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -6034,10 +5569,6 @@ packages: pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -6335,10 +5866,6 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - re-resizable@6.11.2: resolution: {integrity: sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==} peerDependencies: @@ -6357,12 +5884,6 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - react-data-grid@7.0.0-beta.56: - resolution: {integrity: sha512-sET7KFAP2oMz3LlXXg4J6fb3EzHKIckVaXuV+uyx5JRHjPoEDaAc4yrDc6usVeCi//QSZvUX85S0AS/E3uiaMw==} - peerDependencies: - react: ^19.0 - react-dom: ^19.0 - react-day-picker@9.7.0: resolution: {integrity: sha512-urlK4C9XJZVpQ81tmVgd2O7lZ0VQldZeHzNejbwLWZSkzHH498KnArT0EHNfKBOWwKc935iMLGZdxXPRISzUxQ==} engines: {node: '>=18'} @@ -6406,13 +5927,6 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - react-image@4.1.0: - resolution: {integrity: sha512-qwPNlelQe9Zy14K2pGWSwoL+vHsAwmJKS6gkotekDgRpcnRuzXNap00GfibD3eEPYu3WCPlyIUUNzcyHOrLHjw==} - peerDependencies: - '@babel/runtime': '>=7' - react: '>=16.8' - react-dom: '>=16.8' - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -6664,9 +6178,6 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - request-filtering-agent@2.0.1: - resolution: {integrity: sha512-QvD3qwthEt9J+2hCdQ3wTn3Z/ZsgyiMECjY9yVJ0F8FtnGfNQG+dRz65eKayYRHIRQ6OGjH8Zuqr1lw7G6pz1Q==} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -6711,9 +6222,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rope-sequence@1.3.4: - resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -6810,10 +6318,6 @@ packages: engines: {node: '>= 0.10'} hasBin: true - sharp@0.32.6: - resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} - engines: {node: '>=14.15.0'} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -6845,15 +6349,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - socket.io-client@4.8.1: resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} engines: {node: '>=10.0.0'} @@ -6913,9 +6408,6 @@ packages: stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} - streamx@2.22.1: - resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} - strict-uri-encode@2.0.0: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} @@ -6948,17 +6440,10 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - style-mod@4.1.2: - resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} - style-to-js@1.1.17: resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} @@ -7009,22 +6494,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tar-fs@2.1.3: - resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==} - - tar-fs@3.1.0: - resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - - tar-stream@3.1.7: - resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - - text-decoder@1.2.3: - resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -7111,9 +6580,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - tw-animate-css@1.3.4: resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==} @@ -7140,9 +6606,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} @@ -7158,10 +6621,6 @@ packages: undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} - undici@7.11.0: - resolution: {integrity: sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==} - engines: {node: '>=20.18.1'} - unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} @@ -7268,10 +6727,6 @@ packages: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - url-metadata@5.2.1: - resolution: {integrity: sha512-AQL+JOJKX/MY2tPnubRwaocsn25+LCEgcAS/ArCrord4xws0LAEK2w6/9oCp5DALRRpGYj91yUvbyvlz4XTF+w==} - engines: {node: '>=6.0.0'} - use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -7306,11 +6761,6 @@ packages: '@types/react': optional: true - use-stick-to-bottom@1.1.1: - resolution: {integrity: sha512-JkDp0b0tSmv7HQOOpL1hT7t7QaoUBXkq045WWWOFDTlLGRzgIIyW7vyzOIJzY7L2XVIG7j1yUxeDj2LHm9Vwng==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -7521,9 +6971,6 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - w3c-keyname@2.2.8: - resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - wagmi@2.16.1: resolution: {integrity: sha512-iUdaoe/xd5NiNRW72QVctZs+962EORJKAvJTCsmf9n6TnEApPlENuvVRJKgobI4cGUgi5scWAstpLprB+RRo9Q==} peerDependencies: @@ -7544,14 +6991,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -7753,12 +7192,6 @@ snapshots: '@adraffy/ens-normalize@1.11.0': {} - '@ai-sdk/openai@1.3.22(zod@3.25.67)': - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.67) - zod: 3.25.67 - '@ai-sdk/provider-utils@2.2.8(zod@3.25.67)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -7874,13 +7307,13 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.0 + '@babel/generator': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) - '@babel/helpers': 7.28.2 - '@babel/parser': 7.28.0 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.0) + '@babel/helpers': 7.28.3 + '@babel/parser': 7.28.3 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.1 @@ -7898,6 +7331,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.1.0 + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + jsesc: 3.1.0 + '@babel/helper-compilation-targets@7.27.2': dependencies: '@babel/compat-data': 7.28.0 @@ -7926,12 +7367,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 '@babel/helper-module-imports': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.0 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color @@ -7943,7 +7384,7 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.28.2': + '@babel/helpers@7.28.3': dependencies: '@babel/template': 7.27.2 '@babel/types': 7.28.2 @@ -7952,6 +7393,10 @@ snapshots: dependencies: '@babel/types': 7.28.0 + '@babel/parser@7.28.3': + dependencies: + '@babel/types': 7.28.2 + '@babel/plugin-transform-runtime@7.28.0(@babel/core@7.28.0)': dependencies: '@babel/core': 7.28.0 @@ -7984,6 +7429,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.28.3': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -7994,13 +7451,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@babycommando/entity-db@1.0.11': - dependencies: - '@xenova/transformers': 2.17.2 - idb: 8.0.3 - transitivePeerDependencies: - - bare-buffer - '@base-org/account@1.1.1(@types/react@19.1.8)(bufferutil@4.0.9)(immer@10.1.1)(react@19.1.0)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@19.1.0))(utf-8-validate@5.0.10)(zod@3.25.67)': dependencies: '@noble/hashes': 1.4.0 @@ -8080,67 +7530,6 @@ snapshots: '@chevrotain/utils@11.0.3': {} - '@codemirror/autocomplete@6.18.6': - dependencies: - '@codemirror/language': 6.11.1 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 - '@lezer/common': 1.2.3 - - '@codemirror/commands@6.8.1': - dependencies: - '@codemirror/language': 6.11.1 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 - '@lezer/common': 1.2.3 - - '@codemirror/lang-python@6.2.1': - dependencies: - '@codemirror/autocomplete': 6.18.6 - '@codemirror/language': 6.11.1 - '@codemirror/state': 6.5.2 - '@lezer/common': 1.2.3 - '@lezer/python': 1.1.18 - - '@codemirror/language@6.11.1': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 - style-mod: 4.1.2 - - '@codemirror/lint@6.8.5': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 - crelt: 1.0.6 - - '@codemirror/search@6.5.11': - dependencies: - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 - crelt: 1.0.6 - - '@codemirror/state@6.5.2': - dependencies: - '@marijn/find-cluster-break': 1.0.2 - - '@codemirror/theme-one-dark@6.1.3': - dependencies: - '@codemirror/language': 6.11.1 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 - '@lezer/highlight': 1.2.1 - - '@codemirror/view@6.37.2': - dependencies: - '@codemirror/state': 6.5.2 - crelt: 1.0.6 - style-mod: 4.1.2 - w3c-keyname: 2.2.8 - '@coinbase/wallet-sdk@3.9.3': dependencies: bn.js: 5.2.2 @@ -8513,8 +7902,6 @@ snapshots: '@standard-schema/utils': 0.3.0 react-hook-form: 7.58.1(react@19.1.0) - '@huggingface/jinja@0.2.2': {} - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -8543,10 +7930,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@icons-pack/react-simple-icons@13.7.0(react@19.1.0)': - dependencies: - react: 19.1.0 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -8595,22 +7978,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lezer/common@1.2.3': {} - - '@lezer/highlight@1.2.1': - dependencies: - '@lezer/common': 1.2.3 - - '@lezer/lr@1.4.2': - dependencies: - '@lezer/common': 1.2.3 - - '@lezer/python@1.1.18': - dependencies: - '@lezer/common': 1.2.3 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 - '@lit-labs/ssr-dom-shim@1.3.0': {} '@lit/react@1.0.8(@types/react@19.1.8)': @@ -8732,8 +8099,6 @@ snapshots: - acorn - supports-color - '@marijn/find-cluster-break@1.0.2': {} - '@mdx-js/mdx@3.1.0(acorn@8.15.0)': dependencies: '@types/estree': 1.0.8 @@ -8980,6 +8345,10 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@noble/curves@1.9.6': + dependencies: + '@noble/hashes': 1.8.0 + '@noble/hashes@1.4.0': {} '@noble/hashes@1.5.0': {} @@ -9002,10 +8371,10 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nuwa-ai/cap-kit@0.4.4(react@19.1.0)(typescript@5.8.3)': + '@nuwa-ai/cap-kit@0.4.6(react@19.1.0)(typescript@5.8.3)': dependencies: '@modelcontextprotocol/sdk': 1.13.2 - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.6 '@noble/hashes': 1.8.0 '@nuwa-ai/identity-kit': 0.4.0(typescript@5.8.3) '@roochnetwork/rooch-sdk': 0.3.6(typescript@5.8.3) @@ -9037,7 +8406,7 @@ snapshots: - supports-color - typescript - '@nuwa-ai/payment-kit@0.4.4(express@5.1.0)(typescript@5.8.3)': + '@nuwa-ai/payment-kit@0.4.6(express@5.1.0)(typescript@5.8.3)': dependencies: '@nuwa-ai/identity-kit': 0.4.0(typescript@5.8.3) '@roochnetwork/rooch-sdk': 0.3.6(typescript@5.8.3) @@ -9053,13 +8422,6 @@ snapshots: - supports-color - typescript - '@openrouter/ai-sdk-provider@0.7.2(ai@4.3.16(react@19.1.0)(zod@3.25.67))(zod@3.25.67)': - dependencies: - '@ai-sdk/provider': 1.1.3 - '@ai-sdk/provider-utils': 2.2.8(zod@3.25.67) - ai: 4.3.16(react@19.1.0)(zod@3.25.67) - zod: 3.25.67 - '@opentelemetry/api@1.9.0': {} '@paulmillr/qr@0.2.1': {} @@ -9071,29 +8433,6 @@ snapshots: dependencies: object-assign: 4.1.1 - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.4': {} - - '@protobufjs/eventemitter@1.1.0': {} - - '@protobufjs/fetch@1.1.0': - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/inquire@1.1.0': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.0': {} - '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.0.0': @@ -11567,27 +10906,16 @@ snapshots: '@types/katex@0.16.7': {} - '@types/linkify-it@5.0.0': {} - '@types/lodash.debounce@4.0.9': dependencies: '@types/lodash': 4.17.20 '@types/lodash@4.17.20': {} - '@types/long@4.0.2': {} - - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 - '@types/mdurl@2.0.0': {} - '@types/mdx@2.0.13': {} '@types/ms@2.1.0': {} @@ -11750,8 +11078,6 @@ snapshots: '@use-gesture/core': 10.3.1 react: 19.1.0 - '@vercel/functions@2.2.2': {} - '@vitejs/plugin-react-swc@3.10.2(vite@6.3.5(@types/node@24.0.3)(jiti@1.21.7)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.11 @@ -12550,16 +11876,6 @@ snapshots: transitivePeerDependencies: - typescript - '@xenova/transformers@2.17.2': - dependencies: - '@huggingface/jinja': 0.2.2 - onnxruntime-web: 1.14.0 - sharp: 0.32.6 - optionalDependencies: - onnxruntime-node: 1.14.0 - transitivePeerDependencies: - - bare-buffer - abitype@1.0.8(typescript@5.8.3)(zod@3.22.4): optionalDependencies: typescript: 5.8.3 @@ -12751,8 +12067,6 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - b4a@1.6.7: {} - babel-plugin-macros@3.1.0: dependencies: '@babel/runtime': 7.27.6 @@ -12787,31 +12101,6 @@ snapshots: balanced-match@1.0.2: {} - bare-events@2.5.4: - optional: true - - bare-fs@4.1.6: - dependencies: - bare-events: 2.5.4 - bare-path: 3.0.0 - bare-stream: 2.6.5(bare-events@2.5.4) - optional: true - - bare-os@3.6.1: - optional: true - - bare-path@3.0.0: - dependencies: - bare-os: 3.6.1 - optional: true - - bare-stream@2.6.5(bare-events@2.5.4): - dependencies: - streamx: 2.22.1 - optionalDependencies: - bare-events: 2.5.4 - optional: true - base-x@3.0.11: dependencies: safe-buffer: 5.2.1 @@ -12879,12 +12168,6 @@ snapshots: varuint-bitcoin: 1.1.2 optional: true - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - blakejs@1.2.1: {} bn.js@4.12.2: @@ -12981,11 +12264,6 @@ snapshots: buffer-xor@1.0.3: optional: true - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -13039,29 +12317,6 @@ snapshots: character-reference-invalid@2.0.1: {} - cheerio-select@2.1.0: - dependencies: - boolbase: 1.0.0 - css-select: 5.2.2 - css-what: 6.2.2 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - - cheerio@1.1.0: - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.2.2 - encoding-sniffer: 0.2.1 - htmlparser2: 10.0.0 - parse5: 7.3.0 - parse5-htmlparser2-tree-adapter: 7.1.0 - parse5-parser-stream: 7.1.2 - undici: 7.11.0 - whatwg-mimetype: 4.0.0 - chevrotain-allstar@0.3.1(chevrotain@11.0.3): dependencies: chevrotain: 11.0.3 @@ -13092,8 +12347,6 @@ snapshots: dependencies: readdirp: 4.1.2 - chownr@1.1.4: {} - chroma-js@3.1.2: {} cipher-base@1.0.6: @@ -13130,16 +12383,6 @@ snapshots: - '@types/react' - '@types/react-dom' - codemirror@6.0.2: - dependencies: - '@codemirror/autocomplete': 6.18.6 - '@codemirror/commands': 6.8.1 - '@codemirror/language': 6.11.1 - '@codemirror/lint': 6.8.5 - '@codemirror/search': 6.5.11 - '@codemirror/state': 6.5.2 - '@codemirror/view': 6.37.2 - collapse-white-space@2.1.0: {} color-convert@2.0.1: @@ -13148,16 +12391,6 @@ snapshots: color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - colord@2.9.3: {} comma-separated-tokens@2.0.3: {} @@ -13259,8 +12492,6 @@ snapshots: sha.js: 2.4.12 optional: true - crelt@1.0.6: {} - cross-fetch@3.2.0: dependencies: node-fetch: 2.7.0 @@ -13283,18 +12514,8 @@ snapshots: dependencies: uncrypto: 0.1.3 - css-select@5.2.2: - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 - css-selector-parser@3.1.3: {} - css-what@6.2.2: {} - cssesc@3.0.0: {} csstype@3.1.3: {} @@ -13513,12 +12734,6 @@ snapshots: decode-uri-component@0.4.1: {} - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - - deep-extend@0.6.0: {} - deep-is@0.1.4: {} define-data-property@1.1.4: @@ -13545,8 +12760,6 @@ snapshots: detect-browser@5.3.0: {} - detect-libc@2.0.4: {} - detect-node-es@1.1.0: {} devlop@1.1.0: @@ -13565,28 +12778,10 @@ snapshots: dlv@1.1.3: {} - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - dompurify@3.2.6: optionalDependencies: '@types/trusted-types': 2.0.7 - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - drbg.js@1.0.1: dependencies: browserify-aes: 1.2.0 @@ -13662,11 +12857,6 @@ snapshots: encodeurl@2.0.0: {} - encoding-sniffer@0.2.1: - dependencies: - iconv-lite: 0.6.3 - whatwg-encoding: 3.1.1 - end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -13685,8 +12875,6 @@ snapshots: engine.io-parser@5.2.3: {} - entities@4.5.0: {} - entities@6.0.1: {} error-ex@1.3.2: @@ -13913,8 +13101,6 @@ snapshots: safe-buffer: 5.2.1 optional: true - expand-template@2.0.3: {} - express-rate-limit@7.5.1(express@5.1.0): dependencies: express: 5.1.0 @@ -13971,8 +13157,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-fifo@1.3.2: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -14049,8 +13233,6 @@ snapshots: flatted: 3.3.3 keyv: 4.5.4 - flatbuffers@1.12.0: {} - flatted@3.3.3: {} for-each@0.3.5: @@ -14079,8 +13261,6 @@ snapshots: fresh@2.0.0: {} - fs-constants@1.0.0: {} - fsevents@2.3.3: optional: true @@ -14120,8 +13300,6 @@ snapshots: dependencies: lit: 3.3.0 - github-from-package@0.0.0: {} - github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -14149,8 +13327,6 @@ snapshots: graphemer@1.4.0: {} - guid-typescript@1.0.9: {} - h3@1.15.4: dependencies: cookie-es: 1.2.2 @@ -14398,13 +13574,6 @@ snapshots: html-void-elements@3.0.0: {} - htmlparser2@10.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 6.0.1 - http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -14421,8 +13590,6 @@ snapshots: idb-keyval@6.2.2: {} - idb@8.0.3: {} - ieee754@1.2.1: {} ignore@5.3.2: {} @@ -14440,8 +13607,6 @@ snapshots: inherits@2.0.4: {} - ini@1.3.8: {} - inline-style-parser@0.2.4: {} input-otp@1.4.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): @@ -14457,8 +13622,6 @@ snapshots: ipaddr.js@1.9.1: {} - ipaddr.js@2.2.0: {} - iron-webcrypto@1.2.1: {} is-alphabetical@2.0.1: {} @@ -14475,8 +13638,6 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: {} - is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -14671,10 +13832,6 @@ snapshots: lines-and-columns@1.2.4: {} - linkify-it@5.0.0: - dependencies: - uc.micro: 2.1.0 - lit-element@4.2.0: dependencies: '@lit-labs/ssr-dom-shim': 1.3.0 @@ -14717,8 +13874,6 @@ snapshots: lodash@4.17.21: {} - long@4.0.0: {} - longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -14747,15 +13902,6 @@ snapshots: markdown-extensions@2.0.0: {} - markdown-it@14.1.0: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - markdown-table@3.0.4: {} markdown-to-jsx@7.7.8(react@19.1.0): @@ -14953,8 +14099,6 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - mdurl@2.0.0: {} - media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -15280,8 +14424,6 @@ snapshots: dependencies: mime-db: 1.54.0 - mimic-response@3.1.0: {} - minimalistic-assert@1.0.1: optional: true @@ -15296,8 +14438,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minimist@1.2.8: {} - minipass@7.1.2: {} mipd@0.0.7(typescript@5.8.3): @@ -15309,8 +14449,6 @@ snapshots: for-in: 1.0.2 is-extendable: 1.0.1 - mkdirp-classic@0.5.3: {} - mlly@1.7.4: dependencies: acorn: 8.15.0 @@ -15339,30 +14477,15 @@ snapshots: nanoid@3.3.11: {} - nanoid@5.1.5: {} - - napi-build-utils@2.0.0: {} - natural-compare@1.4.0: {} negotiator@1.0.0: {} - next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - - node-abi@3.75.0: - dependencies: - semver: 7.7.2 - node-addon-api@2.0.2: {} node-addon-api@5.1.0: optional: true - node-addon-api@6.1.0: {} - node-fetch-native@1.6.7: {} node-fetch@2.7.0: @@ -15425,26 +14548,6 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 - onnx-proto@4.0.4: - dependencies: - protobufjs: 6.11.4 - - onnxruntime-common@1.14.0: {} - - onnxruntime-node@1.14.0: - dependencies: - onnxruntime-common: 1.14.0 - optional: true - - onnxruntime-web@1.14.0: - dependencies: - flatbuffers: 1.12.0 - guid-typescript: 1.0.9 - long: 4.0.0 - onnx-proto: 4.0.4 - onnxruntime-common: 1.14.0 - platform: 1.3.6 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -15454,8 +14557,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - orderedmap@2.1.1: {} - ox@0.6.7(typescript@5.8.3)(zod@3.25.67): dependencies: '@adraffy/ens-normalize': 1.11.0 @@ -15567,8 +14668,6 @@ snapshots: package-manager-detector@1.3.0: {} - papaparse@5.5.3: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -15592,15 +14691,6 @@ snapshots: parse-numeric-range@1.3.0: {} - parse5-htmlparser2-tree-adapter@7.1.0: - dependencies: - domhandler: 5.0.3 - parse5: 7.3.0 - - parse5-parser-stream@7.1.2: - dependencies: - parse5: 7.3.0 - parse5@7.3.0: dependencies: entities: 6.0.1 @@ -15687,8 +14777,6 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 - platform@1.3.6: {} - pngjs@5.0.0: {} points-on-curve@0.2.0: {} @@ -15762,21 +14850,6 @@ snapshots: preact@10.27.0: {} - prebuild-install@7.1.3: - dependencies: - detect-libc: 2.0.4 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.75.0 - pump: 3.0.3 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.3 - tunnel-agent: 0.6.0 - prelude-ls@1.2.1: {} process-nextick-args@2.0.1: {} @@ -15793,113 +14866,6 @@ snapshots: property-information@7.1.0: {} - prosemirror-commands@1.7.1: - dependencies: - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - - prosemirror-dropcursor@1.8.2: - dependencies: - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.0 - - prosemirror-example-setup@1.2.3: - dependencies: - prosemirror-commands: 1.7.1 - prosemirror-dropcursor: 1.8.2 - prosemirror-gapcursor: 1.3.2 - prosemirror-history: 1.4.1 - prosemirror-inputrules: 1.5.0 - prosemirror-keymap: 1.2.3 - prosemirror-menu: 1.2.5 - prosemirror-schema-list: 1.5.1 - prosemirror-state: 1.4.3 - - prosemirror-gapcursor@1.3.2: - dependencies: - prosemirror-keymap: 1.2.3 - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-view: 1.40.0 - - prosemirror-history@1.4.1: - dependencies: - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.0 - rope-sequence: 1.3.4 - - prosemirror-inputrules@1.5.0: - dependencies: - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - - prosemirror-keymap@1.2.3: - dependencies: - prosemirror-state: 1.4.3 - w3c-keyname: 2.2.8 - - prosemirror-markdown@1.13.2: - dependencies: - '@types/markdown-it': 14.1.2 - markdown-it: 14.1.0 - prosemirror-model: 1.25.1 - - prosemirror-menu@1.2.5: - dependencies: - crelt: 1.0.6 - prosemirror-commands: 1.7.1 - prosemirror-history: 1.4.1 - prosemirror-state: 1.4.3 - - prosemirror-model@1.25.1: - dependencies: - orderedmap: 2.1.1 - - prosemirror-schema-basic@1.2.4: - dependencies: - prosemirror-model: 1.25.1 - - prosemirror-schema-list@1.5.1: - dependencies: - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - - prosemirror-state@1.4.3: - dependencies: - prosemirror-model: 1.25.1 - prosemirror-transform: 1.10.4 - prosemirror-view: 1.40.0 - - prosemirror-transform@1.10.4: - dependencies: - prosemirror-model: 1.25.1 - - prosemirror-view@1.40.0: - dependencies: - prosemirror-model: 1.25.1 - prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.4 - - protobufjs@6.11.4: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 - '@types/long': 4.0.2 - '@types/node': 24.0.3 - long: 4.0.0 - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -15914,8 +14880,6 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 - punycode.js@2.3.1: {} - punycode@2.3.1: {} qrcode@1.5.3: @@ -16363,13 +15327,6 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - re-resizable@6.11.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -16391,12 +15348,6 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - react-data-grid@7.0.0-beta.56(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - clsx: 2.1.1 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-day-picker@9.7.0(react@19.1.0): dependencies: '@date-fns/tz': 1.2.0 @@ -16439,12 +15390,6 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - react-image@4.1.0(@babel/runtime@7.27.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@babel/runtime': 7.27.6 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - react-is@16.13.1: {} react-is@18.3.1: {} @@ -16828,10 +15773,6 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - request-filtering-agent@2.0.1: - dependencies: - ipaddr.js: 2.2.0 - require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -16888,8 +15829,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.44.0 fsevents: 2.3.3 - rope-sequence@1.3.4: {} - roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -17019,19 +15958,6 @@ snapshots: safe-buffer: 5.2.1 to-buffer: 1.2.1 - sharp@0.32.6: - dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - node-addon-api: 6.1.0 - prebuild-install: 7.1.3 - semver: 7.7.2 - simple-get: 4.0.1 - tar-fs: 3.1.0 - tunnel-agent: 0.6.0 - transitivePeerDependencies: - - bare-buffer - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -17079,18 +16005,6 @@ snapshots: signal-exit@4.1.0: {} - simple-concat@1.0.1: {} - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 @@ -17142,13 +16056,6 @@ snapshots: stream-shift@1.0.3: {} - streamx@2.22.1: - dependencies: - fast-fifo: 1.3.2 - text-decoder: 1.2.3 - optionalDependencies: - bare-events: 2.5.4 - strict-uri-encode@2.0.0: {} string-convert@0.2.1: {} @@ -17186,12 +16093,8 @@ snapshots: dependencies: ansi-regex: 6.1.0 - strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} - style-mod@4.1.2: {} - style-to-js@1.1.17: dependencies: style-to-object: 1.0.9 @@ -17263,41 +16166,6 @@ snapshots: transitivePeerDependencies: - ts-node - tar-fs@2.1.3: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.3 - tar-stream: 2.2.0 - - tar-fs@3.1.0: - dependencies: - pump: 3.0.3 - tar-stream: 3.1.7 - optionalDependencies: - bare-fs: 4.1.6 - bare-path: 3.0.0 - transitivePeerDependencies: - - bare-buffer - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - - tar-stream@3.1.7: - dependencies: - b4a: 1.6.7 - fast-fifo: 1.3.2 - streamx: 2.22.1 - - text-decoder@1.2.3: - dependencies: - b4a: 1.6.7 - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -17370,10 +16238,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - tw-animate-css@1.3.4: {} tweetnacl@1.0.3: {} @@ -17399,8 +16263,6 @@ snapshots: typescript@5.8.3: {} - uc.micro@2.1.0: {} - ufo@1.6.1: {} uint8arrays@3.1.0: @@ -17415,8 +16277,6 @@ snapshots: undici-types@7.8.0: {} - undici@7.11.0: {} - unified@11.0.5: dependencies: '@types/unist': 3.0.3 @@ -17497,14 +16357,6 @@ snapshots: url-join@5.0.0: {} - url-metadata@5.2.1: - dependencies: - cheerio: 1.1.0 - node-fetch: 2.7.0 - request-filtering-agent: 2.0.1 - transitivePeerDependencies: - - encoding - use-callback-ref@1.3.3(@types/react@19.1.8)(react@19.1.0): dependencies: react: 19.1.0 @@ -17530,10 +16382,6 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 - use-stick-to-bottom@1.1.1(react@19.1.0): - dependencies: - react: 19.1.0 - use-sync-external-store@1.2.0(react@19.1.0): dependencies: react: 19.1.0 @@ -17774,8 +16622,6 @@ snapshots: vscode-uri@3.0.8: {} - w3c-keyname@2.2.8: {} - wagmi@2.16.1(@tanstack/query-core@5.83.1)(@tanstack/react-query@5.84.1(react@19.1.0))(@types/react@19.1.8)(bufferutil@4.0.9)(immer@10.1.1)(react@19.1.0)(typescript@5.8.3)(utf-8-validate@5.0.10)(viem@2.33.2(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.67))(zod@3.25.67): dependencies: '@tanstack/react-query': 5.84.1(react@19.1.0) @@ -17820,12 +16666,6 @@ snapshots: webidl-conversions@3.0.1: {} - whatwg-encoding@3.1.1: - dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/src/features/cap-store/hooks/use-cap-store.ts b/src/features/cap-store/hooks/use-cap-store.ts index 406fee95..d85ea57f 100644 --- a/src/features/cap-store/hooks/use-cap-store.ts +++ b/src/features/cap-store/hooks/use-cap-store.ts @@ -51,11 +51,8 @@ export const useCapStore = () => { }; const runCap = async (capId: string, capCid?: string) => { - console.log(capId); const installedCap = installedCaps[capId]; - console.log(installedCap); - if (!installedCap) { if (!capCid) { throw new Error('Cap CID is required for downloading cap'); diff --git a/src/features/cap-store/stores.ts b/src/features/cap-store/stores.ts index 2e3fddb2..47f9f4c0 100644 --- a/src/features/cap-store/stores.ts +++ b/src/features/cap-store/stores.ts @@ -84,8 +84,6 @@ export const CapStateStore = create()( return; } - console.log('addInstalledCap', cap); - set((state) => ({ installedCaps: { ...state.installedCaps, diff --git a/src/features/chat/components/header.tsx b/src/features/chat/components/header.tsx index 2efd6ef1..b88de4f0 100644 --- a/src/features/chat/components/header.tsx +++ b/src/features/chat/components/header.tsx @@ -18,9 +18,9 @@ export default function Header({ chatId }: HeaderProps) { const title = session?.title || ''; - const handleRename = (newTitle: string) => { + const handleRename = async (newTitle: string) => { if (chatId) { - updateSession(chatId, { title: newTitle }); + await updateSession(chatId, { title: newTitle }); } }; diff --git a/src/features/chat/components/multimodal-input.tsx b/src/features/chat/components/multimodal-input.tsx index c4ef9cca..c4d0815d 100644 --- a/src/features/chat/components/multimodal-input.tsx +++ b/src/features/chat/components/multimodal-input.tsx @@ -188,12 +188,10 @@ function PureMultimodalInput({ event.preventDefault(); if (status !== 'ready') { - toast.error( - 'Please wait for the model to finish its response!', - ); - } else { - submitForm(); + console.warn('The model is not ready to respond. Currnet status:', status); } + + submitForm(); } }} /> diff --git a/src/features/chat/hooks/use-chat-sessions.ts b/src/features/chat/hooks/use-chat-sessions.ts index 77cb72ba..1c66d994 100644 --- a/src/features/chat/hooks/use-chat-sessions.ts +++ b/src/features/chat/hooks/use-chat-sessions.ts @@ -6,7 +6,7 @@ export const useChatSessions = () => { const store = ChatStateStore(); const getSession = useCallback((id: string) => { - return store.readSession(id); + return store.getChatSession(id); }, []); const deleteSession = useCallback((id: string) => { @@ -14,8 +14,8 @@ export const useChatSessions = () => { }, []); const updateSession = useCallback( - (id: string, updates: Partial>) => { - store.updateSession(id, updates); + async (id: string, updates: Partial>) => { + await store.updateSession(id, updates); }, [], ); @@ -31,8 +31,8 @@ export const useChatSessions = () => { }, [store.sessions]); const deleteMessagesAfterTimestamp = useCallback( - (sessionId: string, timestamp: number) => { - const currentSession = store.readSession(sessionId); + async (chatId: string, timestamp: number) => { + const currentSession = store.getChatSession(chatId); if (!currentSession) return; const updatedMessages = currentSession.messages.filter((msg) => { @@ -48,7 +48,7 @@ export const useChatSessions = () => { updatedAt: Date.now(), }; - store.updateSession(sessionId, updatedSession); + await store.updateSession(chatId, updatedSession); }, [store], ); diff --git a/src/features/chat/hooks/use-update-chat-title.ts b/src/features/chat/hooks/use-update-chat-title.ts index e7694dac..cb78da7f 100644 --- a/src/features/chat/hooks/use-update-chat-title.ts +++ b/src/features/chat/hooks/use-update-chat-title.ts @@ -1,13 +1,12 @@ import { useCallback } from 'react'; import { generateTitleFromUserMessage } from '../services'; import { ChatStateStore } from '../stores'; -import type { ChatSession } from '../types'; -export const useUpdateChatTitle = (sessionId: string) => { +export const useUpdateChatTitle = (chatId: string) => { const store = ChatStateStore(); const updateTitle = useCallback(async () => { - const session = store.readSession(sessionId); + const session = store.getChatSession(chatId); if (!session) return; const firstMessage = session.messages[0]; @@ -15,15 +14,13 @@ export const useUpdateChatTitle = (sessionId: string) => { if (!firstMessage) return; if (session.title !== 'New Chat') return; - const title = await generateTitleFromUserMessage({ message: firstMessage }); + const title = await generateTitleFromUserMessage({ + chatId: chatId, + message: firstMessage, + }); - const updatedSession: ChatSession = { - ...session, - title: title, - }; - - store.updateSession(sessionId, updatedSession); - }, [sessionId]); + await store.updateSession(chatId, { title: title }); + }, [chatId]); return { updateTitle, diff --git a/src/features/chat/services/handler.ts b/src/features/chat/services/handler.ts index 75d11516..e6d3c1f3 100644 --- a/src/features/chat/services/handler.ts +++ b/src/features/chat/services/handler.ts @@ -50,11 +50,11 @@ function appendSourcesToFinalMessages( // Handle AI request, entrance of the AI workflow const handleAIRequest = async ({ - sessionId, + chatId, messages, signal, }: { - sessionId: string; + chatId: string; messages: Message[]; signal?: AbortSignal; }) => { @@ -62,9 +62,24 @@ const handleAIRequest = async ({ const capResolve = new CapResolve(); const { prompt, model, tools } = await capResolve.getResolvedConfig(); - // update the messages state - const { updateMessages } = ChatStateStore.getState(); - updateMessages(sessionId, messages); + // create a new chat session and update the messages + const { updateMessages, addPaymentCtxIdToChatSession } = + ChatStateStore.getState(); + await updateMessages(chatId, messages); + + // create payment CTX id header + const paymentCtxId = generateUUID(); + const headers = { + 'X-Client-Tx-Ref': paymentCtxId, + }; + + // add payment info to chat session + await addPaymentCtxIdToChatSession(chatId, { + type: 'chat-message', + message: messages[messages.length - 1].content, + ctxId: paymentCtxId, + timestamp: Date.now(), + }); const result = streamText({ model, @@ -75,6 +90,7 @@ const handleAIRequest = async ({ experimental_generateMessageId: generateUUID, tools, abortSignal: signal, + headers, async onFinish({ response, sources }) { // append response messages const finalMessages = appendResponseMessages({ @@ -91,7 +107,7 @@ const handleAIRequest = async ({ ); // update the messages state - updateMessages(sessionId, finalMessagesWithSources); + await updateMessages(chatId, finalMessagesWithSources); }, }); diff --git a/src/features/chat/services/index.ts b/src/features/chat/services/index.ts index 6c990e66..7afc43a5 100644 --- a/src/features/chat/services/index.ts +++ b/src/features/chat/services/index.ts @@ -12,10 +12,10 @@ export const createClientAIFetch = (): (( } const requestBody = JSON.parse(init.body as string); - const { id: sessionId, messages } = requestBody; + const { id: chatId, messages } = requestBody; const response = await handleAIRequest({ - sessionId, + chatId, messages, signal: init?.signal ?? undefined, }); diff --git a/src/features/chat/services/utility-ai.ts b/src/features/chat/services/utility-ai.ts index adadd5ac..8fe53f18 100644 --- a/src/features/chat/services/utility-ai.ts +++ b/src/features/chat/services/utility-ai.ts @@ -1,13 +1,32 @@ import { generateText, type Message } from 'ai'; +import { generateUUID } from '@/shared/utils'; +import { ChatStateStore } from '../stores'; import { llmProvider } from './providers'; // Generate a title from the first message a user begins a conversation with // TODO: currently still using the remote AI models, need to switch to local models export async function generateTitleFromUserMessage({ + chatId, message, }: { + chatId: string; message: Message; }) { + const { addPaymentCtxIdToChatSession } = ChatStateStore.getState(); + + // create payment ctx id header + const paymentCtxId = generateUUID(); + const headers = { + 'X-Client-Tx-Ref': paymentCtxId, + }; + + // add payment info to chat session + await addPaymentCtxIdToChatSession(chatId, { + type: 'generate-title', + ctxId: paymentCtxId, + timestamp: Date.now(), + }); + const { text: title } = await generateText({ model: llmProvider.utility(), system: `\n @@ -16,6 +35,7 @@ export async function generateTitleFromUserMessage({ - the title should be a summary of the user's message - do not use quotes or colons`, prompt: JSON.stringify(message), + headers, }); return title; diff --git a/src/features/chat/stores.ts b/src/features/chat/stores.ts index 6773134f..ff6d4472 100644 --- a/src/features/chat/stores.ts +++ b/src/features/chat/stores.ts @@ -7,7 +7,7 @@ import { persist } from 'zustand/middleware'; import { NuwaIdentityKit } from '@/shared/services/identity-kit'; import { createPersistConfig, db } from '@/shared/storage'; import { generateUUID } from '@/shared/utils'; -import type { ChatSession } from './types'; +import type { ChatPayment, ChatSession } from './types'; // ================= Constants ================= // export const createInitialChatSession = (): ChatSession => ({ @@ -16,6 +16,7 @@ export const createInitialChatSession = (): ChatSession => ({ createdAt: Date.now(), updatedAt: Date.now(), messages: [], + payments: [], }); // get current DID @@ -33,16 +34,20 @@ interface ChatStoreState { sessions: Record; // session CRUD operations - createSession: (session?: Partial) => ChatSession; - readSession: (id: string) => ChatSession | null; + getChatSession: (id: string) => ChatSession | null; + getChatSessionsSortedByUpdatedAt: () => ChatSession[]; updateSession: ( id: string, updates: Partial>, - ) => void; + ) => Promise; + addPaymentCtxIdToChatSession: ( + id: string, + payment: ChatPayment, + ) => Promise; deleteSession: (id: string) => void; - // update messages for a session - updateMessages: (sessionId: string, messages: Message[]) => void; + // update messages for a session and create a new session if not founds + updateMessages: (chatId: string, messages: Message[]) => Promise; // utility methods clearAllSessions: () => void; @@ -74,39 +79,35 @@ export const ChatStateStore = create()( (set, get) => ({ sessions: {}, - // Session CRUD operations - createSession: (session?: Partial) => { - const newSession: ChatSession = { - id: session?.id || generateUUID(), - title: session?.title || 'New Chat', - createdAt: session?.createdAt || Date.now(), - updatedAt: Date.now(), - messages: session?.messages || [], - }; - - set((state) => ({ - sessions: { - ...state.sessions, - [newSession.id]: newSession, - }, - })); - - get().saveToDB(); - return newSession; + getChatSession: (id: string) => { + const { sessions } = get(); + return sessions[id] || null; }, - readSession: (id: string) => { + getChatSessionsSortedByUpdatedAt: () => { const { sessions } = get(); - return sessions[id] || null; + return Object.values(sessions).sort( + (a, b) => b.updatedAt - a.updatedAt, + ); }, - updateSession: ( + updateSession: async ( id: string, updates: Partial>, ) => { set((state) => { - const session = state.sessions[id]; - if (!session) return state; + let session = state.sessions[id]; + // if session not found, create new session + if (!session) { + session = { + id: id, + title: 'New Chat', + createdAt: Date.now(), + updatedAt: Date.now(), + messages: [], + payments: [], + }; + } const updatedSession = { ...session, @@ -122,7 +123,49 @@ export const ChatStateStore = create()( }; }); - get().saveToDB(); + await get().saveToDB(); + }, + + addPaymentCtxIdToChatSession: async ( + id: string, + payment: ChatPayment, + ) => { + set((state) => { + const session = state.sessions[id]; + // if session not found, create new session + if (!session) { + const newSession = { + id: id, + title: 'New Chat', + createdAt: Date.now(), + updatedAt: Date.now(), + messages: [], + payments: [payment], + }; + + return { + sessions: { + ...state.sessions, + [id]: newSession, + }, + }; + } else { + const updatedSession = { + ...session, + payments: [...session.payments, payment], + updatedAt: Date.now(), + }; + + return { + sessions: { + ...state.sessions, + [id]: updatedSession, + }, + }; + } + }); + + await get().saveToDB(); }, deleteSession: (id: string) => { @@ -150,20 +193,21 @@ export const ChatStateStore = create()( deleteFromDB(); }, - updateMessages: (sessionId: string, messages: Message[]) => { + updateMessages: async (chatId: string, messages: Message[]) => { set((state) => { - let session = state.sessions[sessionId]; + let session = state.sessions[chatId]; let isNewSession = false; // if session not found, create new session if (!session) { isNewSession = true; session = { - id: sessionId, + id: chatId, title: 'New Chat', createdAt: Date.now(), updatedAt: Date.now(), messages: [], + payments: [], }; } @@ -186,7 +230,7 @@ export const ChatStateStore = create()( const newState = { sessions: { ...state.sessions, - [sessionId]: updatedSession, + [chatId]: updatedSession, }, }; @@ -196,7 +240,7 @@ export const ChatStateStore = create()( return state; }); - get().saveToDB(); + await get().saveToDB(); }, clearAllSessions: () => { diff --git a/src/features/chat/types.ts b/src/features/chat/types.ts index 38db0160..d498b3cb 100644 --- a/src/features/chat/types.ts +++ b/src/features/chat/types.ts @@ -1,5 +1,14 @@ import type { Message } from 'ai'; +export type ChatPaymentType = 'generate-title' | 'chat-message'; + +export interface ChatPayment { + type: ChatPaymentType; + message?: string; + ctxId: string; + timestamp: number; +} + // client chat interface export interface ChatSession { id: string; @@ -7,6 +16,7 @@ export interface ChatSession { createdAt: number; updatedAt: number; messages: Message[]; + payments: ChatPayment[]; pinned?: boolean; did?: string; // Added for IndexedDB storage } diff --git a/src/features/wallet/components/balance-card.tsx b/src/features/wallet/components/balance-card.tsx index 51a2d178..5358cbdd 100644 --- a/src/features/wallet/components/balance-card.tsx +++ b/src/features/wallet/components/balance-card.tsx @@ -8,7 +8,7 @@ import { CardTitle, } from '@/shared/components/ui/card'; import { useDevMode } from '@/shared/hooks/use-dev-mode'; -import { usePaymentHubRgas } from '@/shared/hooks/usePaymentHub'; +import { usePaymentHubRgas } from '@/shared/hooks/use-payment-hub'; import { TestnetFaucetDialog } from './testnet-faucet-dialog'; interface BalanceCardProps { @@ -37,8 +37,8 @@ export function BalanceCard({ onTopUp }: BalanceCardProps) {
- PaymentHub Balance -

RGas on Testnet

+ Balance +

Testnet

@@ -66,7 +66,7 @@ export function BalanceCard({ onTopUp }: BalanceCardProps) { USD
-
{rgasValue} RGas
+ {/*
{rgasValue} RGas
*/} {/*
diff --git a/src/features/wallet/components/chat-item.tsx b/src/features/wallet/components/chat-item.tsx new file mode 100644 index 00000000..77d3659d --- /dev/null +++ b/src/features/wallet/components/chat-item.tsx @@ -0,0 +1,87 @@ +import { formatAmount } from '@nuwa-ai/payment-kit'; +import { ChevronDown, ChevronRight } from 'lucide-react'; +import { Button } from '@/shared/components/ui/button'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/shared/components/ui/collapsible'; +import type { ChatRecord, PaymentTransaction } from '../types'; +import { TransactionItem } from './transaction-item'; + +const formatCost = (cost: bigint | undefined) => { + if (!cost) return undefined; + if (typeof cost === 'bigint') return `$${formatAmount(cost, 12)}`; + if (cost !== undefined && cost !== null) { + return `$${formatAmount(BigInt(String(cost)), 12)}`; + } + return undefined; +}; + +const getTotalCost = (transactions: PaymentTransaction[]) => { + const total = transactions.reduce((sum, tx) => sum + (tx.details?.payment?.costUsd || 0n), 0n); + return formatCost(total); +}; + + +interface ChatHistoryItemProps { + chatRecord: ChatRecord; + isOpen: boolean; + onToggle: (chatId: string) => void; + onSelectTransaction: (transaction: PaymentTransaction) => void; +} + +export function ChatItem({ + chatRecord, + isOpen, + onToggle, + onSelectTransaction, +}: ChatHistoryItemProps) { + if (!chatRecord.chatId || chatRecord.transactions.length === 0) return null; + + const chatId = chatRecord.chatId; + const totalCost = getTotalCost(chatRecord.transactions); + return ( + onToggle(chatId)}> + + + + +
+ {chatRecord.transactions.map((transaction) => ( + + ))} +
+
+
+ ); +} diff --git a/src/features/wallet/components/sidebar-wallet-card.tsx b/src/features/wallet/components/sidebar-wallet-card.tsx index 305917d7..7a8d5ba4 100644 --- a/src/features/wallet/components/sidebar-wallet-card.tsx +++ b/src/features/wallet/components/sidebar-wallet-card.tsx @@ -4,8 +4,8 @@ import { toast } from 'sonner'; import { useCopyToClipboard } from 'usehooks-ts'; import { useAuth } from '@/features/auth/hooks/use-auth'; import { Card, CardContent } from '@/shared/components/ui/card'; +import { usePaymentHubRgas } from '@/shared/hooks/use-payment-hub'; import { cn } from '@/shared/utils'; -import { usePaymentHubRgas } from '@/shared/hooks/usePaymentHub'; interface SidebarWalletCardProps { className?: string; diff --git a/src/features/wallet/components/transaction-details-modal.tsx b/src/features/wallet/components/transaction-details-modal.tsx new file mode 100644 index 00000000..b47ae2fd --- /dev/null +++ b/src/features/wallet/components/transaction-details-modal.tsx @@ -0,0 +1,301 @@ +import { formatAmount } from '@nuwa-ai/payment-kit'; +import { useState } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/shared/components/ui/dialog'; +import { + Table, + TableBody, + TableCell, + TableRow, +} from '@/shared/components/ui/table'; +import type { PaymentTransaction } from '../types'; + +const formatCost = (cost: bigint | undefined) => { + if (!cost) return null; + if (typeof cost === 'bigint') return `$${formatAmount(cost, 12)}`; + if (cost !== undefined && cost !== null) { + return `$${formatAmount(BigInt(String(cost)), 12)}`; + } + return null; +}; + +const formatDate = (timestamp: number) => { + if (!timestamp) return 'undefined'; + const localString = new Date(timestamp).toLocaleString(); + return `${timestamp} (${localString})`; +}; + +const formatDuration = (durationMs: number | undefined) => { + if (!durationMs) return null; + if (durationMs < 1000) return `${durationMs}ms`; + return `${(durationMs / 1000).toFixed(2)}s`; +}; + +interface CopyableCellProps { + value: string | number | boolean | null; + isNested?: boolean; +} + +function CopyableCell({ value, isNested = false }: CopyableCellProps) { + const [copied, setCopied] = useState(false); + + const displayValue = value === null ? 'null' : String(value); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(displayValue); + setCopied(true); + setTimeout(() => setCopied(false), 1000); + } catch (err) { + console.error('Failed to copy:', err); + } + }; + + return ( + + {copied ? Copied! : displayValue} + + ); +} + +interface TableRowItemProps { + label: string; + value: string | number | boolean | null; + isNested?: boolean; +} + +function TableRowItem({ label, value, isNested = false }: TableRowItemProps) { + return ( + + + {label} + + + + ); +} + +interface TransactionDetailsModalProps { + transaction: PaymentTransaction | null; + onClose: () => void; +} + +export function TransactionDetailsModal({ + transaction, + onClose, +}: TransactionDetailsModalProps) { + if (!transaction) return null; + + const details = transaction.details; + const payment = details?.payment; + + return ( + + + + Transaction Details + + +
+ + + {/* Basic Information */} + + + Basic Information + + + + + + + + {/* Transaction Details */} + + + Transaction Details + + + + + + + + + + + + + + + + + + + + {/* Payment Information */} + + + Payment Information + + + + + + + + {/* Headers Summary */} + {details?.headersSummary && ( + <> + + + Headers Summary + + + {Object.entries(details.headersSummary).map( + ([key, value]) => ( + + ), + )} + + )} + + {/* Metadata */} + {details?.meta && ( + <> + + + Metadata + + + {Object.entries(details.meta).map(([key, value]) => ( + + ))} + + )} + +
+
+
+
+ ); +} diff --git a/src/features/wallet/components/transaction-history.tsx b/src/features/wallet/components/transaction-history.tsx index 1726cc60..d8d6b4d5 100644 --- a/src/features/wallet/components/transaction-history.tsx +++ b/src/features/wallet/components/transaction-history.tsx @@ -1,85 +1,71 @@ +import { useState } from 'react'; import { Card, CardContent, CardHeader, CardTitle, } from '@/shared/components/ui/card'; -import { useTransactions } from '@/shared/hooks/useTransactions'; -import { formatAmount } from '@nuwa-ai/payment-kit'; - -function TransactionRow({ - operation, - status, - statusCode, - cost, - costUsd, - paidAt, - stream, -}: { - operation: string; - status: string; - statusCode?: number; - cost?: string; - costUsd?: string; - paidAt?: string; - stream?: boolean; -}) { - return ( -
-
-
- {operation} - {stream && stream} -
-

{paidAt ? new Date(paidAt).toLocaleString() : ''}

-
-
-
{status}{statusCode ? ` (${statusCode})` : ''}
- {cost && ( -
{costUsd ? `${cost} (${costUsd} USD)` : cost}
- )} -
-
- ); -} +import { useChatTransactionInfo } from '../hooks/use-chat-transaction-info'; +import type { PaymentTransaction } from '../types'; +import { ChatItem } from './chat-item'; +import { TransactionDetailsModal } from './transaction-details-modal'; export function TransactionHistory() { - const { items, loading, error, reload } = useTransactions(50); + const { chatRecords, error } = useChatTransactionInfo(); + const [selectedTransaction, setSelectedTransaction] = + useState(null); + const [openChats, setOpenChats] = useState>(new Set()); + + const toggleChat = (chatId: string) => { + const newOpenChats = new Set(); + if (!openChats.has(chatId)) { + newOpenChats.add(chatId); + } + setOpenChats(newOpenChats); + }; + + if (error) { + return ( + + + Payment Transactions + + +

Error loading transactions: {error}

+
+
+ ); + } + return ( - - - Payment Transactions - - - - {loading ?

Loading...

: error ? ( -

{error}

- ) : items.length === 0 ? ( -

No payment transactions

- ) : ( -
- {items.map((tx) => ( - { - const v = tx.payment?.costUsd as unknown; - if (typeof v === 'bigint') return `$${formatAmount(v, 12)}`; - if (v !== undefined && v !== null) { - return `$${formatAmount(BigInt(String(v)), 12)}`; - } - return undefined; - })()} - paidAt={tx.payment?.paidAt} - stream={tx.stream} - /> - ))} + <> + + + Payment Transactions + + +
+ {chatRecords.length === 0 ? ( +

No transactions found

+ ) : ( + chatRecords.map((chatRecord) => ( + + )) + )}
- )} -
-
+ + + + setSelectedTransaction(null)} + /> + ); } diff --git a/src/features/wallet/components/transaction-item.tsx b/src/features/wallet/components/transaction-item.tsx new file mode 100644 index 00000000..9ac3a01a --- /dev/null +++ b/src/features/wallet/components/transaction-item.tsx @@ -0,0 +1,61 @@ +import { formatAmount } from '@nuwa-ai/payment-kit'; +import { Button } from '@/shared/components/ui/button'; +import type { PaymentTransaction } from '../types'; + +const formatCost = (cost: bigint | undefined) => { + if (!cost) return undefined; + if (typeof cost === 'bigint') return `$${formatAmount(cost, 12)}`; + if (cost !== undefined && cost !== null) { + return `$${formatAmount(BigInt(String(cost)), 12)}`; + } + return undefined; +}; + +const formatDate = (timestamp: number) => { + return new Date(timestamp).toLocaleString(); +}; + +const formatTransactionLabel = (transaction: PaymentTransaction) => { + if (transaction.info.type === 'generate-title') { + return 'Chat Title Generation'; + } + return `AI Reply to "${transaction.info.message?.slice(0, 15)}${transaction.info.message?.length && transaction.info.message.length > 15 ? '...' : ''}"`; +}; + +interface TransactionItemProps { + transaction: PaymentTransaction; + onSelect: (transaction: PaymentTransaction) => void; +} + +export function TransactionItem({ + transaction, + onSelect, +}: TransactionItemProps) { + return ( + + ); +} diff --git a/src/features/wallet/hooks/use-account-data.ts b/src/features/wallet/hooks/use-account-data.ts deleted file mode 100644 index 04a8b774..00000000 --- a/src/features/wallet/hooks/use-account-data.ts +++ /dev/null @@ -1,47 +0,0 @@ -import useSWR from 'swr'; -import { useAuth } from '@/features/auth/hooks/use-auth'; -import { - accountApi, - type BalanceData, - type Transaction, -} from '../services/account-api'; - -export function useAccountData() { - const { isConnected } = useAuth(); - - const { - data: balance, - error: balanceError, - isLoading: balanceLoading, - mutate: refreshBalance, - } = useSWR( - isConnected ? 'account/balance' : null, - accountApi.getBalance, - ); - - const { - data: transactions, - error: transactionsError, - isLoading: transactionsLoading, - mutate: refreshTransactions, - } = useSWR( - isConnected ? 'account/transactions' : null, - accountApi.getTransactions, - ); - - const refreshAll = () => { - refreshBalance(); - refreshTransactions(); - }; - - return { - balance: balance || { nuwaTokens: 0, usdRate: 0.02 }, - transactions: transactions || [], - isLoading: (balanceLoading || transactionsLoading) && isConnected, - error: balanceError || transactionsError, - refreshBalance, - refreshTransactions, - refreshAll, - isAuthenticated: isConnected, - }; -} diff --git a/src/features/wallet/hooks/use-chat-transaction-info.ts b/src/features/wallet/hooks/use-chat-transaction-info.ts new file mode 100644 index 00000000..8866e9c8 --- /dev/null +++ b/src/features/wallet/hooks/use-chat-transaction-info.ts @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useState } from 'react'; +import { ChatStateStore } from '@/features/chat/stores'; +import { getHttpClient } from '@/shared/services/payment-clients'; +import type { ChatRecord, PaymentTransaction } from '../types'; + +export const useChatTransactionInfo = () => { + const [chatRecords, setChatRecords] = useState([]); + const [error, setError] = useState(null); + + const { getChatSessionsSortedByUpdatedAt } = ChatStateStore.getState(); + + const fetchChatRecords = useCallback(async () => { + try { + const chatSessions = getChatSessionsSortedByUpdatedAt(); + const client = await getHttpClient(); + const txStore = client.getTransactionStore(); + + const _chatRecords: ChatRecord[] = []; + + // loop through all chat sessions + for (const session of chatSessions) { + const _chatRecord: ChatRecord = { + chatId: session.id, + chatTitle: session.title, + transactions: [], + }; + // loop through all payment ctx ids in each chat session + const transactionPromises: Promise[] = + session.payments.map(async (payment) => { + const res = await txStore.get(payment.ctxId); + if (res) { + return { + ctxId: payment.ctxId, + details: res, + info: payment, + }; + } else { + return { + ctxId: payment.ctxId, + details: null, + info: payment, + }; + } + }); + + const transactions = await Promise.all(transactionPromises); + _chatRecord.transactions.push(...transactions); + _chatRecords.push(_chatRecord); + } + + setChatRecords(_chatRecords); + + return; + } catch (error) { + setError(error as string); + console.error(error); + } + }, []); + + useEffect(() => { + fetchChatRecords(); + }, [fetchChatRecords]); + + return { + chatRecords, + error, + }; +}; diff --git a/src/features/wallet/services/account-api.ts b/src/features/wallet/services/account-api.ts deleted file mode 100644 index 9e57bc79..00000000 --- a/src/features/wallet/services/account-api.ts +++ /dev/null @@ -1,84 +0,0 @@ -export interface BalanceData { - nuwaTokens: number; - usdRate: number; // USD value per NUWA token -} - -export interface Transaction { - id: string; - type: 'credit' | 'debit' | 'top_up'; - amount: number; // Always in NUWA tokens - description: string; - timestamp: string; - status: 'completed' | 'pending' | 'failed'; -} - -export interface TopUpRequest { - amount: number; // Amount in NUWA tokens to purchase - paymentMethod: string; -} - -export const accountApi = { - async getBalance(): Promise { - // For now, always return mock data since API endpoints don't exist yet - // This can be replaced with real API calls when backend is ready - console.log('Using mock balance data'); - return { - nuwaTokens: 1250, - usdRate: 0.02, // $0.02 per NUWA token - }; - }, - - async getTransactions(): Promise { - // For now, always return mock data since API endpoints don't exist yet - // This can be replaced with real API calls when backend is ready - console.log('Using mock transaction data'); - return [ - { - id: '1', - type: 'top_up', - amount: 2500, - description: 'Purchased $2,500 NUWA ($50.00)', - timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), - status: 'completed', - }, - { - id: '2', - type: 'debit', - amount: 763, - description: 'Chat session with GPT-4', - timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), - status: 'completed', - }, - { - id: '3', - type: 'credit', - amount: 500, - description: 'Weekly $NUWA bonus', - timestamp: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), - status: 'completed', - }, - { - id: '4', - type: 'debit', - amount: 438, - description: 'Image generation service', - timestamp: new Date( - Date.now() - 10 * 24 * 60 * 60 * 1000, - ).toISOString(), - status: 'completed', - }, - ]; - }, - - async topUpBalance( - request: TopUpRequest, - ): Promise<{ success: boolean; transactionId?: string }> { - // For now, simulate successful top-up - // This can be replaced with real API calls when backend is ready - console.log('Simulating top-up:', request); - return { - success: true, - transactionId: `txn_${Date.now()}`, - }; - }, -}; diff --git a/src/features/wallet/types.ts b/src/features/wallet/types.ts index da6d04cf..f1c6ccb4 100644 --- a/src/features/wallet/types.ts +++ b/src/features/wallet/types.ts @@ -1,10 +1,25 @@ +import type { TransactionRecord } from '@nuwa-ai/payment-kit'; +import type { ChatPayment } from '@/features/chat/types'; + export type Network = 'ethereum' | 'arbitrum' | 'base' | 'polygon' | 'bsc'; export type Asset = 'usdt' | 'usdc'; type TransactionStatus = 'confirming' | 'completed'; -interface DepositTransaction { +export interface PaymentTransaction { + ctxId: string; + details: TransactionRecord | null; + info: ChatPayment; +} + +export interface ChatRecord { + chatId: string; + chatTitle: string; + transactions: PaymentTransaction[]; +} + +export interface DepositTransaction { id: string; type: 'deposit'; label: string; @@ -13,7 +28,7 @@ interface DepositTransaction { status: TransactionStatus; } -interface SpendTransaction { +export interface SpendTransaction { id: string; type: 'spend'; label: string; diff --git a/src/pages/wallet.tsx b/src/pages/wallet.tsx index fc27b9ab..826f940a 100644 --- a/src/pages/wallet.tsx +++ b/src/pages/wallet.tsx @@ -1,13 +1,7 @@ import { WalletWithProvider } from '@/features/wallet/components'; -import { useAccountData } from '@/features/wallet/hooks/use-account-data'; -import Loading from '@/shared/components/loading'; export default function WalletPage() { - const { isLoading, isAuthenticated } = useAccountData(); - if (!isAuthenticated || isLoading) { - return ; - } return ; } diff --git a/src/shared/hooks/usePaymentHub.ts b/src/shared/hooks/use-payment-hub.ts similarity index 86% rename from src/shared/hooks/usePaymentHub.ts rename to src/shared/hooks/use-payment-hub.ts index f94c6ae5..d42eac94 100644 --- a/src/shared/hooks/usePaymentHub.ts +++ b/src/shared/hooks/use-payment-hub.ts @@ -1,13 +1,18 @@ import { useCallback, useEffect, useState } from 'react'; import { getPaymentHubClient } from '@/shared/services/payment-clients'; -function formatBigIntWithDecimals(value: bigint, decimals: number, fractionDigits: number): string { +function formatBigIntWithDecimals( + value: bigint, + decimals: number, + fractionDigits: number, +): string { const negative = value < 0n; const v = negative ? -value : value; const base = 10n ** BigInt(decimals); const integer = v / base; let fraction = (v % base).toString().padStart(decimals, '0'); - if (fractionDigits >= 0) fraction = fraction.slice(0, Math.min(decimals, fractionDigits)); + if (fractionDigits >= 0) + fraction = fraction.slice(0, Math.min(decimals, fractionDigits)); const fracPart = fractionDigits > 0 ? `.${fraction}` : ''; return `${negative ? '-' : ''}${integer.toString()}${fracPart}`; } @@ -39,5 +44,3 @@ export function usePaymentHubRgas(defaultAssetId = '0x3::gas_coin::RGas') { return { loading, error, amount, usd, refetch }; } - - diff --git a/src/shared/hooks/useTransactions.ts b/src/shared/hooks/useTransactions.ts deleted file mode 100644 index 53e883d3..00000000 --- a/src/shared/hooks/useTransactions.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { getHttpClient } from '@/shared/services/payment-clients'; -import type { TransactionRecord, TransactionStore } from '@nuwa-ai/payment-kit'; - -export function useTransactions(limit = 50) { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [items, setItems] = useState([]); - const [store, setStore] = useState(null); - - const load = useCallback(async () => { - setLoading(true); - try { - const client = await getHttpClient(); - const txStore = client.getTransactionStore(); - setStore(txStore); - const res = await txStore.list({}, { limit }); - setItems(res.items); - setError(null); - } catch (e: any) { - setError(e?.message || String(e)); - } finally { - setLoading(false); - } - }, [limit]); - - useEffect(() => { - let unsub: (() => void) | undefined; - (async () => { - await load(); - try { - if (store && store.subscribe) { - unsub = store.subscribe(() => { - // naive: just reload on any change - load(); - }); - } - } catch {} - })(); - return () => { - if (unsub) { - try { unsub(); } catch {} - } - }; - }, [load, store]); - - return { loading, error, items, reload: load }; -} - - diff --git a/src/shared/services/mcp-transport.ts b/src/shared/services/mcp-transport.ts index ea136c16..b686d912 100644 --- a/src/shared/services/mcp-transport.ts +++ b/src/shared/services/mcp-transport.ts @@ -14,7 +14,6 @@ export async function createDidAuthSigner(baseUrl: string) { params: { body, url: baseUrl }, } as const; const sig = await sdk.sign(payload); - console.log('sig', sig); return DIDAuth.v1.toAuthorizationHeader(sig); }; } From cea543267830ff795713a600df0ae22df867aa2f Mon Sep 17 00:00:00 2001 From: Mine77 Date: Fri, 15 Aug 2025 20:35:44 +0800 Subject: [PATCH 18/28] chore: ui fix --- src/features/wallet/components/balance-card.tsx | 2 +- src/features/wallet/components/transaction-item.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/wallet/components/balance-card.tsx b/src/features/wallet/components/balance-card.tsx index 5358cbdd..90a72b73 100644 --- a/src/features/wallet/components/balance-card.tsx +++ b/src/features/wallet/components/balance-card.tsx @@ -48,7 +48,7 @@ export function BalanceCard({ onTopUp }: BalanceCardProps) { onClick={() => setShowFaucetDialog(true)} > - More Balance + Get More Balance
diff --git a/src/features/wallet/components/transaction-item.tsx b/src/features/wallet/components/transaction-item.tsx index 9ac3a01a..c4b6c308 100644 --- a/src/features/wallet/components/transaction-item.tsx +++ b/src/features/wallet/components/transaction-item.tsx @@ -19,7 +19,7 @@ const formatTransactionLabel = (transaction: PaymentTransaction) => { if (transaction.info.type === 'generate-title') { return 'Chat Title Generation'; } - return `AI Reply to "${transaction.info.message?.slice(0, 15)}${transaction.info.message?.length && transaction.info.message.length > 15 ? '...' : ''}"`; + return `AI Request - "${transaction.info.message?.slice(0, 15)}${transaction.info.message?.length && transaction.info.message.length > 15 ? '...' : ''}"`; }; interface TransactionItemProps { From 01f273e9022719154d7d15c4fca7ca1c3aa4d4c9 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Fri, 15 Aug 2025 21:30:08 +0800 Subject: [PATCH 19/28] chore --- .../cap-studio/components/batch-create.tsx | 76 ++++++++++++++++--- .../cap-studio/components/my-caps/index.tsx | 10 ++- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/features/cap-studio/components/batch-create.tsx b/src/features/cap-studio/components/batch-create.tsx index 35de8a45..41a2ebc7 100644 --- a/src/features/cap-studio/components/batch-create.tsx +++ b/src/features/cap-studio/components/batch-create.tsx @@ -80,32 +80,77 @@ export function BatchCreate({ onBatchCreate }: BatchCreateProps) { // validate simplified Cap input format const validateSimplifiedCap = (capData: any): SimplifiedCapInput | null => { - // basic structure validation + // validate idName if (!capData.idName || typeof capData.idName !== 'string') { throw new Error('Missing or invalid idName'); } + if (capData.idName.length < 6) { + throw new Error('idName must be at least 6 characters'); + } + if (capData.idName.length > 20) { + throw new Error('idName must be at most 20 characters'); + } + if (!/^[a-zA-Z0-9_]+$/.test(capData.idName)) { + throw new Error('idName must contain only letters, numbers, and underscores'); + } if (!capData.metadata || typeof capData.metadata !== 'object') { throw new Error('Missing metadata'); } + // validate displayName if ( !capData.metadata.displayName || typeof capData.metadata.displayName !== 'string' ) { throw new Error('Missing displayName in metadata'); } + if (capData.metadata.displayName.length < 1) { + throw new Error('Display name is required'); + } + if (capData.metadata.displayName.length > 50) { + throw new Error('Display name too long (max 50 characters)'); + } + // validate description if ( !capData.metadata.description || typeof capData.metadata.description !== 'string' ) { throw new Error('Missing description in metadata'); } + if (capData.metadata.description.length < 20) { + throw new Error('Description must be at least 20 characters'); + } + if (capData.metadata.description.length > 500) { + throw new Error('Description too long (max 500 characters)'); + } + // validate tags if (!capData.metadata.tags || !Array.isArray(capData.metadata.tags)) { throw new Error('Missing or invalid tags in metadata'); } + if (!capData.metadata.tags.every((tag: any) => typeof tag === 'string')) { + throw new Error('All tags must be strings'); + } + + // validate optional homepage URL + if (capData.metadata.homepage && capData.metadata.homepage !== '') { + try { + new URL(capData.metadata.homepage); + } catch { + throw new Error('Homepage must be a valid URL'); + } + } + + // validate optional repository URL + if (capData.metadata.repository && capData.metadata.repository !== '') { + try { + new URL(capData.metadata.repository); + } catch { + throw new Error('Repository must be a valid URL'); + } + } if (!capData.core || typeof capData.core !== 'object') { throw new Error('Missing core'); @@ -364,22 +409,22 @@ export function BatchCreate({ onBatchCreate }: BatchCreateProps) {
                 {`[
   {
-    "idName": "my-cap",
+    "idName": "my_awesome_cap",
     "metadata": {
-      "displayName": "My Cap",
-      "description": "Description of the cap",
-      "tags": ["tag1", "tag2"],
+      "displayName": "My Awesome Cap",
+      "description": "A detailed description of what this cap does and how it helps users accomplish their tasks effectively",
+      "tags": ["productivity", "assistant"],
       "thumbnail": {
         "type": "url",
         "url": "https://example.com/thumbnail.png"
       },
-      "homepage": "https://example.com" (optional),
-      "repository": "https://github.com/user/repo" (optional)
+      "homepage": "https://example.com",
+      "repository": "https://github.com/user/repo"
     },
     "core": {
       "prompt": {
-        "value": "System prompt for the cap",
-        "suggestions": ["suggestion1", "suggestion2"] (optional)
+        "value": "You are a helpful assistant that...",
+        "suggestions": ["How can I help you today?", "What would you like to know?"]
       },
       "modelId": "openai/gpt-4o-mini",
       "mcpServers": {
@@ -387,12 +432,21 @@ export function BatchCreate({ onBatchCreate }: BatchCreateProps) {
           "url": "npm:@example/mcp-server",
           "transport": "httpStream"
         }
-      } (optional)
+      }
     }
   }
 ]
 
-Note: The following fields are automatically generated:
+Validation Rules:
+- idName: 6-20 chars, letters/numbers/underscores only (no dashes)
+- displayName: 1-50 characters required
+- description: 20-500 characters required
+- tags: array of strings required
+- homepage/repository: valid URLs (optional)
+- thumbnail/mcpServers: optional
+- prompt.suggestions: optional array
+
+Auto-generated fields:
 - id: "authorDID:idName"
 - authorDID: from your authentication
 - submittedAt: current timestamp`}
diff --git a/src/features/cap-studio/components/my-caps/index.tsx b/src/features/cap-studio/components/my-caps/index.tsx
index acd15400..c964cee2 100644
--- a/src/features/cap-studio/components/my-caps/index.tsx
+++ b/src/features/cap-studio/components/my-caps/index.tsx
@@ -123,11 +123,19 @@ export function MyCaps({
             You haven't created any caps yet. Start building your first
             capability to get started with cap development.
           
-          
+
+
From 343fd022b50ed0d5967c1182937ee3194685a552 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Sun, 17 Aug 2025 14:55:25 +0800 Subject: [PATCH 20/28] chore --- batch-cap-samples.json | 2599 +++++++++++++++++ package.json | 2 +- pnpm-lock.yaml | 10 +- sample-caps-simplified.json | 88 - .../cap-store/hooks/use-remote-cap.ts | 40 +- .../cap-studio/components/batch-create.tsx | 6 - .../cap-studio/components/my-caps/index.tsx | 75 +- .../cap-studio/hooks/use-submit-cap.ts | 69 +- src/features/cap-studio/services.ts | 3 +- src/features/wallet/components/chat-item.tsx | 19 +- .../wallet/components/transaction-history.tsx | 168 +- .../wallet/components/transaction-item.tsx | 15 +- src/shared/components/ui/date-picker.tsx | 54 + .../components/ui/shadcn-io/data-picker.tsx | 54 + 14 files changed, 3053 insertions(+), 149 deletions(-) create mode 100644 batch-cap-samples.json delete mode 100644 sample-caps-simplified.json create mode 100644 src/shared/components/ui/date-picker.tsx create mode 100644 src/shared/components/ui/shadcn-io/data-picker.tsx diff --git a/batch-cap-samples.json b/batch-cap-samples.json new file mode 100644 index 00000000..5fbb62ad --- /dev/null +++ b/batch-cap-samples.json @@ -0,0 +1,2599 @@ +[ + { + "idName": "openai_gpt_5_chat", + "metadata": { + "displayName": "OpenAI: GPT-5 Chat", + "description": "GPT-5 Chat is designed for advanced, natural, multimodal, and context-aware conversations for enterprise applications.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Help me plan an enterprise-level project roadmap", + "Create a comprehensive business analysis report", + "Draft a technical proposal for stakeholders", + "Design a customer onboarding workflow" + ] + }, + "modelId": "openai/gpt-5-chat", + "mcpServers": {} + } + }, + { + "idName": "openai_gpt_5_cap", + "metadata": { + "displayName": "OpenAI: GPT-5", + "description": "GPT-5 is OpenAI's most advanced model offering major improvements in reasoning, code quality, and user experience. Optimized for complex tasks requiring step-by-step reasoning, instruction following, and accuracy in high-stakes use cases. Features test-time routing and advanced prompt understanding with reduced hallucination and sycophancy.", + "tags": [ + "AI Model", + "Coding", + "Content Writing" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Debug this complex Python algorithm step by step", + "Write a technical blog post about AI advancements", + "Solve this multi-step reasoning problem", + "Create a full-stack web application architecture" + ] + }, + "modelId": "openai/gpt-5", + "mcpServers": {} + } + }, + { + "idName": "openai_gpt5_mini", + "metadata": { + "displayName": "OpenAI: GPT-5 Mini", + "description": "GPT-5 Mini is a compact version of GPT-5, designed to handle lighter-weight reasoning tasks. It provides the same instruction-following and safety-tuning benefits as GPT-5, but with reduced latency and cost. GPT-5 Mini is the successor to OpenAI's o4-mini model.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Explain this concept in simple terms", + "Help me organize my daily tasks", + "Answer quick questions about general topics", + "Provide a brief summary of this document" + ] + }, + "modelId": "openai/gpt-5-mini", + "mcpServers": {} + } + }, + { + "idName": "openai_gpt5_nano", + "metadata": { + "displayName": "OpenAI: GPT-5 Nano", + "description": "GPT-5-Nano is the smallest and fastest variant in the GPT-5 system, optimized for developer tools, rapid interactions, and ultra-low latency environments. While limited in reasoning depth compared to its larger counterparts, it retains key instruction-following and safety features. It is the successor to GPT-4.1-nano and offers a lightweight option for cost-sensitive or real-time applications.", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Quick code completion for this function", + "Classify this text into categories", + "Generate autocomplete suggestions", + "Provide instant responses to user queries" + ] + }, + "modelId": "openai/gpt-5-nano", + "mcpServers": {} + } + }, + { + "idName": "openai_oss_120b", + "metadata": { + "displayName": "OpenAI: gpt-oss-120b", + "description": "gpt-oss-120b is an open-weight, 117B-parameter Mixture-of-Experts (MoE) language model from OpenAI designed for high-reasoning, agentic, and general-purpose production use cases. It activates 5.1B parameters per forward pass and is optimized to run on a single H100 GPU with native MXFP4 quantization. The model supports configurable reasoning depth, full chain-of-thought access, and native tool use, including function calling, browsing, and structured output generation.", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Use tools to research and analyze market trends", + "Create a function to process large datasets", + "Browse the web for the latest tech news", + "Generate structured JSON output for API responses" + ] + }, + "modelId": "openai/gpt-oss-120b", + "mcpServers": {} + } + }, + { + "idName": "openai_oss_20b", + "metadata": { + "displayName": "OpenAI: gpt-oss-20b", + "description": "gpt-oss-20b is an open-weight 21B parameter model released by OpenAI under the Apache 2.0 license. It uses a Mixture-of-Experts (MoE) architecture with 3.6B active parameters per forward pass, optimized for lower-latency inference and deployability on consumer or single-GPU hardware. The model is trained in OpenAI’s Harmony response format and supports reasoning level configuration, fine-tuning, and agentic capabilities including function calling, tool use, and structured outputs.", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Use function calling to integrate with APIs", + "Generate code with tool-assisted development", + "Create structured outputs with JSON schema", + "Build an automated workflow with tools" + ] + }, + "modelId": "openai/gpt-oss-20b", + "mcpServers": {} + } + }, + { + "idName": "claude_opus_4_1", + "metadata": { + "displayName": "Anthropic: Claude Opus 4.1", + "description": "Claude Opus 4.1 is an updated version of Anthropic’s flagship model, offering improved performance in coding, reasoning, and agentic tasks. It achieves 74.5% on SWE-bench Verified and shows notable gains in multi-file code refactoring, debugging precision, and detail-oriented reasoning. The model supports extended thinking up to 64K tokens and is optimized for tasks involving research, data analysis, and tool-assisted reasoning.", + "tags": [ + "AI Model", + "Coding", + "Research", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/claude-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Refactor this codebase to improve performance", + "Research the latest developments in AI", + "Debug this complex multi-file project", + "Analyze data patterns in this dataset" + ] + }, + "modelId": "anthropic/claude-opus-4.1", + "mcpServers": {} + } + }, + { + "idName": "qwen3_30b_instruct", + "metadata": { + "displayName": "Qwen: Qwen3 30B A3B Instruct 2507", + "description": "Qwen3-30B-A3B-Instruct-2507 is a 30.5B-parameter MoE model with 3.3B active parameters per inference. Operates in non-thinking mode for high-quality instruction following, multilingual understanding, and agentic tool use. Demonstrates competitive performance in reasoning, coding, and alignment benchmarks.", + "tags": [ + "AI Model", + "Coding", + "Content Writing", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Write a technical blog post in multiple languages", + "Create a React component with TypeScript", + "Generate structured documentation", + "Build a function that handles API requests" + ] + }, + "modelId": "qwen/qwen3-30b-a3b-instruct-2507", + "mcpServers": {} + } + }, + { + "idName": "glm_4_5", + "metadata": { + "displayName": "Z.AI: GLM 4.5", + "description": "GLM-4.5 is a flagship foundation model purpose-built for agent-based applications with MoE architecture and 128k context support. Features hybrid inference with thinking mode for complex reasoning/tool use and non-thinking mode for instant responses. Enhanced capabilities in reasoning, code generation, and agent alignment.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.62.0/files/light/zai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Design an AI agent for complex reasoning", + "Create a Python script for data analysis", + "Use tools to automate repetitive tasks", + "Build a multi-step workflow application" + ] + }, + "modelId": "z-ai/glm-4.5", + "mcpServers": {} + } + }, + { + "idName": "glm_4_5_air", + "metadata": { + "displayName": "Z.AI: GLM 4.5 Air", + "description": "GLM-4.5-Air is the lightweight variant of the flagship model family, purpose-built for agent-centric applications. Uses MoE architecture with compact parameter size. Features hybrid inference modes with thinking mode for advanced reasoning and non-thinking mode for real-time interaction.", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.62.0/files/light/zai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create a lightweight chatbot with tools", + "Generate quick responses with function calls", + "Build a simple automation script", + "Process real-time data streams" + ] + }, + "modelId": "z-ai/glm-4.5-air", + "mcpServers": {} + } + }, + { + "idName": "qwen3_235b_thinking", + "metadata": { + "displayName": "Qwen: Qwen3 235B A22B Thinking 2507", + "description": "Qwen3-235B-A22B-Thinking-2507 is a high-performance MoE model optimized for complex reasoning tasks. Activates 22B of 235B parameters per pass with 262K context support. This thinking-only variant excels at structured logical reasoning, mathematics, science, and long-form generation with state-of-the-art benchmark performance.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve this complex mathematical theorem", + "Analyze scientific research papers", + "Create structured logical arguments", + "Generate code for algorithmic problems" + ] + }, + "modelId": "qwen/qwen3-235b-a22b-thinking-2507", + "mcpServers": {} + } + }, + { + "idName": "glm_4_32b", + "metadata": { + "displayName": "Z.AI: GLM 4 32B ", + "description": "GLM 4 32B is a cost-effective foundation language model.\n\nIt can efficiently perform complex tasks and has significantly enhanced capabilities in tool use, online search, and code-related intelligent tasks.\n\nIt is made by the same lab behind the thudm models.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.62.0/files/light/zai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build a web scraping tool with Python", + "Search online for market research data", + "Create a REST API with authentication", + "Optimize this database query performance" + ] + }, + "modelId": "z-ai/glm-4-32b", + "mcpServers": {} + } + }, + { + "idName": "qwen3_coder", + "metadata": { + "displayName": "Qwen: Qwen3 Coder ", + "description": "Qwen3-Coder-480B-A35B-Instruct is a Mixture-of-Experts (MoE) code generation model developed by the Qwen team. It is optimized for agentic coding tasks such as function calling, tool use, and long-context reasoning over repositories. The model features 480 billion total parameters, with 35 billion active per forward pass (8 out of 160 experts).\n\nPricing for the Alibaba endpoints varies by context length. Once a request is greater than 128k input tokens, the higher pricing is used.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Refactor this entire codebase architecture", + "Create an advanced machine learning pipeline", + "Build a distributed system with microservices", + "Use tools to analyze repository patterns" + ] + }, + "modelId": "qwen/qwen3-coder", + "mcpServers": {} + } + }, + { + "idName": "gemini_2_5_lite", + "metadata": { + "displayName": "Google: Gemini 2.5 Flash Lite", + "description": "Gemini 2.5 Flash-Lite is a lightweight reasoning model optimized for ultra-low latency and cost efficiency. Offers improved throughput and faster token generation. By default, thinking (multi-pass reasoning) is disabled for speed, but can be enabled via Reasoning API parameter.", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create a fast API endpoint with caching", + "Build a real-time chat application", + "Generate automated test cases", + "Optimize this code for performance" + ] + }, + "modelId": "google/gemini-2.5-flash-lite", + "mcpServers": {} + } + }, + { + "idName": "qwen3_235b_instruct", + "metadata": { + "displayName": "Qwen: Qwen3 235B A22B Instruct 2507", + "description": "Qwen3-235B-A22B-Instruct-2507 is a multilingual, instruction-tuned MoE model with 22B active parameters per forward pass. Optimized for general-purpose text generation, instruction following, logical reasoning, math, code, and tool usage with 262K context length and significant gains in multilingual understanding.", + "tags": [ + "AI Model", + "Coding", + "Content Writing", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Write a comprehensive technical report", + "Create a multilingual content strategy", + "Build a complex data processing pipeline", + "Generate structured documentation with tools" + ] + }, + "modelId": "qwen/qwen3-235b-a22b-2507", + "mcpServers": {} + } + }, + { + "idName": "kimi_k2", + "metadata": { + "displayName": "MoonshotAI: Kimi K2", + "description": "Kimi K2 Instruct is a large-scale MoE model with 1 trillion total parameters and 32 billion active per forward pass. Optimized for agentic capabilities including advanced tool use, reasoning, and code synthesis. Excels in coding (LiveCodeBench, SWE-bench), reasoning (ZebraLogic, GPQA), and tool-use tasks with 128K context support.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/moonshot.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Code a complex algorithm with optimization", + "Build an advanced AI agent system", + "Create automated testing frameworks", + "Design scalable software architecture" + ] + }, + "modelId": "moonshotai/kimi-k2", + "mcpServers": {} + } + }, + { + "idName": "grok_4", + "metadata": { + "displayName": "xAI: Grok 4", + "description": "Grok 4 is xAI's latest reasoning model with a 256k context window. It supports parallel tool calling, structured outputs, and both image and text inputs. Note that reasoning is not exposed, reasoning cannot be disabled, and the reasoning effort cannot be specified. Pricing increases once the total tokens in a given request is greater than 128k tokens. See more details on the [xAI docs](https://docs.x.ai/docs/models/grok-4-0709)", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/grok.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Analyze complex data with parallel processing", + "Create structured outputs for API integration", + "Build a tool that processes images and text", + "Generate comprehensive technical reports" + ] + }, + "modelId": "x-ai/grok-4", + "mcpServers": {} + } + }, + { + "idName": "gemini_2_5_lite_06", + "metadata": { + "displayName": "Google: Gemini 2.5 Flash Lite Preview 06-17", + "description": "Gemini 2.5 Flash-Lite is a lightweight reasoning model optimized for ultra-low latency and cost efficiency. Offers improved throughput and faster token generation. By default, thinking (multi-pass reasoning) is disabled for speed, but can be enabled via Reasoning API parameter.", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create a high-speed data processing tool", + "Build a real-time analytics dashboard", + "Generate quick automated responses", + "Optimize this function for low latency" + ] + }, + "modelId": "google/gemini-2.5-flash-lite-preview-06-17", + "mcpServers": {} + } + }, + { + "idName": "gemini_2_5_flash", + "metadata": { + "displayName": "Google: Gemini 2.5 Flash", + "description": "Gemini 2.5 Flash is Google's state-of-the-art workhorse model, specifically designed for advanced reasoning, coding, mathematics, and scientific tasks. It includes built-in \"thinking\" capabilities, enabling it to provide responses with greater accuracy and nuanced context handling. \n\nAdditionally, Gemini 2.5 Flash is configurable through the \"max tokens for reasoning\" parameter, as described in the documentation (https://openrouter.ai/docs/use-cases/reasoning-tokens#max-tokens-for-reasoning).", + "tags": [ + "AI Model", + "Coding", + "Research" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve advanced mathematical problems", + "Research cutting-edge scientific topics", + "Debug complex software architecture", + "Create detailed technical documentation" + ] + }, + "modelId": "google/gemini-2.5-flash", + "mcpServers": {} + } + }, + { + "idName": "gemini_2_5_pro", + "metadata": { + "displayName": "Google: Gemini 2.5 Pro", + "description": "Gemini 2.5 Pro is Google’s state-of-the-art AI model designed for advanced reasoning, coding, mathematics, and scientific tasks. It employs “thinking” capabilities, enabling it to reason through responses with enhanced accuracy and nuanced context handling. Gemini 2.5 Pro achieves top-tier performance on multiple benchmarks, including first-place positioning on the LMArena leaderboard, reflecting superior human-preference alignment and complex problem-solving abilities.", + "tags": [ + "AI Model", + "Coding", + "Research" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Conduct comprehensive literature review", + "Analyze complex scientific datasets", + "Write detailed research methodology", + "Create advanced coding solutions" + ] + }, + "modelId": "google/gemini-2.5-pro", + "mcpServers": {} + } + }, + { + "idName": "grok_3_mini", + "metadata": { + "displayName": "xAI: Grok 3 Mini", + "description": "A lightweight model that thinks before responding. Fast, smart, and great for logic-based tasks that do not require deep domain knowledge. The raw thinking traces are accessible.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/grok.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve this logic puzzle step-by-step", + "Explain complex concepts clearly", + "Analyze basic reasoning problems", + "Help with quick decision-making" + ] + }, + "modelId": "x-ai/grok-3-mini", + "mcpServers": {} + } + }, + { + "idName": "grok_3", + "metadata": { + "displayName": "xAI: Grok 3", + "description": "Grok 3 is the latest model from xAI. It's their flagship model that excels at enterprise use cases like data extraction, coding, and text summarization. Possesses deep domain knowledge in finance, healthcare, law, and science.\n\n", + "tags": [ + "AI Model", + "Coding" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/grok.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Extract structured data from documents", + "Create advanced coding solutions", + "Summarize complex financial reports", + "Analyze healthcare or legal documents" + ] + }, + "modelId": "x-ai/grok-3", + "mcpServers": {} + } + }, + { + "idName": "deepseek_r1_qwen8b", + "metadata": { + "displayName": "DeepSeek: Deepseek R1 0528 Qwen3 8B", + "description": "DeepSeek-R1-0528 is a lightly upgraded release that tops math, programming, and logic leaderboards with step-change depth-of-thought. The distilled variant, DeepSeek-R1-0528-Qwen3-8B, transfers this chain-of-thought into 8B parameters, beating standard Qwen3 8B by +10 pp.", + "tags": [ + "AI Model", + "Coding" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/deepseek-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve advanced programming challenges", + "Debug mathematical algorithms", + "Optimize code for better performance", + "Create efficient data structures" + ] + }, + "modelId": "deepseek/deepseek-r1-0528-qwen3-8b", + "mcpServers": {} + } + }, + { + "idName": "deepseek_r1_0528", + "metadata": { + "displayName": "DeepSeek: R1 0528", + "description": "May 28th update to the [original DeepSeek R1](/deepseek/deepseek-r1) Performance on par with [OpenAI o1](/openai/o1), but open-sourced and with fully open reasoning tokens. It's 671B parameters in size, with 37B active in an inference pass.\n\nFully open-source model.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/deepseek-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Think through complex reasoning problems", + "Analyze philosophical questions deeply", + "Solve challenging mathematical theorems", + "Provide detailed explanations of concepts" + ] + }, + "modelId": "deepseek/deepseek-r1-0528", + "mcpServers": {} + } + }, + { + "idName": "claude_opus_4", + "metadata": { + "displayName": "Anthropic: Claude Opus 4", + "description": "Claude Opus 4 is benchmarked as the world’s best coding model, at time of release, bringing sustained performance on complex, long-running tasks and agent workflows. It sets new benchmarks in software engineering, achieving leading results on SWE-bench (72.5%) and Terminal-bench (43.2%). Opus 4 supports extended, agentic workflows, handling thousands of task steps continuously for hours without degradation. \n\nRead more at the [blog post here](https://www.anthropic.com/news/claude-4)", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/claude-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build enterprise-level software solutions", + "Create complex agentic workflows", + "Debug multi-file codebases efficiently", + "Design scalable system architectures" + ] + }, + "modelId": "anthropic/claude-opus-4", + "mcpServers": {} + } + }, + { + "idName": "claude_sonnet_4", + "metadata": { + "displayName": "Anthropic: Claude Sonnet 4", + "description": "Claude Sonnet 4 significantly enhances coding and reasoning capabilities with improved precision and controllability. Achieving 72.7% on SWE-bench, it balances capability and efficiency for diverse applications from routine coding to complex software development with enhanced autonomous navigation and reduced error rates.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/claude-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Develop production-ready applications", + "Create automated testing suites", + "Build efficient coding tools", + "Design robust software systems" + ] + }, + "modelId": "anthropic/claude-sonnet-4", + "mcpServers": {} + } + }, + { + "idName": "llama_guard_4_12b", + "metadata": { + "displayName": "Meta: Llama Guard 4 12B", + "description": "Llama Guard 4 is a multimodal content safety classifier fine-tuned from Llama 4 Scout. Classifies both input prompts and LLM responses as safe/unsafe, providing violation categories. Supports text and image moderation across multiple languages, handles mixed multimodal inputs, and aligns with MLCommons hazards taxonomy.", + "tags": [ + "AI Model", + "Content Writing", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/metaai-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Classify content for safety compliance", + "Moderate user-generated content", + "Create content filtering systems", + "Analyze text and image safety" + ] + }, + "modelId": "meta-llama/llama-guard-4-12b", + "mcpServers": {} + } + }, + { + "idName": "qwen3_30b_a3b", + "metadata": { + "displayName": "Qwen: Qwen3 30B A3B", + "description": "Qwen3-30B-A3B features dense and MoE architectures with seamless switching between thinking mode for complex reasoning and non-thinking mode for efficient dialogue. Significantly outperforms prior models in mathematics, coding, commonsense reasoning, creative writing, and interactive dialogue with 30.5B parameters (3.3B activated).", + "tags": [ + "AI Model", + "Coding", + "Content Writing", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Write creative technical documentation", + "Build intelligent coding assistants", + "Create versatile programming tools", + "Generate multi-purpose content" + ] + }, + "modelId": "qwen/qwen3-30b-a3b", + "mcpServers": {} + } + }, + { + "idName": "qwen3_8b", + "metadata": { + "displayName": "Qwen: Qwen3 8B", + "description": "Qwen3-8B is a dense 8.2B parameter model designed for reasoning-heavy tasks and efficient dialogue. Supports seamless switching between thinking mode for math, coding, and logical inference, and non-thinking mode for general conversation. Supports 32K context, extendable to 131K tokens with YaRN.", + "tags": [ + "AI Model", + "Coding", + "Content Writing", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create interactive dialogue systems", + "Build math problem-solving tools", + "Write creative content with logical flow", + "Generate code for reasoning tasks" + ] + }, + "modelId": "qwen/qwen3-8b", + "mcpServers": {} + } + }, + { + "idName": "qwen3_14b", + "metadata": { + "displayName": "Qwen: Qwen3 14B", + "description": "Qwen3-14B is a dense 14.8B parameter model designed for complex reasoning and efficient dialogue. Supports seamless switching between thinking mode for math, programming, and logical inference, and non-thinking mode for general conversation. Natively handles 32K token contexts, extendable to 131K tokens.", + "tags": [ + "AI Model", + "Coding", + "Content Writing", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve complex programming challenges", + "Create detailed technical content", + "Build advanced reasoning systems", + "Generate long-form analytical reports" + ] + }, + "modelId": "qwen/qwen3-14b", + "mcpServers": {} + } + }, + { + "idName": "qwen3_32b", + "metadata": { + "displayName": "Qwen: Qwen3 32B", + "description": "Qwen3-32B is a dense 32.8B parameter model optimized for complex reasoning and efficient dialogue. Supports seamless switching between thinking mode for math, coding, and logical inference, and non-thinking mode for faster conversation. Natively handles 32K token contexts, extendable to 131K tokens.", + "tags": [ + "AI Model", + "Coding", + "Content Writing", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Develop sophisticated applications", + "Write comprehensive content strategies", + "Build complex reasoning algorithms", + "Create intelligent automation tools" + ] + }, + "modelId": "qwen/qwen3-32b", + "mcpServers": {} + } + }, + { + "idName": "qwen3_235b_a22b", + "metadata": { + "displayName": "Qwen: Qwen3 235B A22B", + "description": "Qwen3-235B-A22B is a 235B parameter MoE model activating 22B parameters per forward pass. Supports seamless switching between thinking mode for complex reasoning and non-thinking mode for conversational efficiency. Strong reasoning ability, multilingual support, and agent tool-calling capabilities with 32K context.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build enterprise-scale applications", + "Create advanced coding solutions", + "Develop intelligent agent systems", + "Generate complex technical documentation" + ] + }, + "modelId": "qwen/qwen3-235b-a22b", + "mcpServers": {} + } + }, + { + "idName": "o4_mini_high", + "metadata": { + "displayName": "OpenAI: o4 Mini High", + "description": "OpenAI o4-mini-high is the same model as o4-mini with reasoning_effort set to high. A compact reasoning model optimized for fast, cost-efficient performance while retaining strong multimodal and agentic capabilities. Demonstrates competitive performance on AIME (99.5% with Python) and SWE-bench benchmarks.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve advanced mathematical problems", + "Create optimized coding solutions", + "Build intelligent reasoning systems", + "Generate automated test frameworks" + ] + }, + "modelId": "openai/o4-mini-high", + "mcpServers": {} + } + }, + { + "idName": "o4_mini", + "metadata": { + "displayName": "OpenAI: o4 Mini", + "description": "OpenAI o4-mini is a compact reasoning model optimized for fast, cost-efficient performance while retaining strong multimodal and agentic capabilities. Demonstrates competitive reasoning and coding performance on AIME (99.5% with Python) and SWE-bench, outperforming o3-mini with high accuracy in STEM tasks and visual problem solving.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve STEM problems with high accuracy", + "Create efficient coding solutions", + "Build multimodal applications", + "Generate automated testing tools" + ] + }, + "modelId": "openai/o4-mini", + "mcpServers": {} + } + }, + { + "idName": "gpt_4_1", + "metadata": { + "displayName": "OpenAI: GPT-4.1", + "description": "GPT-4.1 is a flagship large language model optimized for advanced instruction following, real-world software engineering, and long-context reasoning. Supports 1 million token context window and outperforms GPT-4o across coding (54.6% SWE-bench), instruction compliance, and multimodal understanding.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build enterprise software solutions", + "Create advanced coding frameworks", + "Generate intelligent automation tools", + "Develop large-scale applications" + ] + }, + "modelId": "openai/gpt-4.1", + "mcpServers": {} + } + }, + { + "idName": "gpt_4_1_mini", + "metadata": { + "displayName": "OpenAI: GPT-4.1 Mini", + "description": "GPT-4.1 Mini is a mid-sized model delivering performance competitive with GPT-4o at substantially lower latency and cost. It retains a 1 million token context window and scores 45.1% on hard instruction evals, 35.8% on MultiChallenge, and 84.1% on IFEval. Mini also shows strong coding ability (e.g., 31.6% on Aider’s polyglot diff benchmark) and vision understanding, making it suitable for interactive applications with tight performance constraints.", + "tags": [ + "AI Model", + "Coding" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create interactive coding assistants", + "Build responsive web applications", + "Generate optimized algorithms", + "Develop efficient automation scripts" + ] + }, + "modelId": "openai/gpt-4.1-mini", + "mcpServers": {} + } + }, + { + "idName": "gpt_4_1_nano", + "metadata": { + "displayName": "OpenAI: GPT-4.1 Nano", + "description": "For tasks that demand low latency, GPT‑4.1 nano is the fastest and cheapest model in the GPT-4.1 series. It delivers exceptional performance at a small size with its 1 million token context window, and scores 80.1% on MMLU, 50.3% on GPQA, and 9.8% on Aider polyglot coding – even higher than GPT‑4o mini. It’s ideal for tasks like classification or autocompletion.", + "tags": [ + "AI Model", + "Coding" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create rapid code completions", + "Build fast classification systems", + "Generate quick automated responses", + "Develop real-time processing tools" + ] + }, + "modelId": "openai/gpt-4.1-nano", + "mcpServers": {} + } + }, + { + "idName": "llama_4_maverick", + "metadata": { + "displayName": "Meta: Llama 4 Maverick", + "description": "Llama 4 Maverick 17B is a high-capacity multimodal MoE model with 128 experts and 17B active parameters. Supports multilingual text and image input with 1M token context. Features early fusion for native multimodality, instruction-tuned for assistant behavior, image reasoning, and general-purpose multimodal interaction.", + "tags": [ + "AI Model", + "Coding", + "Research" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/metaai-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create multimodal research applications", + "Build advanced coding solutions", + "Generate comprehensive analyses", + "Develop high-capacity AI systems" + ] + }, + "modelId": "meta-llama/llama-4-maverick", + "mcpServers": {} + } + }, + { + "idName": "llama_4_scout", + "metadata": { + "displayName": "Meta: Llama 4 Scout", + "description": "Llama 4 Scout 17B is a MoE model activating 17B parameters out of 109B total. Supports native multimodal input (text and image) with 10M token context length. Designed for assistant-style interaction and visual reasoning, incorporating early fusion for seamless modality integration across 12 supported languages.", + "tags": [ + "AI Model", + "Coding" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/metaai-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build visual reasoning applications", + "Create multimodal coding assistants", + "Develop image analysis tools", + "Generate assistant-style interactions" + ] + }, + "modelId": "meta-llama/llama-4-scout", + "mcpServers": {} + } + }, + { + "idName": "qwen2_5_vl_32b", + "metadata": { + "displayName": "Qwen: Qwen2.5 VL 32B Instruct", + "description": "Qwen2.5-VL-32B is a multimodal vision-language model fine-tuned through reinforcement learning for enhanced mathematical reasoning, structured outputs, and visual problem-solving. Excels at visual analysis, object recognition, textual interpretation within images, and precise event localization in extended videos.", + "tags": [ + "AI Model", + "Coding", + "Research" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Analyze visual data and images", + "Research scientific image content", + "Create coding solutions for vision tasks", + "Generate insights from visual media" + ] + }, + "modelId": "qwen/qwen2.5-vl-32b-instruct", + "mcpServers": {} + } + }, + { + "idName": "deepseek_v3_0324", + "metadata": { + "displayName": "DeepSeek: DeepSeek V3 0324", + "description": "DeepSeek V3, a 685B-parameter, mixture-of-experts model, is the latest iteration of the flagship chat model family from the DeepSeek team.\n\nIt succeeds the [DeepSeek V3](/deepseek/deepseek-chat-v3) model and performs really well on a variety of tasks.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/deepseek-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Engage in general conversation", + "Explain complex topics simply", + "Help with creative writing tasks", + "Provide thoughtful analysis" + ] + }, + "modelId": "deepseek/deepseek-chat-v3-0324", + "mcpServers": {} + } + }, + { + "idName": "gemma_3_4b_it", + "metadata": { + "displayName": "Google: Gemma 3 4B", + "description": "Gemma 3 introduces multimodality, supporting vision-language input and text outputs. It handles context windows up to 128k tokens, understands over 140 languages, and offers improved math, reasoning, and chat capabilities, including structured outputs and function calling.", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create multimodal applications with vision", + "Build function calling systems", + "Generate structured outputs", + "Develop multilingual tools" + ] + }, + "modelId": "google/gemma-3-4b-it", + "mcpServers": {} + } + }, + { + "idName": "gemma_3_12b_it", + "metadata": { + "displayName": "Google: Gemma 3 12B", + "description": "Gemma 3 introduces multimodality, supporting vision-language input and text outputs. It handles context windows up to 128k tokens, understands over 140 languages, and offers improved math, reasoning, and chat capabilities, including structured outputs and function calling. Gemma 3 12B is the second largest in the family of Gemma 3 models after [Gemma 3 27B](google/gemma-3-27b-it)", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create advanced multimodal tools", + "Build function calling applications", + "Generate structured multilingual content", + "Develop complex reasoning systems" + ] + }, + "modelId": "google/gemma-3-12b-it", + "mcpServers": {} + } + }, + { + "idName": "gemma_3_27b_it", + "metadata": { + "displayName": "Google: Gemma 3 27B", + "description": "Gemma 3 introduces multimodality, supporting vision-language input and text outputs. It handles context windows up to 128k tokens, understands over 140 languages, and offers improved math, reasoning, and chat capabilities, including structured outputs and function calling. Gemma 3 27B is Google's latest open source model, successor to [Gemma 2](google/gemma-2-27b-it)", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build sophisticated multimodal applications", + "Create advanced function calling systems", + "Generate comprehensive multilingual content", + "Develop enterprise-grade AI tools" + ] + }, + "modelId": "google/gemma-3-27b-it", + "mcpServers": {} + } + }, + { + "idName": "sonar_pro", + "metadata": { + "displayName": "Perplexity: Sonar Pro", + "description": "Note: Sonar Pro pricing includes Perplexity search pricing. See [details here](https://docs.perplexity.ai/guides/pricing#detailed-pricing-breakdown-for-sonar-reasoning-pro-and-sonar-pro)\n\nFor enterprises seeking more advanced capabilities, the Sonar Pro API can handle in-depth, multi-step queries with added extensibility, like double the number of citations per search as Sonar on average. Plus, with a larger context window, it can handle longer and more nuanced searches and follow-up questions. ", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/perplexity-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Research complex topics with web search", + "Create intelligent automation tools", + "Generate comprehensive reports", + "Build advanced query systems" + ] + }, + "modelId": "perplexity/sonar-pro", + "mcpServers": {} + } + }, + { + "idName": "sonar_deep_research", + "metadata": { + "displayName": "Perplexity: Sonar Deep Research", + "description": "Sonar Deep Research autonomously conducts multi-step research across complex topics, searching and evaluating sources to generate comprehensive reports. Excels in finance, technology, health, and current events analysis with advanced retrieval, synthesis, and reasoning capabilities.", + "tags": [ + "AI Model", + "Research" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/perplexity-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Conduct comprehensive market research", + "Analyze financial technology trends", + "Research health and medical topics", + "Generate detailed industry reports" + ] + }, + "modelId": "perplexity/sonar-deep-research", + "mcpServers": {} + } + }, + { + "idName": "qwq_32b", + "metadata": { + "displayName": "Qwen: QwQ 32B", + "description": "QwQ is the reasoning model of the Qwen series. Compared with conventional instruction-tuned models, QwQ, which is capable of thinking and reasoning, can achieve significantly enhanced performance in downstream tasks, especially hard problems. QwQ-32B is the medium-sized reasoning model, which is capable of achieving competitive performance against state-of-the-art reasoning models, e.g., DeepSeek-R1, o1-mini.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve challenging reasoning puzzles", + "Think through complex problems step-by-step", + "Analyze logical inconsistencies", + "Generate structured arguments" + ] + }, + "modelId": "qwen/qwq-32b", + "mcpServers": {} + } + }, + { + "idName": "gemini_2_0_lite", + "metadata": { + "displayName": "Google: Gemini 2.0 Flash Lite", + "description": "Gemini 2.0 Flash Lite offers a significantly faster time to first token (TTFT) compared to [Gemini Flash 1.5](/google/gemini-flash-1.5), while maintaining quality on par with larger models like [Gemini Pro 1.5](/google/gemini-pro-1.5), all at extremely economical token prices.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create fast, efficient applications", + "Build lightweight AI tools", + "Generate quick responses", + "Develop economical solutions" + ] + }, + "modelId": "google/gemini-2.0-flash-lite-001", + "mcpServers": {} + } + }, + { + "idName": "claude_3_7_sonnet", + "metadata": { + "displayName": "Anthropic: Claude 3.7 Sonnet", + "description": "Claude 3.7 Sonnet introduces hybrid reasoning with rapid responses and extended step-by-step processing for complex tasks. Notable improvements in coding, front-end development, and agentic workflows with autonomous multi-step navigation. Maintains performance parity in standard mode while offering enhanced accuracy in extended reasoning.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/claude-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build advanced coding applications", + "Create intelligent automation tools", + "Develop complex reasoning systems", + "Generate comprehensive solutions" + ] + }, + "modelId": "anthropic/claude-3.7-sonnet", + "mcpServers": {} + } + }, + { + "idName": "claude_3_7_thinking", + "metadata": { + "displayName": "Anthropic: Claude 3.7 Sonnet (thinking)", + "description": "Claude 3.7 Sonnet introduces hybrid reasoning with rapid responses and extended step-by-step processing for complex tasks. Notable improvements in coding, front-end development, and agentic workflows with autonomous multi-step navigation. Maintains performance parity in standard mode while offering enhanced accuracy in extended reasoning.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/claude-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Think through complex coding problems", + "Build sophisticated automation tools", + "Create detailed technical solutions", + "Develop step-by-step workflows" + ] + }, + "modelId": "anthropic/claude-3.7-sonnet:thinking", + "mcpServers": {} + } + }, + { + "idName": "claude_3_7_beta", + "metadata": { + "displayName": "Anthropic: Claude 3.7 Sonnet (self-moderated)", + "description": "Claude 3.7 Sonnet introduces hybrid reasoning with rapid responses and extended step-by-step processing for complex tasks. Notable improvements in coding, front-end development, and agentic workflows with autonomous multi-step navigation. Maintains performance parity in standard mode while offering enhanced accuracy in extended reasoning.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/claude-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create self-moderated content systems", + "Build advanced coding solutions", + "Develop intelligent automation tools", + "Generate comprehensive analyses" + ] + }, + "modelId": "anthropic/claude-3.7-sonnet:beta", + "mcpServers": {} + } + }, + { + "idName": "gemini_2_0_flash", + "metadata": { + "displayName": "Google: Gemini 2.0 Flash", + "description": "Gemini Flash 2.0 offers a significantly faster time to first token (TTFT) compared to [Gemini Flash 1.5](/google/gemini-flash-1.5), while maintaining quality on par with larger models like [Gemini Pro 1.5](/google/gemini-pro-1.5). It introduces notable enhancements in multimodal understanding, coding capabilities, complex instruction following, and function calling. These advancements come together to deliver more seamless and robust agentic experiences.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build multimodal coding applications", + "Create intelligent automation systems", + "Generate complex function calling tools", + "Develop advanced AI agents" + ] + }, + "modelId": "google/gemini-2.0-flash-001", + "mcpServers": {} + } + }, + { + "idName": "qwen2_5_vl_72b", + "metadata": { + "displayName": "Qwen: Qwen2.5 VL 72B Instruct", + "description": "Qwen2.5-VL is proficient in recognizing common objects such as flowers, birds, fish, and insects. It is also highly capable of analyzing texts, charts, icons, graphics, and layouts within images.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Analyze visual content and objects", + "Create image-based applications", + "Generate insights from charts and graphics", + "Build visual reasoning tools" + ] + }, + "modelId": "qwen/qwen2.5-vl-72b-instruct", + "mcpServers": {} + } + }, + { + "idName": "o3_mini", + "metadata": { + "displayName": "OpenAI: o3 Mini", + "description": "OpenAI o3-mini is a cost-efficient reasoning model optimized for STEM tasks, excelling in science, mathematics, and coding. Features adjustable reasoning effort levels (high/medium/low) and supports function calling, structured outputs, and streaming. Demonstrates 39% fewer errors than predecessors while matching o1 performance on AIME and GPQA at lower cost.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve complex STEM problems efficiently", + "Create advanced coding tools", + "Build structured output systems", + "Generate mathematical solutions" + ] + }, + "modelId": "openai/o3-mini", + "mcpServers": {} + } + }, + { + "idName": "deepseek_r1_qwen32b", + "metadata": { + "displayName": "DeepSeek: R1 Distill Qwen 32B", + "description": "DeepSeek R1 Distill Qwen 32B is a distilled large language model based on Qwen 2.5 32B, using outputs from DeepSeek R1. Outperforms OpenAI's o1-mini across various benchmarks with AIME 2024 pass@1: 72.6, MATH-500 pass@1: 94.3, and CodeForces Rating: 1691.", + "tags": [ + "AI Model", + "Coding" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/deepseek-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve advanced mathematical challenges", + "Create high-performance coding solutions", + "Build complex reasoning algorithms", + "Generate optimized system architectures" + ] + }, + "modelId": "deepseek/deepseek-r1-distill-qwen-32b", + "mcpServers": {} + } + }, + { + "idName": "perplexity_sonar", + "metadata": { + "displayName": "Perplexity: Sonar", + "description": "Sonar is lightweight, affordable, fast, and simple to use — now featuring citations and the ability to customize sources. It is designed for companies seeking to integrate lightweight question-and-answer features optimized for speed.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/perplexity-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Answer quick questions with sources", + "Search for current information", + "Create simple research summaries", + "Generate fast, cited responses" + ] + }, + "modelId": "perplexity/sonar", + "mcpServers": {} + } + }, + { + "idName": "lfm_7b", + "metadata": { + "displayName": "Liquid: LFM 7B", + "description": "LFM-7B, a new best-in-class language model. LFM-7B is designed for exceptional chat capabilities, including languages like Arabic and Japanese. Powered by the Liquid Foundation Model (LFM) architecture, it exhibits unique features like low memory footprint and fast inference speed. \n\nLFM-7B is the world’s best-in-class multilingual language model in English, Arabic, and Japanese.\n\nSee the [launch announcement](https://www.liquid.ai/lfm-7b) for benchmarks and more info.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/liquid.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create multilingual conversations", + "Build efficient dialogue systems", + "Generate culturally-aware content", + "Develop fast response applications" + ] + }, + "modelId": "liquid/lfm-7b", + "mcpServers": {} + } + }, + { + "idName": "lfm_3b", + "metadata": { + "displayName": "Liquid: LFM 3B", + "description": "Liquid's LFM 3B delivers incredible performance for its size. It positions itself as first place among 3B parameter transformers, hybrids, and RNN models It is also on par with Phi-3.5-mini on multiple benchmarks, while being 18.4% smaller.\n\nLFM-3B is the ideal choice for mobile and other edge text-based applications.\n\nSee the [launch announcement](https://www.liquid.ai/liquid-foundation-models) for benchmarks and more info.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/liquid.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build lightweight mobile applications", + "Create efficient edge computing tools", + "Generate fast text processing systems", + "Develop compact AI solutions" + ] + }, + "modelId": "liquid/lfm-3b", + "mcpServers": {} + } + }, + { + "idName": "deepseek_r1_llama70b", + "metadata": { + "displayName": "DeepSeek: R1 Distill Llama 70B", + "description": "DeepSeek R1 Distill Llama 70B is a distilled large language model based on Llama-3.3-70B-Instruct, using outputs from DeepSeek R1. Achieves high performance with AIME 2024 pass@1: 70.0, MATH-500 pass@1: 94.5, and CodeForces Rating: 1633 through advanced distillation techniques.", + "tags": [ + "AI Model", + "Coding" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/deepseek-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve advanced mathematical problems", + "Create high-performance coding solutions", + "Build complex algorithmic systems", + "Generate optimized data structures" + ] + }, + "modelId": "deepseek/deepseek-r1-distill-llama-70b", + "mcpServers": {} + } + }, + { + "idName": "deepseek_r1", + "metadata": { + "displayName": "DeepSeek: R1", + "description": "DeepSeek R1 is here: Performance on par with [OpenAI o1](/openai/o1), but open-sourced and with fully open reasoning tokens. It's 671B parameters in size, with 37B active in an inference pass.\n\nFully open-source model & [technical report](https://api-docs.deepseek.com/news/news250120).\n\nMIT licensed: Distill & commercialize freely!", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/deepseek-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Reason through complex problems openly", + "Create advanced AI tools with transparency", + "Build open-source reasoning systems", + "Generate detailed problem-solving steps" + ] + }, + "modelId": "deepseek/deepseek-r1", + "mcpServers": {} + } + }, + { + "idName": "minimax_01", + "metadata": { + "displayName": "MiniMax: MiniMax-01", + "description": "MiniMax-01 combines MiniMax-Text-01 for text generation and MiniMax-VL-01 for image understanding. Has 456 billion parameters with 45.9 billion activated per inference and 4 million token context. Uses hybrid architecture combining Lightning Attention, Softmax Attention, and MoE.", + "tags": [ + "AI Model", + "Content Writing" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/minimax-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create innovative content with unique voice", + "Write compelling marketing campaigns", + "Generate creative multimedia content", + "Build engaging storytelling tools" + ] + }, + "modelId": "minimax/minimax-01", + "mcpServers": {} + } + }, + { + "idName": "ms_phi_4", + "metadata": { + "displayName": "Microsoft: Phi 4", + "description": "Microsoft Research Phi-4 is designed for complex reasoning tasks and efficient operation in limited memory situations. At 14 billion parameters, it was trained on high-quality synthetic datasets and academic materials with careful instruction tuning and strong safety standards. Works best with English inputs.", + "tags": [ + "AI Model", + "Research" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/microsoft-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Solve complex reasoning problems", + "Research academic topics thoroughly", + "Create detailed analytical reports", + "Build efficient memory-based systems" + ] + }, + "modelId": "microsoft/phi-4", + "mcpServers": {} + } + }, + { + "idName": "deepseek_chat", + "metadata": { + "displayName": "DeepSeek: DeepSeek V3", + "description": "DeepSeek-V3 is the latest model from DeepSeek, building upon instruction following and coding abilities of previous versions. Pre-trained on nearly 15 trillion tokens, it outperforms other open-source models and rivals leading closed-source models in reported evaluations.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/deepseek-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build advanced coding applications", + "Create intelligent automation tools", + "Develop comprehensive AI systems", + "Generate structured technical solutions" + ] + }, + "modelId": "deepseek/deepseek-chat", + "mcpServers": {} + } + }, + { + "idName": "llama_3_3_70b_inst", + "metadata": { + "displayName": "Meta: Llama 3.3 70B Instruct", + "description": "Meta Llama 3.3 70B is a pretrained and instruction-tuned multilingual LLM optimized for dialogue use cases. Outperforms many available open source and closed chat models on industry benchmarks. Supports English, German, French, Italian, Portuguese, Hindi, Spanish, and Thai.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/metaai-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Engage in multilingual conversations", + "Create diverse dialogue interactions", + "Generate helpful responses", + "Build conversational applications" + ] + }, + "modelId": "meta-llama/llama-3.3-70b-instruct", + "mcpServers": {} + } + }, + { + "idName": "gpt_4o_2024_11_20", + "metadata": { + "displayName": "OpenAI: GPT-4o (2024-11-20)", + "description": "The 2024-11-20 version of GPT-4o offers leveled-up creative writing with more natural, engaging, and tailored writing for improved relevance and readability. Better at working with uploaded files, providing deeper insights and more thorough responses while maintaining GPT-4 Turbo intelligence at improved speed and cost.", + "tags": [ + "AI Model", + "Content Writing" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Write engaging creative content", + "Create detailed narratives", + "Generate compelling storytelling", + "Build content creation tools" + ] + }, + "modelId": "openai/gpt-4o-2024-11-20", + "mcpServers": {} + } + }, + { + "idName": "qwen_2_5_coder_32b", + "metadata": { + "displayName": "Qwen2.5 Coder 32B Instruct", + "description": "Qwen2.5-Coder is the latest Code-Specific Qwen model with significant improvements in code generation, code reasoning, and code fixing. Enhanced foundation for real-world applications like Code Agents while maintaining strengths in mathematics and general competencies for comprehensive coding support.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build comprehensive coding tools", + "Create intelligent code agents", + "Generate automated testing frameworks", + "Fix complex programming bugs" + ] + }, + "modelId": "qwen/qwen-2.5-coder-32b-instruct", + "mcpServers": {} + } + }, + { + "idName": "claude_3_5_haiku", + "metadata": { + "displayName": "Anthropic: Claude 3.5 Haiku", + "description": "Claude 3.5 Haiku offers enhanced capabilities in speed, coding accuracy, and tool use. Engineered for real-time applications with quick response times essential for dynamic tasks like chat interactions and immediate coding suggestions. Highly suitable for software development and customer service bots.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/claude-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build fast coding applications", + "Create real-time AI assistants", + "Generate quick automated responses", + "Develop efficient development tools" + ] + }, + "modelId": "anthropic/claude-3.5-haiku", + "mcpServers": {} + } + }, + { + "idName": "claude_3_5_haiku_10", + "metadata": { + "displayName": "Anthropic: Claude 3.5 Haiku (2024-10-22)", + "description": "Claude 3.5 Haiku features enhancements across coding, tool use, and reasoning. As the fastest model in Anthropic's lineup, it offers rapid response times for high interactivity and low latency applications like chatbots and code completions. Excels in data extraction and real-time content moderation. No image input support.", + "tags": [ + "AI Model", + "Coding", + "Content Writing", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/claude-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create rapid coding solutions", + "Build real-time chatbots", + "Generate quick content moderation tools", + "Write efficient technical content" + ] + }, + "modelId": "anthropic/claude-3.5-haiku-20241022", + "mcpServers": {} + } + }, + { + "idName": "claude_3_5_sonnet", + "metadata": { + "displayName": "Anthropic: Claude 3.5 Sonnet", + "description": "Claude 3.5 Sonnet delivers better-than-Opus capabilities at faster-than-Sonnet speeds. Particularly strong in coding (49% on SWE-Bench Verified), data science with unstructured data navigation, visual processing for charts and graphs, and agentic tasks requiring complex multi-step problem solving with tool use.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/claude-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build advanced coding solutions", + "Create intelligent automation tools", + "Generate complex data analysis systems", + "Develop sophisticated AI agents" + ] + }, + "modelId": "anthropic/claude-3.5-sonnet", + "mcpServers": {} + } + }, + { + "idName": "qwen_2_5_7b_inst", + "metadata": { + "displayName": "Qwen2.5 7B Instruct", + "description": "Qwen2.5 7B offers enhanced coding, mathematics, and instruction following capabilities with 128K context support. Features improved long text generation (8K+ tokens), structured data understanding, JSON output generation, and multilingual support for 29+ languages including Chinese, English, and European languages.", + "tags": [ + "AI Model", + "Coding" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create multilingual coding applications", + "Build structured data processing tools", + "Generate JSON output systems", + "Develop comprehensive AI assistants" + ] + }, + "modelId": "qwen/qwen-2.5-7b-instruct", + "mcpServers": {} + } + }, + { + "idName": "gemini_1_5_8b", + "metadata": { + "displayName": "Google: Gemini 1.5 Flash 8B", + "description": "Gemini Flash 1.5 8B is optimized for speed and efficiency, offering enhanced performance in small prompt tasks like chat, transcription, and translation. With reduced latency, it is highly effective for real-time and large-scale operations while maintaining high-quality results.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create fast, efficient applications", + "Build real-time chat systems", + "Generate quick translations", + "Develop lightweight AI tools" + ] + }, + "modelId": "google/gemini-flash-1.5-8b", + "mcpServers": {} + } + }, + { + "idName": "llama_3_2_3b_inst", + "metadata": { + "displayName": "Meta: Llama 3.2 3B Instruct", + "description": "Llama 3.2 3B is a multilingual large language model optimized for dialogue generation, reasoning, and summarization. Supports eight languages including English, Spanish, and Hindi. Trained on 9 trillion tokens, it excels in instruction-following, complex reasoning, and tool use with balanced accuracy and efficiency.", + "tags": [ + "AI Model", + "Content Writing", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/metaai-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create multilingual content", + "Generate comprehensive summaries", + "Build intelligent dialogue systems", + "Use tools for enhanced interactions" + ] + }, + "modelId": "meta-llama/llama-3.2-3b-instruct", + "mcpServers": {} + } + }, + { + "idName": "qwen_2_5_72b_inst", + "metadata": { + "displayName": "Qwen2.5 72B Instruct", + "description": "Qwen2.5 72B offers enhanced coding, mathematics, and instruction following capabilities with 128K context support. Features improved long text generation (8K+ tokens), structured data understanding, JSON output generation, and multilingual support for 29+ languages including Chinese, English, and European languages.", + "tags": [ + "AI Model", + "Coding" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/qwen-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build large-scale coding applications", + "Create multilingual content systems", + "Generate structured data outputs", + "Develop comprehensive AI solutions" + ] + }, + "modelId": "qwen/qwen-2.5-72b-instruct", + "mcpServers": {} + } + }, + { + "idName": "hermes_3_llama_70b", + "metadata": { + "displayName": "Nous: Hermes 3 70B Instruct", + "description": "Hermes 3 70B is a competitive finetune of Llama-3.1 70B with advanced agentic capabilities, improved roleplaying, reasoning, multi-turn conversation, and long context coherence. Features powerful steering capabilities, reliable function calling, structured output capabilities, and improved code generation skills.", + "tags": [ + "AI Model", + "Coding", + "Research", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/nousresearch.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build advanced coding applications", + "Research complex technical topics", + "Create intelligent automation tools", + "Generate structured function calls" + ] + }, + "modelId": "nousresearch/hermes-3-llama-3.1-70b", + "mcpServers": {} + } + }, + { + "idName": "hermes_3_llama_405b", + "metadata": { + "displayName": "Nous: Hermes 3 405B Instruct", + "description": "Hermes 3 405B is a frontier-level, full-parameter finetune of Llama-3.1 405B focused on aligning LLMs to users with powerful steering capabilities. Features advanced agentic capabilities, improved roleplaying, reasoning, multi-turn conversation, and enhanced function calling and structured output capabilities.", + "tags": [ + "AI Model", + "Coding", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/nousresearch.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build enterprise-level AI systems", + "Create advanced coding solutions", + "Generate complex automation tools", + "Develop sophisticated AI agents" + ] + }, + "modelId": "nousresearch/hermes-3-llama-3.1-405b", + "mcpServers": {} + } + }, + { + "idName": "chatgpt_4o_latest", + "metadata": { + "displayName": "OpenAI: ChatGPT-4o", + "description": "OpenAI ChatGPT 4o is continually updated by OpenAI to point to the current version of GPT-4o used by ChatGPT. It therefore differs slightly from the API version of [GPT-4o](/models/openai/gpt-4o) in that it has additional RLHF. It is intended for research and evaluation.\n\nOpenAI notes that this model is not suited for production use-cases as it may be removed or redirected to another model in the future.", + "tags": [ + "AI Model", + "Research", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Research cutting-edge topics", + "Create comprehensive analysis tools", + "Generate detailed technical reports", + "Build advanced reasoning systems" + ] + }, + "modelId": "openai/chatgpt-4o-latest", + "mcpServers": {} + } + }, + { + "idName": "l3_lunaris_8b", + "metadata": { + "displayName": "Sao10K: Llama 3 8B Lunaris", + "description": "Lunaris 8B is a versatile generalist and roleplaying model based on Llama 3. It's a strategic merge of multiple models, designed to balance creativity with improved logic and general knowledge.\n\nCreated by [Sao10k](https://huggingface.co/Sao10k), this model aims to offer an improved experience over Stheno v3.2, with enhanced creativity and logical reasoning.\n\nFor best results, use with Llama 3 Instruct context template, temperature 1.4, and min_p 0.1.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/sao10k-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create versatile applications", + "Generate balanced creative content", + "Build logical reasoning systems", + "Develop general-purpose AI tools" + ] + }, + "modelId": "sao10k/l3-lunaris-8b", + "mcpServers": {} + } + }, + { + "idName": "gpt_4o_2024_08_06", + "metadata": { + "displayName": "OpenAI: GPT-4o (2024-08-06)", + "description": "The 2024-08-06 version of GPT-4o offers improved structured outputs with JSON schema support in response_format. Maintains GPT-4 Turbo intelligence while being twice as fast and 50% more cost-effective. Enhanced performance in non-English languages and improved visual capabilities for multimodal tasks.", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Generate structured JSON outputs", + "Create intelligent automation tools", + "Build enhanced API integrations", + "Develop multimodal applications" + ] + }, + "modelId": "openai/gpt-4o-2024-08-06", + "mcpServers": {} + } + }, + { + "idName": "llama_3_1_8b_inst", + "metadata": { + "displayName": "Meta: Llama 3.1 8B Instruct", + "description": "Meta's latest class of model (Llama 3.1) launched with a variety of sizes & flavors. This 8B instruct-tuned version is fast and efficient.\n\nIt has demonstrated strong performance compared to leading closed-source models in human evaluations.\n\nTo read more about the model release, [click here](https://ai.meta.com/blog/meta-llama-3-1/). Usage of this model is subject to [Meta's Acceptable Use Policy](https://llama.meta.com/llama3/use-policy/).", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/metaai-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create efficient dialogue systems", + "Generate helpful responses", + "Build general-purpose applications", + "Develop conversational AI tools" + ] + }, + "modelId": "meta-llama/llama-3.1-8b-instruct", + "mcpServers": {} + } + }, + { + "idName": "llama_3_1_405b_inst", + "metadata": { + "displayName": "Meta: Llama 3.1 405B Instruct", + "description": "Llama 3.1 405B is Meta's highly anticipated 400B class model with 128k context and impressive eval scores. This instruct-tuned version is optimized for high quality dialogue use cases, demonstrating strong performance compared to leading closed-source models including GPT-4o and Claude 3.5 Sonnet.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/metaai-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build large-scale AI applications", + "Create advanced reasoning systems", + "Generate comprehensive solutions", + "Develop enterprise-grade tools" + ] + }, + "modelId": "meta-llama/llama-3.1-405b-instruct", + "mcpServers": {} + } + }, + { + "idName": "llama_3_1_70b_inst", + "metadata": { + "displayName": "Meta: Llama 3.1 70B Instruct", + "description": "Meta's latest class of model (Llama 3.1) launched with a variety of sizes & flavors. This 70B instruct-tuned version is optimized for high quality dialogue usecases.\n\nIt has demonstrated strong performance compared to leading closed-source models in human evaluations.\n\nTo read more about the model release, [click here](https://ai.meta.com/blog/meta-llama-3-1/). Usage of this model is subject to [Meta's Acceptable Use Policy](https://llama.meta.com/llama3/use-policy/).", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/metaai-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create high-quality dialogue systems", + "Build comprehensive AI applications", + "Generate detailed responses", + "Develop advanced conversation tools" + ] + }, + "modelId": "meta-llama/llama-3.1-70b-instruct", + "mcpServers": {} + } + }, + { + "idName": "gpt_4o_mini", + "metadata": { + "displayName": "OpenAI: GPT-4o-mini", + "description": "GPT-4o mini is OpenAI's newest small model after GPT-4 Omni, supporting text and image inputs with text outputs. More affordable than frontier models and 60% cheaper than GPT-3.5 Turbo while maintaining SOTA intelligence. Achieves 82% on MMLU and ranks higher than GPT-4 on chat preferences.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create affordable AI applications", + "Generate quick intelligent responses", + "Build cost-effective solutions", + "Develop efficient chat systems" + ] + }, + "modelId": "openai/gpt-4o-mini", + "mcpServers": {} + } + }, + { + "idName": "gpt_4o_mini_240718", + "metadata": { + "displayName": "OpenAI: GPT-4o-mini (2024-07-18)", + "description": "GPT-4o mini is OpenAI's newest small model after GPT-4 Omni, supporting text and image inputs with text outputs. More affordable than frontier models and 60% cheaper than GPT-3.5 Turbo while maintaining SOTA intelligence. Achieves 82% on MMLU and ranks higher than GPT-4 on chat preferences.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create efficient AI applications", + "Generate cost-effective solutions", + "Build reliable chat systems", + "Develop multimodal tools" + ] + }, + "modelId": "openai/gpt-4o-mini-2024-07-18", + "mcpServers": {} + } + }, + { + "idName": "hermes_2_pro_llama3", + "metadata": { + "displayName": "NousResearch: Hermes 2 Pro - Llama-3 8B", + "description": "Hermes 2 Pro is an upgraded, retrained version of Nous Hermes 2, consisting of an updated and cleaned version of the OpenHermes 2.5 Dataset, as well as a newly introduced Function Calling and JSON Mode dataset developed in-house.", + "tags": [ + "AI Model", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/nousresearch.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create function calling systems", + "Build JSON mode applications", + "Generate structured outputs", + "Develop intelligent automation tools" + ] + }, + "modelId": "nousresearch/hermes-2-pro-llama-3-8b", + "mcpServers": {} + } + }, + { + "idName": "gemini_1_5_flash", + "metadata": { + "displayName": "Google: Gemini 1.5 Flash ", + "description": "Gemini 1.5 Flash performs well at multimodal tasks including visual understanding, classification, summarization, and content creation from image, audio and video. Designed for high-volume, high-frequency tasks where cost and latency matter, achieving comparable quality to other Gemini Pro models at reduced cost.", + "tags": [ + "AI Model", + "Content Writing" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create engaging content with visuals", + "Write compelling narratives", + "Generate creative marketing materials", + "Build multimodal content systems" + ] + }, + "modelId": "google/gemini-flash-1.5", + "mcpServers": {} + } + }, + { + "idName": "gpt_4o", + "metadata": { + "displayName": "OpenAI: GPT-4o", + "description": "GPT-4o (omni) is OpenAI's latest AI model, supporting both text and image inputs with text outputs. Maintains GPT-4 Turbo intelligence while being twice as fast and 50% more cost-effective. Offers improved performance in processing non-English languages and enhanced visual capabilities.", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create intelligent applications", + "Generate helpful responses", + "Build conversational systems", + "Develop multimodal solutions" + ] + }, + "modelId": "openai/gpt-4o", + "mcpServers": {} + } + }, + { + "idName": "llama_3_8b_inst", + "metadata": { + "displayName": "Meta: Llama 3 8B Instruct", + "description": "Meta's latest class of model (Llama 3) launched with a variety of sizes & flavors. This 8B instruct-tuned version was optimized for high quality dialogue usecases.\n\nIt has demonstrated strong performance compared to leading closed-source models in human evaluations.\n\nTo read more about the model release, [click here](https://ai.meta.com/blog/meta-llama-3/). Usage of this model is subject to [Meta's Acceptable Use Policy](https://llama.meta.com/llama3/use-policy/).", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/metaai-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create dialogue systems", + "Generate helpful responses", + "Build conversational applications", + "Develop interactive AI tools" + ] + }, + "modelId": "meta-llama/llama-3-8b-instruct", + "mcpServers": {} + } + }, + { + "idName": "wizardlm_2_8x22b", + "metadata": { + "displayName": "WizardLM-2 8x22B", + "description": "WizardLM-2 8x22B is Microsoft AI's most advanced Wizard model. It demonstrates highly competitive performance compared to leading proprietary models, and it consistently outperforms all existing state-of-the-art opensource models.\n\nIt is an instruct finetune of [Mixtral 8x22B](/models/mistralai/mixtral-8x22b).\n\nTo read more about the model release, [click here](https://wizardlm.github.io/WizardLM2/).\n\n#moe", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/microsoft-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create comprehensive AI solutions", + "Generate competitive analyses", + "Build advanced reasoning systems", + "Develop enterprise applications" + ] + }, + "modelId": "microsoft/wizardlm-2-8x22b", + "mcpServers": {} + } + }, + { + "idName": "gemini_pro_1_5", + "metadata": { + "displayName": "Google: Gemini 1.5 Pro", + "description": "Google's latest multimodal model, supports image and video[0] in text or chat prompts.\n\nOptimized for language tasks including:\n\n- Code generation\n- Text generation\n- Text editing\n- Problem solving\n- Recommendations\n- Information extraction\n- Data extraction or generation\n- AI agents\n\nUsage of Gemini is subject to Google's [Gemini Terms of Use](https://ai.google.dev/terms).\n\n* [0]: Video input is not available through OpenRouter at this time.", + "tags": [ + "AI Model", + "Coding", + "Content Writing", + "Tools" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/1.61.0/files/light/google-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Build comprehensive coding solutions", + "Create content generation tools", + "Generate data extraction systems", + "Develop intelligent AI agents" + ] + }, + "modelId": "google/gemini-pro-1.5", + "mcpServers": {} + } + }, + { + "idName": "claude_3_haiku", + "metadata": { + "displayName": "Anthropic: Claude 3 Haiku", + "description": "Claude 3 Haiku is Anthropic's fastest and most compact model for\nnear-instant responsiveness. Quick and accurate targeted performance.\n\nSee the launch announcement and benchmark results [here](https://www.anthropic.com/news/claude-3-haiku)\n\n#multimodal", + "tags": [ + "AI Model", + "Others" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/claude-color.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create fast responsive applications", + "Generate quick helpful responses", + "Build efficient chat systems", + "Develop compact AI solutions" + ] + }, + "modelId": "anthropic/claude-3-haiku", + "mcpServers": {} + } + }, + { + "idName": "gpt_3_5_turbo", + "metadata": { + "displayName": "OpenAI: GPT-3.5 Turbo", + "description": "GPT-3.5 Turbo is OpenAI's fastest model. It can understand and generate natural language or code, and is optimized for chat and traditional completion tasks.\n\nTraining data up to Sep 2021.", + "tags": [ + "AI Model", + "Coding" + ], + "thumbnail": { + "type": "url", + "url": "https://registry.npmmirror.com/@lobehub/icons-static-png/latest/files/light/openai.png" + } + }, + "core": { + "prompt": { + "value": "", + "suggestions": [ + "Create fast coding solutions", + "Generate natural language responses", + "Build efficient chat completions", + "Develop cost-effective AI tools" + ] + }, + "modelId": "openai/gpt-3.5-turbo", + "mcpServers": {} + } + } +] \ No newline at end of file diff --git a/package.json b/package.json index 3e63ca1d..df8fd550 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "mermaid": "^11.8.0", "radix-ui": "^1.4.3", "react": "^19.1.0", - "react-day-picker": "^9.7.0", + "react-day-picker": "^9.8.1", "react-dom": "^19.1.0", "react-hook-form": "^7.58.1", "react-medium-image-zoom": "^5.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2fece84..2dbb24d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,8 +201,8 @@ importers: specifier: ^19.1.0 version: 19.1.0 react-day-picker: - specifier: ^9.7.0 - version: 9.7.0(react@19.1.0) + specifier: ^9.8.1 + version: 9.8.1(react@19.1.0) react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) @@ -5884,8 +5884,8 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - react-day-picker@9.7.0: - resolution: {integrity: sha512-urlK4C9XJZVpQ81tmVgd2O7lZ0VQldZeHzNejbwLWZSkzHH498KnArT0EHNfKBOWwKc935iMLGZdxXPRISzUxQ==} + react-day-picker@9.8.1: + resolution: {integrity: sha512-kMcLrp3PfN/asVJayVv82IjF3iLOOxuH5TNFWezX6lS/T8iVRFPTETpHl3TUSTH99IDMZLubdNPJr++rQctkEw==} engines: {node: '>=18'} peerDependencies: react: '>=16.8.0' @@ -15348,7 +15348,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - react-day-picker@9.7.0(react@19.1.0): + react-day-picker@9.8.1(react@19.1.0): dependencies: '@date-fns/tz': 1.2.0 date-fns: 4.1.0 diff --git a/sample-caps-simplified.json b/sample-caps-simplified.json deleted file mode 100644 index f1e10be1..00000000 --- a/sample-caps-simplified.json +++ /dev/null @@ -1,88 +0,0 @@ -[ - { - "idName": "code-assistant", - "metadata": { - "displayName": "Code Assistant", - "description": "A helpful assistant specialized in code review and programming guidance", - "tags": [ - "coding", - "programming", - "assistant" - ], - "thumbnail": { - "type": "url", - "url": "https://example.com/thumbnail.png" - } - }, - "core": { - "prompt": { - "value": "You are a helpful programming assistant. Help users with code review, debugging, and programming best practices. Always provide clear explanations and examples.", - "suggestions": [] - }, - "modelId": "openai/gpt-4", - "mcpServers": {} - } - }, - { - "idName": "writing-assistant", - "metadata": { - "displayName": "Writing Assistant", - "description": "A creative writing assistant that helps with content creation and editing", - "tags": [ - "writing", - "creative", - "content" - ], - "thumbnail": { - "type": "url", - "url": "https://example.com/writing-thumbnail.png" - } - }, - "core": { - "prompt": { - "value": "You are a creative writing assistant. Help users with content creation, editing, proofreading, and improving their writing style. Provide constructive feedback and suggestions.", - "suggestions": [] - }, - "modelId": "openai/gpt-4o-mini", - "mcpServers": { - "writing-tools": { - "url": "npm:@writing-tools/mcp-server", - "transport": "httpStream" - } - } - } - }, - { - "idName": "data-analyst", - "metadata": { - "displayName": "Data Analyst", - "description": "An AI assistant specialized in data analysis and visualization", - "tags": [ - "data", - "analytics", - "visualization" - ], - "thumbnail": { - "type": "url", - "url": "https://example.com/data-thumbnail.png" - } - }, - "core": { - "prompt": { - "value": "You are a data analyst assistant. Help users analyze data, create visualizations, and derive insights. Provide clear explanations of statistical concepts and data interpretation.", - "suggestions": [] - }, - "modelId": "openai/gpt-4", - "mcpServers": { - "pandas-tools": { - "url": "npm:@data-tools/pandas-mcp", - "transport": "httpStream" - }, - "visualization-tools": { - "url": "npm:@viz-tools/mcp-server", - "transport": "httpStream" - } - } - } - } -] \ No newline at end of file diff --git a/src/features/cap-store/hooks/use-remote-cap.ts b/src/features/cap-store/hooks/use-remote-cap.ts index 3d444ef9..98aaf554 100644 --- a/src/features/cap-store/hooks/use-remote-cap.ts +++ b/src/features/cap-store/hooks/use-remote-cap.ts @@ -47,24 +47,28 @@ export function useRemoteCap() { ); const remoteCaps: RemoteCap[] = - response.data?.items?.map((item) => { - return { - cid: item.cid, - version: item.version, - id: item.id, - idName: item.name, - authorDID: item.id.split(':')[0], - metadata: { - displayName: item.displayName, - description: item.description, - tags: item.tags, - repository: item.repository, - homepage: item.homepage, - submittedAt: item.submittedAt, - thumbnail: item.thumbnail, - }, - }; - }) || []; + response.data?.items + ?.filter((item) => { + return item.displayName !== 'nuwa_test'; + }) + .map((item) => { + return { + cid: item.cid, + version: item.version, + id: item.id, + idName: item.name, + authorDID: item.id.split(':')[0], + metadata: { + displayName: item.displayName, + description: item.description, + tags: item.tags, + repository: item.repository, + homepage: item.homepage, + submittedAt: item.submittedAt, + thumbnail: item.thumbnail, + }, + }; + }) || []; setRemoteCaps(remoteCaps); diff --git a/src/features/cap-studio/components/batch-create.tsx b/src/features/cap-studio/components/batch-create.tsx index 41a2ebc7..310e81eb 100644 --- a/src/features/cap-studio/components/batch-create.tsx +++ b/src/features/cap-studio/components/batch-create.tsx @@ -160,12 +160,6 @@ export function BatchCreate({ onBatchCreate }: BatchCreateProps) { throw new Error('Missing prompt in core'); } - if ( - !capData.core.prompt.value || - typeof capData.core.prompt.value !== 'string' - ) { - throw new Error('Missing prompt value'); - } if (!capData.core.modelId || typeof capData.core.modelId !== 'string') { throw new Error('Missing modelId in core'); diff --git a/src/features/cap-studio/components/my-caps/index.tsx b/src/features/cap-studio/components/my-caps/index.tsx index c964cee2..231935a8 100644 --- a/src/features/cap-studio/components/my-caps/index.tsx +++ b/src/features/cap-studio/components/my-caps/index.tsx @@ -8,9 +8,11 @@ import { CardHeader, CardTitle, Input, + Progress, } from '@/shared/components/ui'; import { useLocalCaps } from '../../hooks'; import { useLocalCapsHandler } from '../../hooks/use-local-caps-handler'; +import { useSubmitCap } from '../../hooks/use-submit-cap'; import type { LocalCap } from '../../types'; import { DashboardGrid } from '../layout/dashboard-layout'; import { CapCard } from './cap-card'; @@ -20,9 +22,7 @@ interface MyCapsProps { onTestCap?: (cap: LocalCap) => void; onSubmitCap?: (cap: LocalCap) => void; onCreateNew?: () => void; - onBatchCreate?: (caps: LocalCap[]) => void; onBulkDelete?: (caps: LocalCap[]) => void; - onBulkPublish?: (caps: LocalCap[]) => void; } export function MyCaps({ @@ -30,14 +30,13 @@ export function MyCaps({ onTestCap, onSubmitCap, onCreateNew, - onBatchCreate, onBulkDelete, - onBulkPublish, }: MyCapsProps) { const navigate = useNavigate(); const localCaps = useLocalCaps(); const [searchQuery, setSearchQuery] = useState(''); - const { createCap, deleteCap } = useLocalCapsHandler(); + const { deleteCap } = useLocalCapsHandler(); + const { bulkSubmitCaps, bulkProgress } = useSubmitCap(); // Multi-select state const [selectedCapIds, setSelectedCapIds] = useState>(new Set()); @@ -78,17 +77,14 @@ export function MyCaps({ clearSelection(); }; - const handleBulkPublish = () => { + const handleBulkPublish = async () => { const selectedCaps = allCaps.filter((cap) => selectedCapIds.has(cap.id)); - selectedCaps.forEach((cap) => { - if (onSubmitCap) { - onSubmitCap(cap); - } - }); - if (onBulkPublish) { - onBulkPublish(selectedCaps); + try { + await bulkSubmitCaps(selectedCaps); + clearSelection(); + } catch (error) { + console.error('Bulk publish failed:', error); } - clearSelection(); }; // Get and filter all caps @@ -144,6 +140,51 @@ export function MyCaps({ return (
+ {/* Bulk submission progress bar */} + {bulkProgress.isSubmitting && ( + + +
+
+ + Publishing Caps ({bulkProgress.completed}/{bulkProgress.total}) + + {bulkProgress.currentCap && ( + + Currently publishing: {bulkProgress.currentCap} + + )} +
+
+ +
+
+ )} + + {/* Error display */} + {bulkProgress.errors.length > 0 && !bulkProgress.isSubmitting && ( + + + + Bulk Publish Errors ({bulkProgress.errors.length}) + +
+ {bulkProgress.errors.map((error) => ( +
+ {error.capName}: {error.error} +
+ ))} +
+
+
+ )} + {/* Header with controls */}
@@ -191,9 +232,13 @@ export function MyCaps({ onClick={handleBulkPublish} variant="default" size="sm" + disabled={bulkProgress.isSubmitting} > - Publish ({selectedCaps.length}) + {bulkProgress.isSubmitting + ? `Publishing... (${bulkProgress.completed}/${bulkProgress.total})` + : `Publish (${selectedCaps.length})` + } )} diff --git a/src/features/cap-studio/hooks/use-submit-cap.ts b/src/features/cap-studio/hooks/use-submit-cap.ts index 81246efd..6009e0a9 100644 --- a/src/features/cap-studio/hooks/use-submit-cap.ts +++ b/src/features/cap-studio/hooks/use-submit-cap.ts @@ -1,6 +1,7 @@ -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { useCapKit } from '@/shared/hooks/use-capkit'; import type { Cap } from '@/shared/types/cap'; +import type { LocalCap } from '../types'; interface CapSubmitResponse { success: boolean; @@ -9,8 +10,22 @@ interface CapSubmitResponse { errors?: string[]; } +interface BulkSubmitProgress { + total: number; + completed: number; + currentCap?: string; + isSubmitting: boolean; + errors: Array<{ capName: string; error: string }>; +} + export const useSubmitCap = () => { const { capKit } = useCapKit(); + const [bulkProgress, setBulkProgress] = useState({ + total: 0, + completed: 0, + isSubmitting: false, + errors: [], + }); const submitCap = useCallback( async (capData: Cap): Promise => { @@ -42,7 +57,59 @@ export const useSubmitCap = () => { [capKit], ); + const bulkSubmitCaps = useCallback( + async (caps: LocalCap[]): Promise => { + if (!capKit) { + throw new Error('Failed to initialize CapKit'); + } + + setBulkProgress({ + total: caps.length, + completed: 0, + isSubmitting: true, + errors: [], + }); + + const errors: Array<{ capName: string; error: string }> = []; + + for (let i = 0; i < caps.length; i++) { + const cap = caps[i]; + const displayName = cap.capData.metadata.displayName; + + setBulkProgress(prev => ({ + ...prev, + currentCap: displayName, + })); + + try { + await capKit.registerCap(cap.capData); + setBulkProgress(prev => ({ + ...prev, + completed: prev.completed + 1, + })); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to submit capability'; + errors.push({ capName: displayName, error: errorMessage }); + setBulkProgress(prev => ({ + ...prev, + completed: prev.completed + 1, + errors: [...prev.errors, { capName: displayName, error: errorMessage }], + })); + } + } + + setBulkProgress(prev => ({ + ...prev, + isSubmitting: false, + currentCap: undefined, + })); + }, + [capKit], + ); + return { submitCap, + bulkSubmitCaps, + bulkProgress, }; }; diff --git a/src/features/cap-studio/services.ts b/src/features/cap-studio/services.ts index 513dd669..ab4c7a84 100644 --- a/src/features/cap-studio/services.ts +++ b/src/features/cap-studio/services.ts @@ -7,8 +7,7 @@ import type { async function modelFetch(): Promise { const authorizedFetch = createAuthorizedFetch(); - const endpoint = 'https://test-llm.nuwa.dev/api/v1/models'; - // const endpoint = 'https://openrouter.ai/api/v1/models'; + const endpoint = 'https://openrouter.ai/api/v1/models'; try { const response = await authorizedFetch(endpoint, { diff --git a/src/features/wallet/components/chat-item.tsx b/src/features/wallet/components/chat-item.tsx index 77d3659d..ff1897b7 100644 --- a/src/features/wallet/components/chat-item.tsx +++ b/src/features/wallet/components/chat-item.tsx @@ -18,6 +18,10 @@ const formatCost = (cost: bigint | undefined) => { return undefined; }; +const formatDate = (timestamp: number) => { + return new Date(timestamp).toLocaleString(); +}; + const getTotalCost = (transactions: PaymentTransaction[]) => { const total = transactions.reduce((sum, tx) => sum + (tx.details?.payment?.costUsd || 0n), 0n); return formatCost(total); @@ -41,6 +45,9 @@ export function ChatItem({ const chatId = chatRecord.chatId; const totalCost = getTotalCost(chatRecord.transactions); + const chatTime = chatRecord.transactions.length > 0 + ? Math.max(...chatRecord.transactions.map(tx => tx.details?.timestamp || 0)) + : 0; return ( onToggle(chatId)}> @@ -52,9 +59,8 @@ export function ChatItem({

{chatRecord.chatTitle || 'Untitled Chat'}

-

- {chatRecord.transactions.length} transaction - {chatRecord.transactions.length !== 1 ? 's' : ''} +

+ {chatTime > 0 ? formatDate(chatTime) : 'No date'}

@@ -62,6 +68,10 @@ export function ChatItem({
{totalCost || '$0.00'}
+

+ {chatRecord.transactions.length} transaction + {chatRecord.transactions.length !== 1 ? 's' : ''} +

{isOpen ? ( @@ -73,9 +83,10 @@ export function ChatItem({
- {chatRecord.transactions.map((transaction) => ( + {chatRecord.transactions.map((transaction, index) => ( diff --git a/src/features/wallet/components/transaction-history.tsx b/src/features/wallet/components/transaction-history.tsx index d8d6b4d5..4281a38f 100644 --- a/src/features/wallet/components/transaction-history.tsx +++ b/src/features/wallet/components/transaction-history.tsx @@ -1,20 +1,40 @@ -import { useState } from 'react'; +import { CalendarArrowDown, CalendarArrowUp, CalendarIcon, ListFilter, SortAsc, X } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { Button } from '@/shared/components/ui/button'; +import { Calendar } from '@/shared/components/ui/calendar'; import { Card, CardContent, CardHeader, CardTitle, } from '@/shared/components/ui/card'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/shared/components/ui/dropdown-menu'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/shared/components/ui/popover'; import { useChatTransactionInfo } from '../hooks/use-chat-transaction-info'; import type { PaymentTransaction } from '../types'; import { ChatItem } from './chat-item'; import { TransactionDetailsModal } from './transaction-details-modal'; +type SortOption = 'time-desc' | 'time-asc' | 'amount-desc' | 'amount-asc'; + export function TransactionHistory() { const { chatRecords, error } = useChatTransactionInfo(); const [selectedTransaction, setSelectedTransaction] = useState(null); const [openChats, setOpenChats] = useState>(new Set()); + const [sortBy, setSortBy] = useState('time-desc'); + const [filterDate, setFilterDate] = useState(undefined); const toggleChat = (chatId: string) => { const newOpenChats = new Set(); @@ -24,6 +44,71 @@ export function TransactionHistory() { setOpenChats(newOpenChats); }; + const filteredAndSortedChatRecords = useMemo(() => { + let filtered = chatRecords; + + // Filter by date if a date is selected + if (filterDate) { + const filterDateStart = new Date(filterDate); + filterDateStart.setHours(0, 0, 0, 0); + const filterDateEnd = new Date(filterDate); + filterDateEnd.setHours(23, 59, 59, 999); + + filtered = chatRecords + .map((chatRecord) => ({ + ...chatRecord, + transactions: chatRecord.transactions.filter((transaction) => { + const transactionDate = new Date(transaction.info.timestamp); + return ( + transactionDate >= filterDateStart && + transactionDate <= filterDateEnd + ); + }), + })) + .filter((chatRecord) => chatRecord.transactions.length > 0); + } + + // Sort transactions within each chat and sort chats + const sorted = filtered.map((chatRecord) => { + const sortedTransactions = [...chatRecord.transactions].sort((a, b) => { + switch (sortBy) { + case 'time-asc': + return a.info.timestamp - b.info.timestamp; + case 'time-desc': + return b.info.timestamp - a.info.timestamp; + case 'amount-asc': { + const amountA = Number(a.details?.payment?.costUsd || 0); + const amountB = Number(b.details?.payment?.costUsd || 0); + return amountA - amountB; + } + case 'amount-desc': { + const amountA = Number(a.details?.payment?.costUsd || 0); + const amountB = Number(b.details?.payment?.costUsd || 0); + return amountB - amountA; + } + default: + return b.info.timestamp - a.info.timestamp; + } + }); + + return { + ...chatRecord, + transactions: sortedTransactions, + }; + }); + + // Sort chats by the most recent transaction timestamp + return sorted.sort((a, b) => { + const mostRecentA = a.transactions[0]?.info.timestamp || 0; + const mostRecentB = b.transactions[0]?.info.timestamp || 0; + return mostRecentB - mostRecentA; + }); + }, [chatRecords, sortBy, filterDate]); + + const clearDateFilter = () => { + setFilterDate(undefined); + }; + if (error) { return ( @@ -42,13 +127,88 @@ export function TransactionHistory() { Payment Transactions +
+ {/* Combined filter and sort dropdown */} + + + + + + Sort by + setSortBy('time-desc')}> + + Latest + {sortBy === 'time-desc' && } + + setSortBy('time-asc')}> + + Earliest + {sortBy === 'time-asc' && } + + setSortBy('amount-desc')}> + + Most Cost + {sortBy === 'amount-desc' && ( + + )} + + setSortBy('amount-asc')}> + + Least Cost + {sortBy === 'amount-asc' && ( + + )} + + + + + Filter by date +
+ + + + + + + + +
+ + {filterDate && ( + + + Clear date filter + + )} +
+
+
- {chatRecords.length === 0 ? ( -

No transactions found

+ {filteredAndSortedChatRecords.length === 0 ? ( +

+ {filterDate + ? 'No transactions found for selected date' + : 'No transactions found'} +

) : ( - chatRecords.map((chatRecord) => ( + filteredAndSortedChatRecords.map((chatRecord) => ( { interface TransactionItemProps { transaction: PaymentTransaction; onSelect: (transaction: PaymentTransaction) => void; + index: number; } export function TransactionItem({ transaction, onSelect, + index, }: TransactionItemProps) { return ( + + + + + + ); +} \ No newline at end of file diff --git a/src/shared/components/ui/shadcn-io/data-picker.tsx b/src/shared/components/ui/shadcn-io/data-picker.tsx new file mode 100644 index 00000000..cbccaa3b --- /dev/null +++ b/src/shared/components/ui/shadcn-io/data-picker.tsx @@ -0,0 +1,54 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "../../../utils" +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + return ( + + ) +} +export { Button, buttonVariants } From 6a2afca575f89942d219b60f829769c28eef252b Mon Sep 17 00:00:00 2001 From: Mine77 Date: Sun, 17 Aug 2025 16:20:06 +0800 Subject: [PATCH 21/28] feat: add infinite scroll loading to cap store --- .../cap-store/components/cap-avatar.tsx | 7 +- .../cap-store/components/cap-card.tsx | 42 +++++- .../components/cap-store-content.tsx | 111 +++++++++----- .../cap-store/components/cap-store-modal.tsx | 9 +- .../cap-store/hooks/use-remote-cap.ts | 45 +++++- .../hooks/use-intersection-observer.tsx | 136 ++++++++++++++++++ src/shared/locales/cn.ts | 1 + src/shared/locales/en.ts | 12 +- 8 files changed, 310 insertions(+), 53 deletions(-) create mode 100644 src/shared/hooks/use-intersection-observer.tsx diff --git a/src/features/cap-store/components/cap-avatar.tsx b/src/features/cap-store/components/cap-avatar.tsx index 9cccfd1c..e60873d5 100644 --- a/src/features/cap-store/components/cap-avatar.tsx +++ b/src/features/cap-store/components/cap-avatar.tsx @@ -6,17 +6,20 @@ const sizeClasses = { sm: 'size-6', // 24px md: 'size-8', // 32px lg: 'size-10', // 40px - xl: 'size-12', // 48px + xl: 'size-16', // 48px } as const; + export function CapAvatar({ capName, capThumbnail, size = 'md', + className, }: { capName: string; capThumbnail: CapThumbnail; size?: keyof typeof sizeClasses; + className?: string; }) { const sizeClass = sizeClasses[size] || sizeClasses['md']; @@ -25,7 +28,7 @@ export function CapAvatar({ return ( (null); + const [descriptionClamp, setDescriptionClamp] = useState(2); + + /** + * Dynamically calculate the description line number, so that the title (up to 2 lines) and description together take up 4 lines. + */ + const recomputeClamp = () => { + const el = titleRef.current; + if (!el) return; + const computed = window.getComputedStyle(el); + const lineHeightPx = parseFloat(computed.lineHeight); + if (!lineHeightPx) return; + const height = el.getBoundingClientRect().height; + const lines = Math.max(1, Math.round(height / lineHeightPx)); + const titleLines = Math.min(lines, 2); + const clamp = Math.max(0, 4 - titleLines); + setDescriptionClamp(clamp); + }; + + useEffect(() => { + recomputeClamp(); + window.addEventListener('resize', recomputeClamp); + return () => window.removeEventListener('resize', recomputeClamp); + }, [cap]); const handleCapClick = async (cap: Cap | RemoteCap) => { setIsLoading(true); @@ -47,7 +71,7 @@ export function CapCard({ cap, actions }: CapCardProps) { const capMetadata = cap.metadata; return ( handleCapClick(cap)} >
@@ -55,14 +79,20 @@ export function CapCard({ cap, actions }: CapCardProps) { capName={capMetadata.displayName} capThumbnail={capMetadata.thumbnail} size="xl" + className="rounded-md" />
-

+

{capMetadata.displayName}

-

- {capMetadata.description} -

+ {descriptionClamp > 0 ? ( +

+ {capMetadata.description} +

+ ) : null}
{isLoading ? ( diff --git a/src/features/cap-store/components/cap-store-content.tsx b/src/features/cap-store/components/cap-store-content.tsx index 40ab02aa..8564e754 100644 --- a/src/features/cap-store/components/cap-store-content.tsx +++ b/src/features/cap-store/components/cap-store-content.tsx @@ -1,26 +1,32 @@ import { Clock, Loader2, Package, Star } from 'lucide-react'; -import { Button } from '@/shared/components/ui'; +import { Button, ScrollArea } from '@/shared/components/ui'; import { useLanguage } from '@/shared/hooks'; +import { useIntersectionObserver } from '@/shared/hooks/use-intersection-observer'; import type { Cap } from '@/shared/types/cap'; import { useCapStore } from '../hooks/use-cap-store'; import type { CapStoreSidebarSection, RemoteCap } from '../types'; -import { sortCapsByMetadata } from '../utils'; import { CapCard } from './cap-card'; export interface CapStoreContentProps { caps: (Cap | RemoteCap)[]; activeSection: CapStoreSidebarSection; isLoading?: boolean; + isLoadingMore?: boolean; + hasMoreData?: boolean; error?: string | null; onRefresh?: () => void; + onLoadMore?: () => void; } export function CapStoreContent({ caps, activeSection, isLoading = false, + isLoadingMore = false, + hasMoreData = false, error = null, onRefresh, + onLoadMore, }: CapStoreContentProps) { const { t } = useLanguage(); const { @@ -30,10 +36,20 @@ export function CapStoreContent({ isCapFavorite, } = useCapStore(); - const isShowingInstalled = ['favorites', 'recent'].includes(activeSection.id); + const isShowingInstalledCaps = ['favorites', 'recent'].includes( + activeSection.id, + ); + + const { ref: loadingTriggerRef } = useIntersectionObserver({ + threshold: 0.5, + freezeOnceVisible: false, + onChange: (isIntersecting) => { + if (isIntersecting) { + onLoadMore?.(); + } + }, + }); - // sort the caps by metadata - const sortedCaps = sortCapsByMetadata(caps); // Function to get actions based on cap type and active section const getCapActions = (cap: Cap | RemoteCap) => { @@ -67,7 +83,7 @@ export function CapStoreContent({ return actions; }; - if (error && !isShowingInstalled) { + if (error && !isShowingInstalledCaps) { return (
@@ -84,7 +100,7 @@ export function CapStoreContent({ ); } - if (isLoading && !isShowingInstalled) { + if (isLoading && !isShowingInstalledCaps) { return (
@@ -95,7 +111,7 @@ export function CapStoreContent({ ); } - if (sortedCaps.length === 0) { + if (caps.length === 0) { return (
@@ -110,34 +126,61 @@ export function CapStoreContent({ } return ( -
- {/* Section Title with Count */} -
-

- {activeSection.label} ({sortedCaps.length}) -

+
+ {/* Fixed Section Title */} +
+

{activeSection.label}

- {/* Caps Grid */} -
- {sortedCaps.length > 0 && - sortedCaps.map((cap) => { - // Type guard to check if cap is RemoteCap (has cid property) - const isRemoteCap = 'cid' in cap; - - if (isRemoteCap) { - // RemoteCap type - use cid as unique key - return ( - - ); - } else { - // Cap type - use id as unique key - return ( - - ); - } - })} -
+ {/* Caps Grid Container with ScrollArea */} + +
+ {caps.length > 0 && + caps.map((cap) => { + // Type guard to check if cap is RemoteCap (has cid property) + const isRemoteCap = 'cid' in cap; + + if (isRemoteCap) { + // RemoteCap type - use cid as unique key + return ( + + ); + } else { + // Cap type - use id as unique key + return ( + + ); + } + })} +
+ + {/* Infinite scroll trigger and loading indicator */} + {!isShowingInstalledCaps && ( + <> +
+ {isLoadingMore && ( +
+ +
+ )} + {!hasMoreData && caps.length > 0 && ( +
+ {t('capStore.status.noMoreCaps') || 'No more caps to load'} +
+ )} + + )} +
); } diff --git a/src/features/cap-store/components/cap-store-modal.tsx b/src/features/cap-store/components/cap-store-modal.tsx index 1d7585cb..40466a17 100644 --- a/src/features/cap-store/components/cap-store-modal.tsx +++ b/src/features/cap-store/components/cap-store-modal.tsx @@ -14,7 +14,7 @@ export function CapStoreModal() { const { t } = useLanguage(); const { toggleModal, isOpen, activeSection, setActiveSection } = useCapStoreModal(); - const { remoteCaps, isLoading, error, fetchCaps, refetch } = useRemoteCap(); + const { remoteCaps, isLoading, isLoadingMore, hasMoreData, error, fetchCaps, loadMore, refetch } = useRemoteCap(); const { getRecentCaps, getFavoriteCaps } = useCapStore(); const handleSearchChange = (query: string) => { @@ -73,14 +73,17 @@ export function CapStoreModal() { /> {/* Content Area */} -
-
+
+
refetch()} + onLoadMore={loadMore} />
diff --git a/src/features/cap-store/hooks/use-remote-cap.ts b/src/features/cap-store/hooks/use-remote-cap.ts index 98aaf554..0c10642e 100644 --- a/src/features/cap-store/hooks/use-remote-cap.ts +++ b/src/features/cap-store/hooks/use-remote-cap.ts @@ -20,9 +20,12 @@ export function useRemoteCap() { ); const [isLoading, setIsLoading] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); const [error, setError] = useState(null); + const [hasMoreData, setHasMoreData] = useState(true); + const [currentPage, setCurrentPage] = useState(0); - const fetchCaps = async (params: UseRemoteCapParams = {}) => { + const fetchCaps = async (params: UseRemoteCapParams = {}, append = false) => { const { searchQuery: queryString = '', page: pageNum = 0, @@ -30,7 +33,13 @@ export function useRemoteCap() { tags: tagsArray = [], } = params; - setIsLoading(true); + if (append) { + setIsLoadingMore(true); + } else { + setIsLoading(true); + setCurrentPage(0); + setHasMoreData(true); + } setError(null); setLastSearchParams(params); @@ -46,7 +55,7 @@ export function useRemoteCap() { sizeNum, ); - const remoteCaps: RemoteCap[] = + const newRemoteCaps: RemoteCap[] = response.data?.items ?.filter((item) => { return item.displayName !== 'nuwa_test'; @@ -70,15 +79,27 @@ export function useRemoteCap() { }; }) || []; - setRemoteCaps(remoteCaps); + // Check if we have more data + const totalItems = response.data?.items?.length || 0; + setHasMoreData(totalItems === sizeNum); + + if (append) { + setRemoteCaps([...remoteCaps, ...newRemoteCaps]); + setCurrentPage(pageNum); + } else { + setRemoteCaps(newRemoteCaps); + setCurrentPage(pageNum); + } setIsLoading(false); + setIsLoadingMore(false); return response; } catch (err) { console.error('Error fetching caps:', err); setError('Failed to fetch caps. Please try again.'); setIsLoading(false); + setIsLoadingMore(false); throw err; } }; @@ -87,6 +108,19 @@ export function useRemoteCap() { fetchCaps(lastSearchParams); }; + const loadMore = async () => { + if (!hasMoreData || isLoadingMore) return; + + const nextPage = currentPage + 1; + return fetchCaps( + { + ...lastSearchParams, + page: nextPage, + }, + true, + ); + }; + const goToPage = (newPage: number) => { return fetchCaps({ searchQuery: '', @@ -97,8 +131,11 @@ export function useRemoteCap() { return { remoteCaps, isLoading, + isLoadingMore, + hasMoreData, error, fetchCaps, + loadMore, goToPage, refetch, }; diff --git a/src/shared/hooks/use-intersection-observer.tsx b/src/shared/hooks/use-intersection-observer.tsx new file mode 100644 index 00000000..c258ad50 --- /dev/null +++ b/src/shared/hooks/use-intersection-observer.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useEffect, useRef, useState } from 'react'; + +type State = { + isIntersecting: boolean; + entry?: IntersectionObserverEntry; +}; + +type UseIntersectionObserverOptions = { + root?: Element | Document | null; + rootMargin?: string; + threshold?: number | number[]; + freezeOnceVisible?: boolean; + onChange?: (isIntersecting: boolean, entry: IntersectionObserverEntry) => void; + initialIsIntersecting?: boolean; +}; + +type IntersectionReturn = [ + (node?: Element | null) => void, + boolean, + IntersectionObserverEntry | undefined, +] & { + ref: (node?: Element | null) => void; + isIntersecting: boolean; + entry?: IntersectionObserverEntry; +}; + +export function useIntersectionObserver({ + threshold = 0, + root = null, + rootMargin = '0%', + freezeOnceVisible = false, + initialIsIntersecting = false, + onChange, +}: UseIntersectionObserverOptions = {}): IntersectionReturn { + const [ref, setRef] = useState(null); + + const [state, setState] = useState(() => ({ + isIntersecting: initialIsIntersecting, + entry: undefined, + })); + + const callbackRef = useRef(undefined); + + callbackRef.current = onChange; + + const frozen = state.entry?.isIntersecting && freezeOnceVisible; + + useEffect(() => { + // Ensure we have a ref to observe + if (!ref) return; + + // Ensure the browser supports the Intersection Observer API + if (!('IntersectionObserver' in window)) return; + + // Skip if frozen + if (frozen) return; + + let unobserve: (() => void) | undefined; + + const observer = new IntersectionObserver( + (entries: IntersectionObserverEntry[]): void => { + const thresholds = Array.isArray(observer.thresholds) + ? observer.thresholds + : [observer.thresholds]; + + entries.forEach(entry => { + const isIntersecting = + entry.isIntersecting && + thresholds.some(threshold => entry.intersectionRatio >= threshold); + + setState({ isIntersecting, entry }); + + if (callbackRef.current) { + callbackRef.current(isIntersecting, entry); + } + + if (isIntersecting && freezeOnceVisible && unobserve) { + unobserve(); + unobserve = undefined; + } + }); + }, + { threshold, root, rootMargin }, + ); + + observer.observe(ref); + + return () => { + observer.disconnect(); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + ref, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(threshold), + root, + rootMargin, + frozen, + freezeOnceVisible, + ]); + + // ensures that if the observed element changes, the intersection observer is reinitialized + const prevRef = useRef(null); + + useEffect(() => { + if ( + !ref && + state.entry?.target && + !freezeOnceVisible && + !frozen && + prevRef.current !== state.entry.target + ) { + prevRef.current = state.entry.target; + setState({ isIntersecting: initialIsIntersecting, entry: undefined }); + } + }, [ref, state.entry, freezeOnceVisible, frozen, initialIsIntersecting]); + + const result = [ + setRef, + !!state.isIntersecting, + state.entry, + ] as IntersectionReturn; + + // Support object destructuring, by adding the specific values. + result.ref = result[0]; + result.isIntersecting = result[1]; + result.entry = result[2]; + + return result; +} + +// Export types +export type { UseIntersectionObserverOptions, IntersectionReturn }; \ No newline at end of file diff --git a/src/shared/locales/cn.ts b/src/shared/locales/cn.ts index 2405e2c8..b6c26028 100644 --- a/src/shared/locales/cn.ts +++ b/src/shared/locales/cn.ts @@ -147,6 +147,7 @@ export const cn: typeof en = { noFavoriteCapsDesc: '您还没有收藏任何能力。浏览商店并收藏您喜欢的能力。', noRecentCapsDesc: '您最近还没有使用任何能力。试试运行一个能力吧。', noInstalledCapsDesc: '您还没有安装任何能力。浏览商店寻找要安装的能力。', + noMoreCaps: '已经到底了', }, card: { update: '有可用更新', diff --git a/src/shared/locales/en.ts b/src/shared/locales/en.ts index 0cd04500..1cfc775d 100644 --- a/src/shared/locales/en.ts +++ b/src/shared/locales/en.ts @@ -126,7 +126,7 @@ export const en = { searchPlaceholder: 'Search caps...', sidebar: { favorites: 'Favorite Caps', - recent: 'Recent Caps', + recent: 'Recent Caps', installed: 'Installed', all: 'All Caps', }, @@ -145,9 +145,13 @@ export const en = { 'Try adjusting your search terms or browse different categories.', category: 'No caps available in this category yet.', }, - noFavoriteCapsDesc: 'You haven\'t marked any caps as favorites yet. Browse the store and favorite caps you like.', - noRecentCapsDesc: 'You haven\'t used any caps recently. Try running a cap to see it here.', - noInstalledCapsDesc: 'You haven\'t installed any caps yet. Browse the store to find caps to install.', + noFavoriteCapsDesc: + "You haven't marked any caps as favorites yet. Browse the store and favorite caps you like.", + noRecentCapsDesc: + "You haven't used any caps recently. Try running a cap to see it here.", + noInstalledCapsDesc: + "You haven't installed any caps yet. Browse the store to find caps to install.", + noMoreCaps: 'You have reached the end of the list.', }, card: { update: 'Update Available', From 515131e1a82b8be04d2e7dc0b5970a2bb66c5821 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Sun, 17 Aug 2025 20:50:36 +0800 Subject: [PATCH 22/28] feat: cap store UI and UX improvements --- .../cap-store/components/cap-avatar.tsx | 1 - .../cap-store/components/cap-card.tsx | 91 +++-- .../cap-store/components/cap-details.tsx | 341 ++++++++++++++++++ .../cap-store/components/cap-selector.tsx | 5 +- .../components/cap-store-content.tsx | 125 ++----- .../cap-store/components/cap-store-header.tsx | 26 ++ .../components/cap-store-modal-context.tsx | 52 ++- .../cap-store/components/cap-store-modal.tsx | 64 +--- .../components/cap-store-sidebar.tsx | 73 ++-- src/features/cap-store/components/index.ts | 1 + src/features/cap-store/hooks/use-cap-store.ts | 3 + .../cap-store/hooks/use-remote-cap.ts | 235 +++++++----- src/features/cap-store/stores.ts | 53 +++ src/features/cap-store/types.ts | 1 + .../cap-studio/components/batch-create.tsx | 10 +- .../components/cap-edit/cap-tags.tsx | 131 ++++--- .../components/cap-submit/cap-submit-form.tsx | 13 +- .../components/cap-submit/index.tsx | 4 +- .../components/my-caps/cap-card.tsx | 2 - .../cap-studio/components/my-caps/index.tsx | 6 +- .../cap-studio/hooks/use-submit-cap.ts | 18 +- .../chat/components/message-reasoning.tsx | 27 +- src/features/chat/components/messages.tsx | 11 +- .../chat/components/multimodal-input.tsx | 5 +- src/features/chat/components/source-card.tsx | 41 ++- .../components/sections/system-section.tsx | 32 +- src/features/wallet/components/chat-item.tsx | 15 +- .../components/transaction-details-modal.tsx | 5 +- .../wallet/components/transaction-history.tsx | 15 +- .../wallet/components/transaction-item.tsx | 18 +- src/pages/cap-studio-batch-create.tsx | 2 +- src/pages/wallet.tsx | 2 - src/router.tsx | 5 +- src/shared/config/llm-gateway.ts | 5 +- src/shared/hooks/use-copy-to-clipboard.tsx | 30 ++ .../hooks/use-intersection-observer.tsx | 18 +- src/shared/hooks/use-unmount.tsx | 14 +- src/shared/services/payment-clients.ts | 18 +- src/shared/services/payment-fetch.ts | 31 +- 39 files changed, 1056 insertions(+), 493 deletions(-) create mode 100644 src/features/cap-store/components/cap-details.tsx create mode 100644 src/features/cap-store/components/cap-store-header.tsx create mode 100644 src/shared/hooks/use-copy-to-clipboard.tsx diff --git a/src/features/cap-store/components/cap-avatar.tsx b/src/features/cap-store/components/cap-avatar.tsx index e60873d5..485f0259 100644 --- a/src/features/cap-store/components/cap-avatar.tsx +++ b/src/features/cap-store/components/cap-avatar.tsx @@ -9,7 +9,6 @@ const sizeClasses = { xl: 'size-16', // 48px } as const; - export function CapAvatar({ capName, capThumbnail, diff --git a/src/features/cap-store/components/cap-card.tsx b/src/features/cap-store/components/cap-card.tsx index 158713a1..65bba3e1 100644 --- a/src/features/cap-store/components/cap-card.tsx +++ b/src/features/cap-store/components/cap-card.tsx @@ -1,4 +1,4 @@ -import { Loader2, MoreHorizontal } from 'lucide-react'; +import { Clock, Loader2, MoreHorizontal, Star } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { Card, @@ -9,28 +9,30 @@ import { } from '@/shared/components/ui'; import type { Cap } from '@/shared/types/cap'; import { useCapStore } from '../hooks/use-cap-store'; +import { useRemoteCap } from '../hooks/use-remote-cap'; import type { RemoteCap } from '../types'; import { CapAvatar } from './cap-avatar'; import { useCapStoreModal } from './cap-store-modal-context'; -interface CapCardActions { - icon: React.ReactNode; - label: string; - onClick: () => void; -} - export interface CapCardProps { cap: Cap | RemoteCap; - actions?: CapCardActions[]; } -export function CapCard({ cap, actions }: CapCardProps) { - const { runCap } = useCapStore(); - const { closeModal } = useCapStoreModal(); +export function CapCard({ cap }: CapCardProps) { + const { + addCapToFavorite, + removeCapFromFavorite, + removeCapFromRecents, + isCapFavorite, + } = useCapStore(); const [isLoading, setIsLoading] = useState(false); const titleRef = useRef(null); const [descriptionClamp, setDescriptionClamp] = useState(2); + const { downloadCap } = useRemoteCap(); + const { activeSection, setSelectedCap } = useCapStoreModal(); + const { installedCaps } = useCapStore(); + /** * Dynamically calculate the description line number, so that the title (up to 2 lines) and description together take up 4 lines. */ @@ -54,21 +56,57 @@ export function CapCard({ cap, actions }: CapCardProps) { }, [cap]); const handleCapClick = async (cap: Cap | RemoteCap) => { - setIsLoading(true); - try { - const isRemoteCap = 'cid' in cap; - if (isRemoteCap) { - await runCap(cap.id, cap.cid); - } else { - await runCap(cap.id); + const installedCap = installedCaps[cap.id]; + if (installedCap) { + setSelectedCap(installedCap); + } else { + if ('cid' in cap) { + setIsLoading(true); + try { + const downloadedCap = await downloadCap(cap); + setSelectedCap(downloadedCap); + } finally { + setIsLoading(false); + } } - closeModal(); - } finally { - setIsLoading(false); } }; + const getCapActions = (cap: Cap | RemoteCap) => { + const actions = []; + + const isRemoteCap = 'cid' in cap; + + if (isCapFavorite(cap.id)) { + actions.push({ + icon: , + label: 'Remove from Favorites', + onClick: () => removeCapFromFavorite(cap.id), + }); + } else { + actions.push({ + icon: , + label: 'Add to Favorites', + onClick: () => + addCapToFavorite(cap.id, isRemoteCap ? cap.cid : undefined), + }); + } + + if (activeSection.id === 'recent') { + actions.push({ + icon: , + label: 'Remove from Recents', + onClick: () => removeCapFromRecents(cap.id), + }); + } + + return actions; + }; + const capMetadata = cap.metadata; + + const actions = getCapActions(cap); + return (
-

+

{capMetadata.displayName}

{descriptionClamp > 0 ? (

{capMetadata.description}

diff --git a/src/features/cap-store/components/cap-details.tsx b/src/features/cap-store/components/cap-details.tsx new file mode 100644 index 00000000..78546a95 --- /dev/null +++ b/src/features/cap-store/components/cap-details.tsx @@ -0,0 +1,341 @@ +import { format } from 'date-fns'; +import { Copy, Home, Play, Star, Tag } from 'lucide-react'; +import { useState } from 'react'; +import { toast } from 'sonner'; +import { + Badge, + Button, + Card, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@/shared/components/ui'; +import { useCopyToClipboard } from '@/shared/hooks/use-copy-to-clipboard'; +import { useCapStore } from '../hooks/use-cap-store'; +import { CapAvatar } from './cap-avatar'; +import { useCapStoreModal } from './cap-store-modal-context'; + +export function CapDetails() { + const { runCap, addCapToFavorite, removeCapFromFavorite, isCapFavorite } = + useCapStore(); + const { closeModal, selectedCap: cap } = useCapStoreModal(); + const [isLoading, setIsLoading] = useState(false); + const [copyToClipboard, isCopied] = useCopyToClipboard(); + + if (!cap) { + return ( +
+

Cap not found, please try again

+ +
+ ); + } + + const handleRunCap = async () => { + setIsLoading(true); + try { + await runCap(cap.capData.id); + closeModal(); + } finally { + setIsLoading(false); + } + }; + + const handleToggleFavorite = () => { + if (isCapFavorite(cap.capData.id)) { + removeCapFromFavorite(cap.capData.id); + toast.success( + `Removed ${cap.capData.metadata.displayName} from favorites`, + ); + } else { + addCapToFavorite(cap.capData.id); + toast.success(`Added ${cap.capData.metadata.displayName} to favorites`); + } + }; + + const formatDate = (timestamp: number | string | undefined) => { + if (!timestamp) return 'Unknown'; + try { + const date = new Date(timestamp); + if (Number.isNaN(date.getTime())) return 'Unknown'; + return format(date, 'MMM d, yyyy'); + } catch { + return 'Unknown'; + } + }; + + const handleCopyAuthor = async () => { + if (cap?.capData.authorDID) { + await copyToClipboard(cap.capData.authorDID); + toast.success('Author DID copied to clipboard!'); + } + }; + + const truncateAuthor = (did: string) => { + if (did.length > 16) { + return `${did.slice(0, 8)}...${did.slice(-8)}`; + } + return did; + }; + + console.log(cap); + + return ( +
+ {/* Content */} +
+
+ {/* Main Layout: Left Content + Right Information Panel */} +
+ {/* Left Content Area */} +
+ {/* Header Section */} +
+
+ +
+ +
+

+ {cap.capData.metadata.displayName} +

+ + {/* ID Name */} + {cap.capData.id && ( +

+ @{cap.capData.idName} +

+ )} + + {/* Action Buttons */} +
+ + {isCapFavorite(cap.capData.id) ? ( + + ) : ( + + )} +
+
+
+ + {/* Description Section */} + {cap.capData.metadata.description && ( +
+

Description

+

+ {cap.capData.metadata.description} +

+
+ )} + + {/* Details Section */} +
+

Configuration

+ + + + Prompt + Model + MCP Servers + + + +
+

+ {typeof cap.capData.core.prompt === 'string' + ? cap.capData.core.prompt + : cap.capData.core.prompt?.value || + 'No prompt configured.'} +

+
+
+ + + {cap.capData.core.model ? ( +
+ {/* Model Name and Provider */} +
+
+ + Model Name: + + + {cap.capData.core.model.name} + +
+
+ + Provider Name: + + + {cap.capData.core.model.providerName} + +
+
+ + Input Price: + + + ${Number(cap.capData.core.model.pricing.input_per_million_tokens.toPrecision(3))}{' / 1M Tokens'} + +
+
+ + Output Price: + + + ${Number(cap.capData.core.model.pricing.output_per_million_tokens.toPrecision(3))}{' / 1M Tokens'} + +
+
+ + Context Length: + + + {cap.capData.core.model.contextLength} + +
+
+
+ ) : ( +

+ No model configuration available. +

+ )} +
+ + + {cap.capData.core.mcpServers && + Object.keys(cap.capData.core.mcpServers).length > 0 ? ( +
+ {Object.entries(cap.capData.core.mcpServers).map( + ([name, server]: [string, { + url: string; + transport: string; + }]) => ( +
+ + {name} + + + {server.url} + + + {server.transport || 'Unknown'} + +
+ ), + )} +
+ ) : ( +

+ No MCP servers configured. +

+ )} +
+
+
+
+
+ + {/* Right Information Panel */} +
+ {/* Cap Information */} + +

Information

+
+ + {/* Tags */} + {cap.capData.metadata.tags && + cap.capData.metadata.tags.length > 0 && ( +
+ Tags: +
+ {cap.capData.metadata.tags.map((tag) => ( + + + {tag} + + ))} +
+
+ )} + + {/* Author Badge */} + {cap.capData.authorDID && ( +
+ Author: + + + + {isCopied + ? 'Copied!' + : truncateAuthor(cap.capData.authorDID)} + + +
+ )} + + {cap.capData.metadata.submittedAt && ( +
+ Created At: + + {formatDate(cap.capData.metadata.submittedAt)} + +
+ )} +
+
+
+
+
+
+
+ ); +} diff --git a/src/features/cap-store/components/cap-selector.tsx b/src/features/cap-store/components/cap-selector.tsx index 384cc0fa..ed6abac8 100644 --- a/src/features/cap-store/components/cap-selector.tsx +++ b/src/features/cap-store/components/cap-selector.tsx @@ -10,7 +10,10 @@ import { useCurrentCap } from '@/shared/hooks'; import type { Cap } from '@/shared/types'; import { CapAvatar } from './cap-avatar'; import { CapStoreModal } from './cap-store-modal'; -import { CapStoreModalProvider, useCapStoreModal } from './cap-store-modal-context'; +import { + CapStoreModalProvider, + useCapStoreModal, +} from './cap-store-modal-context'; const CapInfo = ({ cap }: { cap: Cap }) => ( <> diff --git a/src/features/cap-store/components/cap-store-content.tsx b/src/features/cap-store/components/cap-store-content.tsx index 8564e754..af3dc2ab 100644 --- a/src/features/cap-store/components/cap-store-content.tsx +++ b/src/features/cap-store/components/cap-store-content.tsx @@ -1,87 +1,55 @@ -import { Clock, Loader2, Package, Star } from 'lucide-react'; +import { Loader2, Package } from 'lucide-react'; import { Button, ScrollArea } from '@/shared/components/ui'; import { useLanguage } from '@/shared/hooks'; import { useIntersectionObserver } from '@/shared/hooks/use-intersection-observer'; -import type { Cap } from '@/shared/types/cap'; import { useCapStore } from '../hooks/use-cap-store'; -import type { CapStoreSidebarSection, RemoteCap } from '../types'; +import { useRemoteCap } from '../hooks/use-remote-cap'; import { CapCard } from './cap-card'; +import { CapDetails } from './cap-details'; +import { useCapStoreModal } from './cap-store-modal-context'; -export interface CapStoreContentProps { - caps: (Cap | RemoteCap)[]; - activeSection: CapStoreSidebarSection; - isLoading?: boolean; - isLoadingMore?: boolean; - hasMoreData?: boolean; - error?: string | null; - onRefresh?: () => void; - onLoadMore?: () => void; -} - -export function CapStoreContent({ - caps, - activeSection, - isLoading = false, - isLoadingMore = false, - hasMoreData = false, - error = null, - onRefresh, - onLoadMore, -}: CapStoreContentProps) { +export function CapStoreContent() { const { t } = useLanguage(); + const { activeSection, selectedCap } = useCapStoreModal(); const { - addCapToFavorite, - removeCapFromFavorite, - removeCapFromRecents, - isCapFavorite, - } = useCapStore(); - + remoteCaps, + isFetching, + isLoadingMore, + hasMoreData, + error, + loadMore, + refetch, + } = useRemoteCap(); + const { getRecentCaps, getFavoriteCaps } = useCapStore(); + + // get caps based on active section + const caps = + activeSection.id === 'favorites' + ? getFavoriteCaps() + : activeSection.id === 'recent' + ? getRecentCaps() + : remoteCaps; + + // check if showing installed caps const isShowingInstalledCaps = ['favorites', 'recent'].includes( activeSection.id, ); + // infinite scroll trigger and loading indicator const { ref: loadingTriggerRef } = useIntersectionObserver({ threshold: 0.5, freezeOnceVisible: false, onChange: (isIntersecting) => { if (isIntersecting) { - onLoadMore?.(); + loadMore(); } }, }); - - // Function to get actions based on cap type and active section - const getCapActions = (cap: Cap | RemoteCap) => { - const actions = []; - - const isRemoteCap = 'cid' in cap; - - if (isCapFavorite(cap.id)) { - actions.push({ - icon: , - label: 'Remove from Favorites', - onClick: () => removeCapFromFavorite(cap.id), - }); - } else { - actions.push({ - icon: , - label: 'Add to Favorites', - onClick: () => - addCapToFavorite(cap.id, isRemoteCap ? cap.cid : undefined), - }); - } - - if (activeSection.id === 'recent') { - actions.push({ - icon: , - label: 'Remove from Recents', - onClick: () => removeCapFromRecents(cap.id), - }); - } - - return actions; - }; + // Show cap details if a cap is selected + if (selectedCap) { + return ; + } if (error && !isShowingInstalledCaps) { return ( @@ -93,14 +61,14 @@ export function CapStoreContent({

{t('capStore.status.errorDesc')}

-
); } - if (isLoading && !isShowingInstalledCaps) { + if (isFetching && !isShowingInstalledCaps) { return (
@@ -127,15 +95,8 @@ export function CapStoreContent({ return (
- {/* Fixed Section Title */} -
-

{activeSection.label}

-
- {/* Caps Grid Container with ScrollArea */} - +
{caps.length > 0 && caps.map((cap) => { @@ -144,22 +105,10 @@ export function CapStoreContent({ if (isRemoteCap) { // RemoteCap type - use cid as unique key - return ( - - ); + return ; } else { // Cap type - use id as unique key - return ( - - ); + return ; } })}
@@ -170,7 +119,7 @@ export function CapStoreContent({
{isLoadingMore && (
- +
)} {!hasMoreData && caps.length > 0 && ( diff --git a/src/features/cap-store/components/cap-store-header.tsx b/src/features/cap-store/components/cap-store-header.tsx new file mode 100644 index 00000000..7c8d13ac --- /dev/null +++ b/src/features/cap-store/components/cap-store-header.tsx @@ -0,0 +1,26 @@ +import { ArrowLeft } from 'lucide-react'; +import { Button } from '@/shared/components/ui'; +import { useCapStoreModal } from './cap-store-modal-context'; + +export function CapStoreHeader() { + const { selectedCap, setSelectedCap, activeSection } = useCapStoreModal(); + const onBack = () => { + setSelectedCap(null); + }; + + return ( +
+ {selectedCap ? ( + + ) : ( +

{activeSection.label}

+ )} +
+ ); +} + + + diff --git a/src/features/cap-store/components/cap-store-modal-context.tsx b/src/features/cap-store/components/cap-store-modal-context.tsx index e181f725..15f49f95 100644 --- a/src/features/cap-store/components/cap-store-modal-context.tsx +++ b/src/features/cap-store/components/cap-store-modal-context.tsx @@ -1,5 +1,12 @@ -import { createContext, type ReactNode, useContext, useState } from 'react'; -import type { CapStoreSidebarSection } from '../types'; +import { + createContext, + type ReactNode, + useContext, + useEffect, + useState, +} from 'react'; +import { useRemoteCap } from '../hooks/use-remote-cap'; +import type { CapStoreSidebarSection, InstalledCap } from '../types'; interface CapStoreModalContextValue { isOpen: boolean; @@ -8,9 +15,10 @@ interface CapStoreModalContextValue { toggleModal: () => void; activeSection: CapStoreSidebarSection; setActiveSection: (section: CapStoreSidebarSection) => void; + selectedCap: InstalledCap | null; + setSelectedCap: (cap: InstalledCap | null) => void; } - const initialActiveSection = { id: 'all', label: 'All Caps', @@ -19,16 +27,17 @@ const initialActiveSection = { const defaultContextValue: CapStoreModalContextValue = { isOpen: false, - openModal: () => { }, - closeModal: () => { }, - toggleModal: () => { }, + openModal: () => {}, + closeModal: () => {}, + toggleModal: () => {}, activeSection: initialActiveSection, - setActiveSection: () => { }, + setActiveSection: () => {}, + selectedCap: null, + setSelectedCap: () => {}, }; -const CapStoreModalContext = createContext( - defaultContextValue -); +const CapStoreModalContext = + createContext(defaultContextValue); interface CapStoreModalProviderProps { children: ReactNode; @@ -38,10 +47,10 @@ export function CapStoreModalProvider({ children, }: CapStoreModalProviderProps) { const [isOpen, setIsOpen] = useState(false); - - - const [activeSection, setActiveSection] = + const [selectedCap, setSelectedCap] = useState(null); + const [activeSection, _setActiveSection] = useState(initialActiveSection); + const { fetchCaps } = useRemoteCap(); const openModal = () => setIsOpen(true); const closeModal = () => { @@ -50,6 +59,17 @@ export function CapStoreModalProvider({ }; const toggleModal = () => setIsOpen((prev) => !prev); + const setActiveSection = (section: CapStoreSidebarSection) => { + _setActiveSection(section); + setSelectedCap(null); + }; + + useEffect(() => { + if (isOpen) { + fetchCaps(); + } + }, [isOpen]); + const value: CapStoreModalContextValue = { isOpen, openModal, @@ -57,6 +77,8 @@ export function CapStoreModalProvider({ toggleModal, activeSection, setActiveSection, + selectedCap, + setSelectedCap, }; return ( @@ -71,11 +93,11 @@ export function useCapStoreModal() { if (!context) { throw new Error( - 'useCapStoreModal must be used within a CapStoreModalProvider' + 'useCapStoreModal must be used within a CapStoreModalProvider', ); } return context; } -export { CapStoreModalContext }; \ No newline at end of file +export { CapStoreModalContext }; diff --git a/src/features/cap-store/components/cap-store-modal.tsx b/src/features/cap-store/components/cap-store-modal.tsx index 40466a17..123302e5 100644 --- a/src/features/cap-store/components/cap-store-modal.tsx +++ b/src/features/cap-store/components/cap-store-modal.tsx @@ -1,51 +1,14 @@ import * as Dialog from '@/shared/components/ui'; import { useLanguage } from '@/shared/hooks'; -import type { Cap } from '@/shared/types/cap'; -import { useCapStore } from '../hooks/use-cap-store'; -import { useRemoteCap } from '../hooks/use-remote-cap'; -import type { CapStoreSidebarSection, RemoteCap } from '../types'; import { CapStoreContent } from './cap-store-content'; +import { CapStoreHeader } from './cap-store-header'; import { useCapStoreModal } from './cap-store-modal-context'; import { CapStoreSidebar } from './cap-store-sidebar'; - - export function CapStoreModal() { const { t } = useLanguage(); - const { toggleModal, isOpen, activeSection, setActiveSection } = useCapStoreModal(); - - const { remoteCaps, isLoading, isLoadingMore, hasMoreData, error, fetchCaps, loadMore, refetch } = useRemoteCap(); - const { getRecentCaps, getFavoriteCaps } = useCapStore(); - - const handleSearchChange = (query: string) => { - if (activeSection.type === 'tag') { - fetchCaps({ searchQuery: query, tags: [activeSection.label] }); - } else if (activeSection.type === 'section') { - fetchCaps({ searchQuery: query }); - } - }; - const handleActiveSectionChange = (section: CapStoreSidebarSection) => { - setActiveSection(section); - if (section.type === 'tag') { - fetchCaps({ tags: [section.label] }); - } else if (section.id === 'all') { - fetchCaps({ searchQuery: '' }); - } - }; - - // Determine which caps to display based on active section - const getDisplayCaps = (): (Cap | RemoteCap)[] => { - if (activeSection.id === 'favorites') { - return getFavoriteCaps(); - } else if (activeSection.id === 'recent') { - return getRecentCaps(); - } else { - return remoteCaps; - } - }; - - const displayCaps: (Cap | RemoteCap)[] = getDisplayCaps(); + const { toggleModal, isOpen } = useCapStoreModal(); return ( @@ -66,25 +29,14 @@ export function CapStoreModal() { {/* Main Content with Sidebar */}
- + {/* Content Area */} -
-
- refetch()} - onLoadMore={loadMore} - /> +
+ + +
+
diff --git a/src/features/cap-store/components/cap-store-sidebar.tsx b/src/features/cap-store/components/cap-store-sidebar.tsx index df50d32f..9970909a 100644 --- a/src/features/cap-store/components/cap-store-sidebar.tsx +++ b/src/features/cap-store/components/cap-store-sidebar.tsx @@ -4,12 +4,12 @@ import { Code, Coins, Grid3X3, - Heart, History, MoreHorizontal, Package, PenTool, Search, + Star, Wrench, X, } from 'lucide-react'; @@ -17,25 +17,19 @@ import { useEffect, useState } from 'react'; import { Input } from '@/shared/components/ui'; import { predefinedTags } from '@/shared/constants/cap'; import { useDebounceValue, useLanguage } from '@/shared/hooks'; +import { useRemoteCap } from '../hooks/use-remote-cap'; import type { CapStoreSidebarSection } from '../types'; +import { useCapStoreModal } from './cap-store-modal-context'; -export interface CapStoreSidebarProps { - activeSection: CapStoreSidebarSection; - onSectionChange: (section: CapStoreSidebarSection) => void; - onSearchChange: (query: string) => void; -} - - - -export function CapStoreSidebar({ - activeSection, - onSectionChange, - onSearchChange, -}: CapStoreSidebarProps) { +export function CapStoreSidebar() { const { t } = useLanguage(); const [searchValue, setSearchValue] = useState(''); - const [debouncedSearchValue, setDebouncedSearchValue] = useDebounceValue('', 500) - + const [debouncedSearchValue, setDebouncedSearchValue] = useDebounceValue( + '', + 500, + ); + const { fetchCaps } = useRemoteCap(); + const { setActiveSection, activeSection } = useCapStoreModal(); const sidebarSections: CapStoreSidebarSection[] = [ { @@ -65,7 +59,7 @@ export function CapStoreSidebar({ if (type === 'section') { switch (sectionId) { case 'favorites': - return Heart; + return Star; case 'recent': return History; case 'all': @@ -99,25 +93,51 @@ export function CapStoreSidebar({ return Package; }; + // reset search value when active section changes useEffect(() => { setSearchValue(''); }, [activeSection]); + // fetch caps when debounced search value changes useEffect(() => { - onSearchChange(debouncedSearchValue); + handleDebouncedSearchChange(debouncedSearchValue); }, [debouncedSearchValue]); + // handle search change const handleSearchChange = (value: string) => { - setSearchValue(value) - setDebouncedSearchValue(value) + setSearchValue(value); + setDebouncedSearchValue(value); }; + // handle clear search const handleClearSearch = () => { setSearchValue(''); - onSearchChange(''); + handleDebouncedSearchChange(''); setDebouncedSearchValue(''); }; + // handle debounced search change + const handleDebouncedSearchChange = (value: string) => { + if (activeSection.type === 'tag') { + fetchCaps({ + searchQuery: debouncedSearchValue, + tags: [activeSection.label], + }); + } else if (activeSection.type === 'section') { + fetchCaps({ searchQuery: debouncedSearchValue }); + } + }; + + // handle active section change + const handleActiveSectionChange = (section: CapStoreSidebarSection) => { + setActiveSection(section); + if (section.type === 'tag') { + fetchCaps({ tags: [section.label] }); + } else if (section.id === 'all') { + fetchCaps({ searchQuery: '' }); + } + }; + return (
{/* Search Section */} @@ -169,11 +189,12 @@ export function CapStoreSidebar({
- +
-

{getModelName(cap.capData.core.model)}

-

{getProviderName(cap.capData.core.model)}

+

+ {getModelName(cap.capData.core.model)} +

+

+ {getProviderName(cap.capData.core.model)} +

diff --git a/src/features/cap-studio/components/cap-submit/index.tsx b/src/features/cap-studio/components/cap-submit/index.tsx index c4db2aff..82be4b35 100644 --- a/src/features/cap-studio/components/cap-submit/index.tsx +++ b/src/features/cap-studio/components/cap-submit/index.tsx @@ -25,8 +25,8 @@ export function Submit() { return ( - setIsHovered(false)} > -
{(isMultiSelectMode || isHovered) && (
@@ -259,7 +258,6 @@ export function CapCard({
)}
-
diff --git a/src/features/cap-studio/components/my-caps/index.tsx b/src/features/cap-studio/components/my-caps/index.tsx index 231935a8..1d031010 100644 --- a/src/features/cap-studio/components/my-caps/index.tsx +++ b/src/features/cap-studio/components/my-caps/index.tsx @@ -147,7 +147,8 @@ export function MyCaps({
- Publishing Caps ({bulkProgress.completed}/{bulkProgress.total}) + Publishing Caps ({bulkProgress.completed}/{bulkProgress.total} + ) {bulkProgress.currentCap && ( @@ -237,8 +238,7 @@ export function MyCaps({ {bulkProgress.isSubmitting ? `Publishing... (${bulkProgress.completed}/${bulkProgress.total})` - : `Publish (${selectedCaps.length})` - } + : `Publish (${selectedCaps.length})`} )} diff --git a/src/features/cap-studio/hooks/use-submit-cap.ts b/src/features/cap-studio/hooks/use-submit-cap.ts index 6009e0a9..b4e1e1df 100644 --- a/src/features/cap-studio/hooks/use-submit-cap.ts +++ b/src/features/cap-studio/hooks/use-submit-cap.ts @@ -76,29 +76,35 @@ export const useSubmitCap = () => { const cap = caps[i]; const displayName = cap.capData.metadata.displayName; - setBulkProgress(prev => ({ + setBulkProgress((prev) => ({ ...prev, currentCap: displayName, })); try { await capKit.registerCap(cap.capData); - setBulkProgress(prev => ({ + setBulkProgress((prev) => ({ ...prev, completed: prev.completed + 1, })); } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to submit capability'; + const errorMessage = + error instanceof Error + ? error.message + : 'Failed to submit capability'; errors.push({ capName: displayName, error: errorMessage }); - setBulkProgress(prev => ({ + setBulkProgress((prev) => ({ ...prev, completed: prev.completed + 1, - errors: [...prev.errors, { capName: displayName, error: errorMessage }], + errors: [ + ...prev.errors, + { capName: displayName, error: errorMessage }, + ], })); } } - setBulkProgress(prev => ({ + setBulkProgress((prev) => ({ ...prev, isSubmitting: false, currentCap: undefined, diff --git a/src/features/chat/components/message-reasoning.tsx b/src/features/chat/components/message-reasoning.tsx index d81b181b..6c40c265 100644 --- a/src/features/chat/components/message-reasoning.tsx +++ b/src/features/chat/components/message-reasoning.tsx @@ -131,11 +131,17 @@ export const AIReasoningTrigger = memo( > {children ?? ( <> - + + + {isStreaming && duration === 0 ? (

Reasoning...

) : ( -

{duration === 0 ? 'Reasoned for a few seconds' : `Reasoned for ${duration} seconds`}

+

+ {duration === 0 + ? 'Reasoned for a few seconds' + : `Reasoned for ${duration} seconds`} +

)} ( {children} @@ -176,14 +185,14 @@ interface MessageReasoningProps { content: string; } -export const MessageReasoning = ({ isStreaming, content }: MessageReasoningProps) => { +export const MessageReasoning = ({ + isStreaming, + content, +}: MessageReasoningProps) => { return ( - + {content} ); -}; \ No newline at end of file +}; diff --git a/src/features/chat/components/messages.tsx b/src/features/chat/components/messages.tsx index 086efb9e..9994a663 100644 --- a/src/features/chat/components/messages.tsx +++ b/src/features/chat/components/messages.tsx @@ -40,8 +40,13 @@ function PureMessages({ className="flex flex-col min-w-0 gap-6 h-full overflow-y-scroll pt-4 relative" > {messages.map((message, index) => { - const isStreaming = status === 'streaming' && messages.length - 1 === index; - const isStreamingReasoning = isStreaming && message.role === 'assistant' && message.parts?.some((part) => part.type === 'reasoning') && !message.parts?.some((part) => part.type === 'text'); + const isStreaming = + status === 'streaming' && messages.length - 1 === index; + const isStreamingReasoning = + isStreaming && + message.role === 'assistant' && + message.parts?.some((part) => part.type === 'reasoning') && + !message.parts?.some((part) => part.type === 'text'); return ( - ) + ); })} {status === 'submitted' && diff --git a/src/features/chat/components/multimodal-input.tsx b/src/features/chat/components/multimodal-input.tsx index c4d0815d..590c1ce0 100644 --- a/src/features/chat/components/multimodal-input.tsx +++ b/src/features/chat/components/multimodal-input.tsx @@ -188,7 +188,10 @@ function PureMultimodalInput({ event.preventDefault(); if (status !== 'ready') { - console.warn('The model is not ready to respond. Currnet status:', status); + console.warn( + 'The model is not ready to respond. Currnet status:', + status, + ); } submitForm(); diff --git a/src/features/chat/components/source-card.tsx b/src/features/chat/components/source-card.tsx index 6f8a2306..fde3e714 100644 --- a/src/features/chat/components/source-card.tsx +++ b/src/features/chat/components/source-card.tsx @@ -51,41 +51,42 @@ export const SourceCard = ({ (f: any) => f.rel === 'icon' && getMaxSize(f.sizes) >= 180, (f: any) => f.rel === 'icon' && getMaxSize(f.sizes) >= 128, (f: any) => f.rel === 'icon' && getMaxSize(f.sizes) >= 64, - + // Apple touch icons (usually high quality) (f: any) => f.rel === 'apple-touch-icon' && getMaxSize(f.sizes) >= 152, - + // Standard sizes (f: any) => f.rel === 'icon' && f.sizes === '32x32', (f: any) => f.rel === 'icon' && f.sizes === '16x16', - + // Flexible size icons (f: any) => f.rel === 'icon' && f.sizes && f.sizes.includes('32'), (f: any) => f.rel === 'icon' && f.sizes && f.sizes.includes('16'), - + // Any icon with PNG type (usually higher quality than ICO) (f: any) => f.rel === 'icon' && f.type === 'image/png', - + // Any icon with SVG type (scalable) (f: any) => f.rel === 'icon' && f.type === 'image/svg+xml', - + // Any icon with specified type (f: any) => f.rel === 'icon' && f.type, - + // Any icon relation (f: any) => f.rel === 'icon', - + // Apple touch icon as fallback (f: any) => f.rel === 'apple-touch-icon', - + // Shortcut icon as fallback (f: any) => f.rel === 'shortcut icon', - + // Any favicon-like entry - (f: any) => f.href && (f.href.includes('favicon') || f.href.includes('icon')), - + (f: any) => + f.href && (f.href.includes('favicon') || f.href.includes('icon')), + // Last resort - first entry - () => true + () => true, ]; for (const priorityFn of priorityOrder) { @@ -100,7 +101,7 @@ export const SourceCard = ({ return null; } } - + // Return absolute URLs as-is return selected.href || null; } @@ -112,12 +113,12 @@ export const SourceCard = ({ const faviconUrl = getBestFavicon(); const hostname = url ? (() => { - try { - return new URL(url).hostname; - } catch { - return url; - } - })() + try { + return new URL(url).hostname; + } catch { + return url; + } + })() : ''; if (!isExternalUrl) { diff --git a/src/features/settings/components/sections/system-section.tsx b/src/features/settings/components/sections/system-section.tsx index b611d75c..35aeb9ba 100644 --- a/src/features/settings/components/sections/system-section.tsx +++ b/src/features/settings/components/sections/system-section.tsx @@ -49,21 +49,23 @@ export function SystemSection() { disabled={false} /> - {isDevMode && } + {isDevMode && ( + + )}
); } diff --git a/src/features/wallet/components/chat-item.tsx b/src/features/wallet/components/chat-item.tsx index ff1897b7..8155b638 100644 --- a/src/features/wallet/components/chat-item.tsx +++ b/src/features/wallet/components/chat-item.tsx @@ -23,11 +23,13 @@ const formatDate = (timestamp: number) => { }; const getTotalCost = (transactions: PaymentTransaction[]) => { - const total = transactions.reduce((sum, tx) => sum + (tx.details?.payment?.costUsd || 0n), 0n); + const total = transactions.reduce( + (sum, tx) => sum + (tx.details?.payment?.costUsd || 0n), + 0n, + ); return formatCost(total); }; - interface ChatHistoryItemProps { chatRecord: ChatRecord; isOpen: boolean; @@ -45,9 +47,12 @@ export function ChatItem({ const chatId = chatRecord.chatId; const totalCost = getTotalCost(chatRecord.transactions); - const chatTime = chatRecord.transactions.length > 0 - ? Math.max(...chatRecord.transactions.map(tx => tx.details?.timestamp || 0)) - : 0; + const chatTime = + chatRecord.transactions.length > 0 + ? Math.max( + ...chatRecord.transactions.map((tx) => tx.details?.timestamp || 0), + ) + : 0; return ( onToggle(chatId)}> diff --git a/src/features/wallet/components/transaction-details-modal.tsx b/src/features/wallet/components/transaction-details-modal.tsx index b47ae2fd..7abcf3da 100644 --- a/src/features/wallet/components/transaction-details-modal.tsx +++ b/src/features/wallet/components/transaction-details-modal.tsx @@ -57,8 +57,9 @@ function CopyableCell({ value, isNested = false }: CopyableCellProps) { return ( diff --git a/src/features/wallet/components/transaction-history.tsx b/src/features/wallet/components/transaction-history.tsx index 4281a38f..46d5de49 100644 --- a/src/features/wallet/components/transaction-history.tsx +++ b/src/features/wallet/components/transaction-history.tsx @@ -1,4 +1,11 @@ -import { CalendarArrowDown, CalendarArrowUp, CalendarIcon, ListFilter, SortAsc, X } from 'lucide-react'; +import { + CalendarArrowDown, + CalendarArrowUp, + CalendarIcon, + ListFilter, + SortAsc, + X, +} from 'lucide-react'; import { useMemo, useState } from 'react'; import { Button } from '@/shared/components/ui/button'; import { Calendar } from '@/shared/components/ui/calendar'; @@ -178,7 +185,11 @@ export function TransactionHistory() { : 'Pick a date'} - + onSelect(transaction)} >
- #{index + 1} + + #{index + 1} +
-

{formatTransactionLabel(transaction)}

+

+ {formatTransactionLabel(transaction)} +

{formatDate(transaction.details?.timestamp || 0)}

- {!transaction.details ?

- No transaction record -

: transaction.details.status === "pending" ? ( -

- Pending... -

+ {!transaction.details ? ( +

No transaction record

+ ) : transaction.details.status === 'pending' ? ( +

Pending...

) : (

{formatCost(transaction.details?.payment?.costUsd) || '$0.00'} diff --git a/src/pages/cap-studio-batch-create.tsx b/src/pages/cap-studio-batch-create.tsx index 9fcb0b0e..1e1c8284 100644 --- a/src/pages/cap-studio-batch-create.tsx +++ b/src/pages/cap-studio-batch-create.tsx @@ -2,4 +2,4 @@ import { BatchCreate } from '@/features/cap-studio/components/batch-create'; export default function CapStudioBatchCreatePage() { return ; -} \ No newline at end of file +} diff --git a/src/pages/wallet.tsx b/src/pages/wallet.tsx index 826f940a..f849453f 100644 --- a/src/pages/wallet.tsx +++ b/src/pages/wallet.tsx @@ -1,7 +1,5 @@ import { WalletWithProvider } from '@/features/wallet/components'; export default function WalletPage() { - - return ; } diff --git a/src/router.tsx b/src/router.tsx index 5d28ac59..6afc62a0 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -30,7 +30,10 @@ const router = createBrowserRouter([ { path: 'settings', element: }, { path: 'cap-studio', element: }, { path: 'cap-studio/create', element: }, - { path: 'cap-studio/batch-create', element: }, + { + path: 'cap-studio/batch-create', + element: , + }, { path: 'cap-studio/edit/:id', element: }, { path: 'cap-studio/submit/:id', element: }, { path: 'cap-studio/mcp', element: }, diff --git a/src/shared/config/llm-gateway.ts b/src/shared/config/llm-gateway.ts index 8eac61fd..a97edfe8 100644 --- a/src/shared/config/llm-gateway.ts +++ b/src/shared/config/llm-gateway.ts @@ -1,6 +1,5 @@ // Centralized LLM Gateway configuration for reuse across features // TODO: consider moving to environment-configurable source -export const LLM_GATEWAY_BASE_URL = 'https://llm-gateway-payment-test.up.railway.app/api/v1'; - - +export const LLM_GATEWAY_BASE_URL = + 'https://llm-gateway-payment-test.up.railway.app/api/v1'; diff --git a/src/shared/hooks/use-copy-to-clipboard.tsx b/src/shared/hooks/use-copy-to-clipboard.tsx new file mode 100644 index 00000000..8d0e8c53 --- /dev/null +++ b/src/shared/hooks/use-copy-to-clipboard.tsx @@ -0,0 +1,30 @@ +import { useCallback, useState } from 'react'; + +export type UseCopyToClipboardReturn = [ + (text: string) => Promise, + boolean, +]; + +export const useCopyToClipboard = (): UseCopyToClipboardReturn => { + const [isCopied, setIsCopied] = useState(false); + + const copy = useCallback(async (text: string): Promise => { + if (!navigator?.clipboard) { + console.warn('Clipboard not supported'); + return; + } + + try { + await navigator.clipboard.writeText(text); + setIsCopied(true); + + // Reset the copied state after 2 seconds + setTimeout(() => setIsCopied(false), 2000); + } catch (error) { + console.warn('Copy failed', error); + setIsCopied(false); + } + }, []); + + return [copy, isCopied]; +}; diff --git a/src/shared/hooks/use-intersection-observer.tsx b/src/shared/hooks/use-intersection-observer.tsx index c258ad50..c4aa23ae 100644 --- a/src/shared/hooks/use-intersection-observer.tsx +++ b/src/shared/hooks/use-intersection-observer.tsx @@ -1,4 +1,4 @@ -"use client"; +'use client'; import { useEffect, useRef, useState } from 'react'; @@ -12,7 +12,10 @@ type UseIntersectionObserverOptions = { rootMargin?: string; threshold?: number | number[]; freezeOnceVisible?: boolean; - onChange?: (isIntersecting: boolean, entry: IntersectionObserverEntry) => void; + onChange?: ( + isIntersecting: boolean, + entry: IntersectionObserverEntry, + ) => void; initialIsIntersecting?: boolean; }; @@ -41,7 +44,8 @@ export function useIntersectionObserver({ entry: undefined, })); - const callbackRef = useRef(undefined); + const callbackRef = + useRef(undefined); callbackRef.current = onChange; @@ -65,10 +69,12 @@ export function useIntersectionObserver({ ? observer.thresholds : [observer.thresholds]; - entries.forEach(entry => { + entries.forEach((entry) => { const isIntersecting = entry.isIntersecting && - thresholds.some(threshold => entry.intersectionRatio >= threshold); + thresholds.some( + (threshold) => entry.intersectionRatio >= threshold, + ); setState({ isIntersecting, entry }); @@ -133,4 +139,4 @@ export function useIntersectionObserver({ } // Export types -export type { UseIntersectionObserverOptions, IntersectionReturn }; \ No newline at end of file +export type { UseIntersectionObserverOptions, IntersectionReturn }; diff --git a/src/shared/hooks/use-unmount.tsx b/src/shared/hooks/use-unmount.tsx index b138e86a..7cc276f4 100644 --- a/src/shared/hooks/use-unmount.tsx +++ b/src/shared/hooks/use-unmount.tsx @@ -1,10 +1,10 @@ -import * as React from "react"; +import * as React from 'react'; /** * A React hook that runs a cleanup function when the component unmounts. - * + * * @param fn - The cleanup function to run on unmount - * + * * @example * ```tsx * function MyComponent() { @@ -12,14 +12,14 @@ import * as React from "react"; * // Cleanup logic here * console.log('Component is unmounting'); * }); - * + * * return

Hello world
; * } * ``` */ export function useUnmount(fn: () => void): void { - if (typeof fn !== "function") { - throw new Error("useUnmount expects a function as argument"); + if (typeof fn !== 'function') { + throw new Error('useUnmount expects a function as argument'); } const fnRef = React.useRef(fn); @@ -33,4 +33,4 @@ export function useUnmount(fn: () => void): void { fnRef.current(); }; }, []); -} \ No newline at end of file +} diff --git a/src/shared/services/payment-clients.ts b/src/shared/services/payment-clients.ts index 6cea4e56..164aa618 100644 --- a/src/shared/services/payment-clients.ts +++ b/src/shared/services/payment-clients.ts @@ -1,5 +1,9 @@ import { IdentityKitWeb } from '@nuwa-ai/identity-kit-web'; -import { createHttpClient, RoochPaymentChannelContract, PaymentHubClient } from '@nuwa-ai/payment-kit'; +import { + createHttpClient, + RoochPaymentChannelContract, + PaymentHubClient, +} from '@nuwa-ai/payment-kit'; import type { PaymentChannelHttpClient } from '@nuwa-ai/payment-kit'; import { LLM_GATEWAY_BASE_URL } from '@/shared/config/llm-gateway'; @@ -23,7 +27,9 @@ export async function getHttpClient(): Promise { return httpClientPromise; } -export async function getPaymentHubClient(defaultAssetId?: string): Promise { +export async function getPaymentHubClient( + defaultAssetId?: string, +): Promise { if (!hubClientPromise) { hubClientPromise = (async () => { const { env, signer } = await getIdentityEnvAndSigner(); @@ -33,10 +39,12 @@ export async function getPaymentHubClient(defaultAssetId?: string): Promise { - const targetUrl = new URL(typeof input === 'string' ? input : (input as any).url ?? input.toString()); - const methodFromInit = (init?.method ?? 'POST').toUpperCase() as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; +export function createPaymentFetch( + baseUrl: string, + _options?: { maxAmount?: bigint }, +) { + return async function paymentFetch( + input: RequestInfo | URL, + init?: RequestInit, + ): Promise { + const targetUrl = new URL( + typeof input === 'string' + ? input + : ((input as any).url ?? input.toString()), + ); + const methodFromInit = (init?.method ?? 'POST').toUpperCase() as + | 'GET' + | 'POST' + | 'PUT' + | 'DELETE' + | 'PATCH'; const client = await getHttpClient(); - const handle = await client.requestWithPayment(methodFromInit, targetUrl.toString(), init); + const handle = await client.requestWithPayment( + methodFromInit, + targetUrl.toString(), + init, + ); return handle.response; }; } - - From 6067348f178d2b977b2ccb0b4d011f6e54f0ad03 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Sun, 17 Aug 2025 21:22:22 +0800 Subject: [PATCH 23/28] Update readme --- README.md | 168 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 117 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index da984443..e75c6e59 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,120 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default tseslint.config({ - extends: [ - // Remove ...tseslint.configs.recommended and replace with this - ...tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - ...tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - ...tseslint.configs.stylisticTypeChecked, - ], - languageOptions: { - // other options... - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - }, -}) +# Nuwa Client + +A local-first AI chat client that enables users to create, share, and interact with Caps. + +## ✨ Features + +### 🔐 Decentralized Identity & Payment +- **Self-Sovereign Identity**: Your data belongs to you with DID-based authentication +- **Crypto Payment**: Use cryptos to pay for your day-to-day ai +- **Data Portability**: Export and migrate your data anywhere + +### 🎨 Modern User Experience +- **Beautiful UI**: Clean, responsive design with dark/light theme support +- **Accessibility**: Built on Radix UI primitives for full accessibility +- **Performance**: Optimized with React 19 and Vite for fast loading + +## 🚀 Beta Release + +We're excited to announce the **Nuwa Client Beta**! This release includes: + +- ✅ Core CAP creation and sharing functionality +- ✅ Multi-model AI chat with streaming +- ✅ Web3 wallet integration +- ✅ Decentralized identity system +- ✅ MCP server integration +- ✅ Payment system for premium features + +### What's Coming Next +- Enhanced CAP marketplace with ratings and reviews +- Advanced MCP tool ecosystem +- Mobile applications (iOS/Android) +- Advanced collaboration features +- Enterprise integrations + +## 🛠️ Technology Stack + +- **Frontend**: React 19, TypeScript, Vite +- **Routing**: React Router v7 +- **Styling**: Tailwind CSS + Shadcn UI +- **State Management**: Zustand with persistence +- **Database**: Dexie (IndexedDB) for local storage +- **Identity**: DID from @nuwa-ai/identity-kit +- **Payment**: Payment Channel from @nuwa-ai/payment-kit +- **Cap Integration**: Cap integration from @nuwa-ai/cap-kit +- **Code Quality**: Biome for linting and formatting + +## 🚀 Quick Start + +### Prerequisites +- Node.js 18+ +- pnpm (recommended package manager) + +### Installation + +```bash +# Clone the repository +git clone https://github.com/nuwa-protocol/nuwa-client.git +cd nuwa-client + +# Install dependencies +pnpm install + +# Start development server +pnpm dev ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default tseslint.config({ - plugins: { - // Add the react-x and react-dom plugins - 'react-x': reactX, - 'react-dom': reactDom, - }, - rules: { - // other rules... - // Enable its recommended typescript rules - ...reactX.configs['recommended-typescript'].rules, - ...reactDom.configs.recommended.rules, - }, -}) +Visit `http://localhost:5173` to start using Nuwa Client. + +## 📖 Development + +### Project Structure + ``` +src/ +├── features/ # Feature-based modules +│ ├── auth/ # Authentication +│ ├── chat/ # Chat functionality +│ ├── cap-studio/ # CAP creation interface +│ ├── cap-store/ # CAP marketplace +│ ├── settings/ # User preferences +│ ├── sidebar/ # Navigation +│ └── wallet/ # Web3 integration +├── shared/ # Shared utilities and components +├── pages/ # Route components +└── layout/ # Layout components +``` + +Each feature follows a consistent structure: +- `components` - React components +- `hooks` - Custom React hooks +- `stores` - Zustand state management +- `services` - Backend logics +- `types` - TypeScript definitions + + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 🤝 Contributing + +We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details on how to get started. + +## 🆘 Support + +- **Documentation**: [docs.nuwa.ai](https://docs.nuwa.dev) +- **Issues**: [GitHub Issues](https://github.com/nuwa-protocol/nuwa-client/issues) +- **Community**: [Discord](https://discord.gg/nuwaai) +- **Email**: haichao@nuwa.dev + +## 🎯 Roadmap + +- [ ] Cap UI Support with inline card and side artifacts +- [ ] Desktop App with Tauri + +--- + +**Built with ❤️ by the Nuwa team** + +Ready to experience the future of AI chat? [Try Nuwa Client Beta](https://test-app.nuwa.dev) today! \ No newline at end of file From 87a5c1ce5bc0ab4d7c0d91b22cd9428ed9c1e44e Mon Sep 17 00:00:00 2001 From: Mine77 Date: Sun, 17 Aug 2025 21:22:30 +0800 Subject: [PATCH 24/28] chore --- src/features/cap-store/components/cap-details.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/features/cap-store/components/cap-details.tsx b/src/features/cap-store/components/cap-details.tsx index 78546a95..d0b29f8f 100644 --- a/src/features/cap-store/components/cap-details.tsx +++ b/src/features/cap-store/components/cap-details.tsx @@ -82,8 +82,6 @@ export function CapDetails() { return did; }; - console.log(cap); - return (
{/* Content */} From 584f85e245410d1c62de02ae0b62d9b957b870db Mon Sep 17 00:00:00 2001 From: Mine77 Date: Mon, 18 Aug 2025 10:52:14 +0800 Subject: [PATCH 25/28] Update readme --- .gitignore | 4 +- CLAUDE.md | 133 --------------------------------------- README.md | 38 ++++++----- src/assets/readme-bg.png | Bin 0 -> 236355 bytes src/main.tsx | 2 +- 5 files changed, 22 insertions(+), 155 deletions(-) delete mode 100644 CLAUDE.md create mode 100644 src/assets/readme-bg.png diff --git a/.gitignore b/.gitignore index be3899a2..c5d7e0aa 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,6 @@ public/caps.json .claude # cursor -.cursor \ No newline at end of file +.cursor + +claude.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 78c1e00a..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,133 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Package Management & Scripts - -This project uses **pnpm** for package management. Common commands: - -```bash -# Development -pnpm dev # Start development server -pnpm build # Build for production -pnpm preview # Preview production build - -# Code Quality -pnpm lint # Lint with Biome -pnpm lint:fix # Fix linting issues -pnpm format # Check formatting -pnpm format:fix # Fix formatting -pnpm check # Run all checks -pnpm check:fix # Fix all issues -``` - -**Important**: Always use `pnpm` commands. The project uses Biome (not ESLint/Prettier) for linting and formatting. - -## Application Architecture - -**Nuwa Client** is a React 19 + TypeScript + Vite application for AI chat with CAP (Conversational AI Programs) creation capabilities, Web3 wallet integration, and decentralized identity. - -### Core Technology Stack - -- **Frontend**: React 19, TypeScript, Vite, React Router v7 -- **Styling**: Tailwind CSS + Radix UI components -- **State**: Zustand with persistence middleware -- **Storage**: Dexie (IndexedDB) for structured data -- **AI**: AI SDK (@ai-sdk/react), OpenRouter, LiteLLM providers -- **Web3**: Reown AppKit, Wagmi, Viem -- **Identity**: @nuwa-ai/identity-kit (decentralized identity) -- **Code Quality**: Biome for linting/formatting - -### Feature-Based Architecture - -The codebase uses a feature-based structure under `src/features/`: - -- **`auth/`** - Authentication and authorization -- **`chat/`** - Core chat functionality with AI models -- **`cap-studio/`** - CAP creation/editing interface (like an IDE) -- **`cap-store/`** - CAP marketplace and discovery -- **`settings/`** - User preferences and configuration -- **`sidebar/`** - Navigation and chat history -- **`wallet/`** - Web3 wallet integration and payments - -Each feature follows this structure: -``` -feature/ -├── components/ # React components -├── hooks/ # Custom React hooks -├── stores.ts # Zustand state stores -├── services.ts # Business logic -├── types.ts # TypeScript definitions -└── utils.ts # Utility functions -``` - -### Key Architectural Concepts - -**CAPs (Conversational AI Programs)**: User-configurable AI assistants with custom prompts, models, and MCP (Model Context Protocol) tool integrations. Users can create, edit, and share CAPs. - -**MCP Integration**: The app connects to MCP servers to provide tools and capabilities to AI models. Managed by `GlobalMCPManager` singleton. - -**Decentralized Identity**: All user data is scoped to their DID (Decentralized Identifier) for privacy and portability. - -**Multi-Layer Storage**: -- Zustand stores (in-memory state) -- localStorage (user preferences) -- IndexedDB via Dexie (structured data: chats, CAPs, settings) - -### Core Services - -**Global Services** (in `src/shared/services/`): -- **`global-mcp-manager.ts`** - Manages MCP server connections and tool registration -- **`identity-kit.ts`** - Decentralized identity management -- **`mcp-client.ts`** - Model Context Protocol client -- **`authorized-fetch.ts`** - Authenticated HTTP requests - -**Key Data Entities**: -- **ChatSession** - Chat conversations with message history -- **Cap** - AI assistant configuration (prompt, model, MCP servers) -- **Settings** - User preferences and app configuration - -### UI Components - -**Shared Components** (in `src/shared/components/ui/`): -- Based on Radix UI primitives with Tailwind styling -- Do not modify files in `ui/` folder - they are generated components -- For custom components, create in feature-specific `components/` folders - -### Development Patterns - -**State Management**: -- Use Zustand stores with persistence middleware -- Store files typically export both store and selectors -- User data automatically scoped by DID - -**Data Fetching**: -- Use SWR for server state management -- Custom hooks in feature `hooks/` folders -- Services handle business logic and API calls - -**Routing**: -- React Router v7 with nested layouts -- Route components in `src/pages/` -- Layout components in `src/layout/` - -**Styling**: -- Tailwind CSS with custom design system -- Radix UI for accessible primitives -- Theme support via next-themes - -### Important Files - -- **`src/main.tsx`** - Application entry point -- **`src/router.tsx`** - Route configuration -- **`src/layout/main-layout.tsx`** - Main application layout -- **`biome.json`** - Biome configuration for linting/formatting -- **`tailwind.config.ts`** - Tailwind CSS configuration - -### Development Notes - -- The app supports both light and dark themes -- All user interfaces are internationalized (i18n support) -- Web3 functionality uses Reown AppKit for wallet connections -- AI model switching is supported via the model selector -- MCP servers can be dynamically added/removed per CAP \ No newline at end of file diff --git a/README.md b/README.md index e75c6e59..732ef37a 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,37 @@ -# Nuwa Client +![Nuwa AI Readme Background](./src/assets/readme-bg.png) -A local-first AI chat client that enables users to create, share, and interact with Caps. +# Nuwa AI Client + +A local-first AI chat client implemented for [Nuwa AI](https://nuwa.dev/) that enables users to create, share, and interact with Caps. + +| Caps (i.e. capability) are mini-apps in Nuwa AI, the minimium functional AI unit. Cap is designed to be an abstraction of AI models and agents. Currently it is the composation of Prompt, Model and MCP Servers. ## ✨ Features -### 🔐 Decentralized Identity & Payment -- **Self-Sovereign Identity**: Your data belongs to you with DID-based authentication -- **Crypto Payment**: Use cryptos to pay for your day-to-day ai +### 🔐 Decentralized Identity & Crypto Payment +- **Decentralized Identity**: You control your data with DID-based authentication, fully anonymous +- **Crypto Payment**: Use cryptos to pay for your day-to-day AI - **Data Portability**: Export and migrate your data anywhere ### 🎨 Modern User Experience -- **Beautiful UI**: Clean, responsive design with dark/light theme support -- **Accessibility**: Built on Radix UI primitives for full accessibility -- **Performance**: Optimized with React 19 and Vite for fast loading +- **Shadcn UI**: Clean, responsive design with dark/light theme support +- **No MCP Configuration**: MCPs use DID authentication directly, eliminates the need for user to config + ## 🚀 Beta Release We're excited to announce the **Nuwa Client Beta**! This release includes: -- ✅ Core CAP creation and sharing functionality -- ✅ Multi-model AI chat with streaming +- ✅ Core CAP creation and publishing functionality - ✅ Web3 wallet integration - ✅ Decentralized identity system - ✅ MCP server integration -- ✅ Payment system for premium features +- ✅ Payment system -### What's Coming Next -- Enhanced CAP marketplace with ratings and reviews -- Advanced MCP tool ecosystem -- Mobile applications (iOS/Android) -- Advanced collaboration features -- Enterprise integrations ## 🛠️ Technology Stack -- **Frontend**: React 19, TypeScript, Vite -- **Routing**: React Router v7 +- **Framerwork**: React 19, TypeScript, Vite - **Styling**: Tailwind CSS + Shadcn UI - **State Management**: Zustand with persistence - **Database**: Dexie (IndexedDB) for local storage @@ -110,7 +106,9 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f ## 🎯 Roadmap -- [ ] Cap UI Support with inline card and side artifacts +- [ ] Cap Inline UI Support +- [ ] Cap Artifacts UI Support +- [ ] Multi-Modal Input Support - [ ] Desktop App with Tauri --- diff --git a/src/assets/readme-bg.png b/src/assets/readme-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..3962085e2f711a18d02a61dc50e3617bbb811cbd GIT binary patch literal 236355 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

b01;6P;_0T?&8oPZsSPCbZN}Xo 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 fBEc8(yfY+K{`%r&?md2n-aR|Nn9ewE6?4jgj= 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 literal 0 HcmV?d00001 diff --git a/src/main.tsx b/src/main.tsx index 9f4c889a..27a0ca5a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,8 +2,8 @@ import './index.css'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { RouterProvider } from 'react-router-dom'; -import router from './router.tsx'; import { GlobalMCPManager } from '@/shared/services/global-mcp-manager'; +import router from './router.tsx'; import '@fontsource/geist-sans/400.css'; import '@fontsource/geist-sans/500.css'; import '@fontsource/geist-sans/600.css'; From ebc17ff70fa75f121bace2b639e0acf21ea0b681 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Mon, 18 Aug 2025 10:57:39 +0800 Subject: [PATCH 26/28] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 732ef37a..14926817 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A local-first AI chat client implemented for [Nuwa AI](https://nuwa.dev/) that enables users to create, share, and interact with Caps. -| Caps (i.e. capability) are mini-apps in Nuwa AI, the minimium functional AI unit. Cap is designed to be an abstraction of AI models and agents. Currently it is the composation of Prompt, Model and MCP Servers. +> **Caps** (i.e. capability) are mini-apps in Nuwa AI, the minimium functional AI unit. Cap is designed to be an abstraction of AI models and agents. Currently it is the composation of Prompt, Model and MCP Servers. ## ✨ Features @@ -99,7 +99,7 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f ## 🆘 Support -- **Documentation**: [docs.nuwa.ai](https://docs.nuwa.dev) +- **Documentation**: [docs.nuwa.dev](https://docs.nuwa.dev) - **Issues**: [GitHub Issues](https://github.com/nuwa-protocol/nuwa-client/issues) - **Community**: [Discord](https://discord.gg/nuwaai) - **Email**: haichao@nuwa.dev @@ -115,4 +115,4 @@ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) f **Built with ❤️ by the Nuwa team** -Ready to experience the future of AI chat? [Try Nuwa Client Beta](https://test-app.nuwa.dev) today! \ No newline at end of file +Ready to experience the future of AI chat? [Try Nuwa Client Beta](https://test-app.nuwa.dev) today! From b83a18f04045ed7b48b723b8df48a6e4de124a2a Mon Sep 17 00:00:00 2001 From: Mine77 Date: Mon, 18 Aug 2025 12:04:56 +0800 Subject: [PATCH 27/28] feat: add resend button to user message and fix the edit function --- .../chat/components/message-editor.tsx | 21 ++-- src/features/chat/components/message-text.tsx | 98 +++++++++++++++---- src/features/chat/components/message.tsx | 5 - src/features/chat/hooks/use-chat-sessions.ts | 40 ++++---- 4 files changed, 111 insertions(+), 53 deletions(-) diff --git a/src/features/chat/components/message-editor.tsx b/src/features/chat/components/message-editor.tsx index 0ca33e37..16562862 100644 --- a/src/features/chat/components/message-editor.tsx +++ b/src/features/chat/components/message-editor.tsx @@ -30,7 +30,7 @@ export function MessageEditor({ const [draftContent, setDraftContent] = useState(message.content); const textareaRef = useRef(null); - const { deleteMessagesAfterTimestamp } = useChatSessions(); + const { deleteMessagesAfterId } = useChatSessions(); useEffect(() => { if (textareaRef.current) { @@ -78,11 +78,7 @@ export function MessageEditor({ onClick={async () => { setIsSubmitting(true); - // Delete trailing messages using client store - if (message.createdAt) { - const messageTime = new Date(message.createdAt).getTime(); - deleteMessagesAfterTimestamp(chatId, messageTime); - } + // @ts-expect-error todo: support UIMessage in setMessages setMessages((messages) => { @@ -95,12 +91,21 @@ export function MessageEditor({ parts: [{ type: 'text', text: draftContent }], }; - return [...messages.slice(0, index), updatedMessage]; - } + const updatedMessages = [...messages.slice(0, index), updatedMessage]; + console.log('updatedMessage UI', updatedMessage); + return updatedMessages; + } return messages; }); + await deleteMessagesAfterId(chatId, message.id, { + id: message.id, + content: draftContent, + role: message.role, + parts: [{ type: 'text', text: draftContent }], + }); + setMode('view'); reload(); }} diff --git a/src/features/chat/components/message-text.tsx b/src/features/chat/components/message-text.tsx index 9a5e893c..379cc497 100644 --- a/src/features/chat/components/message-text.tsx +++ b/src/features/chat/components/message-text.tsx @@ -1,14 +1,23 @@ import type { UseChatHelpers } from '@ai-sdk/react'; import type { UIMessage } from 'ai'; -import { ChevronDownIcon, ChevronUpIcon, PencilIcon } from 'lucide-react'; +import { + ChevronDownIcon, + ChevronUpIcon, + CopyIcon, + PencilIcon, + RotateCcwIcon, +} from 'lucide-react'; import { useState } from 'react'; +import { toast } from 'sonner'; import { Button } from '@/shared/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/shared/components/ui/tooltip'; +import { useCopyToClipboard } from '@/shared/hooks/use-copy-to-clipboard'; import { cn, sanitizeText } from '@/shared/utils'; +import { useChatSessions } from '../hooks'; import { Markdown } from './markdown'; import { MessageEditor } from './message-editor'; @@ -37,6 +46,8 @@ export const MessageText = ({ }: MessageTextProps) => { const [mode, setMode] = useState<'view' | 'edit'>('view'); const [isExpanded, setIsExpanded] = useState(false); + const [copyToClipboard, isCopied] = useCopyToClipboard(); + const { deleteMessagesAfterId } = useChatSessions(); const key = `message-${message.id}-part-${index}`; @@ -45,6 +56,21 @@ export const MessageText = ({ onModeChange(newMode); }; + const handleResend = async () => { + // TODO: Implement resend functionality + console.log('Resend message:', message); + + // Delete trailing messages using client store + await deleteMessagesAfterId(chatId, message.id); + + reload(); + }; + + const handleCopy = () => { + copyToClipboard(part.text); + toast.success('Copied to clipboard'); + }; + if (mode === 'view') { // Check if user message is long and should be collapsible const isUserMessageLong = @@ -55,25 +81,7 @@ export const MessageText = ({ : part.text; return ( -

- {message.role === 'user' && !isReadonly && ( - - - - - Edit message - - )} - +
)}
+ + {message.role === 'user' && !isReadonly && ( +
+ + + + + + {isCopied ? 'Copied!' : 'Copy message'} + + + + + + + + Resend message + + + + + + + Edit message + +
+ )}
); } diff --git a/src/features/chat/components/message.tsx b/src/features/chat/components/message.tsx index 3ebbb28f..241048f2 100644 --- a/src/features/chat/components/message.tsx +++ b/src/features/chat/components/message.tsx @@ -59,11 +59,6 @@ const PurePreviewMessage = ({ {message.parts?.map((part, index) => { if (part.type !== 'reasoning') return null; return ( - // { ); }, [store.sessions]); - const deleteMessagesAfterTimestamp = useCallback( - async (chatId: string, timestamp: number) => { - const currentSession = store.getChatSession(chatId); - if (!currentSession) return; + const deleteMessagesAfterId = async ( + chatId: string, + messageId: string, + lastMessage?: Message, + ) => { + const currentSession = store.getChatSession(chatId); + if (!currentSession) return; - const updatedMessages = currentSession.messages.filter((msg) => { - const messageTime = msg.createdAt - ? new Date(msg.createdAt).getTime() - : 0; - return messageTime < timestamp; - }); + const messageIndex = currentSession.messages.findIndex( + (msg) => msg.id === messageId, + ); + if (messageIndex === -1) return; - const updatedSession: ChatSession = { - ...currentSession, - messages: updatedMessages, - updatedAt: Date.now(), - }; + const updatedMessages = lastMessage + ? [...currentSession.messages.slice(0, messageIndex), lastMessage] + : currentSession.messages.slice(0, messageIndex + 1); - await store.updateSession(chatId, updatedSession); - }, - [store], - ); + console.log('updatedMessages', updatedMessages); + + await store.updateSession(chatId, { messages: updatedMessages }); + }; return { sessions: getSortedSessions(), @@ -60,6 +60,6 @@ export const useChatSessions = () => { deleteSession, updateSession, clearAllSessions, - deleteMessagesAfterTimestamp, + deleteMessagesAfterId, }; }; From 3ebcb5e6c6822bdd7cde282c8ff38615d63c64d2 Mon Sep 17 00:00:00 2001 From: Mine77 Date: Mon, 18 Aug 2025 12:13:03 +0800 Subject: [PATCH 28/28] fix: user message style --- src/features/chat/components/message-text.tsx | 13 ++++++------- src/shared/utils/index.ts | 6 +----- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/features/chat/components/message-text.tsx b/src/features/chat/components/message-text.tsx index 379cc497..23712690 100644 --- a/src/features/chat/components/message-text.tsx +++ b/src/features/chat/components/message-text.tsx @@ -16,7 +16,7 @@ import { TooltipTrigger, } from '@/shared/components/ui/tooltip'; import { useCopyToClipboard } from '@/shared/hooks/use-copy-to-clipboard'; -import { cn, sanitizeText } from '@/shared/utils'; +import { cn } from '@/shared/utils'; import { useChatSessions } from '../hooks'; import { Markdown } from './markdown'; import { MessageEditor } from './message-editor'; @@ -57,9 +57,6 @@ export const MessageText = ({ }; const handleResend = async () => { - // TODO: Implement resend functionality - console.log('Resend message:', message); - // Delete trailing messages using client store await deleteMessagesAfterId(chatId, message.id); @@ -81,15 +78,17 @@ export const MessageText = ({ : part.text; return ( -
+
- {sanitizeText(displayText)} + {displayText} {isUserMessageLong && (