285 lines
11 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useMCPApp } from '../../hooks/useMCPApp';
import { useSmartAction } from '../../hooks/useSmartAction';
import { PageHeader } from '../../components/layout/PageHeader';
import { Card } from '../../components/layout/Card';
import { Section } from '../../components/layout/Section';
import { StatusBadge } from '../../components/data/StatusBadge';
import { SidChainTracker } from '../../components/data/SidChainTracker';
import { Timeline, TimelineEntry } from '../../components/data/Timeline';
import { Button } from '../../components/shared/Button';
import { Modal } from '../../components/shared/Modal';
import { Toast, useToast } from '../../components/shared/Toast';
export function App() {
const { app, isConnected, toolResult } = useMCPApp();
const { execute, isLoading } = useSmartAction(app);
const { toast, showToast, hideToast } = useToast();
const [submission, setSubmission] = useState<any>(null);
const [showCancelModal, setShowCancelModal] = useState(false);
useEffect(() => {
if (toolResult?.submission) {
setSubmission(toolResult.submission);
}
}, [toolResult]);
const handleRetry = async () => {
try {
await execute('a2p_retry_submission', { submissionId: submission.id });
showToast('Retry initiated successfully', 'success');
} catch (error) {
showToast(`Retry failed: ${error}`, 'error');
}
};
const handleCancel = async () => {
try {
await execute('a2p_cancel_submission', { submissionId: submission.id });
setShowCancelModal(false);
showToast('Submission cancelled', 'success');
} catch (error) {
showToast(`Cancel failed: ${error}`, 'error');
}
};
const handleRefresh = async () => {
try {
await execute('a2p_check_status', { submissionId: submission.id });
showToast('Status refreshed', 'info');
} catch (error) {
showToast(`Refresh failed: ${error}`, 'error');
}
};
const handleViewLandingPage = async () => {
await app.sendMessage(`Please show the landing page preview for submission ${submission.id}`);
};
if (!isConnected) {
return (
<div style={{ padding: 'var(--spacing-8)', textAlign: 'center' }}>
<p>Connecting to MCP host...</p>
</div>
);
}
if (!submission) {
return (
<div style={{ padding: 'var(--spacing-8)', textAlign: 'center' }}>
<p style={{ color: 'var(--color-text-secondary)' }}>No submission data loaded</p>
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-tertiary)', marginTop: 'var(--spacing-2)' }}>
This app displays data passed via tool result. Request a submission from the model.
</p>
</div>
);
}
const canRetry = ['brand_failed', 'campaign_failed', 'manual_review'].includes(submission.status);
const canCancel = ['pending', 'brand_pending', 'campaign_pending'].includes(submission.status);
const remediationTimeline: TimelineEntry[] = submission.remediationHistory.map((entry: any) => ({
timestamp: entry.timestamp,
title: entry.fixApplied,
description: `Reason: ${entry.failureReason}`,
type: 'warning' as const,
}));
const auditTimeline: TimelineEntry[] = [
{ timestamp: submission.createdAt, title: 'Submission Created', type: 'info' },
...(submission.brandSubmittedAt
? [{ timestamp: submission.brandSubmittedAt, title: 'Brand Submitted to TCR', type: 'info' }]
: []),
...(submission.brandResolvedAt
? [
{
timestamp: submission.brandResolvedAt,
title: submission.status.includes('approved') ? 'Brand Approved' : 'Brand Decision',
type: submission.status.includes('approved') ? ('success' as const) : ('error' as const),
},
]
: []),
...(submission.campaignSubmittedAt
? [{ timestamp: submission.campaignSubmittedAt, title: 'Campaign Submitted', type: 'info' }]
: []),
...(submission.campaignResolvedAt
? [
{
timestamp: submission.campaignResolvedAt,
title: submission.status === 'campaign_approved' ? 'Campaign Approved' : 'Campaign Decision',
type: submission.status === 'campaign_approved' ? ('success' as const) : ('error' as const),
},
]
: []),
].filter(Boolean) as TimelineEntry[];
return (
<div style={{ minHeight: '100vh', background: 'var(--color-background-primary)' }}>
<PageHeader
title={submission.input.business.businessName}
subtitle={`Submission ID: ${submission.id}`}
/>
<div className="container" style={{ padding: 'var(--spacing-6)' }}>
{/* Header Card */}
<Card padding="lg" className="mb-6">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--spacing-4)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
<StatusBadge status={submission.status} size="lg" />
{submission.brandTrustScore && (
<div>
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>
Brand Trust Score:{' '}
</span>
<span
style={{
fontSize: 'var(--font-size-xl)',
fontWeight: 700,
color:
submission.brandTrustScore >= 75
? 'var(--color-success)'
: submission.brandTrustScore >= 50
? 'var(--color-warning)'
: 'var(--color-error)',
}}
>
{submission.brandTrustScore}
</span>
</div>
)}
</div>
<div style={{ display: 'flex', gap: 'var(--spacing-2)' }}>
<Button variant="secondary" onClick={handleRefresh} loading={isLoading}>
Refresh
</Button>
{canRetry && (
<Button onClick={handleRetry} loading={isLoading}>
Retry
</Button>
)}
{canCancel && (
<Button variant="danger" onClick={() => setShowCancelModal(true)}>
Cancel
</Button>
)}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 'var(--spacing-4)', fontSize: 'var(--font-size-sm)' }}>
<div>
<div style={{ color: 'var(--color-text-tertiary)', marginBottom: 'var(--spacing-1)' }}>Created</div>
<div style={{ fontWeight: 600 }}>{new Date(submission.createdAt).toLocaleString()}</div>
</div>
<div>
<div style={{ color: 'var(--color-text-tertiary)', marginBottom: 'var(--spacing-1)' }}>Last Updated</div>
<div style={{ fontWeight: 600 }}>{new Date(submission.updatedAt).toLocaleString()}</div>
</div>
<div>
<div style={{ color: 'var(--color-text-tertiary)', marginBottom: 'var(--spacing-1)' }}>Attempts</div>
<div style={{ fontWeight: 600 }}>
{submission.attemptCount} / {submission.maxAttempts}
</div>
</div>
</div>
{submission.failureReason && (
<div
style={{
marginTop: 'var(--spacing-4)',
padding: 'var(--spacing-3)',
background: '#fee2e2',
color: '#991b1b',
borderRadius: 'var(--border-radius-md)',
fontSize: 'var(--font-size-sm)',
}}
>
<strong>Failure Reason:</strong> {submission.failureReason}
</div>
)}
</Card>
{/* SID Chain Progress */}
<div className="mb-6">
<SidChainTracker sidChain={submission.sidChain} />
</div>
{/* Business Info Summary */}
<Section title="Business Information">
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-4)' }}>
<div>
<div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-tertiary)', marginBottom: 'var(--spacing-1)' }}>
Business Name
</div>
<div style={{ fontWeight: 600 }}>{submission.input.business.businessName}</div>
</div>
<div>
<div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-tertiary)', marginBottom: 'var(--spacing-1)' }}>
Industry
</div>
<div style={{ fontWeight: 600 }}>{submission.input.business.businessIndustry}</div>
</div>
<div>
<div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-tertiary)', marginBottom: 'var(--spacing-1)' }}>
Website
</div>
<div style={{ fontWeight: 600 }}>{submission.input.business.websiteUrl}</div>
</div>
<div>
<div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-tertiary)', marginBottom: 'var(--spacing-1)' }}>
Campaign Use Case
</div>
<div style={{ fontWeight: 600 }}>{submission.input.campaign.useCase}</div>
</div>
</div>
{submission.landingPageUrl && (
<div style={{ marginTop: 'var(--spacing-4)' }}>
<Button variant="secondary" onClick={handleViewLandingPage}>
View Landing Page
</Button>
</div>
)}
</Section>
{/* Remediation History */}
{remediationTimeline.length > 0 && (
<Section title="Remediation History" collapsible defaultOpen={false}>
<Timeline entries={remediationTimeline} />
</Section>
)}
{/* Audit Log */}
<Section title="Audit Log" collapsible defaultOpen={false}>
<Timeline entries={auditTimeline} emptyMessage="No audit events yet" />
</Section>
</div>
{/* Cancel Confirmation Modal */}
<Modal
isOpen={showCancelModal}
onClose={() => setShowCancelModal(false)}
title="Cancel Submission"
danger
actions={
<>
<Button variant="secondary" onClick={() => setShowCancelModal(false)}>
Keep It
</Button>
<Button variant="danger" onClick={handleCancel} loading={isLoading}>
Cancel Submission
</Button>
</>
}
>
<p style={{ margin: 0 }}>
Are you sure you want to cancel this submission? This action cannot be undone and you'll need to start a new
registration.
</p>
</Modal>
<Toast {...toast} onClose={hideToast} />
</div>
);
}