285 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|