diff --git a/kits/supply-chain-risk-oracle/.env.example b/kits/supply-chain-risk-oracle/.env.example new file mode 100644 index 000000000..5413d72a3 --- /dev/null +++ b/kits/supply-chain-risk-oracle/.env.example @@ -0,0 +1,5 @@ +SUPPLY_CHAIN_SCAN_FLOW_ID= +SUPPLY_CHAIN_EMAIL_DRAFT_FLOW_ID= +LAMATIC_API_URL= +LAMATIC_PROJECT_ID= +LAMATIC_API_KEY= diff --git a/kits/supply-chain-risk-oracle/.gitignore b/kits/supply-chain-risk-oracle/.gitignore new file mode 100644 index 000000000..e916ce5aa --- /dev/null +++ b/kits/supply-chain-risk-oracle/.gitignore @@ -0,0 +1,5 @@ +.lamatic/ +node_modules/ +.next/ +.env +.env.local diff --git a/kits/supply-chain-risk-oracle/README.md b/kits/supply-chain-risk-oracle/README.md new file mode 100644 index 000000000..bd8b349ea --- /dev/null +++ b/kits/supply-chain-risk-oracle/README.md @@ -0,0 +1,112 @@ +# Supply Chain Risk Oracle + +An autonomous AI agent that ingests a supplier list, scans global news and weather for disruption events, assigns a **Disruption Probability Score** to each supplier, and auto-drafts professional mitigation emails — all in one pipeline. + +## The Problem + +Global supply chains are vulnerable to geopolitical unrest, extreme weather, labor strikes, and port closures. Procurement teams learn about disruptions **after** they have already caused delays. This agent flips that — it monitors the world for you and surfaces risks before they hit your operations. + +## What It Does + +1. **Upload** a CSV or JSON file listing your suppliers (name, location, components supplied) +2. **Scan** — the agent searches live news, weather alerts, and geopolitical feeds for each supplier's region +3. **Score** — every supplier gets a Disruption Probability Score (0–100) with an explanation +4. **Dashboard** — see a color-coded risk matrix at a glance +5. **Draft** — for Critical/High-risk suppliers, the agent generates a professional supplier inquiry email ready for human review before sending + +## Flows + +| Flow | Purpose | Env Key | +|------|---------|---------| +| `supply-chain-scan` | Parses suppliers, searches for disruption events, returns scored risk matrix | `SUPPLY_CHAIN_SCAN_FLOW_ID` | +| `supply-chain-email-draft` | Generates a professional outreach email for a specific high-risk supplier | `SUPPLY_CHAIN_EMAIL_DRAFT_FLOW_ID` | + +## Tech Stack + +- **Platform:** Lamatic.ai (visual flow builder + edge-deployed GraphQL API) +- **Frontend:** Next.js 15, shadcn/ui, Tailwind CSS +- **LLM:** Configurable in Lamatic Studio (works with any provider — free tiers of Google Gemini or Groq recommended) +- **Web Search:** Configurable in Lamatic Studio (Tavily free tier recommended) + +## Setup + +### Prerequisites + +- Node.js 18+ +- A [Lamatic.ai](https://lamatic.ai) account (free tier works) +- Deployed flows (see below) + +### 1. Build and Deploy Flows in Lamatic Studio + +Sign in at [studio.lamatic.ai](https://studio.lamatic.ai) and create two flows: + +#### Flow 1: Supply Chain Scan +- **Trigger:** API Request +- **Input fields:** `suppliers` (string — raw CSV or JSON), `scan_focus` (string — optional focus area) +- **Nodes:** + 1. `LLMNode` — Parse suppliers from raw input, output structured JSON array + 2. `LLMNode` (with web search tool) — For each supplier, search for disruption events near their location + 3. `LLMNode` — Score each supplier 0–100 and output full risk matrix JSON +- **Output:** `{ risk_matrix: [...], high_risk_suppliers: [...], scan_timestamp: "..." }` + +#### Flow 2: Email Draft +- **Trigger:** API Request +- **Input fields:** `supplier_name`, `location`, `risk_score`, `risk_factors`, `components_supplied` +- **Nodes:** + 1. `LLMNode` — Generate professional supplier inquiry email +- **Output:** `{ email_subject: "...", email_body: "...", urgency_level: "..." }` + +### 2. Get Your Credentials + +From Lamatic Studio → **Settings**: +- `LAMATIC_API_KEY` +- `LAMATIC_PROJECT_ID` +- `LAMATIC_API_URL` + +From each deployed flow's detail panel: +- `SUPPLY_CHAIN_SCAN_FLOW_ID` +- `SUPPLY_CHAIN_EMAIL_DRAFT_FLOW_ID` + +> **API Keys for NewsAPI and OpenWeatherMap:** Store these as environment variables directly inside Lamatic Studio (open your flow → Settings → Environment Variables). Reference them in your API nodes as `{{env.NEWS_API_KEY}}` and `{{env.WEATHER_API_KEY}}`. This keeps them out of execution logs and away from the app layer. + +### 3. Run Locally + +```bash +cd kits/supply-chain-risk-oracle/apps +cp .env.example .env.local +# Fill in .env.local with your real values +npm install +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) + +### 4. Deploy to Vercel + +Click the deploy button or run: + +```bash +vercel --cwd kits/supply-chain-risk-oracle/apps +``` + +## Sample Supplier CSV Format + +```csv +name,location,lat,lng,components_supplied,tier +Apex Electronics,Shenzhen China,22.5431,114.0579,Microcontrollers,1 +Pacific Textiles,Dhaka Bangladesh,23.8103,90.4125,Fabric,1 +Euro Chemicals,Rotterdam Netherlands,51.9244,4.4777,Adhesives,2 +``` + +## Risk Score Interpretation + +| Score | Level | Color | Recommended Action | +|-------|-------|-------|--------------------| +| 80–100 | Critical | 🔴 Red | Immediate outreach + activate backup supplier | +| 60–79 | High | 🟠 Orange | Draft inquiry email + monitor daily | +| 40–59 | Elevated | 🟡 Yellow | Monitor every 48 hours | +| 0–39 | Normal | 🟢 Green | Standard monitoring cadence | + +## License + +MIT diff --git a/kits/supply-chain-risk-oracle/agent.md b/kits/supply-chain-risk-oracle/agent.md new file mode 100644 index 000000000..2110713fe --- /dev/null +++ b/kits/supply-chain-risk-oracle/agent.md @@ -0,0 +1,23 @@ +# Supply Chain Risk Oracle + +## Identity +You are the **Supply Chain Risk Oracle**, an autonomous AI agent built on Lamatic.ai. Your mission is to protect organizations from supply chain disruptions by scanning the world for threats before they become crises. + +## Capabilities +- Parse a supplier list (CSV or JSON) containing supplier names, geolocations, and components supplied +- Execute parallel web searches for breaking news, labor strikes, severe weather, port closures, and geopolitical unrest intersecting each supplier's location +- Assign a **Disruption Probability Score** (0–100) to each supplier node based on proximity, severity, and criticality of identified events +- Classify risk level: **Critical** (>80), **High** (60–80), **Elevated** (40–60), **Normal** (<40) +- Auto-draft professional supplier inquiry emails for all Critical and High-risk nodes +- Return a structured risk matrix ready to power a real-time dashboard + +## Behavior Rules +- Always cite the specific news event or data point that drove a risk score +- Never fabricate events — if no real threat is found, score the supplier 0–20 (Normal) +- Flag uncertainty explicitly when data for a region is limited +- Keep email drafts professional, factual, and non-alarmist +- Treat all supplier data as confidential + +## Pipeline +1. **Scan Flow** — Takes the raw supplier list, searches for relevant disruption signals, scores each node, returns a risk matrix +2. **Email Draft Flow** — Takes a high-risk supplier's profile and risk factors, generates a professional outreach email diff --git a/kits/supply-chain-risk-oracle/apps/.env.example b/kits/supply-chain-risk-oracle/apps/.env.example new file mode 100644 index 000000000..43aeeefbe --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/.env.example @@ -0,0 +1,11 @@ +# Deployed Lamatic flow IDs (Studio → open flow → copy Flow ID) +SUPPLY_CHAIN_SCAN_FLOW_ID= +SUPPLY_CHAIN_EMAIL_DRAFT_FLOW_ID= + +# Lamatic project credentials (Studio → Settings → API) +LAMATIC_API_URL= +LAMATIC_PROJECT_ID= +LAMATIC_API_KEY= + +# Note: NEWS_API_KEY and WEATHER_API_KEY are stored as environment variables +# inside Lamatic Studio (flow settings) — not passed through the app. diff --git a/kits/supply-chain-risk-oracle/apps/.gitignore b/kits/supply-chain-risk-oracle/apps/.gitignore new file mode 100644 index 000000000..f0148a489 --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/.gitignore @@ -0,0 +1,5 @@ +.next/ +node_modules/ +.env.* +!.env.example +.vercel diff --git a/kits/supply-chain-risk-oracle/apps/actions/orchestrate.ts b/kits/supply-chain-risk-oracle/apps/actions/orchestrate.ts new file mode 100644 index 000000000..38fc28dbb --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/actions/orchestrate.ts @@ -0,0 +1,128 @@ +"use server"; + +import { lamaticClient } from "@/lib/lamatic-client"; +import { config } from "@/lib/config"; +import type { ActionResult, ScanResult, SupplierRisk, EmailDraft } from "@/lib/types"; + +function toMessage(error: unknown): string { + if (error instanceof Error) { + if (error.message.includes("fetch failed")) + return "Network error reaching Lamatic. Check LAMATIC_API_URL and your connection."; + if (error.message.toLowerCase().includes("api key")) + return "Authentication error. Check LAMATIC_API_KEY in .env.local."; + return error.message; + } + return "Something went wrong."; +} + +function normalizeSupplier(s: unknown, index: number): SupplierRisk { + const r = (s ?? {}) as Record; + return { + id: typeof r.id === "string" ? r.id : `supplier_${index + 1}`, + name: typeof r.name === "string" ? r.name : "Unknown Supplier", + location: typeof r.location === "string" ? r.location : "", + lat: typeof r.lat === "number" ? r.lat : 0, + lng: typeof r.lng === "number" ? r.lng : 0, + components_supplied: typeof r.components_supplied === "string" ? r.components_supplied : "", + tier: typeof r.tier === "number" ? r.tier : 1, + risk_score: typeof r.risk_score === "number" ? Math.min(100, Math.max(0, r.risk_score)) : 0, + risk_level: ["Critical","High","Elevated","Normal"].includes(r.risk_level as string) + ? (r.risk_level as SupplierRisk["risk_level"]) + : "Normal", + risk_factors: Array.isArray(r.risk_factors) ? r.risk_factors.map(String) : [], + recommended_action: typeof r.recommended_action === "string" ? r.recommended_action : "", + data_confidence: ["high","medium","low"].includes(r.data_confidence as string) + ? (r.data_confidence as SupplierRisk["data_confidence"]) + : "low", + }; +} + +export async function runSupplyChainScan( + suppliers: string, + scanFocus: string +): Promise> { + if (!suppliers.trim()) { + return { success: false, error: "Please paste or upload your supplier data." }; + } + + try { + const workflowId = config.flows["supply-chain-scan"].workflowId; + const res = await lamaticClient.executeFlow(workflowId, { + suppliers: suppliers.trim(), + scan_focus: scanFocus.trim(), + }); + + const result = (res as { result?: Partial })?.result; + if (!result?.risk_matrix) { + throw new Error("The scan flow returned no risk matrix. Check the flow deployment."); + } + + const riskMatrix = Array.isArray(result.risk_matrix) + ? result.risk_matrix.map(normalizeSupplier) + : []; + + return { + success: true, + data: { + risk_matrix: riskMatrix, + high_risk_suppliers: riskMatrix.filter((s) => s.risk_score >= 60), + scan_timestamp: result.scan_timestamp ?? new Date().toISOString(), + summary: result.summary ?? "", + }, + }; + } catch (error) { + return { success: false, error: toMessage(error) }; + } +} + +export async function draftSupplierEmail( + supplierName: string, + location: string, + riskScore: number, + riskFactors: string[], + componentsSupplied: string +): Promise> { + try { + const workflowId = config.flows["supply-chain-email-draft"].workflowId; + const res = await lamaticClient.executeFlow(workflowId, { + supplier_name: supplierName, + location, + risk_score: riskScore, + risk_factors: riskFactors.join(", "), + components_supplied: componentsSupplied, + }); + + const raw = (res as { result?: unknown })?.result; + + let draft: Partial; + if (typeof raw === "string") { + try { + const cleaned = raw + .trim() + .replace(/^```(?:json)?/i, "") + .replace(/```$/, "") + .trim(); + draft = JSON.parse(cleaned); + } catch { + throw new Error("Email draft flow returned an unparseable response."); + } + } else { + draft = (raw ?? {}) as Partial; + } + + if (!draft.email_subject || !draft.email_body) { + throw new Error("Email draft flow is missing subject or body. Check the flow deployment."); + } + + return { + success: true, + data: { + email_subject: draft.email_subject, + email_body: draft.email_body, + urgency_level: draft.urgency_level ?? "elevated", + }, + }; + } catch (error) { + return { success: false, error: toMessage(error) }; + } +} diff --git a/kits/supply-chain-risk-oracle/apps/app/globals.css b/kits/supply-chain-risk-oracle/apps/app/globals.css new file mode 100644 index 000000000..da20c0faf --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/app/globals.css @@ -0,0 +1,747 @@ +@import "tailwindcss"; + +:root { + --ice: #a0d2eb; + --freeze: #e5eaf5; + --medium: #d0bdf4; + --purple: #8458B3; + --heavy: #a28089; + + --bg: #0d0b14; + --surface: #13101f; + --surface2: #1c1730; + --border: #2e2550; + --text: #e5eaf5; + --muted: #7a6e8a; + + --critical: #f87171; + --high: #fb923c; + --elevated: #facc15; + --normal: #34d399; + + --r: 14px; +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html, body { + background: var(--bg); + color: var(--text); + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + min-height: 100vh; +} + +/* ── Nav ── */ +.nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 32px; + height: 58px; + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + background: rgba(13,11,20,0.85); + backdrop-filter: blur(12px); + z-index: 40; +} +.nav-brand { + display: flex; + align-items: center; + gap: 10px; + font-size: 15px; + font-weight: 700; + color: var(--freeze); + text-decoration: none; +} +.nav-icon { + width: 32px; height: 32px; + border-radius: 8px; + background: linear-gradient(135deg, var(--purple), #5c3a9e); + display: flex; align-items: center; justify-content: center; + font-size: 16px; +} +.nav-links { + display: flex; + align-items: center; + gap: 24px; + font-size: 13px; + color: var(--muted); +} +.nav-links a { + color: var(--muted); + text-decoration: none; + transition: color 0.15s; +} +.nav-links a:hover { color: var(--freeze); } +.nav-badge { + display: inline-flex; + align-items: center; + gap: 5px; + border: 1px solid var(--border); + border-radius: 999px; + padding: 4px 12px; + font-size: 12px; + color: var(--medium); + text-decoration: none; +} +.nav-badge:hover { border-color: var(--purple); color: var(--freeze); } + +/* ── Hero ── */ +.hero { + text-align: center; + padding: 72px 24px 56px; + max-width: 680px; + margin: 0 auto; +} +.hero-pill { + display: inline-flex; + align-items: center; + gap: 6px; + border: 1px solid rgba(160,210,235,0.25); + background: rgba(160,210,235,0.06); + border-radius: 999px; + padding: 5px 14px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--ice); + margin-bottom: 24px; +} +.hero h1 { + font-size: clamp(32px, 5vw, 52px); + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1.1; + margin-bottom: 18px; + background: linear-gradient(135deg, var(--freeze) 30%, var(--medium)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} +.hero p { + font-size: 16px; + color: var(--muted); + line-height: 1.7; + margin-bottom: 28px; +} +.hero-checks { + display: flex; + justify-content: center; + gap: 20px; + flex-wrap: wrap; + font-size: 13px; + color: var(--muted); +} +.hero-checks span { + display: flex; + align-items: center; + gap: 5px; +} +.hero-checks span::before { + content: "✓"; + color: var(--normal); + font-weight: 700; +} + +/* ── Input card ── */ +.input-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 20px; + padding: 28px 32px 32px; + max-width: 720px; + margin: 0 auto 60px; + box-shadow: 0 8px 48px rgba(0,0,0,0.4); +} +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 22px; +} +.card-label { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--purple); + margin-bottom: 4px; +} +.card-title { + font-size: 20px; + font-weight: 700; + color: var(--freeze); +} +.server-badge { + display: inline-flex; + align-items: center; + gap: 5px; + background: rgba(52,211,153,0.1); + border: 1px solid rgba(52,211,153,0.2); + border-radius: 999px; + padding: 4px 12px; + font-size: 11px; + font-weight: 600; + color: var(--normal); +} + +.tabs { + display: flex; + background: var(--surface2); + border-radius: 10px; + padding: 3px; + margin-bottom: 22px; + gap: 3px; +} +.tab { + flex: 1; + background: transparent; + border: none; + border-radius: 8px; + padding: 9px 16px; + font-size: 13px; + font-weight: 600; + color: var(--muted); + cursor: pointer; + transition: all 0.15s; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transform: none; +} +.tab:hover:not(:disabled) { color: var(--freeze); transform: none; opacity: 1; } +.tab.active { + background: var(--surface); + color: var(--freeze); + box-shadow: 0 1px 4px rgba(0,0,0,0.3); +} + +.field-label { + display: block; + font-size: 12px; + font-weight: 600; + color: var(--muted); + margin-bottom: 7px; +} +textarea, input[type="text"] { + width: 100%; + background: var(--surface2); + border: 1px solid var(--border); + color: var(--text); + border-radius: 10px; + padding: 12px 14px; + font-size: 13px; + font-family: inherit; + resize: vertical; + transition: border-color 0.15s, box-shadow 0.15s; +} +textarea:focus, input[type="text"]:focus { + outline: none; + border-color: var(--purple); + box-shadow: 0 0 0 3px rgba(132,88,179,0.15); +} +textarea::placeholder, input::placeholder { color: #3d3458; } + +.field-hint { + font-size: 11px; + color: var(--muted); + margin-top: 5px; +} + +.two-col { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; + margin-top: 16px; +} + +.run-btn { + width: 100%; + margin-top: 22px; + background: linear-gradient(135deg, var(--purple), #6b3fa0); + color: #fff; + border: none; + border-radius: 11px; + padding: 14px 24px; + font-size: 15px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: opacity 0.15s, transform 0.1s; + letter-spacing: -0.01em; +} +.run-btn:hover:not(:disabled) { opacity: 0.88; transform: translateY(-1px); } +.run-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; } + +.sample-link { + background: none; + border: none; + color: var(--medium); + font-size: 13px; + font-weight: 600; + cursor: pointer; + display: block; + margin: 14px auto 0; + text-align: center; + text-decoration: underline; + text-underline-offset: 3px; + opacity: 0.8; + transition: opacity 0.15s; + transform: none; + width: auto; + padding: 0; + border-radius: 0; +} +.sample-link:hover:not(:disabled) { opacity: 1; transform: none; } + +/* ── Error ── */ +.error { + border: 1px solid rgba(248,113,113,0.3); + background: rgba(248,113,113,0.07); + color: #fca5a5; + padding: 12px 16px; + border-radius: 10px; + margin-top: 16px; + font-size: 13px; +} + +/* ── Results section ── */ +.results { + max-width: 960px; + margin: 0 auto 80px; + padding: 0 24px; +} + +.results-header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 6px; +} +.audit-label { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--purple); + margin-bottom: 4px; +} +.results-title { + font-size: 28px; + font-weight: 800; + color: var(--freeze); + letter-spacing: -0.02em; +} +.results-meta { + font-size: 13px; + color: var(--muted); + margin-bottom: 20px; + line-height: 1.6; +} + +/* ── Stats row ── */ +.stats-row { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 10px; + margin-bottom: 28px; +} +.stat-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 14px 16px; + text-align: center; +} +.stat-top { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 6px; +} +.stat-num { + font-size: 28px; + font-weight: 800; + line-height: 1; +} +.stat-total .stat-top { color: var(--medium); } +.stat-total .stat-num { color: var(--freeze); } +.stat-critical .stat-top { color: var(--critical); } +.stat-critical .stat-num { color: var(--critical); } +.stat-high .stat-top { color: var(--high); } +.stat-high .stat-num { color: var(--high); } +.stat-elevated .stat-top { color: var(--elevated); } +.stat-elevated .stat-num { color: var(--elevated); } +.stat-normal .stat-top { color: var(--normal); } +.stat-normal .stat-num { color: var(--normal); } + +/* ── Summary box ── */ +.summary-box { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 16px 20px; + font-size: 14px; + color: var(--freeze); + line-height: 1.7; + margin-bottom: 28px; +} + +/* ── Findings header ── */ +.findings-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; +} +.findings-title { + font-size: 18px; + font-weight: 700; + color: var(--freeze); + display: flex; + align-items: center; + gap: 10px; +} +.findings-count { + background: rgba(132,88,179,0.2); + color: var(--medium); + border-radius: 999px; + padding: 2px 10px; + font-size: 12px; + font-weight: 700; +} +.timestamp { + font-size: 12px; + color: var(--muted); +} + +/* ── Supplier rows (accordion style) ── */ +.supplier-list { display: flex; flex-direction: column; gap: 8px; } + +.supplier-row { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; + transition: border-color 0.15s; +} +.supplier-row:hover { border-color: rgba(132,88,179,0.4); } +.supplier-row.expanded { border-color: var(--purple); } + +.supplier-row-head { + display: flex; + align-items: center; + padding: 14px 18px; + cursor: pointer; + gap: 14px; + user-select: none; +} +.row-dot { + width: 9px; height: 9px; + border-radius: 50%; + flex-shrink: 0; +} +.row-dot.critical { background: var(--critical); } +.row-dot.high { background: var(--high); } +.row-dot.elevated { background: var(--elevated); } +.row-dot.normal { background: var(--normal); } + +.row-meta { + flex: 1; + min-width: 0; +} +.row-id { + font-size: 11px; + color: var(--muted); + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + margin-bottom: 2px; +} +.row-name { + font-size: 15px; + font-weight: 700; + color: var(--freeze); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.row-right { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} +.chevron { + color: var(--muted); + font-size: 12px; + transition: transform 0.2s; +} +.chevron.open { transform: rotate(180deg); } + +/* ── Expanded detail ── */ +.supplier-detail { + border-top: 1px solid var(--border); + display: grid; + grid-template-columns: 1fr 280px; + gap: 0; +} +.detail-main { + padding: 20px 20px 20px 20px; + border-right: 1px solid var(--border); +} +.detail-side { + padding: 20px; +} +.detail-section { + margin-bottom: 16px; +} +.detail-section:last-child { margin-bottom: 0; } +.detail-section-label { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 6px; +} +.detail-section-value { + font-size: 13px; + color: var(--text); + line-height: 1.6; +} +.risk-factor-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 5px; +} +.risk-factor-list li { + font-size: 13px; + color: var(--text); + display: flex; + align-items: flex-start; + gap: 7px; + line-height: 1.5; +} +.risk-factor-list li::before { + content: "▸"; + color: var(--high); + font-size: 10px; + margin-top: 3px; + flex-shrink: 0; +} + +.score-big { + font-size: 42px; + font-weight: 900; + line-height: 1; + margin-bottom: 4px; +} +.score-bar-wrap { margin-bottom: 14px; } +.score-bar { + height: 5px; + background: var(--border); + border-radius: 999px; + overflow: hidden; + margin-top: 6px; +} +.score-bar-fill { + height: 100%; + border-radius: 999px; + transition: width 0.5s ease; +} + +.draft-btn { + width: 100%; + background: linear-gradient(135deg, var(--purple), #6b3fa0); + color: #fff; + border: none; + border-radius: 9px; + padding: 10px 16px; + font-size: 13px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + margin-top: 12px; + transition: opacity 0.15s, transform 0.1s; +} +.draft-btn:hover:not(:disabled) { opacity: 0.88; transform: translateY(-1px); } +.draft-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; } + +/* ── Badges ── */ +.badge { + display: inline-block; + font-size: 11px; + font-weight: 700; + padding: 3px 9px; + border-radius: 999px; + white-space: nowrap; + letter-spacing: 0.03em; + text-transform: uppercase; +} +.badge-critical { background: rgba(248,113,113,0.12); color: var(--critical); border: 1px solid rgba(248,113,113,0.25); } +.badge-high { background: rgba(251,146,60,0.12); color: var(--high); border: 1px solid rgba(251,146,60,0.25); } +.badge-elevated { background: rgba(250,204,21,0.10); color: var(--elevated); border: 1px solid rgba(250,204,21,0.2); } +.badge-normal { background: rgba(52,211,153,0.10); color: var(--normal); border: 1px solid rgba(52,211,153,0.2); } + +/* ── Footer note ── */ +.footer-note { + margin-top: 18px; + padding: 13px 18px; + background: rgba(132,88,179,0.07); + border: 1px solid rgba(132,88,179,0.2); + border-radius: 10px; + font-size: 13px; + color: var(--medium); + display: flex; + align-items: center; + gap: 10px; +} + +/* ── Modal ── */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(8,6,18,0.8); + backdrop-filter: blur(6px); + display: flex; + align-items: center; + justify-content: center; + z-index: 50; + padding: 20px; +} +.modal { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 18px; + padding: 0; + max-width: 580px; + width: 100%; + max-height: 88vh; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 24px 80px rgba(0,0,0,0.5); +} +.modal-head { + padding: 24px 28px 20px; + border-bottom: 1px solid var(--border); +} +.modal-head h2 { + font-size: 18px; + font-weight: 800; + color: var(--freeze); + margin-bottom: 8px; +} +.modal-subtitle { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--muted); + flex-wrap: wrap; +} +.modal-body { + padding: 24px 28px; + overflow-y: auto; + flex: 1; +} +.modal-foot { + padding: 16px 28px; + border-top: 1px solid var(--border); + display: flex; + gap: 10px; + align-items: center; + justify-content: flex-end; +} + +.email-subject-box { + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 9px; + padding: 12px 14px; + font-size: 15px; + font-weight: 700; + color: var(--freeze); + margin-bottom: 14px; +} +.email-body-box { + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 9px; + padding: 14px; + font-size: 13px; + white-space: pre-wrap; + line-height: 1.7; + max-height: 260px; + overflow-y: auto; + color: var(--text); +} + +.urgency-tag { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 11px; + font-weight: 700; + padding: 3px 10px; + border-radius: 999px; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.urgency-critical { background: rgba(248,113,113,0.12); color: var(--critical); } +.urgency-high { background: rgba(251,146,60,0.12); color: var(--high); } +.urgency-elevated { background: rgba(250,204,21,0.10); color: var(--elevated); } + +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 9px; + padding: 9px 18px; + font-size: 13px; + font-weight: 700; + cursor: pointer; + transition: opacity 0.15s, transform 0.1s; + border: none; + white-space: nowrap; +} +.btn:hover:not(:disabled) { opacity: 0.85; transform: translateY(-1px); } +.btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; } +.btn-primary { background: linear-gradient(135deg, var(--purple), #6b3fa0); color: #fff; } +.btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--muted); } +.btn-ghost:hover:not(:disabled) { border-color: var(--medium); color: var(--freeze); } + +.spinner { + display: inline-block; + width: 14px; height: 14px; + border: 2px solid rgba(255,255,255,0.2); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.65s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } + +.field-row { margin-bottom: 14px; } +.field-row:last-child { margin-bottom: 0; } + +@media (max-width: 640px) { + .stats-row { grid-template-columns: repeat(3, 1fr); } + .supplier-detail { grid-template-columns: 1fr; } + .detail-main { border-right: none; border-bottom: 1px solid var(--border); } + .two-col { grid-template-columns: 1fr; } + .nav-links { display: none; } +} diff --git a/kits/supply-chain-risk-oracle/apps/app/layout.tsx b/kits/supply-chain-risk-oracle/apps/app/layout.tsx new file mode 100644 index 000000000..7a858b862 --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/app/layout.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Supply Chain Risk Oracle", + description: + "Autonomous agent that scans global news and weather for supply chain disruptions, scores each supplier 0–100, and drafts mitigation emails for high-risk nodes.", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/kits/supply-chain-risk-oracle/apps/app/page.tsx b/kits/supply-chain-risk-oracle/apps/app/page.tsx new file mode 100644 index 000000000..1bcebc83e --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/app/page.tsx @@ -0,0 +1,444 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { runSupplyChainScan, draftSupplierEmail } from "@/actions/orchestrate"; +import type { ScanResult, SupplierRisk, EmailDraft } from "@/lib/types"; + +const SAMPLE_CSV = `name,location,lat,lng,components_supplied,tier +Apex Electronics,Shenzhen China,22.5431,114.0579,Microcontrollers,1 +Pacific Textiles,Dhaka Bangladesh,23.8103,90.4125,Fabric,1 +Euro Chemicals,Rotterdam Netherlands,51.9244,4.4777,Industrial Adhesives,2 +Nippon Precision,Osaka Japan,34.6937,135.5023,Optical Sensors,1 +Atlas Metals,Johannesburg South Africa,-26.2041,28.0473,Aluminum Alloy,2`; + +function dotClass(level: string) { + switch (level) { + case "Critical": return "row-dot critical"; + case "High": return "row-dot high"; + case "Elevated": return "row-dot elevated"; + default: return "row-dot normal"; + } +} +function badgeClass(level: string) { + switch (level) { + case "Critical": return "badge badge-critical"; + case "High": return "badge badge-high"; + case "Elevated": return "badge badge-elevated"; + default: return "badge badge-normal"; + } +} +function scoreColor(score: number) { + if (score >= 80) return "var(--critical)"; + if (score >= 60) return "var(--high)"; + if (score >= 40) return "var(--elevated)"; + return "var(--normal)"; +} +function urgencyClass(level: string) { + if (level === "critical") return "urgency-tag urgency-critical"; + if (level === "high") return "urgency-tag urgency-high"; + return "urgency-tag urgency-elevated"; +} + +interface EmailModalProps { + supplier: SupplierRisk; + onClose: () => void; +} + +function EmailModal({ supplier, onClose }: EmailModalProps) { + const [loading, setLoading] = useState(false); + const [draft, setDraft] = useState(null); + const [error, setError] = useState(""); + const [copied, setCopied] = useState(false); + + useEffect(() => { + function onKey(e: KeyboardEvent) { + if (e.key === "Escape") onClose(); + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onClose]); + + async function generate() { + setLoading(true); + setError(""); + const res = await draftSupplierEmail( + supplier.name, supplier.location, supplier.risk_score, + supplier.risk_factors, supplier.components_supplied + ); + setLoading(false); + if (res.success) setDraft(res.data); + else setError(res.error); + } + + function copy() { + if (!draft) return; + navigator.clipboard.writeText(`Subject: ${draft.email_subject}\n\n${draft.email_body}`) + .then(() => { setCopied(true); setTimeout(() => setCopied(false), 1800); }); + } + + return ( +
+
e.stopPropagation()}> + +
+

