From bf5e22f5dce5b41d710b2f500e4f60936f17bacd Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Wed, 19 Aug 2026 01:17:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(I):=20I-MINI-MANIFEST/RENDER/SDK=20+=20I-W?= =?UTF-8?q?ORK-PROACTIVE/E2E=20frontend=20=E2=80=94=20MiniAppBlock,=20Mini?= =?UTF-8?q?AppSDK,=20ProactiveFeed,=20Workstream=20page,=20i18n,=20tsc=20c?= =?UTF-8?q?lean?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/miniapps.ts | 87 +++++ .../components/workstream/MiniAppBlock.tsx | 64 ++++ .../src/components/workstream/MiniAppSDK.tsx | 290 +++++++++++++++++ .../components/workstream/ProactiveFeed.tsx | 155 +++++++++ frontend/src/i18n/locales/de.json | 47 ++- frontend/src/i18n/locales/en.json | 47 ++- frontend/src/pages/Workstream.tsx | 296 ++++++++++++++++++ 7 files changed, 984 insertions(+), 2 deletions(-) create mode 100644 frontend/src/api/miniapps.ts create mode 100644 frontend/src/components/workstream/MiniAppBlock.tsx create mode 100644 frontend/src/components/workstream/MiniAppSDK.tsx create mode 100644 frontend/src/components/workstream/ProactiveFeed.tsx create mode 100644 frontend/src/pages/Workstream.tsx diff --git a/frontend/src/api/miniapps.ts b/frontend/src/api/miniapps.ts new file mode 100644 index 0000000..503563f --- /dev/null +++ b/frontend/src/api/miniapps.ts @@ -0,0 +1,87 @@ +/** + * MiniApp manifest types + workstream block types (Phase I.2 Workstream & MiniApps). + * + * Mirrors backend contracts: + * - app/plugins/manifest.py → MiniAppContribution + * - app/ai/workstream_contract.py → WorkstreamBlock / WorkstreamMessage + * - app/ai/proactive_feed.py → ProactiveSuggestion + */ + +// ── MiniApp manifest contribution (backend: MiniAppContribution) ── + +export interface MiniAppContribution { + app_id: string; + name: string; + icon: string; + description: string; + render_schema: Record; +} + +/** + * Plugin manifest with optional `miniapps` field. + * Only the fields relevant to MiniApps are declared here; the full manifest + * is typed in store/pluginStore.ts (PluginUiManifest). + */ +export interface PluginManifestWithMiniApps { + name: string; + display_name: string; + version: string; + miniapps?: MiniAppContribution[]; +} + +// ── Workstream block types (backend: WorkstreamBlock) ── + +export type WorkstreamBlockType = + | 'text' + | 'entity_card' + | 'action_card' + | 'evidence_card' + | 'approval_card' + | 'miniapp' + | 'workflow_status' + | 'workflow_handoff' + | 'error'; + +export interface WorkstreamBlock { + type: WorkstreamBlockType; + content: string; + metadata: Record; +} + +export type WorkstreamActorType = 'human' | 'system' | 'agent' | 'workflow'; + +export interface WorkstreamMessage { + actor_type: WorkstreamActorType; + actor_id?: string | null; + content: string; + blocks: WorkstreamBlock[]; + conversation_id?: string | null; + tenant_id?: string | null; +} + +// ── Proactive suggestion (backend: ProactiveSuggestion) ── + +export type ProactivePriority = 'low' | 'medium' | 'high' | 'urgent'; + +export interface ProactiveSuggestion { + id: string; + trigger: string; + title: string; + description: string; + priority: ProactivePriority; + action_type: 'suggestion' | 'action_required' | 'info'; + action_url: string; + entity_type?: string | null; + entity_id?: string | null; + blocks: WorkstreamBlock[]; + created_at: string; + expires_at?: string | null; + metadata: Record; +} + +export interface ProactiveFeedSettings { + enabled: boolean; + min_priority: ProactivePriority; + max_per_hour: number; + triggers_enabled: Record; +} diff --git a/frontend/src/components/workstream/MiniAppBlock.tsx b/frontend/src/components/workstream/MiniAppBlock.tsx new file mode 100644 index 0000000..69d7763 --- /dev/null +++ b/frontend/src/components/workstream/MiniAppBlock.tsx @@ -0,0 +1,64 @@ +/** + * MiniAppBlock — renders a `miniapp` workstream block. + * + * Uses `metadata.app_id` and `metadata.render_schema` to render the MiniApp. + * Falls back to a placeholder state for unknown apps. + * + * Backend: app/ai/workstream_contract.py → build_miniapp_block + */ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { AppWindow, AlertCircle } from 'lucide-react'; +import type { WorkstreamBlock } from '@/api/miniapps'; + +interface MiniAppBlockProps { + block: WorkstreamBlock; +} + +/** + * Render a MiniApp block. If the app is unknown or render_schema is empty, + * show a fallback state with the app_id. + */ +export function MiniAppBlock({ block }: MiniAppBlockProps) { + const { t } = useTranslation(); + const metadata = block.metadata ?? {}; + const appId = typeof metadata.app_id === 'string' ? metadata.app_id : ''; + const title = typeof metadata.title === 'string' ? metadata.title : ''; + const renderSchema = + metadata.render_schema && typeof metadata.render_schema === 'object' + ? (metadata.render_schema as Record) + : {}; + + const hasRenderSchema = Object.keys(renderSchema).length > 0; + + return ( +
+
+
+
+
+

