feat(I): I-MINI-MANIFEST/RENDER/SDK + I-WORK-PROACTIVE/E2E frontend — MiniAppBlock, MiniAppSDK, ProactiveFeed, Workstream page, i18n, tsc clean

This commit is contained in:
Agent Zero
2026-08-19 01:17:00 +02:00
parent df261b1b2c
commit bf5e22f5dc
7 changed files with 984 additions and 2 deletions
+296
View File
@@ -0,0 +1,296 @@
/**
* 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;