-
{job.title}
-
{job.employer}
+
+
+
+
+
{job.title}
+
{job.employer}
+
+
+
+
+
+
-
-
-
+
+
+ {job.location && (
+
+
+ {job.location}
+
+ )}
+ {deadline && (
+
+
+ {deadline}
+
+ )}
+ {job.salary && (
+
+
+ {job.salary}
+
+ )}
+ {job.degreeRequired && (
+
+
+ {job.degreeRequired}
+
+ )}
-
-
-
- {job.location && (
-
-
- {job.location}
-
- )}
- {job.deadline && (
-
-
- {job.deadline}
-
- )}
- {job.salary && (
-
-
- {job.salary}
-
- )}
- {job.degreeRequired && (
-
-
- {job.degreeRequired}
-
- )}
-
-
- {job.suitabilityReason && (
-
- "{job.suitabilityReason}"
-
+
+
+ {(job.suitabilityReason || canApply || canReject || canProcess || hasPdf) && (
+
+ {job.suitabilityReason && (
+
+ "{job.suitabilityReason}"
+
+ )}
+
)}
-
-
-
+
+
);
};
+
diff --git a/orchestrator/src/client/components/JobList.tsx b/orchestrator/src/client/components/JobList.tsx
index 03aa198..9e48c97 100644
--- a/orchestrator/src/client/components/JobList.tsx
+++ b/orchestrator/src/client/components/JobList.tsx
@@ -2,10 +2,14 @@
* Job list with filtering tabs.
*/
-import React, { useState } from 'react';
-import type { Job, JobStatus } from '../../shared/types';
-import { JobCard } from './JobCard';
-import { RefreshIcon } from './Icons';
+import React, { useMemo, useState } from "react";
+import { Loader2, RefreshCcw } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import type { Job, JobStatus } from "../../shared/types";
+import { JobCard } from "./JobCard";
interface JobListProps {
jobs: Job[];
@@ -17,15 +21,22 @@ interface JobListProps {
isProcessingAll: boolean;
}
-type FilterTab = 'ready' | 'discovered' | 'applied' | 'all';
+type FilterTab = "ready" | "discovered" | "applied" | "all";
const tabs: Array<{ id: FilterTab; label: string; statuses: JobStatus[] }> = [
- { id: 'ready', label: '✨ Ready to Apply', statuses: ['ready'] },
- { id: 'discovered', label: '🔍 Discovered', statuses: ['discovered', 'processing'] },
- { id: 'applied', label: '✅ Applied', statuses: ['applied'] },
- { id: 'all', label: '📋 All Jobs', statuses: [] },
+ { id: "ready", label: "Ready", statuses: ["ready"] },
+ { id: "discovered", label: "Discovered", statuses: ["discovered", "processing"] },
+ { id: "applied", label: "Applied", statuses: ["applied"] },
+ { id: "all", label: "All Jobs", statuses: [] },
];
+const emptyStateCopy: Record
= {
+ ready: "Run the pipeline to discover and process new jobs.",
+ discovered: "All discovered jobs have been processed.",
+ applied: "You haven't applied to any jobs yet.",
+ all: "No jobs in the system yet. Run the pipeline to get started!",
+};
+
export const JobList: React.FC = ({
jobs,
onApply,
@@ -35,86 +46,104 @@ export const JobList: React.FC = ({
processingJobId,
isProcessingAll,
}) => {
- const [activeTab, setActiveTab] = useState('ready');
-
- const filteredJobs = React.useMemo(() => {
- const tab = tabs.find(t => t.id === activeTab);
- if (!tab || tab.statuses.length === 0) {
- return jobs;
+ const [activeTab, setActiveTab] = useState("ready");
+
+ const counts = useMemo(() => {
+ const byTab: Record = {
+ ready: 0,
+ discovered: 0,
+ applied: 0,
+ all: jobs.length,
+ };
+
+ for (const job of jobs) {
+ if (job.status === "ready") byTab.ready += 1;
+ if (job.status === "applied") byTab.applied += 1;
+ if (job.status === "discovered" || job.status === "processing") byTab.discovered += 1;
}
- return jobs.filter(job => tab.statuses.includes(job.status));
- }, [jobs, activeTab]);
-
- const discoveredCount = jobs.filter(j => j.status === 'discovered').length;
-
+
+ return byTab;
+ }, [jobs]);
+
+ const jobsForTab = useMemo(() => {
+ const map = new Map();
+
+ for (const tab of tabs) {
+ if (tab.statuses.length === 0) {
+ map.set(tab.id, jobs);
+ } else {
+ map.set(tab.id, jobs.filter((job) => tab.statuses.includes(job.status)));
+ }
+ }
+
+ return map;
+ }, [jobs]);
+
return (
-
-
-
- {tabs.map(tab => {
- const count = tab.statuses.length === 0
- ? jobs.length
- : jobs.filter(j => tab.statuses.includes(j.status)).length;
-
- return (
- setActiveTab(tab.id)}
- >
- {tab.label} ({count})
-
- );
- })}
-
-
- {activeTab === 'discovered' && discoveredCount > 0 && (
-
+ setActiveTab(value as FilterTab)}
+ className="space-y-4"
+ >
+
+
+ {tabs.map((tab) => (
+
+ {tab.label}
+
+ ({counts[tab.id]})
+
+
+ ))}
+
+
+ {activeTab === "discovered" && counts.discovered > 0 && (
+
{isProcessingAll ? (
<>
-
+
Processing...
>
) : (
<>
-
- Process All ({discoveredCount})
+
+ Process All ({counts.discovered})
>
)}
-
+
)}
-
- {filteredJobs.length === 0 ? (
-
-
📭
-
No jobs found
-
- {activeTab === 'ready' && 'Run the pipeline to discover and process new jobs.'}
- {activeTab === 'discovered' && 'All discovered jobs have been processed.'}
- {activeTab === 'applied' && "You haven't applied to any jobs yet."}
- {activeTab === 'all' && 'No jobs in the system yet. Run the pipeline to get started!'}
-
-
- ) : (
-
- {filteredJobs.map(job => (
-
- ))}
-
- )}
-
+
+ {tabs.map((tab) => {
+ const filteredJobs = jobsForTab.get(tab.id) ?? [];
+
+ return (
+
+ {filteredJobs.length === 0 ? (
+
+
+ No jobs found
+ {emptyStateCopy[tab.id]}
+
+
+ ) : (
+
+ {filteredJobs.map((job) => (
+
+ ))}
+
+ )}
+
+ );
+ })}
+
);
};
+
diff --git a/orchestrator/src/client/components/PipelineProgress.tsx b/orchestrator/src/client/components/PipelineProgress.tsx
index 2097f3d..a0a3b26 100644
--- a/orchestrator/src/client/components/PipelineProgress.tsx
+++ b/orchestrator/src/client/components/PipelineProgress.tsx
@@ -2,10 +2,17 @@
* Live pipeline progress display component.
*/
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useMemo, useState } from "react";
+import { Loader2 } from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Progress } from "@/components/ui/progress";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
interface PipelineProgress {
- step: 'idle' | 'crawling' | 'importing' | 'scoring' | 'processing' | 'completed' | 'failed';
+ step: "idle" | "crawling" | "importing" | "scoring" | "processing" | "completed" | "failed";
message: string;
detail?: string;
crawlingListPagesProcessed: number;
@@ -14,7 +21,7 @@ interface PipelineProgress {
crawlingJobPagesEnqueued: number;
crawlingJobPagesSkipped: number;
crawlingJobPagesProcessed: number;
- crawlingPhase?: 'list' | 'job';
+ crawlingPhase?: "list" | "job";
crawlingCurrentUrl?: string;
jobsDiscovered: number;
jobsScored: number;
@@ -34,26 +41,28 @@ interface PipelineProgressProps {
isRunning: boolean;
}
-const stepLabels: Record
= {
- idle: 'Ready',
- crawling: 'Crawling Jobs',
- importing: 'Importing',
- scoring: 'Scoring Jobs',
- processing: 'Generating Resumes',
- completed: 'Complete',
- failed: 'Failed',
+const stepLabels: Record = {
+ idle: "Ready",
+ crawling: "Crawling",
+ importing: "Importing",
+ scoring: "Scoring",
+ processing: "Processing",
+ completed: "Complete",
+ failed: "Failed",
};
-const stepColors: Record = {
- idle: 'var(--color-muted)',
- crawling: 'var(--color-info)',
- importing: 'var(--color-info)',
- scoring: 'var(--color-warning)',
- processing: 'var(--color-primary-500)',
- completed: 'var(--color-success)',
- failed: 'var(--color-error)',
+const stepBadgeClasses: Record = {
+ idle: "bg-muted text-muted-foreground border-border",
+ crawling: "bg-sky-500/10 text-sky-400 border-sky-500/20",
+ importing: "bg-sky-500/10 text-sky-400 border-sky-500/20",
+ scoring: "bg-amber-500/10 text-amber-400 border-amber-500/20",
+ processing: "bg-primary/10 text-primary border-primary/20",
+ completed: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
+ failed: "bg-destructive/10 text-destructive border-destructive/20",
};
+const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
+
export const PipelineProgress: React.FC = ({ isRunning }) => {
const [progress, setProgress] = useState(null);
const [isConnected, setIsConnected] = useState(false);
@@ -61,29 +70,28 @@ export const PipelineProgress: React.FC = ({ isRunning })
useEffect(() => {
if (!isRunning) {
setProgress(null);
+ setIsConnected(false);
return;
}
- // Connect to SSE endpoint
- const eventSource = new EventSource('/api/pipeline/progress');
-
+ const eventSource = new EventSource("/api/pipeline/progress");
+
eventSource.onopen = () => {
setIsConnected(true);
};
-
+
eventSource.onmessage = (event) => {
try {
- const data = JSON.parse(event.data);
- setProgress(data);
+ setProgress(JSON.parse(event.data));
} catch {
// Ignore parse errors
}
};
-
+
eventSource.onerror = () => {
setIsConnected(false);
};
-
+
return () => {
eventSource.close();
setIsConnected(false);
@@ -94,203 +102,137 @@ export const PipelineProgress: React.FC = ({ isRunning })
return null;
}
- const step = progress?.step || 'idle';
- const isActive = progress && step !== 'idle' && step !== 'completed' && step !== 'failed';
+ const step = progress?.step ?? "idle";
+ const isActive = step !== "idle" && step !== "completed" && step !== "failed";
- // Calculate overall progress percentage
- let percentage = 0;
- if (progress) {
- switch (step) {
- case 'crawling':
+ const percentage = useMemo(() => {
+ if (!progress) return 0;
+
+ switch (progress.step) {
+ case "crawling": {
if (progress.crawlingListPagesTotal > 0) {
- percentage = (progress.crawlingListPagesProcessed / progress.crawlingListPagesTotal) * 15;
- } else if (progress.crawlingListPagesProcessed > 0) {
- percentage = 8;
- } else {
- percentage = 5;
+ return clamp((progress.crawlingListPagesProcessed / progress.crawlingListPagesTotal) * 15, 0, 15);
}
- break;
- case 'importing':
- percentage = 20;
- break;
- case 'scoring':
+ if (progress.crawlingListPagesProcessed > 0) return 8;
+ return 5;
+ }
+ case "importing":
+ return 20;
+ case "scoring": {
if (progress.jobsScored > 0) {
- percentage = 20 + (progress.jobsScored / Math.max(progress.jobsDiscovered, 1)) * 30;
- } else {
- percentage = 25;
+ return clamp(20 + (progress.jobsScored / Math.max(progress.jobsDiscovered, 1)) * 30, 20, 50);
}
- break;
- case 'processing':
+ return 25;
+ }
+ case "processing": {
if (progress.totalToProcess > 0) {
- percentage = 50 + (progress.jobsProcessed / progress.totalToProcess) * 50;
- } else {
- percentage = 55;
+ return clamp(50 + (progress.jobsProcessed / progress.totalToProcess) * 50, 50, 100);
}
- break;
- case 'completed':
- percentage = 100;
- break;
- case 'failed':
- percentage = 100;
- break;
+ return 55;
+ }
+ case "completed":
+ case "failed":
+ return 100;
+ case "idle":
+ default:
+ return 0;
}
- }
+ }, [progress]);
+
+ const showStats = !!progress && ["crawling", "scoring", "processing", "completed"].includes(step);
return (
-
- {/* Header */}
-
-
- {isActive && (
-
- )}
-
- {stepLabels[step]}
-
+
+
+
+
+ Pipeline
+
+ {stepLabels[step]}
+
+
+ {isConnected ? "Live" : "Connecting…"}
+
+
+
+
+ {isActive && }
+ {Math.round(percentage)}%
+
-
- {Math.round(percentage)}%
-
-
-
- {/* Progress bar */}
-
-
- {/* Message */}
+
+
+
+
{progress && (
-
-
- {progress.message}
-
- {progress.detail && (
-
- {progress.detail}
-
- )}
-
- )}
-
- {/* Stats */}
- {progress && (step === 'crawling' || step === 'scoring' || step === 'processing' || step === 'completed') && (
-
- {step === 'crawling' && (
+
+
+
{progress.message}
+ {progress.detail &&
{progress.detail}
}
+
+
+ {showStats && (
<>
-
-
Sources:
-
- {progress.crawlingListPagesProcessed}
- {progress.crawlingListPagesTotal > 0 ? `/${progress.crawlingListPagesTotal}` : ''}
-
+
+
+ {step === "crawling" ? (
+ <>
+
+
Sources
+
+ {progress.crawlingListPagesProcessed}
+ {progress.crawlingListPagesTotal > 0 ? `/${progress.crawlingListPagesTotal}` : ""}
+
+
+
+
Pages
+
+ {progress.crawlingJobPagesProcessed}/{Math.max(progress.crawlingJobPagesEnqueued, 0)}
+
+
+
+
Enqueued
+
{progress.crawlingJobPagesEnqueued}
+
+
+
Skipped
+
{progress.crawlingJobPagesSkipped}
+
+ >
+ ) : (
+ <>
+
+
Discovered
+
{progress.jobsDiscovered}
+
+
+
Scored
+
{progress.jobsScored}
+
+
+
Processed
+
+ {progress.totalToProcess > 0 ? `${progress.jobsProcessed}/${progress.totalToProcess}` : progress.jobsProcessed}
+
+
+
+
To process
+
{progress.totalToProcess}
+
+ >
+ )}
-
- Pages:
-
- {progress.crawlingJobPagesProcessed}/{Math.max(progress.crawlingJobPagesEnqueued, 0)}
-
-
-
- Enqueued:
-
- {progress.crawlingJobPagesEnqueued}
-
-
- {progress.crawlingJobPagesSkipped > 0 && (
-
- Skipped:
-
- {progress.crawlingJobPagesSkipped}
-
-
- )}
- {progress.crawlingJobCardsFound > 0 && (
-
- Cards:
-
- {progress.crawlingJobCardsFound}
-
-
- )}
>
)}
- {step !== 'crawling' && (
-
-
Discovered:
-
- {progress.jobsDiscovered}
-
+
+ {step === "failed" && progress.error && (
+
+ {progress.error}
)}
- {progress.jobsScored > 0 && (
-
- Scored:
-
- {progress.jobsScored}
-
-
- )}
- {progress.totalToProcess > 0 && (
-
- Processed:
-
- {progress.jobsProcessed}/{progress.totalToProcess}
-
-
- )}
-
+
)}
-
- {/* Error state */}
- {step === 'failed' && progress?.error && (
-
- {progress.error}
-
- )}
-
+
);
};
+
diff --git a/orchestrator/src/client/components/ScoreIndicator.tsx b/orchestrator/src/client/components/ScoreIndicator.tsx
index 0d7ab54..2138841 100644
--- a/orchestrator/src/client/components/ScoreIndicator.tsx
+++ b/orchestrator/src/client/components/ScoreIndicator.tsx
@@ -2,7 +2,9 @@
* Suitability score display component.
*/
-import React from 'react';
+import React from "react";
+
+import { Progress } from "@/components/ui/progress";
interface ScoreIndicatorProps {
score: number | null;
@@ -10,28 +12,14 @@ interface ScoreIndicatorProps {
export const ScoreIndicator: React.FC = ({ score }) => {
if (score === null) {
- return (
-
- Not scored
-
- );
+ return Not scored ;
}
-
- const getScoreClass = () => {
- if (score >= 70) return 'score-high';
- if (score >= 40) return 'score-medium';
- return 'score-low';
- };
-
+
return (
-
-
-
{score}
+
);
};
+
diff --git a/orchestrator/src/client/components/Stats.tsx b/orchestrator/src/client/components/Stats.tsx
index 39e2230..abd0349 100644
--- a/orchestrator/src/client/components/Stats.tsx
+++ b/orchestrator/src/client/components/Stats.tsx
@@ -2,8 +2,18 @@
* Stats dashboard showing job counts by status.
*/
-import React from 'react';
-import type { JobStatus } from '../../shared/types';
+import React from "react";
+import {
+ CheckCircle2,
+ Clock,
+ Loader2,
+ Search,
+ Sparkles,
+ XCircle,
+} from "lucide-react";
+
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import type { JobStatus } from "../../shared/types";
interface StatsProps {
stats: Record
;
@@ -12,39 +22,56 @@ interface StatsProps {
const statConfig: Array<{
key: JobStatus;
label: string;
- emoji: string;
+ Icon: React.ComponentType<{ className?: string }>;
}> = [
- { key: 'discovered', label: 'Discovered', emoji: '🔍' },
- { key: 'processing', label: 'Processing', emoji: '⚙️' },
- { key: 'ready', label: 'Ready', emoji: '✨' },
- { key: 'applied', label: 'Applied', emoji: '✅' },
- { key: 'rejected', label: 'Rejected', emoji: '❌' },
- { key: 'expired', label: 'Expired', emoji: '⏰' },
+ { key: "discovered", label: "Discovered", Icon: Search },
+ { key: "processing", label: "Processing", Icon: Loader2 },
+ { key: "ready", label: "Ready", Icon: Sparkles },
+ { key: "applied", label: "Applied", Icon: CheckCircle2 },
+ { key: "rejected", label: "Rejected", Icon: XCircle },
+ { key: "expired", label: "Expired", Icon: Clock },
];
export const Stats: React.FC = ({ stats }) => {
const total = Object.values(stats).reduce((a, b) => a + b, 0);
-
+
return (
-
-
-
Overview
-
- {total} total jobs
-
-
-
-
- {statConfig.map(({ key, label, emoji }) => (
-
-
{stats[key] || 0}
-
- {emoji}
- {label}
-
-
- ))}
-
-
+
+
+ Overview
+ {total} total jobs
+
+
+
+
+ {statConfig.map(({ key, label, Icon }) => (
+
+
+
+
+
+
+
+
+ {stats[key] || 0}
+
+
+ {label}
+
+
+
+
+
+ ))}
+
+
+
);
};
+
diff --git a/orchestrator/src/client/components/StatusBadge.tsx b/orchestrator/src/client/components/StatusBadge.tsx
index c7fe138..b2158ac 100644
--- a/orchestrator/src/client/components/StatusBadge.tsx
+++ b/orchestrator/src/client/components/StatusBadge.tsx
@@ -2,27 +2,46 @@
* Status badge component.
*/
-import React from 'react';
-import type { JobStatus } from '../../shared/types';
+import React from "react";
+import { Loader2 } from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import { cn } from "@/lib/utils";
+import type { JobStatus } from "../../shared/types";
interface StatusBadgeProps {
status: JobStatus;
}
const statusLabels: Record = {
- discovered: 'Discovered',
- processing: 'Processing',
- ready: 'Ready',
- applied: 'Applied',
- rejected: 'Rejected',
- expired: 'Expired',
+ discovered: "Discovered",
+ processing: "Processing",
+ ready: "Ready",
+ applied: "Applied",
+ rejected: "Rejected",
+ expired: "Expired",
+};
+
+const statusStyles: Record<
+ JobStatus,
+ { variant: "default" | "secondary" | "destructive" | "outline"; className?: string }
+> = {
+ discovered: { variant: "secondary" },
+ processing: { variant: "secondary" },
+ ready: { variant: "default" },
+ applied: { variant: "outline", className: "text-emerald-400 border-emerald-500/30" },
+ rejected: { variant: "destructive" },
+ expired: { variant: "outline", className: "text-muted-foreground" },
};
export const StatusBadge: React.FC = ({ status }) => {
+ const { variant, className } = statusStyles[status];
+
return (
-
- {status === 'processing' && ● }
+
+ {status === "processing" && }
{statusLabels[status]}
-
+
);
};
+
diff --git a/orchestrator/src/client/components/Toast.tsx b/orchestrator/src/client/components/Toast.tsx
deleted file mode 100644
index ae520ac..0000000
--- a/orchestrator/src/client/components/Toast.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Toast notification component.
- */
-
-import React, { useEffect } from 'react';
-
-export interface Toast {
- id: string;
- message: string;
- type: 'success' | 'error' | 'info';
-}
-
-interface ToastContainerProps {
- toasts: Toast[];
- onDismiss: (id: string) => void;
-}
-
-export const ToastContainer: React.FC = ({ toasts, onDismiss }) => {
- return (
-
- {toasts.map(toast => (
-
- ))}
-
- );
-};
-
-const ToastItem: React.FC<{ toast: Toast; onDismiss: (id: string) => void }> = ({
- toast,
- onDismiss,
-}) => {
- useEffect(() => {
- const timer = setTimeout(() => {
- onDismiss(toast.id);
- }, 5000);
- return () => clearTimeout(timer);
- }, [toast.id, onDismiss]);
-
- return (
-
- {toast.message}
-
- );
-};
diff --git a/orchestrator/src/client/components/index.ts b/orchestrator/src/client/components/index.ts
index 3246784..b8adf8b 100644
--- a/orchestrator/src/client/components/index.ts
+++ b/orchestrator/src/client/components/index.ts
@@ -4,6 +4,4 @@ export { StatusBadge } from './StatusBadge';
export { ScoreIndicator } from './ScoreIndicator';
export { JobCard } from './JobCard';
export { JobList } from './JobList';
-export { ToastContainer, type Toast } from './Toast';
export { PipelineProgress } from './PipelineProgress';
-export * from './Icons';
diff --git a/orchestrator/src/client/main.tsx b/orchestrator/src/client/main.tsx
index f8136ef..ac226f6 100644
--- a/orchestrator/src/client/main.tsx
+++ b/orchestrator/src/client/main.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './App';
-import './styles/index.css';
+import '../index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
diff --git a/orchestrator/src/client/styles/index.css b/orchestrator/src/client/styles/index.css
deleted file mode 100644
index 3d4ab42..0000000
--- a/orchestrator/src/client/styles/index.css
+++ /dev/null
@@ -1,680 +0,0 @@
-/* ===================================================================
- Job Ops Orchestrator - Design System
- A modern, dark-mode first design with glassmorphism and gradients
- =================================================================== */
-
-/* CSS Custom Properties (Design Tokens) */
-:root {
- /* Colors */
- --color-background: #0a0a0f;
- --color-surface: #12121a;
- --color-surface-elevated: #1a1a25;
- --color-surface-glass: rgba(26, 26, 37, 0.7);
-
- --color-border: rgba(255, 255, 255, 0.08);
- --color-border-light: rgba(255, 255, 255, 0.12);
-
- --color-text-primary: #f5f5f7;
- --color-text-secondary: #a1a1aa;
- --color-text-muted: #71717a;
-
- /* Accent colors */
- --color-primary: #6366f1;
- --color-primary-light: #818cf8;
- --color-primary-dark: #4f46e5;
- --color-primary-glow: rgba(99, 102, 241, 0.3);
-
- --color-success: #10b981;
- --color-success-light: #34d399;
- --color-success-glow: rgba(16, 185, 129, 0.2);
-
- --color-warning: #f59e0b;
- --color-warning-light: #fbbf24;
-
- --color-danger: #ef4444;
- --color-danger-light: #f87171;
- --color-danger-glow: rgba(239, 68, 68, 0.2);
-
- --color-info: #3b82f6;
- --color-info-light: #60a5fa;
-
- /* Gradients */
- --gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
- --gradient-success: linear-gradient(135deg, #10b981 0%, #34d399 100%);
- --gradient-mesh: radial-gradient(at 40% 20%, hsla(250, 80%, 60%, 0.1) 0px, transparent 50%),
- radial-gradient(at 80% 0%, hsla(280, 80%, 50%, 0.1) 0px, transparent 50%),
- radial-gradient(at 0% 50%, hsla(220, 100%, 60%, 0.05) 0px, transparent 50%);
-
- /* Typography */
- --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- --font-mono: 'SF Mono', Monaco, 'Cascadia Code', monospace;
-
- /* Spacing */
- --space-1: 0.25rem;
- --space-2: 0.5rem;
- --space-3: 0.75rem;
- --space-4: 1rem;
- --space-5: 1.25rem;
- --space-6: 1.5rem;
- --space-8: 2rem;
- --space-10: 2.5rem;
- --space-12: 3rem;
-
- /* Border Radius */
- --radius-sm: 0.375rem;
- --radius-md: 0.5rem;
- --radius-lg: 0.75rem;
- --radius-xl: 1rem;
- --radius-2xl: 1.5rem;
- --radius-full: 9999px;
-
- /* Shadows */
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.4);
- --shadow-glow: 0 0 20px var(--color-primary-glow);
-
- /* Transitions */
- --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
- --transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1);
- --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-/* Reset & Base */
-*, *::before, *::after {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
-}
-
-html {
- font-size: 16px;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-body {
- font-family: var(--font-sans);
- background-color: var(--color-background);
- color: var(--color-text-primary);
- line-height: 1.6;
- min-height: 100vh;
-}
-
-#root {
- min-height: 100vh;
- background-image: var(--gradient-mesh);
- background-attachment: fixed;
-}
-
-/* Typography */
-h1, h2, h3, h4, h5, h6 {
- font-weight: 600;
- line-height: 1.3;
- letter-spacing: -0.02em;
-}
-
-h1 { font-size: 2rem; }
-h2 { font-size: 1.5rem; }
-h3 { font-size: 1.25rem; }
-h4 { font-size: 1.125rem; }
-
-a {
- color: var(--color-primary-light);
- text-decoration: none;
- transition: color var(--transition-fast);
-}
-
-a:hover {
- color: var(--color-primary);
-}
-
-/* Utility Classes */
-.container {
- max-width: 1400px;
- margin: 0 auto;
- padding: 0 var(--space-6);
-}
-
-.flex { display: flex; }
-.flex-col { flex-direction: column; }
-.items-center { align-items: center; }
-.justify-between { justify-content: space-between; }
-.gap-2 { gap: var(--space-2); }
-.gap-3 { gap: var(--space-3); }
-.gap-4 { gap: var(--space-4); }
-.gap-6 { gap: var(--space-6); }
-
-/* Glass Card */
-.card {
- background: var(--color-surface-glass);
- backdrop-filter: blur(12px);
- border: 1px solid var(--color-border);
- border-radius: var(--radius-xl);
- padding: var(--space-6);
- transition: all var(--transition-normal);
-}
-
-.card:hover {
- border-color: var(--color-border-light);
- box-shadow: var(--shadow-lg);
-}
-
-.card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: var(--space-4);
-}
-
-/* Buttons */
-.btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: var(--space-2);
- padding: var(--space-3) var(--space-5);
- font-family: var(--font-sans);
- font-size: 0.875rem;
- font-weight: 500;
- border: none;
- border-radius: var(--radius-lg);
- cursor: pointer;
- transition: all var(--transition-fast);
- white-space: nowrap;
-}
-
-.btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.btn-primary {
- background: var(--gradient-primary);
- color: white;
- box-shadow: var(--shadow-sm), 0 0 20px var(--color-primary-glow);
-}
-
-.btn-primary:hover:not(:disabled) {
- transform: translateY(-1px);
- box-shadow: var(--shadow-md), 0 0 30px var(--color-primary-glow);
-}
-
-.btn-success {
- background: var(--gradient-success);
- color: white;
-}
-
-.btn-success:hover:not(:disabled) {
- transform: translateY(-1px);
- box-shadow: var(--shadow-md), 0 0 20px var(--color-success-glow);
-}
-
-.btn-danger {
- background: var(--color-danger);
- color: white;
-}
-
-.btn-danger:hover:not(:disabled) {
- background: var(--color-danger-light);
-}
-
-.btn-ghost {
- background: transparent;
- color: var(--color-text-secondary);
- border: 1px solid var(--color-border);
-}
-
-.btn-ghost:hover:not(:disabled) {
- background: var(--color-surface);
- color: var(--color-text-primary);
- border-color: var(--color-border-light);
-}
-
-.btn-icon {
- padding: var(--space-2);
- border-radius: var(--radius-md);
-}
-
-/* Status Badges */
-.badge {
- display: inline-flex;
- align-items: center;
- gap: var(--space-1);
- padding: var(--space-1) var(--space-3);
- font-size: 0.75rem;
- font-weight: 500;
- border-radius: var(--radius-full);
- text-transform: uppercase;
- letter-spacing: 0.05em;
-}
-
-.badge-discovered {
- background: rgba(59, 130, 246, 0.15);
- color: var(--color-info-light);
- border: 1px solid rgba(59, 130, 246, 0.3);
-}
-
-.badge-processing {
- background: rgba(245, 158, 11, 0.15);
- color: var(--color-warning-light);
- border: 1px solid rgba(245, 158, 11, 0.3);
-}
-
-.badge-ready {
- background: rgba(16, 185, 129, 0.15);
- color: var(--color-success-light);
- border: 1px solid rgba(16, 185, 129, 0.3);
-}
-
-.badge-applied {
- background: rgba(99, 102, 241, 0.15);
- color: var(--color-primary-light);
- border: 1px solid rgba(99, 102, 241, 0.3);
-}
-
-.badge-rejected {
- background: rgba(239, 68, 68, 0.15);
- color: var(--color-danger-light);
- border: 1px solid rgba(239, 68, 68, 0.3);
-}
-
-.badge-expired {
- background: rgba(113, 113, 122, 0.15);
- color: var(--color-text-muted);
- border: 1px solid rgba(113, 113, 122, 0.3);
-}
-
-/* Score indicator */
-.score {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- font-weight: 600;
- font-size: 0.875rem;
-}
-
-.score-bar {
- width: 60px;
- height: 6px;
- background: var(--color-surface);
- border-radius: var(--radius-full);
- overflow: hidden;
-}
-
-.score-bar-fill {
- height: 100%;
- border-radius: var(--radius-full);
- transition: width var(--transition-slow);
-}
-
-.score-high .score-bar-fill { background: var(--color-success); }
-.score-medium .score-bar-fill { background: var(--color-warning); }
-.score-low .score-bar-fill { background: var(--color-danger); }
-
-/* Stats Grid */
-.stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
- gap: var(--space-4);
-}
-
-.stat-card {
- background: var(--color-surface);
- border: 1px solid var(--color-border);
- border-radius: var(--radius-lg);
- padding: var(--space-4);
- text-align: center;
-}
-
-.stat-value {
- font-size: 2rem;
- font-weight: 700;
- background: var(--gradient-primary);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
-}
-
-.stat-label {
- font-size: 0.75rem;
- color: var(--color-text-muted);
- text-transform: uppercase;
- letter-spacing: 0.05em;
- margin-top: var(--space-1);
-}
-
-/* Job List */
-.job-list {
- display: flex;
- flex-direction: column;
- gap: var(--space-4);
-}
-
-.job-card {
- background: var(--color-surface-glass);
- backdrop-filter: blur(12px);
- border: 1px solid var(--color-border);
- border-radius: var(--radius-xl);
- padding: var(--space-5);
- transition: all var(--transition-normal);
-}
-
-.job-card:hover {
- border-color: var(--color-primary);
- box-shadow: var(--shadow-glow);
- transform: translateY(-2px);
-}
-
-.job-card-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: var(--space-4);
- margin-bottom: var(--space-3);
-}
-
-.job-title {
- font-size: 1.125rem;
- font-weight: 600;
- color: var(--color-text-primary);
- margin-bottom: var(--space-1);
-}
-
-.job-employer {
- font-size: 0.875rem;
- color: var(--color-text-secondary);
-}
-
-.job-meta {
- display: flex;
- flex-wrap: wrap;
- gap: var(--space-4);
- margin-top: var(--space-3);
- padding-top: var(--space-3);
- border-top: 1px solid var(--color-border);
-}
-
-.job-meta-item {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- font-size: 0.8125rem;
- color: var(--color-text-muted);
-}
-
-.job-meta-item svg {
- width: 14px;
- height: 14px;
- opacity: 0.7;
-}
-
-.job-actions {
- display: flex;
- gap: var(--space-2);
- margin-top: var(--space-4);
-}
-
-/* Tabs */
-.tabs {
- display: flex;
- gap: var(--space-1);
- background: var(--color-surface);
- padding: var(--space-1);
- border-radius: var(--radius-lg);
- margin-bottom: var(--space-6);
-}
-
-.tab {
- flex: 1;
- padding: var(--space-3) var(--space-4);
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--color-text-muted);
- background: transparent;
- border: none;
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all var(--transition-fast);
-}
-
-.tab:hover {
- color: var(--color-text-secondary);
-}
-
-.tab.active {
- background: var(--gradient-primary);
- color: white;
-}
-
-/* Empty State */
-.empty-state {
- text-align: center;
- padding: var(--space-12);
- color: var(--color-text-muted);
-}
-
-.empty-state-icon {
- font-size: 3rem;
- margin-bottom: var(--space-4);
- opacity: 0.5;
-}
-
-.empty-state-title {
- font-size: 1.125rem;
- font-weight: 600;
- color: var(--color-text-secondary);
- margin-bottom: var(--space-2);
-}
-
-/* Loading Spinner */
-.spinner {
- width: 20px;
- height: 20px;
- border: 2px solid var(--color-border);
- border-top-color: var(--color-primary);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-
-@keyframes spin {
- to { transform: rotate(360deg); }
-}
-
-/* Pulse animation for processing jobs */
-@keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.5; }
-}
-
-.pulse {
- animation: pulse 2s ease-in-out infinite;
-}
-
-/* Header */
-.header {
- padding: var(--space-6) 0;
- border-bottom: 1px solid var(--color-border);
- margin-bottom: var(--space-8);
-}
-
-.header-content {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.logo {
- display: flex;
- align-items: center;
- gap: var(--space-3);
-}
-
-.logo-icon {
- width: 40px;
- height: 40px;
- border-radius: var(--radius-lg);
- background: var(--gradient-primary);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.25rem;
-}
-
-.logo-text {
- font-size: 1.25rem;
- font-weight: 700;
-}
-
-/* Modal */
-.modal-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.7);
- backdrop-filter: blur(4px);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 100;
- opacity: 0;
- animation: fadeIn var(--transition-fast) forwards;
-}
-
-@keyframes fadeIn {
- to { opacity: 1; }
-}
-
-.modal {
- background: var(--color-surface);
- border: 1px solid var(--color-border);
- border-radius: var(--radius-2xl);
- max-width: 600px;
- width: 90%;
- max-height: 80vh;
- overflow-y: auto;
- animation: slideUp var(--transition-normal) forwards;
-}
-
-@keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: var(--space-5) var(--space-6);
- border-bottom: 1px solid var(--color-border);
-}
-
-.modal-title {
- font-size: 1.125rem;
- font-weight: 600;
-}
-
-.modal-body {
- padding: var(--space-6);
-}
-
-.modal-footer {
- display: flex;
- justify-content: flex-end;
- gap: var(--space-3);
- padding: var(--space-4) var(--space-6);
- border-top: 1px solid var(--color-border);
- background: var(--color-surface-elevated);
- border-radius: 0 0 var(--radius-2xl) var(--radius-2xl);
-}
-
-/* Toast */
-.toast-container {
- position: fixed;
- bottom: var(--space-6);
- right: var(--space-6);
- z-index: 200;
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
-}
-
-.toast {
- background: var(--color-surface-elevated);
- border: 1px solid var(--color-border);
- border-radius: var(--radius-lg);
- padding: var(--space-4) var(--space-5);
- box-shadow: var(--shadow-lg);
- min-width: 280px;
- animation: slideIn var(--transition-normal) forwards;
-}
-
-@keyframes slideIn {
- from {
- opacity: 0;
- transform: translateX(100%);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
-
-.toast-success { border-left: 3px solid var(--color-success); }
-.toast-error { border-left: 3px solid var(--color-danger); }
-.toast-info { border-left: 3px solid var(--color-info); }
-
-/* Scrollbar */
-::-webkit-scrollbar {
- width: 8px;
- height: 8px;
-}
-
-::-webkit-scrollbar-track {
- background: var(--color-surface);
-}
-
-::-webkit-scrollbar-thumb {
- background: var(--color-border-light);
- border-radius: var(--radius-full);
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: var(--color-text-muted);
-}
-
-/* Responsive */
-@media (max-width: 768px) {
- .container {
- padding: 0 var(--space-4);
- }
-
- h1 { font-size: 1.5rem; }
-
- .header-content {
- flex-direction: column;
- gap: var(--space-4);
- align-items: flex-start;
- }
-
- .stats-grid {
- grid-template-columns: repeat(3, 1fr);
- }
-
- .job-card-header {
- flex-direction: column;
- }
-
- .job-actions {
- flex-direction: column;
- }
-
- .job-actions .btn {
- width: 100%;
- }
-}
diff --git a/orchestrator/src/components/ui/alert-dialog.tsx b/orchestrator/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..fa2b442
--- /dev/null
+++ b/orchestrator/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/orchestrator/src/components/ui/badge.tsx b/orchestrator/src/components/ui/badge.tsx
new file mode 100644
index 0000000..e87d62b
--- /dev/null
+++ b/orchestrator/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/orchestrator/src/components/ui/button.tsx b/orchestrator/src/components/ui/button.tsx
new file mode 100644
index 0000000..65d4fcd
--- /dev/null
+++ b/orchestrator/src/components/ui/button.tsx
@@ -0,0 +1,57 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/orchestrator/src/components/ui/card.tsx b/orchestrator/src/components/ui/card.tsx
new file mode 100644
index 0000000..cabfbfc
--- /dev/null
+++ b/orchestrator/src/components/ui/card.tsx
@@ -0,0 +1,76 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/orchestrator/src/components/ui/progress.tsx b/orchestrator/src/components/ui/progress.tsx
new file mode 100644
index 0000000..4fc3b47
--- /dev/null
+++ b/orchestrator/src/components/ui/progress.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "@/lib/utils"
+
+const Progress = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+))
+Progress.displayName = ProgressPrimitive.Root.displayName
+
+export { Progress }
diff --git a/orchestrator/src/components/ui/separator.tsx b/orchestrator/src/components/ui/separator.tsx
new file mode 100644
index 0000000..6d7f122
--- /dev/null
+++ b/orchestrator/src/components/ui/separator.tsx
@@ -0,0 +1,29 @@
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/orchestrator/src/components/ui/sonner.tsx b/orchestrator/src/components/ui/sonner.tsx
new file mode 100644
index 0000000..41c699c
--- /dev/null
+++ b/orchestrator/src/components/ui/sonner.tsx
@@ -0,0 +1,26 @@
+import { Toaster as Sonner } from "sonner"
+
+type ToasterProps = React.ComponentProps
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ return (
+
+ )
+}
+
+export { Toaster }
diff --git a/orchestrator/src/components/ui/tabs.tsx b/orchestrator/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..85d83be
--- /dev/null
+++ b/orchestrator/src/components/ui/tabs.tsx
@@ -0,0 +1,53 @@
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/orchestrator/src/index.css b/orchestrator/src/index.css
new file mode 100644
index 0000000..c864fe0
--- /dev/null
+++ b/orchestrator/src/index.css
@@ -0,0 +1,130 @@
+@import "tailwindcss";
+
+@import "tw-animate-css";
+
+@plugin "tailwindcss-animate";
+
+@custom-variant dark (&:is(.dark *));
+
+@tailwind utilities;
+
+@theme inline {
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --radius-2xl: calc(var(--radius) + 8px);
+ --radius-3xl: calc(var(--radius) + 12px);
+ --radius-4xl: calc(var(--radius) + 16px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI",
+ Roboto, Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
+ @apply bg-background text-foreground antialiased;
+ }
+}
diff --git a/orchestrator/src/lib/utils.ts b/orchestrator/src/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/orchestrator/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/orchestrator/tailwind.config.ts b/orchestrator/tailwind.config.ts
new file mode 100644
index 0000000..a2766a1
--- /dev/null
+++ b/orchestrator/tailwind.config.ts
@@ -0,0 +1,11 @@
+import type { Config } from "tailwindcss";
+
+export default {
+ darkMode: "class",
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+} satisfies Config;
+