+ {title || appId || t('workstream.miniapp.untitled')} +

+ {hasRenderSchema ? ( +
+ {t('workstream.miniapp.renderSchema')} +
+                {JSON.stringify(renderSchema, null, 2)}
+              
+
+ ) : ( +
+
+ )} +
+
+
+ ); +} + +export default MiniAppBlock; diff --git a/frontend/src/components/workstream/MiniAppSDK.tsx b/frontend/src/components/workstream/MiniAppSDK.tsx new file mode 100644 index 0000000..a999d01 --- /dev/null +++ b/frontend/src/components/workstream/MiniAppSDK.tsx @@ -0,0 +1,290 @@ +/** + * MiniAppSDK — standard building blocks for MiniApp rendering. + * + * Provides reusable components used by MiniAppBlock and the Workstream page: + * - EntityCard: link to a CRM entity + * - ActionButtons: list of action buttons (deep links / callbacks) + * - ApprovalCard: approve/reject actions + * - ProgressIndicator: progress bar with label + * - DeepLink: safe external/internal link + */ + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { + ExternalLink, + CheckCircle2, + XCircle, + ArrowRight, +} from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { Badge } from '@/components/ui/Badge'; + +// ── EntityCard ── + +export interface EntityCardProps { + entityType: string; + entityId: string; + title?: string; + subtitle?: string; + url?: string; +} + +/** + * Card linking to a CRM entity (contact, company, mail, etc.). + */ +export function EntityCard({ + entityType, + entityId, + title, + subtitle, + url, +}: EntityCardProps) { + const { t } = useTranslation(); + const href = url || `/${entityType}/${entityId}`; + const safeHref = hrefSafe(href); + + return ( +
+
+
+ {entityType} +

+ {title || entityId} +

+ {subtitle && ( +

{subtitle}

+ )} +
+ {safeHref && ( + + + )} +
+
+ ); +}; + +// ── ActionButtons ── + +export interface ActionButtonItem { + label: string; + action: string; + type?: 'primary' | 'secondary' | 'danger'; +} + +export interface ActionButtonsProps { + actions: ActionButtonItem[]; + onAction?: (action: ActionButtonItem) => void; +} + +/** + * Renders a list of action buttons. Actions are either deep links (http/https) + * or callbacks handled by the parent via `onAction`. + */ +export function ActionButtons({ actions, onAction }: ActionButtonsProps) { + const { t } = useTranslation(); + + if (!actions || actions.length === 0) return null; + + const handleClick = (action: ActionButtonItem) => { + if (onAction) { + onAction(action); + return; + } + const url = action.action; + if (isSafeUrl(url)) { + window.open(url, '_blank', 'noopener,noreferrer'); + } + }; + + return ( +
+ {actions.map((action, idx) => { + const variant = + action.type === 'danger' + ? 'danger' + : action.type === 'secondary' + ? 'secondary' + : 'primary'; + return ( + + ); + })} +
+ ); +} + +// ── ApprovalCard ── + +export interface ApprovalCardProps { + approvalId: string; + action: string; + description?: string; + onApprove?: (approvalId: string) => void; + onReject?: (approvalId: string) => void; + isPending?: boolean; +} + +/** + * Approval card with approve/reject actions. + */ +export function ApprovalCard({ + approvalId, + action, + description, + onApprove, + onReject, + isPending = false, +}: ApprovalCardProps) { + const { t } = useTranslation(); + + return ( +
+
+
+
+
+

