cleanup: remove all unconnected Phase I+J scaffold code and unconnected Phase F+H modules (workstream_contract, proactive_feed, dashboard, dsgvo_export, onboarding, mcp_exposure, integration_tools, self_improvement, agent_workstream, workflows/workstream, knowledge_sources, knowledge_extraction, knowledge_lifecycle, platform.py routes, 13 frontend files, 2 test files), restore Dashboard.tsx, tsc clean, backend OK
This commit is contained in:
@@ -1,26 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Bot,
|
||||
Workflow,
|
||||
Search,
|
||||
BookOpen,
|
||||
DollarSign,
|
||||
HeartPulse,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { StatCard } from '@/components/shared/StatCard';
|
||||
import { ActivityFeed, ActivityItem } from '@/components/shared/ActivityFeed';
|
||||
import { DashboardGrid } from '@/components/dashboard/DashboardGrid';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { useUnifiedContacts, useAuditLog } from '@/api/hooks';
|
||||
import { useDashboardWidgets } from '@/api/dashboard';
|
||||
import {
|
||||
usePlatformDashboard,
|
||||
useCostDashboard,
|
||||
useUsageAnalytics,
|
||||
} from '@/api/platform';
|
||||
import { formatDateTime } from '@/utils/date';
|
||||
|
||||
export function DashboardPage() {
|
||||
@@ -30,22 +13,6 @@ export function DashboardPage() {
|
||||
const { data: auditData, isError: auditError } = useAuditLog(1, 5);
|
||||
const { data: widgetsData, isError: widgetsError } = useDashboardWidgets();
|
||||
|
||||
const {
|
||||
data: platformData,
|
||||
isLoading: platformLoading,
|
||||
isError: platformError,
|
||||
} = usePlatformDashboard();
|
||||
const {
|
||||
data: costData,
|
||||
isLoading: costLoading,
|
||||
isError: costError,
|
||||
} = useCostDashboard(30);
|
||||
const {
|
||||
data: usageData,
|
||||
isLoading: usageLoading,
|
||||
isError: usageError,
|
||||
} = useUsageAnalytics(30);
|
||||
|
||||
const totalCompanies = companiesData?.total ?? 0;
|
||||
const totalContacts = contactsData?.total ?? 0;
|
||||
|
||||
@@ -78,20 +45,6 @@ export function DashboardPage() {
|
||||
|
||||
const widgets = widgetsData?.items ?? [];
|
||||
|
||||
const agents = platformData?.agents;
|
||||
const workflows = platformData?.workflows;
|
||||
const search = platformData?.search;
|
||||
const knowledge = platformData?.knowledge;
|
||||
const systemHealth = platformData?.system_health;
|
||||
const searchQueries = usageData?.search_queries;
|
||||
|
||||
const costUsd = costData?.total_cost_usd ?? 0;
|
||||
const budget = costData?.budget;
|
||||
const budgetUtilization = budget?.utilization_pct ?? 0;
|
||||
|
||||
const healthLabel = systemHealth?.status ?? 'unknown';
|
||||
const healthVariant = healthStatusVariant(healthLabel);
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto" data-testid="dashboard-page">
|
||||
<h1 className="text-2xl font-bold text-secondary-900 mb-6">{t('dashboard.title')}</h1>
|
||||
@@ -119,155 +72,6 @@ export function DashboardPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Platform Dashboard — Phase I.7 (I-DASH) */}
|
||||
<section className="mb-8" aria-label={t('dashboard.platform.title')}>
|
||||
<h2 className="text-lg font-semibold text-secondary-800 mb-4">
|
||||
{t('dashboard.platform.title')}
|
||||
</h2>
|
||||
|
||||
{platformLoading || costLoading || usageLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="animate-spin h-6 w-6 text-primary-500" aria-hidden="true" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{/* Agent status */}
|
||||
<Card title={t('dashboard.platform.agents')}>
|
||||
{agents?.error || platformError ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MetricRow
|
||||
icon={<Bot className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.activeAgents')}
|
||||
value={agents?.active_agents ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Bot className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.totalRuns')}
|
||||
value={agents?.total_runs ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Bot className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.recentRuns7d')}
|
||||
value={agents?.recent_runs_7d ?? 0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Workflow stats */}
|
||||
<Card title={t('dashboard.platform.workflows')}>
|
||||
{workflows?.error || platformError ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MetricRow
|
||||
icon={<Workflow className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.activeWorkflows')}
|
||||
value={workflows?.active_workflows ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Workflow className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.runningInstances')}
|
||||
value={workflows?.running_instances ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Workflow className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.completedInstances')}
|
||||
value={workflows?.completed_instances ?? 0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Search metrics */}
|
||||
<Card title={t('dashboard.platform.search')}>
|
||||
{usageError || searchQueries?.error ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MetricRow
|
||||
icon={<Search className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.totalQueries')}
|
||||
value={searchQueries?.total ?? search?.total_queries ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<Search className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.avgLatency')}
|
||||
value={
|
||||
typeof search?.avg_latency_ms === 'number'
|
||||
? `${search.avg_latency_ms} ms`
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Knowledge coverage */}
|
||||
<Card title={t('dashboard.platform.knowledge')}>
|
||||
{platformError || knowledge?.error ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MetricRow
|
||||
icon={<BookOpen className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.wikiArticles')}
|
||||
value={knowledge?.wiki_articles ?? 0}
|
||||
/>
|
||||
<MetricRow
|
||||
icon={<BookOpen className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.coverage')}
|
||||
value={
|
||||
typeof knowledge?.coverage_pct === 'number'
|
||||
? `${knowledge.coverage_pct}%`
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Cost tracking */}
|
||||
<Card title={t('dashboard.platform.cost')}>
|
||||
{costError || costData?.error ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MetricRow
|
||||
icon={<DollarSign className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.totalCost')}
|
||||
value={`$${costUsd.toFixed(2)}`}
|
||||
/>
|
||||
{budget?.monthly_limit_usd ? (
|
||||
<MetricRow
|
||||
icon={<DollarSign className="h-4 w-4" aria-hidden="true" />}
|
||||
label={t('dashboard.platform.budgetUtilization')}
|
||||
value={`${budgetUtilization.toFixed(1)}%`}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* System health */}
|
||||
<Card title={t('dashboard.platform.systemHealth')}>
|
||||
{platformError ? (
|
||||
<p className="text-sm text-secondary-500">{t('dashboard.platform.unavailable')}</p>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<HeartPulse className="h-4 w-4 text-primary-500" aria-hidden="true" />
|
||||
<Badge variant={healthVariant} dot>
|
||||
{t(`dashboard.platform.health.${healthLabel}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Dynamic Plugin Widgets */}
|
||||
{widgetsError ? (
|
||||
<p className="text-sm text-secondary-500 mb-6" data-testid="dashboard-widgets-unavailable">
|
||||
@@ -291,31 +95,4 @@ export function DashboardPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function MetricRow({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string | number;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2 text-sm text-secondary-600">
|
||||
<span className="text-primary-500" aria-hidden="true">{icon}</span>
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-secondary-900">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function healthStatusVariant(status: string): 'success' | 'warning' | 'danger' | 'secondary' {
|
||||
if (status === 'healthy') return 'success';
|
||||
if (status === 'degraded') return 'warning';
|
||||
if (status === 'down') return 'danger';
|
||||
return 'secondary';
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* OnboardingPage — Route wrapper for SetupWizard component.
|
||||
* Renders the SetupWizard as a full-page dialog.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SetupWizard } from '@/components/onboarding/SetupWizard';
|
||||
|
||||
export function OnboardingPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
navigate('/dashboard');
|
||||
}, [navigate]);
|
||||
|
||||
const handleComplete = useCallback(() => {
|
||||
navigate('/dashboard');
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<SetupWizard open={true} onClose={handleClose} onComplete={handleComplete} />
|
||||
);
|
||||
}
|
||||
|
||||
export default OnboardingPage;
|
||||
@@ -1,296 +0,0 @@
|
||||
/**
|
||||
* Workstream page — central view rendering all workstream block types.
|
||||
*
|
||||
* Block types: text, entity_card, action_card, evidence_card, approval_card,
|
||||
* miniapp, workflow_status, workflow_handoff.
|
||||
*
|
||||
* Backend: app/ai/workstream_contract.py, app/workflows/workstream.py
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
MessageSquare,
|
||||
Link2,
|
||||
AlertTriangle,
|
||||
Workflow,
|
||||
ArrowRight,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { MiniAppBlock } from '@/components/workstream/MiniAppBlock';
|
||||
import {
|
||||
EntityCard,
|
||||
ActionButtons,
|
||||
ApprovalCard,
|
||||
ProgressIndicator,
|
||||
DeepLink,
|
||||
type ActionButtonItem,
|
||||
} from '@/components/workstream/MiniAppSDK';
|
||||
import { ProactiveFeed } from '@/components/workstream/ProactiveFeed';
|
||||
import type {
|
||||
WorkstreamMessage,
|
||||
WorkstreamBlock,
|
||||
ProactiveSuggestion,
|
||||
} from '@/api/miniapps';
|
||||
|
||||
interface WorkstreamPageProps {
|
||||
messages?: WorkstreamMessage[];
|
||||
suggestions?: ProactiveSuggestion[];
|
||||
isLoading?: boolean;
|
||||
onApprove?: (approvalId: string) => void;
|
||||
onReject?: (approvalId: string) => void;
|
||||
onAction?: (action: ActionButtonItem) => void;
|
||||
onSuggestionAction?: (suggestion: ProactiveSuggestion) => void;
|
||||
onDismissSuggestion?: (id: string) => void;
|
||||
}
|
||||
|
||||
const ACTOR_VARIANT: Record<
|
||||
string,
|
||||
'secondary' | 'primary' | 'info' | 'success'
|
||||
> = {
|
||||
human: 'secondary',
|
||||
system: 'info',
|
||||
agent: 'primary',
|
||||
workflow: 'success',
|
||||
};
|
||||
|
||||
/**
|
||||
* Render a single workstream block by type.
|
||||
*/
|
||||
function BlockView({
|
||||
block,
|
||||
onApprove,
|
||||
onReject,
|
||||
onAction,
|
||||
}: {
|
||||
block: WorkstreamBlock;
|
||||
onApprove?: (approvalId: string) => void;
|
||||
onReject?: (approvalId: string) => void;
|
||||
onAction?: (action: ActionButtonItem) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const meta = block.metadata ?? {};
|
||||
|
||||
switch (block.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<div className="text-sm whitespace-pre-wrap break-words text-secondary-800">
|
||||
{block.content || String(meta.text ?? '')}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'entity_card':
|
||||
return (
|
||||
<EntityCard
|
||||
entityType={String(meta.entity_type ?? '')}
|
||||
entityId={String(meta.entity_id ?? '')}
|
||||
title={String(meta.title ?? block.content)}
|
||||
subtitle={String(meta.subtitle ?? '')}
|
||||
url={String(meta.url ?? '')}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'action_card': {
|
||||
const actions: ActionButtonItem[] = Array.isArray(meta.actions)
|
||||
? (meta.actions as ActionButtonItem[])
|
||||
: [];
|
||||
return (
|
||||
<div className="border border-secondary-200 rounded-lg p-4 bg-white shadow-sm space-y-2">
|
||||
<h4 className="text-sm font-semibold text-secondary-900">
|
||||
{String(meta.title ?? block.content)}
|
||||
</h4>
|
||||
{meta.description ? (
|
||||
<p className="text-sm text-secondary-600">
|
||||
{String(meta.description)}
|
||||
</p>
|
||||
) : null}
|
||||
<ActionButtons actions={actions} onAction={onAction} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'evidence_card':
|
||||
return (
|
||||
<div className="border border-secondary-200 rounded-lg p-4 bg-white shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2 className="h-4 w-4 text-primary-500" aria-hidden="true" />
|
||||
<h4 className="text-sm font-semibold text-secondary-900">
|
||||
{String(meta.title ?? block.content)}
|
||||
</h4>
|
||||
{typeof meta.confidence === 'number' && (
|
||||
<Badge variant="secondary">
|
||||
{Math.round(meta.confidence * 100)}%
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{meta.snippet ? (
|
||||
<p className="mt-2 text-xs text-secondary-500">
|
||||
{String(meta.snippet)}
|
||||
</p>
|
||||
) : null}
|
||||
{meta.url ? (
|
||||
<div className="mt-2">
|
||||
<DeepLink href={String(meta.url)} label={t('workstream.block.openSource')} external />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'approval_card':
|
||||
return (
|
||||
<ApprovalCard
|
||||
approvalId={String(meta.approval_id ?? '')}
|
||||
action={String(meta.action ?? block.content)}
|
||||
description={String(meta.description ?? '')}
|
||||
onApprove={onApprove}
|
||||
onReject={onReject}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'miniapp':
|
||||
return <MiniAppBlock block={block} />;
|
||||
|
||||
case 'workflow_status':
|
||||
return (
|
||||
<div className="border border-secondary-200 rounded-lg p-4 bg-white shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Workflow className="h-4 w-4 text-primary-500" aria-hidden="true" />
|
||||
<h4 className="text-sm font-semibold text-secondary-900">
|
||||
{String(meta.workflow_name ?? block.content)}
|
||||
</h4>
|
||||
<Badge variant="info">{String(meta.status ?? '')}</Badge>
|
||||
</div>
|
||||
{meta.step_name ? (
|
||||
<p className="mt-2 text-xs text-secondary-500">
|
||||
{t('workstream.block.step')}: {String(meta.step_name)}
|
||||
</p>
|
||||
) : null}
|
||||
{typeof meta.step_index === 'number' && (
|
||||
<div className="mt-3">
|
||||
<ProgressIndicator
|
||||
label={t('workstream.block.progress')}
|
||||
value={Math.min(100, (meta.step_index + 1) * 10)}
|
||||
status="in_progress"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'workflow_handoff':
|
||||
return (
|
||||
<div className="border border-warning-200 rounded-lg p-4 bg-warning-50 shadow-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowRight className="h-4 w-4 text-warning-700" aria-hidden="true" />
|
||||
<h4 className="text-sm font-semibold text-secondary-900">
|
||||
{t('workstream.block.handoff')}: {String(meta.handoff_type ?? '')}
|
||||
</h4>
|
||||
</div>
|
||||
{meta.description ? (
|
||||
<p className="mt-2 text-xs text-secondary-600">
|
||||
{String(meta.description)}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'error':
|
||||
return (
|
||||
<div className="border border-danger-200 rounded-lg p-4 bg-danger-50 shadow-sm flex items-start gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-danger-600 mt-0.5" aria-hidden="true" />
|
||||
<div className="text-sm text-danger-700">
|
||||
{block.content || String(meta.error ?? '')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<div className="text-xs text-secondary-400 italic p-2 rounded bg-secondary-50">
|
||||
{t('workstream.block.unknown', { type: block.type })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Central Workstream page.
|
||||
*/
|
||||
export function WorkstreamPage({
|
||||
messages = [],
|
||||
suggestions = [],
|
||||
isLoading = false,
|
||||
onApprove,
|
||||
onReject,
|
||||
onAction,
|
||||
onSuggestionAction,
|
||||
onDismissSuggestion,
|
||||
}: WorkstreamPageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto p-4 space-y-6">
|
||||
<header className="flex items-center gap-2">
|
||||
<MessageSquare className="h-5 w-5 text-primary-500" aria-hidden="true" />
|
||||
<h1 className="text-lg font-semibold text-secondary-900">
|
||||
{t('workstream.title')}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<ProactiveFeed
|
||||
suggestions={suggestions}
|
||||
onAction={onSuggestionAction}
|
||||
onDismiss={onDismissSuggestion}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="animate-spin h-6 w-6 text-primary-500" aria-hidden="true" />
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<MessageSquare className="h-8 w-8" aria-hidden="true" />}
|
||||
title={t('workstream.empty.title')}
|
||||
description={t('workstream.empty.description')}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{messages.map((message, idx) => (
|
||||
<article
|
||||
key={message.actor_id ? `${message.actor_id}-${idx}` : idx}
|
||||
className="border border-secondary-200 rounded-lg bg-white shadow-sm p-4"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Badge variant={ACTOR_VARIANT[message.actor_type] ?? 'secondary'}>
|
||||
{t(`workstream.actor.${message.actor_type}`)}
|
||||
</Badge>
|
||||
{message.content && (
|
||||
<span className="text-sm text-secondary-600 truncate">
|
||||
{message.content}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{message.blocks && message.blocks.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{message.blocks.map((block, bidx) => (
|
||||
<BlockView
|
||||
key={bidx}
|
||||
block={block}
|
||||
onApprove={onApprove}
|
||||
onReject={onReject}
|
||||
onAction={onAction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkstreamPage;
|
||||
Reference in New Issue
Block a user