Draft Supplier Email

+
+ {supplier.risk_level} + {supplier.name} + · + {supplier.location} +
+
+ +
+ {!draft && !loading && ( + <> +
+
+ Risk Score + {supplier.risk_score} + /100 +
+
+
+
+
Risk Factors
+
    + {(supplier.risk_factors ?? []).map((f, i) =>
  • {f}
  • )} +
+
+ + + )} + + {loading && ( +
+ Drafting professional inquiry… +
+ )} + + {error &&
{error}
} + + {draft && ( + <> +
+ + {draft.urgency_level === "critical" ? "🔴" : draft.urgency_level === "high" ? "🟠" : "🟡"} +  {draft.urgency_level} priority + +
+
+
Subject
+
{draft.email_subject}
+
+
+
Body
+
{draft.email_body}
+
+ + )} +
+ +
+ {draft && ( + + )} + +
+
+
+ ); +} + +function SupplierAccordion({ supplier, index, onDraft }: { + supplier: SupplierRisk; + index: number; + onDraft: (s: SupplierRisk) => void; +}) { + const [open, setOpen] = useState(false); + + return ( +
+
setOpen(!open)} + onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen(!open); } }} + > +
+
+
+ SUP-{String(index + 1).padStart(3, "0")} · Tier {supplier.tier} · {supplier.location} +
+
{supplier.name}
+
+
+ {supplier.risk_level} + + {supplier.risk_score} + + +
+
+ + {open && ( +
+
+
+
Components Supplied
+
{supplier.components_supplied}
+
+
+
Disruption Signals
+
    + {(supplier.risk_factors ?? []).map((f, i) =>
  • {f}
  • )} +
+
+
+
Recommended Action
+
{supplier.recommended_action}
+
+
+ +
+
+ Disruption Score +
+
+ {supplier.risk_score}/100 +
+
+
+
+
+
+
Data Confidence
+
{supplier.data_confidence}
+ + {supplier.risk_score >= 60 ? ( + + ) : ( +
+ ✓ Normal monitoring +
+ )} +
+
+ )} +
+ ); +} + +export default function Page() { + const [suppliersInput, setSuppliersInput] = useState(""); + const [scanFocus, setScanFocus] = useState(""); + const [scanning, setScanning] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(""); + const [selectedSupplier, setSelectedSupplier] = useState(null); + const [activeTab, setActiveTab] = useState<"csv" | "json">("csv"); + + async function onScan() { + if (!suppliersInput.trim()) return; + setScanning(true); + setError(""); + setResult(null); + const res = await runSupplyChainScan(suppliersInput, scanFocus); + setScanning(false); + if (res.success) setResult(res.data); + else setError(res.error); + } + + function loadSample() { + setActiveTab("csv"); + setSuppliersInput(SAMPLE_CSV); + setResult(null); + setError(""); + } + + const sorted = result?.risk_matrix.slice().sort((a, b) => b.risk_score - a.risk_score) ?? []; + const counts = result ? { + critical: sorted.filter(s => s.risk_level === "Critical").length, + high: sorted.filter(s => s.risk_level === "High").length, + elevated: sorted.filter(s => s.risk_level === "Elevated").length, + normal: sorted.filter(s => s.risk_level === "Normal").length, + } : null; + + return ( + <> + {/* Nav */} + + + {/* Hero */} +
+
✦ Lamatic AgentKit
+

Supply chain risks your team can act on.

+

Paste your supplier list and get live disruption scores backed by NewsAPI and OpenWeatherMap — with auto-drafted mitigation emails for at-risk nodes.

+
+ Live news grounded + Weather aware + Email ready +
+
+ + {/* Input card */} +
+
+
+
+
Start a scan
+
Give the Oracle your supplier data
+
+ ⚡ Edge deployed +
+ +
+ + +
+ +
+ +