chore: reorganize frontend into app and admin roots
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Crosshair, Loader2, MapPinned, Sparkles } from 'lucide-react';
|
||||
import type { DeepResearchPreview } from '@/shared/types';
|
||||
import { createDeepResearchBatch, previewDeepResearch } from '../lib/database';
|
||||
import { DeepResearchPreviewMap } from './DeepResearchPreviewMap';
|
||||
import { Alert, Badge, Button, FieldLabel, Input, MetricPill, PageContainer, PageShell, SectionHeader, Surface } from './ui';
|
||||
|
||||
interface DeepResearchViewProps {
|
||||
onShowBatchOnMap: (jobIds: string[]) => void;
|
||||
topContent?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function DeepResearchView({ onShowBatchOnMap, topContent }: DeepResearchViewProps) {
|
||||
const [pin, setPin] = useState<google.maps.LatLngLiteral | null>(null);
|
||||
const [businessType, setBusinessType] = useState('');
|
||||
const [keywords, setKeywords] = useState('');
|
||||
const [propagation, setPropagation] = useState(1);
|
||||
const [preview, setPreview] = useState<DeepResearchPreview | null>(null);
|
||||
const [previewError, setPreviewError] = useState<string | null>(null);
|
||||
const [isPreviewing, setIsPreviewing] = useState(false);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setPreview(null);
|
||||
setPreviewError(null);
|
||||
}, [businessType, keywords, pin?.lat, pin?.lng, propagation]);
|
||||
|
||||
const canPreview = Boolean(pin && businessType.trim());
|
||||
|
||||
const previewSummary = useMemo(() => {
|
||||
if (!preview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${preview.totalAreas} postal areas across ${preview.countryCode} with ${preview.estimatedChildJobs} child researches.`;
|
||||
}, [preview]);
|
||||
|
||||
const handlePreview = async () => {
|
||||
if (!pin || !businessType.trim()) {
|
||||
setPreviewError('Drop a pin and add a business type before previewing deep research.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPreviewing(true);
|
||||
setPreviewError(null);
|
||||
|
||||
try {
|
||||
const nextPreview = await previewDeepResearch({
|
||||
lat: pin.lat,
|
||||
lng: pin.lng,
|
||||
propagation,
|
||||
businessType: businessType.trim(),
|
||||
keywords: keywords.trim() || undefined,
|
||||
});
|
||||
setPreview(nextPreview);
|
||||
} catch (err) {
|
||||
setPreviewError(err instanceof Error ? err.message : 'Failed to preview deep research.');
|
||||
} finally {
|
||||
setIsPreviewing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunDeepResearch = async () => {
|
||||
if (!pin || !businessType.trim()) {
|
||||
setPreviewError('Drop a pin and add a business type before running deep research.');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRunning(true);
|
||||
setPreviewError(null);
|
||||
|
||||
try {
|
||||
const batch = await createDeepResearchBatch({
|
||||
lat: pin.lat,
|
||||
lng: pin.lng,
|
||||
propagation,
|
||||
businessType: businessType.trim(),
|
||||
keywords: keywords.trim() || undefined,
|
||||
});
|
||||
|
||||
if (batch.jobIds.length > 0) {
|
||||
onShowBatchOnMap(batch.jobIds);
|
||||
}
|
||||
} catch (err) {
|
||||
setPreviewError(err instanceof Error ? err.message : 'Failed to run deep research.');
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell>
|
||||
<PageContainer>
|
||||
{topContent}
|
||||
|
||||
<SectionHeader
|
||||
title="Deep Research"
|
||||
description="Drop a pin, choose a propagation depth, preview the ZIP or FSA areas that will be covered, and run one bundled research batch across every adjacent postal area."
|
||||
/>
|
||||
|
||||
<section className="grid grid-cols-1 items-stretch gap-6 xl:grid-cols-[400px_minmax(0,1fr)]">
|
||||
<Surface className="p-5 sm:p-7">
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<FieldLabel>Business Type</FieldLabel>
|
||||
<Input
|
||||
type="text"
|
||||
value={businessType}
|
||||
onChange={(event) => setBusinessType(event.target.value)}
|
||||
placeholder="e.g. dentists, HVAC, bakeries"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<FieldLabel>Keywords</FieldLabel>
|
||||
<Input
|
||||
type="text"
|
||||
value={keywords}
|
||||
onChange={(event) => setKeywords(event.target.value)}
|
||||
placeholder="Optional comma-separated keywords"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<FieldLabel>Location</FieldLabel>
|
||||
<div className="rounded-2xl border border-stone-200 bg-stone-50 p-4 text-sm text-stone-600">
|
||||
Click directly on the map to place the deep research center. The preview uses that active pin to find the base ZIP or FSA and expand outward.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pin && (
|
||||
<Alert variant="success" title="Active research center">
|
||||
<p>{pin.lat.toFixed(5)}, {pin.lng.toFixed(5)}</p>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<FieldLabel>Propagation</FieldLabel>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="5"
|
||||
value={propagation}
|
||||
onChange={(event) => setPropagation(Number.parseInt(event.target.value, 10) || 0)}
|
||||
className="w-full accent-emerald-600"
|
||||
/>
|
||||
<div className="mt-2 flex items-center justify-between text-xs text-stone-500">
|
||||
<span>Base postal area only</span>
|
||||
<Badge variant="primary">{propagation} hop{propagation === 1 ? '' : 's'}</Badge>
|
||||
<span>Expand outward</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-stone-200 bg-stone-50 p-4 text-sm text-stone-600">
|
||||
<div className="flex items-center gap-2 font-semibold text-stone-900">
|
||||
<Crosshair className="h-4 w-4 text-emerald-600" />
|
||||
Pin placement
|
||||
</div>
|
||||
<p className="mt-2">Click the map to drop a pin. The preview will find the containing ZIP or FSA and expand to adjacent postal areas based on the propagation depth.</p>
|
||||
{pin && <p className="mt-3 font-medium text-stone-800">Pin: {pin.lat.toFixed(5)}, {pin.lng.toFixed(5)}</p>}
|
||||
</div>
|
||||
|
||||
{previewError && (
|
||||
<Alert variant="error">{previewError}</Alert>
|
||||
)}
|
||||
|
||||
{previewSummary && (
|
||||
<Alert variant="success" title="Preview ready">
|
||||
<p>{previewSummary}</p>
|
||||
<p className="mt-2 text-emerald-800">
|
||||
Base area: <span className="font-semibold">{preview?.baseArea.displayName}</span>
|
||||
</p>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => void handlePreview()}
|
||||
disabled={!canPreview || isPreviewing || isRunning}
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
>
|
||||
{isPreviewing ? <Loader2 className="h-4 w-4 animate-spin" /> : <MapPinned className="h-4 w-4" />}
|
||||
Preview areas
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => void handleRunDeepResearch()}
|
||||
disabled={!canPreview || isPreviewing || isRunning}
|
||||
className="flex-1"
|
||||
>
|
||||
{isRunning ? <Loader2 className="h-4 w-4 animate-spin" /> : <Sparkles className="h-4 w-4" />}
|
||||
Run deep research
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Surface>
|
||||
|
||||
<div className="flex h-full flex-col gap-4">
|
||||
<DeepResearchPreviewMap pin={pin} preview={preview} onPinChange={setPin} />
|
||||
|
||||
<Surface className="p-5">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold uppercase tracking-[0.18em] text-stone-500">
|
||||
<MapPinned className="h-4 w-4" />
|
||||
Map Controls
|
||||
</div>
|
||||
<h3 className="mt-3 text-lg font-semibold text-stone-950">Preview center and coverage</h3>
|
||||
<p className="mt-2 text-sm text-stone-600">
|
||||
Drop a pin directly on the map to define the base postal area. Preview colors and rings show how propagation expands the deep research batch into adjacent areas.
|
||||
</p>
|
||||
</Surface>
|
||||
|
||||
{preview && (
|
||||
<Surface className="p-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-stone-950">Preview coverage</h3>
|
||||
<p className="mt-1 text-sm text-stone-600">These postal areas will become child researches in the batch.</p>
|
||||
</div>
|
||||
<MetricPill className="bg-stone-50 text-stone-700 shadow-none">
|
||||
{preview.totalAreas} areas
|
||||
</MetricPill>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{preview.areas.map((area) => (
|
||||
<Badge
|
||||
key={area.id}
|
||||
variant={area.propagationRing === 0 ? 'primary' : 'neutral'}
|
||||
>
|
||||
{area.displayName} · ring {area.propagationRing}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</Surface>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</PageContainer>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user