mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: initialize monorepo with package.json for CCW workflow platform
This commit is contained in:
@@ -32,7 +32,7 @@ type TabType = 'providers' | 'endpoints' | 'cache' | 'modelPools' | 'cliSettings
|
||||
|
||||
export function ApiSettingsPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const { showNotification } = useNotifications();
|
||||
const { success, error } = useNotifications();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('providers');
|
||||
|
||||
// Get providers, endpoints, model pools, and CLI settings data
|
||||
@@ -172,9 +172,9 @@ export function ApiSettingsPage() {
|
||||
try {
|
||||
// TODO: Implement actual sync API call
|
||||
// For now, just show a success message
|
||||
showNotification('success', formatMessage({ id: 'apiSettings.messages.configSynced' }));
|
||||
} catch (error) {
|
||||
showNotification('error', formatMessage({ id: 'apiSettings.providers.saveError' }));
|
||||
success(formatMessage({ id: 'apiSettings.messages.configSynced' }));
|
||||
} catch (err) {
|
||||
error(formatMessage({ id: 'apiSettings.providers.saveError' }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -80,21 +80,13 @@ export function CommandsManagerPage() {
|
||||
};
|
||||
|
||||
// Toggle individual command
|
||||
const handleToggleCommand = async (name: string, enabled: boolean) => {
|
||||
try {
|
||||
await toggleCommand(name, enabled, locationFilter);
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle command:', error);
|
||||
}
|
||||
const handleToggleCommand = (name: string, enabled: boolean) => {
|
||||
toggleCommand(name, enabled, locationFilter);
|
||||
};
|
||||
|
||||
// Toggle all commands in a group
|
||||
const handleToggleGroup = async (groupName: string, enable: boolean) => {
|
||||
try {
|
||||
await toggleGroup(groupName, enable, locationFilter);
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle group:', error);
|
||||
}
|
||||
const handleToggleGroup = (groupName: string, enable: boolean) => {
|
||||
toggleGroup(groupName, enable, locationFilter);
|
||||
};
|
||||
|
||||
// Calculate command counts per location
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// ========================================
|
||||
// Help Page
|
||||
// ========================================
|
||||
// Help documentation and guides
|
||||
// Help documentation and guides with link to full documentation
|
||||
|
||||
import {
|
||||
HelpCircle,
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
Workflow,
|
||||
FolderKanban,
|
||||
Terminal,
|
||||
FileText,
|
||||
ArrowRight,
|
||||
Search,
|
||||
Code,
|
||||
Layers,
|
||||
} from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useIntl } from 'react-intl';
|
||||
@@ -25,6 +30,7 @@ interface HelpSection {
|
||||
icon: React.ElementType;
|
||||
link?: string;
|
||||
isExternal?: boolean;
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
interface HelpSectionConfig {
|
||||
@@ -34,6 +40,7 @@ interface HelpSectionConfig {
|
||||
icon: React.ElementType;
|
||||
link?: string;
|
||||
isExternal?: boolean;
|
||||
badge?: string;
|
||||
}
|
||||
|
||||
const helpSectionsConfig: HelpSectionConfig[] = [
|
||||
@@ -42,13 +49,25 @@ const helpSectionsConfig: HelpSectionConfig[] = [
|
||||
descriptionKey: 'home.help.gettingStarted.description',
|
||||
headingKey: 'home.help.gettingStarted.heading',
|
||||
icon: Book,
|
||||
link: '#getting-started',
|
||||
link: '/docs/overview',
|
||||
isExternal: false,
|
||||
badge: 'Docs',
|
||||
},
|
||||
{
|
||||
i18nKey: 'home.help.orchestratorGuide.title',
|
||||
descriptionKey: 'home.help.orchestratorGuide.description',
|
||||
icon: Workflow,
|
||||
link: '/orchestrator',
|
||||
link: '/docs/workflows/introduction',
|
||||
isExternal: false,
|
||||
badge: 'Docs',
|
||||
},
|
||||
{
|
||||
i18nKey: 'home.help.commands.title',
|
||||
descriptionKey: 'home.help.commands.description',
|
||||
icon: Terminal,
|
||||
link: '/docs/commands',
|
||||
isExternal: false,
|
||||
badge: 'Docs',
|
||||
},
|
||||
{
|
||||
i18nKey: 'home.help.sessionsManagement.title',
|
||||
@@ -56,13 +75,6 @@ const helpSectionsConfig: HelpSectionConfig[] = [
|
||||
icon: FolderKanban,
|
||||
link: '/sessions',
|
||||
},
|
||||
{
|
||||
i18nKey: 'home.help.cliIntegration.title',
|
||||
descriptionKey: 'home.help.cliIntegration.description',
|
||||
headingKey: 'home.help.cliIntegration.heading',
|
||||
icon: Terminal,
|
||||
link: '#cli-integration',
|
||||
},
|
||||
];
|
||||
|
||||
export function HelpPage() {
|
||||
@@ -76,44 +88,71 @@ export function HelpPage() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Page Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
|
||||
<HelpCircle className="w-6 h-6 text-primary" />
|
||||
{formatMessage({ id: 'help.title' })}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'help.description' })}
|
||||
</p>
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
{/* Page Header with CTA */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h1 className="text-3xl font-bold text-foreground flex items-center gap-3">
|
||||
<HelpCircle className="w-8 h-8 text-primary" />
|
||||
{formatMessage({ id: 'help.title' })}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2 text-lg">
|
||||
{formatMessage({ id: 'help.description' })}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
className="gap-2"
|
||||
asChild
|
||||
>
|
||||
<a href="/docs" target="_blank" rel="noopener noreferrer">
|
||||
<FileText className="w-4 h-4" />
|
||||
{formatMessage({ id: 'help.fullDocs' })}
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{helpSections.map((section) => {
|
||||
const Icon = section.icon;
|
||||
const isDocsLink = section.link?.startsWith('/docs');
|
||||
const content = (
|
||||
<Card className="p-4 h-full hover:shadow-md hover:border-primary/50 transition-all cursor-pointer group">
|
||||
<Card className="p-5 h-full hover:shadow-lg hover:border-primary/50 transition-all cursor-pointer group relative">
|
||||
{section.badge && (
|
||||
<div className="absolute top-3 right-3 px-2 py-0.5 text-xs font-medium bg-primary/10 text-primary rounded-full">
|
||||
{section.badge}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10 text-primary">
|
||||
<div className="p-2.5 rounded-lg bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-foreground group-hover:text-primary transition-colors">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-foreground group-hover:text-primary transition-colors">
|
||||
{formatMessage({ id: section.i18nKey })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
<p className="text-sm text-muted-foreground mt-1.5 line-clamp-2">
|
||||
{formatMessage({ id: section.descriptionI18nKey })}
|
||||
</p>
|
||||
</div>
|
||||
{section.isExternal && (
|
||||
<ExternalLink className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
{isDocsLink || section.isExternal ? (
|
||||
<ExternalLink className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors flex-shrink-0 mt-1" />
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
if (section.link?.startsWith('/')) {
|
||||
if (section.link?.startsWith('/docs')) {
|
||||
return (
|
||||
<a key={section.i18nKey} href={section.link} className="block">
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (section.link?.startsWith('/') && !section.link.startsWith('/docs')) {
|
||||
return (
|
||||
<Link key={section.i18nKey} to={section.link}>
|
||||
{content}
|
||||
@@ -122,77 +161,143 @@ export function HelpPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<a key={section.i18nKey} href={section.link}>
|
||||
<a key={section.i18nKey} href={section.link} target="_blank" rel="noopener noreferrer">
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Getting Started Section */}
|
||||
<Card className="p-6" id="getting-started">
|
||||
<h2 className="text-xl font-semibold text-foreground mb-4">
|
||||
{formatMessage({ id: 'home.help.gettingStarted.heading' })}
|
||||
</h2>
|
||||
<div className="prose prose-sm max-w-none text-muted-foreground">
|
||||
<p>
|
||||
CCW (Claude Code Workflow) Dashboard is your central hub for managing
|
||||
AI-powered development workflows. Here are the key concepts:
|
||||
{/* Documentation Overview Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Commands Card */}
|
||||
<Card className="p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
|
||||
<Terminal className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{formatMessage({ id: 'help.commandsOverview.title' })}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{formatMessage({ id: 'help.commandsOverview.description' })}
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2">
|
||||
<li>
|
||||
<strong className="text-foreground">Sessions</strong> - Track the
|
||||
progress of multi-step development tasks
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Orchestrator</strong> - Visual
|
||||
workflow builder for creating automation flows
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Loops</strong> - Monitor
|
||||
iterative development cycles in real-time
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Skills</strong> - Extend Claude
|
||||
Code with custom capabilities
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Memory</strong> - Store context
|
||||
and knowledge for better AI assistance
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Layers className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Workflow Commands</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Layers className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Issue Commands</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Layers className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">CLI & Memory Commands</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="w-full mt-4" asChild>
|
||||
<a href="/docs/commands">
|
||||
{formatMessage({ id: 'help.viewAll' })}
|
||||
<ArrowRight className="w-4 h-4 ml-1" />
|
||||
</a>
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* CLI Integration Section */}
|
||||
<Card className="p-6" id="cli-integration">
|
||||
<h2 className="text-xl font-semibold text-foreground mb-4">
|
||||
{formatMessage({ id: 'home.help.cliIntegration.heading' })}
|
||||
</h2>
|
||||
<div className="prose prose-sm max-w-none text-muted-foreground">
|
||||
<p>
|
||||
CCW integrates with multiple CLI tools for AI-assisted development:
|
||||
{/* Workflows Card */}
|
||||
<Card className="p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-green-500/10 text-green-500">
|
||||
<Workflow className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{formatMessage({ id: 'help.workflowsOverview.title' })}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{formatMessage({ id: 'help.workflowsOverview.description' })}
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2">
|
||||
<li>
|
||||
<code className="bg-muted px-1.5 py-0.5 rounded text-foreground">
|
||||
ccw cli -p "prompt" --tool gemini
|
||||
</code>
|
||||
- Execute with Gemini
|
||||
</li>
|
||||
<li>
|
||||
<code className="bg-muted px-1.5 py-0.5 rounded text-foreground">
|
||||
ccw cli -p "prompt" --tool qwen
|
||||
</code>
|
||||
- Execute with Qwen
|
||||
</li>
|
||||
<li>
|
||||
<code className="bg-muted px-1.5 py-0.5 rounded text-foreground">
|
||||
ccw cli -p "prompt" --tool codex
|
||||
</code>
|
||||
- Execute with Codex
|
||||
</li>
|
||||
</ul>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Code className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Level 1-5 Workflows</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Search className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Interactive Diagrams</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<FileText className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Best Practices</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="w-full mt-4" asChild>
|
||||
<a href="/docs/workflows">
|
||||
{formatMessage({ id: 'help.viewAll' })}
|
||||
<ArrowRight className="w-4 h-4 ml-1" />
|
||||
</a>
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* Quick Start Card */}
|
||||
<Card className="p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-500">
|
||||
<Book className="w-5 h-5" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-foreground">
|
||||
{formatMessage({ id: 'help.quickStart.title' })}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{formatMessage({ id: 'help.quickStart.description' })}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<ExternalLink className="w-4 h-4 text-muted-foreground" />
|
||||
<a href="/docs/overview" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
{formatMessage({ id: 'help.quickStart.guide' })}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<MessageCircle className="w-4 h-4 text-muted-foreground" />
|
||||
<a href="/docs/faq" className="text-muted-foreground hover:text-foreground transition-colors">
|
||||
{formatMessage({ id: 'help.quickStart.faq' })}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="w-full mt-4" asChild>
|
||||
<a href="/docs/overview">
|
||||
{formatMessage({ id: 'help.getStarted' })}
|
||||
<ArrowRight className="w-4 h-4 ml-1" />
|
||||
</a>
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search Documentation CTA */}
|
||||
<Card className="p-8 bg-gradient-to-r from-primary/5 to-primary/10 border-primary/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 rounded-lg bg-primary/20">
|
||||
<Search className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
{formatMessage({ id: 'help.searchDocs.title' })}
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{formatMessage({ id: 'help.searchDocs.description' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="default" className="gap-2" asChild>
|
||||
<a href="/docs">
|
||||
{formatMessage({ id: 'help.searchDocs.button' })}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -210,13 +315,17 @@ export function HelpPage() {
|
||||
{formatMessage({ id: 'help.support.description' })}
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" size="sm">
|
||||
<Book className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'help.support.documentation' })}
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href="/docs/faq">
|
||||
<Book className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'help.support.documentation' })}
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Video className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'help.support.tutorials' })}
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href="https://github.com/catlog22/Claude-Code-Workflow/issues" target="_blank" rel="noopener noreferrer">
|
||||
<Video className="w-4 h-4 mr-2" />
|
||||
{formatMessage({ id: 'help.support.tutorials' })}
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,154 +4,60 @@
|
||||
// Dashboard home page with stat cards and recent sessions
|
||||
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
FolderKanban,
|
||||
ListChecks,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
XCircle,
|
||||
Activity,
|
||||
RefreshCw,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { useDashboardStats } from '@/hooks/useDashboardStats';
|
||||
import { useSessions } from '@/hooks/useSessions';
|
||||
import { StatCard, StatCardSkeleton } from '@/components/shared/StatCard';
|
||||
import { SessionCard, SessionCardSkeleton } from '@/components/shared/SessionCard';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { DashboardHeader } from '@/components/dashboard/DashboardHeader';
|
||||
import { DashboardGridContainer } from '@/components/dashboard/DashboardGridContainer';
|
||||
import { DetailedStatsWidget } from '@/components/dashboard/widgets/DetailedStatsWidget';
|
||||
import { RecentSessionsWidget } from '@/components/dashboard/widgets/RecentSessionsWidget';
|
||||
import { ChartSkeleton } from '@/components/charts';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useUserDashboardLayout } from '@/hooks/useUserDashboardLayout';
|
||||
import { WIDGET_IDS } from '@/components/dashboard/defaultLayouts';
|
||||
|
||||
// Code-split chart widgets for better initial load performance
|
||||
const WorkflowStatusPieChartWidget = lazy(() => import('@/components/dashboard/widgets/WorkflowStatusPieChartWidget'));
|
||||
const ActivityLineChartWidget = lazy(() => import('@/components/dashboard/widgets/ActivityLineChartWidget'));
|
||||
const TaskTypeBarChartWidget = lazy(() => import('@/components/dashboard/widgets/TaskTypeBarChartWidget'));
|
||||
|
||||
/**
|
||||
* HomePage component - Dashboard overview with statistics and recent sessions
|
||||
* HomePage component - Dashboard overview with widget-based layout
|
||||
*/
|
||||
export function HomePage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const { resetLayout } = useUserDashboardLayout();
|
||||
|
||||
// Stat card configuration
|
||||
const statCards = React.useMemo(() => [
|
||||
{
|
||||
key: 'activeSessions',
|
||||
title: formatMessage({ id: 'home.stats.activeSessions' }),
|
||||
icon: FolderKanban,
|
||||
variant: 'primary' as const,
|
||||
getValue: (stats: { activeSessions: number }) => stats.activeSessions,
|
||||
},
|
||||
{
|
||||
key: 'totalTasks',
|
||||
title: formatMessage({ id: 'home.stats.totalTasks' }),
|
||||
icon: ListChecks,
|
||||
variant: 'info' as const,
|
||||
getValue: (stats: { totalTasks: number }) => stats.totalTasks,
|
||||
},
|
||||
{
|
||||
key: 'completedTasks',
|
||||
title: formatMessage({ id: 'home.stats.completedTasks' }),
|
||||
icon: CheckCircle2,
|
||||
variant: 'success' as const,
|
||||
getValue: (stats: { completedTasks: number }) => stats.completedTasks,
|
||||
},
|
||||
{
|
||||
key: 'pendingTasks',
|
||||
title: formatMessage({ id: 'home.stats.pendingTasks' }),
|
||||
icon: Clock,
|
||||
variant: 'warning' as const,
|
||||
getValue: (stats: { pendingTasks: number }) => stats.pendingTasks,
|
||||
},
|
||||
{
|
||||
key: 'failedTasks',
|
||||
title: formatMessage({ id: 'common.status.failed' }),
|
||||
icon: XCircle,
|
||||
variant: 'danger' as const,
|
||||
getValue: (stats: { failedTasks: number }) => stats.failedTasks,
|
||||
},
|
||||
{
|
||||
key: 'todayActivity',
|
||||
title: formatMessage({ id: 'common.stats.todayActivity' }),
|
||||
icon: Activity,
|
||||
variant: 'default' as const,
|
||||
getValue: (stats: { todayActivity: number }) => stats.todayActivity,
|
||||
},
|
||||
], [formatMessage]);
|
||||
// Track errors from widgets (optional, for future enhancements)
|
||||
const [hasError, _setHasError] = React.useState(false);
|
||||
|
||||
// Fetch dashboard stats
|
||||
const {
|
||||
stats,
|
||||
isLoading: statsLoading,
|
||||
isFetching: statsFetching,
|
||||
error: statsError,
|
||||
refetch: refetchStats,
|
||||
} = useDashboardStats({
|
||||
refetchInterval: 60000, // Refetch every minute
|
||||
});
|
||||
|
||||
// Fetch recent sessions (active only, limited)
|
||||
const {
|
||||
activeSessions,
|
||||
isLoading: sessionsLoading,
|
||||
isFetching: sessionsFetching,
|
||||
error: sessionsError,
|
||||
refetch: refetchSessions,
|
||||
} = useSessions({
|
||||
filter: { location: 'active' },
|
||||
});
|
||||
|
||||
// Get recent sessions (max 6)
|
||||
const recentSessions = React.useMemo(
|
||||
() =>
|
||||
[...activeSessions]
|
||||
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||
.slice(0, 6),
|
||||
[activeSessions]
|
||||
);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await Promise.all([refetchStats(), refetchSessions()]);
|
||||
const handleRefresh = () => {
|
||||
// Trigger refetch by reloading the page or using React Query's invalidateQueries
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const handleSessionClick = (sessionId: string) => {
|
||||
navigate(`/sessions/${sessionId}`);
|
||||
const handleResetLayout = () => {
|
||||
resetLayout();
|
||||
};
|
||||
|
||||
const handleViewAllSessions = () => {
|
||||
navigate('/sessions');
|
||||
};
|
||||
|
||||
const isLoading = statsLoading || sessionsLoading;
|
||||
const isFetching = statsFetching || sessionsFetching;
|
||||
const hasError = statsError || sessionsError;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">{formatMessage({ id: 'home.title' })}</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{formatMessage({ id: 'home.description' })}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isFetching}
|
||||
>
|
||||
<RefreshCw className={cn('h-4 w-4 mr-2', isFetching && 'animate-spin')} />
|
||||
{formatMessage({ id: 'common.actions.refresh' })}
|
||||
</Button>
|
||||
</div>
|
||||
<DashboardHeader
|
||||
titleKey="home.dashboard.title"
|
||||
descriptionKey="home.dashboard.description"
|
||||
onRefresh={handleRefresh}
|
||||
onResetLayout={handleResetLayout}
|
||||
/>
|
||||
|
||||
{/* Error alert */}
|
||||
{/* Error alert (optional, shown if widgets encounter critical errors) */}
|
||||
{hasError && (
|
||||
<div className="flex items-center gap-2 p-4 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive">
|
||||
<AlertCircle className="h-5 w-5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">{formatMessage({ id: 'home.errors.loadFailed' })}</p>
|
||||
<p className="text-xs mt-0.5">
|
||||
{(statsError || sessionsError)?.message || formatMessage({ id: 'common.errors.unknownError' })}
|
||||
{formatMessage({ id: 'common.errors.unknownError' })}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh}>
|
||||
@@ -160,67 +66,29 @@ export function HomePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Grid */}
|
||||
<section>
|
||||
<h2 className="text-lg font-medium text-foreground mb-4">{formatMessage({ id: 'home.sections.statistics' })}</h2>
|
||||
<div className="grid gap-4 grid-cols-2 md:grid-cols-3 lg:grid-cols-6">
|
||||
{isLoading
|
||||
? // Loading skeletons
|
||||
Array.from({ length: 6 }).map((_, i) => <StatCardSkeleton key={i} />)
|
||||
: // Actual stat cards
|
||||
statCards.map((card) => (
|
||||
<StatCard
|
||||
key={card.key}
|
||||
title={card.title}
|
||||
value={stats ? card.getValue(stats as any) : 0}
|
||||
icon={card.icon}
|
||||
variant={card.variant}
|
||||
isLoading={isFetching && !stats}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
{/* Dashboard Grid with Widgets */}
|
||||
<DashboardGridContainer isDraggable={true} isResizable={true}>
|
||||
{/* Widget 1: Detailed Stats */}
|
||||
<DetailedStatsWidget key={WIDGET_IDS.STATS} />
|
||||
|
||||
{/* Recent Sessions */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-medium text-foreground">{formatMessage({ id: 'home.sections.recentSessions' })}</h2>
|
||||
<Button variant="link" size="sm" onClick={handleViewAllSessions}>
|
||||
{formatMessage({ id: 'common.actions.viewAll' })}
|
||||
</Button>
|
||||
</div>
|
||||
{/* Widget 2: Recent Sessions */}
|
||||
<RecentSessionsWidget key={WIDGET_IDS.RECENT_SESSIONS} />
|
||||
|
||||
{sessionsLoading ? (
|
||||
// Loading skeletons
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<SessionCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
) : recentSessions.length === 0 ? (
|
||||
// Empty state
|
||||
<div className="flex flex-col items-center justify-center py-12 px-4 border border-dashed border-border rounded-lg">
|
||||
<FolderKanban className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-1">{formatMessage({ id: 'home.emptyState.noSessions.title' })}</h3>
|
||||
<p className="text-sm text-muted-foreground text-center max-w-sm">
|
||||
{formatMessage({ id: 'home.emptyState.noSessions.message' })}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
// Session cards grid
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||
{recentSessions.map((session) => (
|
||||
<SessionCard
|
||||
key={session.session_id}
|
||||
session={session}
|
||||
onClick={handleSessionClick}
|
||||
onView={handleSessionClick}
|
||||
showActions={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
{/* Widget 3: Workflow Status Pie Chart (code-split with Suspense fallback) */}
|
||||
<Suspense fallback={<ChartSkeleton type="pie" height={280} />}>
|
||||
<WorkflowStatusPieChartWidget key={WIDGET_IDS.WORKFLOW_STATUS} />
|
||||
</Suspense>
|
||||
|
||||
{/* Widget 4: Activity Line Chart (code-split with Suspense fallback) */}
|
||||
<Suspense fallback={<ChartSkeleton type="line" height={280} />}>
|
||||
<ActivityLineChartWidget key={WIDGET_IDS.ACTIVITY} />
|
||||
</Suspense>
|
||||
|
||||
{/* Widget 5: Task Type Bar Chart (code-split with Suspense fallback) */}
|
||||
<Suspense fallback={<ChartSkeleton type="bar" height={280} />}>
|
||||
<TaskTypeBarChartWidget key={WIDGET_IDS.TASK_TYPES} />
|
||||
</Suspense>
|
||||
</DashboardGridContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,15 +19,17 @@ import {
|
||||
Brain,
|
||||
Shield,
|
||||
Sparkles,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from 'lucide-react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { EventGroup, HookFormDialog, HookQuickTemplates, HookWizard, type HookCardData, type HookFormData, type HookTriggerType, HOOK_TEMPLATES, type WizardType } from '@/components/hook';
|
||||
import { HookCard, HookFormDialog, HookQuickTemplates, HookWizard, type HookCardData, type HookFormData, type HookTriggerType, HOOK_TEMPLATES, type WizardType } from '@/components/hook';
|
||||
import { useHooks, useToggleHook } from '@/hooks';
|
||||
import { installHookTemplate, createHook } from '@/lib/api';
|
||||
import { installHookTemplate } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// ========== Types ==========
|
||||
@@ -100,30 +102,42 @@ function getTriggerStats(hooksByTrigger: HooksByTrigger) {
|
||||
export function HookManagerPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedTrigger, setSelectedTrigger] = useState<HookTriggerType | 'all'>('all');
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [dialogMode, setDialogMode] = useState<'create' | 'edit'>('create');
|
||||
const [editingHook, setEditingHook] = useState<HookCardData | undefined>();
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [wizardType, setWizardType] = useState<WizardType>('memory-update');
|
||||
const [expandedHooks, setExpandedHooks] = useState<Set<string>>(new Set());
|
||||
const [templatesExpanded, setTemplatesExpanded] = useState(false);
|
||||
const [wizardsExpanded, setWizardsExpanded] = useState(false);
|
||||
|
||||
const { hooks, enabledCount, totalCount, isLoading, refetch } = useHooks();
|
||||
const { toggleHook } = useToggleHook();
|
||||
|
||||
// Convert hooks to HookCardData and filter by search query
|
||||
// Convert hooks to HookCardData and filter by search query and trigger type
|
||||
const filteredHooks = useMemo(() => {
|
||||
const validHooks = hooks.map(toHookCardData).filter((h): h is HookCardData => h !== null);
|
||||
let validHooks = hooks.map(toHookCardData).filter((h): h is HookCardData => h !== null);
|
||||
|
||||
if (!searchQuery.trim()) return validHooks;
|
||||
// Filter by trigger type
|
||||
if (selectedTrigger !== 'all') {
|
||||
validHooks = validHooks.filter(h => h.trigger === selectedTrigger);
|
||||
}
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return validHooks.filter(
|
||||
(h) =>
|
||||
h.name.toLowerCase().includes(query) ||
|
||||
(h.description && h.description.toLowerCase().includes(query)) ||
|
||||
h.trigger.toLowerCase().includes(query) ||
|
||||
(h.command && h.command.toLowerCase().includes(query))
|
||||
);
|
||||
}, [hooks, searchQuery]);
|
||||
// Filter by search query
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
validHooks = validHooks.filter(
|
||||
(h) =>
|
||||
h.name.toLowerCase().includes(query) ||
|
||||
(h.description && h.description.toLowerCase().includes(query)) ||
|
||||
h.trigger.toLowerCase().includes(query) ||
|
||||
(h.command && h.command.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
return validHooks;
|
||||
}, [hooks, searchQuery, selectedTrigger]);
|
||||
|
||||
// Group hooks by trigger type
|
||||
const hooksByTrigger = useMemo(() => groupHooksByTrigger(filteredHooks), [filteredHooks]);
|
||||
@@ -155,6 +169,18 @@ export function HookManagerPage() {
|
||||
await refetch();
|
||||
};
|
||||
|
||||
const handleToggleHookExpand = (hookName: string) => {
|
||||
setExpandedHooks((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(hookName)) {
|
||||
next.delete(hookName);
|
||||
} else {
|
||||
next.add(hookName);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// ========== Wizard Handlers ==========
|
||||
|
||||
const wizardTypes: Array<{ type: WizardType; icon: typeof Brain; label: string; description: string }> = [
|
||||
@@ -183,17 +209,6 @@ export function HookManagerPage() {
|
||||
setWizardOpen(true);
|
||||
};
|
||||
|
||||
const handleWizardComplete = async (hookConfig: {
|
||||
name: string;
|
||||
description: string;
|
||||
trigger: string;
|
||||
matcher?: string;
|
||||
command: string;
|
||||
}) => {
|
||||
await createHook(hookConfig);
|
||||
await refetch();
|
||||
};
|
||||
|
||||
// ========== Quick Templates Logic ==========
|
||||
|
||||
// Determine which templates are already installed
|
||||
@@ -221,12 +236,13 @@ export function HookManagerPage() {
|
||||
await installMutation.mutateAsync(templateId);
|
||||
};
|
||||
|
||||
const TRIGGER_TYPES: Array<{ type: HookTriggerType; icon: typeof Zap }> = [
|
||||
{ type: 'SessionStart', icon: Play },
|
||||
{ type: 'UserPromptSubmit', icon: Zap },
|
||||
{ type: 'PreToolUse', icon: Wrench },
|
||||
{ type: 'PostToolUse', icon: CheckCircle },
|
||||
{ type: 'Stop', icon: StopCircle },
|
||||
const FILTER_OPTIONS: Array<{ type: HookTriggerType | 'all'; icon: typeof Zap; label: string }> = [
|
||||
{ type: 'all', icon: GitFork, label: formatMessage({ id: 'common.all' }) },
|
||||
{ type: 'SessionStart', icon: Play, label: formatMessage({ id: 'cliHooks.trigger.SessionStart' }) },
|
||||
{ type: 'UserPromptSubmit', icon: Zap, label: formatMessage({ id: 'cliHooks.trigger.UserPromptSubmit' }) },
|
||||
{ type: 'PreToolUse', icon: Wrench, label: formatMessage({ id: 'cliHooks.trigger.PreToolUse' }) },
|
||||
{ type: 'PostToolUse', icon: CheckCircle, label: formatMessage({ id: 'cliHooks.trigger.PostToolUse' }) },
|
||||
{ type: 'Stop', icon: StopCircle, label: formatMessage({ id: 'cliHooks.trigger.Stop' }) },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -263,110 +279,159 @@ export function HookManagerPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{TRIGGER_TYPES.map(({ type, icon: Icon }) => {
|
||||
const stats = triggerStats[type];
|
||||
return (
|
||||
<Card key={type} className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Icon className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: `cliHooks.trigger.${type}` })}
|
||||
</p>
|
||||
<p className="text-lg font-semibold text-foreground">
|
||||
{stats.enabled}/{stats.total}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Search and Global Stats */}
|
||||
{/* Search and Filters */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={formatMessage({ id: 'cliHooks.filters.searchPlaceholder' })}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
{/* Search Bar */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={formatMessage({ id: 'cliHooks.filters.searchPlaceholder' })}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-sm">
|
||||
{formatMessage({ id: 'cliHooks.stats.total' }, { count: totalCount })}
|
||||
</Badge>
|
||||
<Badge variant="default" className="text-sm">
|
||||
{formatMessage({ id: 'cliHooks.stats.enabled' }, { count: enabledCount })}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge variant="outline" className="text-sm">
|
||||
{formatMessage({ id: 'cliHooks.stats.total' }, { count: totalCount })}
|
||||
</Badge>
|
||||
<Badge variant="default" className="text-sm">
|
||||
{formatMessage({ id: 'cliHooks.stats.enabled' }, { count: enabledCount })}
|
||||
</Badge>
|
||||
|
||||
{/* Trigger Type Filters */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{FILTER_OPTIONS.map(({ type, icon: Icon, label }) => {
|
||||
const isSelected = selectedTrigger === type;
|
||||
const stats = type === 'all'
|
||||
? { enabled: enabledCount, total: totalCount }
|
||||
: triggerStats[type as HookTriggerType];
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={type}
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setSelectedTrigger(type)}
|
||||
className="gap-2"
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{label}
|
||||
<Badge
|
||||
variant={isSelected ? 'secondary' : 'outline'}
|
||||
className="ml-1"
|
||||
>
|
||||
{stats.enabled}/{stats.total}
|
||||
</Badge>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Hook Cards Grid */}
|
||||
{filteredHooks.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredHooks.map((hook) => (
|
||||
<HookCard
|
||||
key={hook.name}
|
||||
hook={hook}
|
||||
isExpanded={expandedHooks.has(hook.name)}
|
||||
onToggleExpand={() => handleToggleHookExpand(hook.name)}
|
||||
onToggle={toggleHook}
|
||||
onEdit={handleEditClick}
|
||||
onDelete={handleDeleteClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Templates */}
|
||||
<Card className="p-6">
|
||||
<HookQuickTemplates
|
||||
onInstallTemplate={handleInstallTemplate}
|
||||
installedTemplates={installedTemplates}
|
||||
isLoading={installMutation.isPending}
|
||||
/>
|
||||
<Card className="overflow-hidden">
|
||||
<div
|
||||
className="p-4 cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-between border-b border-border"
|
||||
onClick={() => setTemplatesExpanded(!templatesExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Zap className="w-5 h-5 text-primary" />
|
||||
<h2 className="text-base font-semibold text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.quickTemplates.title' })}
|
||||
</h2>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
{templatesExpanded ? (
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{templatesExpanded && (
|
||||
<div className="p-6">
|
||||
<HookQuickTemplates
|
||||
onInstallTemplate={handleInstallTemplate}
|
||||
installedTemplates={installedTemplates}
|
||||
isLoading={installMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Wizard Launchers */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Wand2 className="w-5 h-5 text-primary" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.sectionTitle' })}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.sectionDescription' })}
|
||||
</p>
|
||||
<Card className="overflow-hidden">
|
||||
<div
|
||||
className="p-4 cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-between"
|
||||
onClick={() => setWizardsExpanded(!wizardsExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Wand2 className="w-5 h-5 text-primary" />
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.sectionTitle' })}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'cliHooks.wizards.sectionDescription' })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
{wizardsExpanded ? (
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{wizardTypes.map(({ type, icon: Icon, label, description }) => (
|
||||
<Card key={type} className="p-4 cursor-pointer hover:bg-muted/50 transition-colors" onClick={() => handleLaunchWizard(type)}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10 shrink-0">
|
||||
<Icon className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium text-foreground mb-1">
|
||||
{label}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
{wizardsExpanded && (
|
||||
<div className="border-t border-border p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{wizardTypes.map(({ type, icon: Icon, label, description }) => (
|
||||
<Card key={type} className="p-4 cursor-pointer hover:bg-muted/50 transition-colors" onClick={() => handleLaunchWizard(type)}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="p-2 rounded-lg bg-primary/10 shrink-0">
|
||||
<Icon className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium text-foreground mb-1">
|
||||
{label}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Event Groups */}
|
||||
<div className="space-y-4">
|
||||
{TRIGGER_TYPES.map(({ type }) => (
|
||||
<EventGroup
|
||||
key={type}
|
||||
eventType={type}
|
||||
hooks={hooksByTrigger[type]}
|
||||
onHookToggle={(hookName, enabled) => toggleHook(hookName, enabled)}
|
||||
onHookEdit={handleEditClick}
|
||||
onHookDelete={handleDeleteClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
{!isLoading && filteredHooks.length === 0 && (
|
||||
<Card className="p-12 text-center">
|
||||
@@ -398,7 +463,6 @@ export function HookManagerPage() {
|
||||
wizardType={wizardType}
|
||||
open={wizardOpen}
|
||||
onClose={() => setWizardOpen(false)}
|
||||
onComplete={handleWizardComplete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// ========================================
|
||||
// Browse and manage skills library with search/filter
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Sparkles,
|
||||
@@ -33,9 +33,11 @@ import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
} from '@/components/ui';
|
||||
import { SkillCard } from '@/components/shared/SkillCard';
|
||||
import { SkillCard, SkillDetailPanel } from '@/components/shared';
|
||||
import { LocationSwitcher } from '@/components/commands/LocationSwitcher';
|
||||
import { useSkills, useSkillMutations } from '@/hooks';
|
||||
import { fetchSkillDetail } from '@/lib/api';
|
||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||
import type { Skill } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -101,6 +103,8 @@ function SkillGrid({ skills, isLoading, onToggle, onClick, isToggling, compact }
|
||||
|
||||
export function SkillsManagerPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const projectPath = useWorkflowStore(selectProjectPath);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [categoryFilter, setCategoryFilter] = useState<string>('all');
|
||||
const [sourceFilter, setSourceFilter] = useState<string>('all');
|
||||
@@ -110,6 +114,11 @@ export function SkillsManagerPage() {
|
||||
const [confirmDisable, setConfirmDisable] = useState<{ skill: Skill; enable: boolean } | null>(null);
|
||||
const [locationFilter, setLocationFilter] = useState<'project' | 'user'>('project');
|
||||
|
||||
// Skill detail panel state
|
||||
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
|
||||
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
||||
const [isDetailPanelOpen, setIsDetailPanelOpen] = useState(false);
|
||||
|
||||
const {
|
||||
skills,
|
||||
categories,
|
||||
@@ -166,6 +175,33 @@ export function SkillsManagerPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Skill detail panel handlers
|
||||
const handleSkillClick = useCallback(async (skill: Skill) => {
|
||||
setIsDetailLoading(true);
|
||||
setIsDetailPanelOpen(true);
|
||||
setSelectedSkill(skill);
|
||||
|
||||
try {
|
||||
// Fetch full skill details from API
|
||||
const data = await fetchSkillDetail(
|
||||
skill.name,
|
||||
skill.location || 'project',
|
||||
projectPath
|
||||
);
|
||||
setSelectedSkill(data.skill);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch skill details:', error);
|
||||
// Keep the basic skill info if fetch fails
|
||||
} finally {
|
||||
setIsDetailLoading(false);
|
||||
}
|
||||
}, [projectPath]);
|
||||
|
||||
const handleCloseDetailPanel = useCallback(() => {
|
||||
setIsDetailPanelOpen(false);
|
||||
setSelectedSkill(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
@@ -327,7 +363,7 @@ export function SkillsManagerPage() {
|
||||
skills={filteredSkills}
|
||||
isLoading={isLoading}
|
||||
onToggle={handleToggleWithConfirm}
|
||||
onClick={() => {}}
|
||||
onClick={handleSkillClick}
|
||||
isToggling={isToggling}
|
||||
compact={viewMode === 'compact'}
|
||||
/>
|
||||
@@ -350,7 +386,7 @@ export function SkillsManagerPage() {
|
||||
skills={skills.filter((s) => !s.enabled)}
|
||||
isLoading={false}
|
||||
onToggle={handleToggleWithConfirm}
|
||||
onClick={() => {}}
|
||||
onClick={handleSkillClick}
|
||||
isToggling={isToggling}
|
||||
compact={true}
|
||||
/>
|
||||
@@ -378,6 +414,14 @@ export function SkillsManagerPage() {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Skill Detail Panel */}
|
||||
<SkillDetailPanel
|
||||
skill={selectedSkill}
|
||||
isOpen={isDetailPanelOpen}
|
||||
onClose={handleCloseDetailPanel}
|
||||
isLoading={isDetailLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
238
ccw/frontend/src/pages/TickerDemo.tsx
Normal file
238
ccw/frontend/src/pages/TickerDemo.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
// ========================================
|
||||
// Ticker Demo Page
|
||||
// ========================================
|
||||
// Development demo for TickerMarquee component
|
||||
|
||||
import * as React from 'react';
|
||||
import { TickerMarquee } from '@/components/shared';
|
||||
import type { TickerMessage } from '@/hooks/useRealtimeUpdates';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
const MOCK_MESSAGES: TickerMessage[] = [
|
||||
{
|
||||
id: 'msg-1',
|
||||
text: 'Session WFS-feature-auth completed successfully',
|
||||
type: 'session',
|
||||
link: '/sessions/WFS-feature-auth',
|
||||
timestamp: Date.now() - 60000,
|
||||
},
|
||||
{
|
||||
id: 'msg-2',
|
||||
text: 'Task IMPL-001 completed: Implement authentication module',
|
||||
type: 'task',
|
||||
link: '/tasks/IMPL-001',
|
||||
timestamp: Date.now() - 50000,
|
||||
},
|
||||
{
|
||||
id: 'msg-3',
|
||||
text: 'Workflow authentication-system started',
|
||||
type: 'workflow',
|
||||
link: '/workflows/authentication-system',
|
||||
timestamp: Date.now() - 40000,
|
||||
},
|
||||
{
|
||||
id: 'msg-4',
|
||||
text: 'Build status changed to passing',
|
||||
type: 'status',
|
||||
timestamp: Date.now() - 30000,
|
||||
},
|
||||
{
|
||||
id: 'msg-5',
|
||||
text: 'Session WFS-bugfix-login created',
|
||||
type: 'session',
|
||||
link: '/sessions/WFS-bugfix-login',
|
||||
timestamp: Date.now() - 20000,
|
||||
},
|
||||
{
|
||||
id: 'msg-6',
|
||||
text: 'Task IMPL-002 completed: Add JWT validation',
|
||||
type: 'task',
|
||||
link: '/tasks/IMPL-002',
|
||||
timestamp: Date.now() - 10000,
|
||||
},
|
||||
];
|
||||
|
||||
export default function TickerDemo() {
|
||||
const [speed, setSpeed] = React.useState(30);
|
||||
const [showMockMessages, setShowMockMessages] = React.useState(true);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-8">
|
||||
<div className="mx-auto max-w-6xl space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold">Ticker Marquee Demo</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Real-time WebSocket ticker with CSS marquee animation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Live Demo */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Live Ticker</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<TickerMarquee
|
||||
duration={speed}
|
||||
mockMessages={showMockMessages ? MOCK_MESSAGES : undefined}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-4 border-t pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium">Speed (seconds):</label>
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="60"
|
||||
value={speed}
|
||||
onChange={(e) => setSpeed(Number(e.target.value))}
|
||||
className="w-32"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">{speed}s</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowMockMessages(!showMockMessages)}
|
||||
>
|
||||
{showMockMessages ? 'Use WebSocket' : 'Use Mock Data'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Usage Examples */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Usage Examples</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium">Basic Usage</h3>
|
||||
<pre className="rounded-md bg-muted p-4 text-sm">
|
||||
{`<TickerMarquee />`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium">Custom Endpoint and Duration</h3>
|
||||
<pre className="rounded-md bg-muted p-4 text-sm">
|
||||
{`<TickerMarquee
|
||||
endpoint="ws/custom-ticker"
|
||||
duration={45}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium">With Mock Messages (Development)</h3>
|
||||
<pre className="rounded-md bg-muted p-4 text-sm">
|
||||
{`<TickerMarquee
|
||||
mockMessages={[
|
||||
{
|
||||
id: '1',
|
||||
text: 'Session completed',
|
||||
type: 'session',
|
||||
link: '/sessions/WFS-001',
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
]}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Message Format */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>WebSocket Message Format</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="rounded-md bg-muted p-4 text-sm">
|
||||
{JSON.stringify(
|
||||
{
|
||||
id: 'msg-001',
|
||||
text: 'Session WFS-feature-auth completed',
|
||||
type: 'session',
|
||||
link: '/sessions/WFS-feature-auth',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</pre>
|
||||
<div className="mt-4 space-y-2 text-sm">
|
||||
<p>
|
||||
<strong>Message Types:</strong>
|
||||
</p>
|
||||
<ul className="list-inside list-disc space-y-1 text-muted-foreground">
|
||||
<li>
|
||||
<code className="rounded bg-primary/10 px-1 text-primary">session</code> -
|
||||
Session events (primary color)
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-success/10 px-1 text-success">task</code> - Task
|
||||
completions (success color)
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-info/10 px-1 text-info">workflow</code> - Workflow
|
||||
events (info color)
|
||||
</li>
|
||||
<li>
|
||||
<code className="rounded bg-warning/10 px-1 text-warning">status</code> - Status
|
||||
changes (warning color)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Features */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Features</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="grid gap-2 text-sm">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-success">✓</span>
|
||||
60 FPS CSS marquee animation (GPU-accelerated)
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-success">✓</span>
|
||||
Pause-on-hover interaction
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-success">✓</span>
|
||||
Automatic WebSocket reconnection (exponential backoff)
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-success">✓</span>
|
||||
Type-safe message validation (Zod schema)
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-success">✓</span>
|
||||
Clickable message links
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-success">✓</span>
|
||||
Internationalization support (i18n)
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-success">✓</span>
|
||||
Message buffer management (max 50 messages)
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-success">✓</span>
|
||||
Mock message support for development
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
138
ccw/frontend/src/pages/coordinator/CoordinatorPage.tsx
Normal file
138
ccw/frontend/src/pages/coordinator/CoordinatorPage.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
// ========================================
|
||||
// Coordinator Page
|
||||
// ========================================
|
||||
// Page for monitoring and managing coordinator workflow execution with timeline, logs, and node details
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Play } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import {
|
||||
CoordinatorInputModal,
|
||||
CoordinatorTimeline,
|
||||
CoordinatorLogStream,
|
||||
NodeDetailsPanel,
|
||||
} from '@/components/coordinator';
|
||||
import {
|
||||
useCoordinatorStore,
|
||||
selectCommandChain,
|
||||
selectCurrentNode,
|
||||
selectCoordinatorStatus,
|
||||
selectIsPipelineLoaded,
|
||||
} from '@/stores/coordinatorStore';
|
||||
|
||||
export function CoordinatorPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const [isInputModalOpen, setIsInputModalOpen] = useState(false);
|
||||
const [selectedNode, setSelectedNode] = useState<string | null>(null);
|
||||
|
||||
// Store selectors
|
||||
const commandChain = useCoordinatorStore(selectCommandChain);
|
||||
const currentNode = useCoordinatorStore(selectCurrentNode);
|
||||
const status = useCoordinatorStore(selectCoordinatorStatus);
|
||||
const isPipelineLoaded = useCoordinatorStore(selectIsPipelineLoaded);
|
||||
const syncStateFromServer = useCoordinatorStore((state) => state.syncStateFromServer);
|
||||
const reset = useCoordinatorStore((state) => state.reset);
|
||||
|
||||
// Sync state on mount (for page refresh scenarios)
|
||||
useEffect(() => {
|
||||
if (status === 'running' || status === 'paused' || status === 'initializing') {
|
||||
syncStateFromServer();
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle open input modal
|
||||
const handleOpenInputModal = useCallback(() => {
|
||||
setIsInputModalOpen(true);
|
||||
}, []);
|
||||
|
||||
// Handle node click from timeline
|
||||
const handleNodeClick = useCallback((nodeId: string) => {
|
||||
setSelectedNode(nodeId);
|
||||
}, []);
|
||||
|
||||
// Get selected node object
|
||||
const selectedNodeObject = commandChain.find((node) => node.id === selectedNode) || currentNode || null;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col -m-4 md:-m-6">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center gap-3 p-3 bg-card border-b border-border">
|
||||
{/* Page Title and Status */}
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Play className="w-5 h-5 text-primary flex-shrink-0" />
|
||||
<div className="flex flex-col min-w-0">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{formatMessage({ id: 'coordinator.page.title' })}
|
||||
</span>
|
||||
{isPipelineLoaded && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatMessage({ id: 'coordinator.page.status' }, {
|
||||
status: formatMessage({ id: `coordinator.status.${status}` }),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleOpenInputModal}
|
||||
disabled={status === 'running' || status === 'initializing'}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'coordinator.page.startButton' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area - 3 Panel Layout */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left Panel: Timeline */}
|
||||
<div className="w-1/3 min-w-[300px] border-r border-border bg-card">
|
||||
<CoordinatorTimeline
|
||||
autoScroll={true}
|
||||
onNodeClick={handleNodeClick}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Center Panel: Log Stream */}
|
||||
<div className="flex-1 min-w-0 bg-card">
|
||||
<CoordinatorLogStream />
|
||||
</div>
|
||||
|
||||
{/* Right Panel: Node Details */}
|
||||
<div className="w-80 min-w-[320px] max-w-[400px] border-l border-border bg-card overflow-y-auto">
|
||||
{selectedNodeObject ? (
|
||||
<NodeDetailsPanel
|
||||
node={selectedNodeObject}
|
||||
isExpanded={true}
|
||||
onToggle={(expanded) => {
|
||||
if (!expanded) {
|
||||
setSelectedNode(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground text-sm p-4 text-center">
|
||||
{formatMessage({ id: 'coordinator.page.noNodeSelected' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Coordinator Input Modal */}
|
||||
<CoordinatorInputModal
|
||||
open={isInputModalOpen}
|
||||
onClose={() => setIsInputModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CoordinatorPage;
|
||||
@@ -10,9 +10,9 @@ export { ProjectOverviewPage } from './ProjectOverviewPage';
|
||||
export { SessionDetailPage } from './SessionDetailPage';
|
||||
export { HistoryPage } from './HistoryPage';
|
||||
export { OrchestratorPage } from './orchestrator';
|
||||
export { CoordinatorPage } from './coordinator';
|
||||
export { LoopMonitorPage } from './LoopMonitorPage';
|
||||
export { IssueHubPage } from './IssueHubPage';
|
||||
export { IssueManagerPage } from './IssueManagerPage';
|
||||
export { QueuePage } from './QueuePage';
|
||||
export { DiscoveryPage } from './DiscoveryPage';
|
||||
export { SkillsManagerPage } from './SkillsManagerPage';
|
||||
@@ -23,7 +23,6 @@ export { HelpPage } from './HelpPage';
|
||||
export { HookManagerPage } from './HookManagerPage';
|
||||
export { NotFoundPage } from './NotFoundPage';
|
||||
export { LiteTasksPage } from './LiteTasksPage';
|
||||
export { LiteTaskDetailPage } from './LiteTaskDetailPage';
|
||||
export { ReviewSessionPage } from './ReviewSessionPage';
|
||||
export { McpManagerPage } from './McpManagerPage';
|
||||
export { EndpointsPage } from './EndpointsPage';
|
||||
|
||||
Reference in New Issue
Block a user