+ {t('workstream.sdk.approvalNeeded')}: {action} +

+ {description && ( +

{description}

+ )} +
+ + +
+
+
+
+ ); +} + +// ── ProgressIndicator ── + +export interface ProgressIndicatorProps { + label?: string; + value: number; // 0-100 + status?: 'pending' | 'in_progress' | 'completed' | 'error'; +} + +/** + * Progress bar with optional label and status color. + */ +export function ProgressIndicator({ + label, + value, + status = 'in_progress', +}: ProgressIndicatorProps) { + const { t } = useTranslation(); + const clamped = Math.max(0, Math.min(100, value)); + const barColor = + status === 'error' + ? 'bg-danger-500' + : status === 'completed' + ? 'bg-success-500' + : 'bg-primary-500'; + + return ( +
+ {label && ( +
+ {label} + {clamped}% +
+ )} +
+
+
+
+ ); +} + +// ── DeepLink ── + +export interface DeepLinkProps { + href: string; + label?: string; + external?: boolean; +} + +/** + * Safe deep link — only allows http(s) and internal paths. + */ +export function DeepLink({ href, label, external = false }: DeepLinkProps) { + const { t } = useTranslation(); + const safeHref = hrefSafe(href); + if (!safeHref) { + return {label || href}; + } + return ( + + {label || href} + {external && + ); +} + +// ── Helpers ── + +function hrefSafe(href: string): string | null { + if (!href) return null; + if (href.startsWith('/')) return href; + try { + const parsed = new URL(href); + if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return href; + } catch { + return null; + } + return null; +} + +function isSafeUrl(url: string): boolean { + return hrefSafe(url) !== null; +} diff --git a/frontend/src/components/workstream/ProactiveFeed.tsx b/frontend/src/components/workstream/ProactiveFeed.tsx new file mode 100644 index 0000000..9011f57 --- /dev/null +++ b/frontend/src/components/workstream/ProactiveFeed.tsx @@ -0,0 +1,155 @@ +/** + * ProactiveFeed — contextual suggestions/actions in the workstream. + * + * Renders ProactiveSuggestion items with priority, client-side dedupe and + * cooldown (mirrors backend app/ai/proactive_feed.py). Suggestions are + * non-intrusive: no popups, just a feed section. + */ + +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Sparkles, Info, ArrowRight, X } from 'lucide-react'; +import { Badge } from '@/components/ui/Badge'; +import type { ProactiveSuggestion, ProactivePriority } from '@/api/miniapps'; + +interface ProactiveFeedProps { + suggestions: ProactiveSuggestion[]; + /** Cooldown in ms per trigger; default 300000 (5 min). */ + cooldowns?: Record; + /** Minimum priority to show. */ + minPriority?: ProactivePriority; + onDismiss?: (id: string) => void; + onAction?: (suggestion: ProactiveSuggestion) => void; +} + +const PRIORITY_ORDER: Record = { + low: 0, + medium: 1, + high: 2, + urgent: 3, +}; + +const PRIORITY_VARIANT: Record< + ProactivePriority, + 'secondary' | 'info' | 'warning' | 'danger' +> = { + low: 'secondary', + medium: 'info', + high: 'warning', + urgent: 'danger', +}; + +const ACTION_TYPE_ICON: Record = { + suggestion: