From 6482090789388367f05c0808e6ae7f67c1f41179 Mon Sep 17 00:00:00 2001 From: KURO Date: Mon, 20 Jul 2026 18:34:32 +0530 Subject: [PATCH 1/5] feat: Add supply-chain-risk-oracle kit --- kits/supply-chain-risk-oracle/.env.example | 7 + kits/supply-chain-risk-oracle/.gitignore | 5 + kits/supply-chain-risk-oracle/README.md | 110 + kits/supply-chain-risk-oracle/agent.md | 23 + .../apps/.env.example | 14 + kits/supply-chain-risk-oracle/apps/.gitignore | 4 + .../apps/actions/orchestrate.ts | 100 + .../apps/app/globals.css | 749 ++++++ .../apps/app/layout.tsx | 16 + .../apps/app/page.tsx | 429 ++++ .../apps/lib/config.ts | 17 + .../apps/lib/lamatic-client.ts | 26 + .../apps/lib/types.ts | 33 + .../apps/next-env.d.ts | 6 + .../apps/next.config.mjs | 13 + .../apps/package-lock.json | 2156 +++++++++++++++++ .../apps/package.json | 27 + .../apps/postcss.config.mjs | 8 + .../apps/tailwind.config.ts | 16 + .../apps/tsconfig.json | 21 + .../constitutions/default.md | 25 + .../flows/supply-chain-email-draft.ts | 126 + .../flows/supply-chain-scan.ts | 339 +++ .../lamatic.config.ts | 24 + .../supply-chain-email-draft_draft.ts | 5 + .../model-configs/supply-chain-scan_parse.ts | 5 + .../model-configs/supply-chain-scan_score.ts | 5 + .../model-configs/supply-chain-scan_search.ts | 5 + .../prompts/email-system.md | 35 + .../prompts/scan-system.md | 24 + .../supply-chain-email-draft_draft_user.md | 10 + .../prompts/supply-chain-scan_parse_user.md | 25 + .../prompts/supply-chain-scan_score_user.md | 37 + .../prompts/supply-chain-scan_search_user.md | 35 + 34 files changed, 4480 insertions(+) create mode 100644 kits/supply-chain-risk-oracle/.env.example create mode 100644 kits/supply-chain-risk-oracle/.gitignore create mode 100644 kits/supply-chain-risk-oracle/README.md create mode 100644 kits/supply-chain-risk-oracle/agent.md create mode 100644 kits/supply-chain-risk-oracle/apps/.env.example create mode 100644 kits/supply-chain-risk-oracle/apps/.gitignore create mode 100644 kits/supply-chain-risk-oracle/apps/actions/orchestrate.ts create mode 100644 kits/supply-chain-risk-oracle/apps/app/globals.css create mode 100644 kits/supply-chain-risk-oracle/apps/app/layout.tsx create mode 100644 kits/supply-chain-risk-oracle/apps/app/page.tsx create mode 100644 kits/supply-chain-risk-oracle/apps/lib/config.ts create mode 100644 kits/supply-chain-risk-oracle/apps/lib/lamatic-client.ts create mode 100644 kits/supply-chain-risk-oracle/apps/lib/types.ts create mode 100644 kits/supply-chain-risk-oracle/apps/next-env.d.ts create mode 100644 kits/supply-chain-risk-oracle/apps/next.config.mjs create mode 100644 kits/supply-chain-risk-oracle/apps/package-lock.json create mode 100644 kits/supply-chain-risk-oracle/apps/package.json create mode 100644 kits/supply-chain-risk-oracle/apps/postcss.config.mjs create mode 100644 kits/supply-chain-risk-oracle/apps/tailwind.config.ts create mode 100644 kits/supply-chain-risk-oracle/apps/tsconfig.json create mode 100644 kits/supply-chain-risk-oracle/constitutions/default.md create mode 100644 kits/supply-chain-risk-oracle/flows/supply-chain-email-draft.ts create mode 100644 kits/supply-chain-risk-oracle/flows/supply-chain-scan.ts create mode 100644 kits/supply-chain-risk-oracle/lamatic.config.ts create mode 100644 kits/supply-chain-risk-oracle/model-configs/supply-chain-email-draft_draft.ts create mode 100644 kits/supply-chain-risk-oracle/model-configs/supply-chain-scan_parse.ts create mode 100644 kits/supply-chain-risk-oracle/model-configs/supply-chain-scan_score.ts create mode 100644 kits/supply-chain-risk-oracle/model-configs/supply-chain-scan_search.ts create mode 100644 kits/supply-chain-risk-oracle/prompts/email-system.md create mode 100644 kits/supply-chain-risk-oracle/prompts/scan-system.md create mode 100644 kits/supply-chain-risk-oracle/prompts/supply-chain-email-draft_draft_user.md create mode 100644 kits/supply-chain-risk-oracle/prompts/supply-chain-scan_parse_user.md create mode 100644 kits/supply-chain-risk-oracle/prompts/supply-chain-scan_score_user.md create mode 100644 kits/supply-chain-risk-oracle/prompts/supply-chain-scan_search_user.md diff --git a/kits/supply-chain-risk-oracle/.env.example b/kits/supply-chain-risk-oracle/.env.example new file mode 100644 index 000000000..bd29bb4de --- /dev/null +++ b/kits/supply-chain-risk-oracle/.env.example @@ -0,0 +1,7 @@ +SUPPLY_CHAIN_SCAN_FLOW_ID= +SUPPLY_CHAIN_EMAIL_DRAFT_FLOW_ID= +LAMATIC_API_URL= +LAMATIC_PROJECT_ID= +LAMATIC_API_KEY= +NEWS_API_KEY= +WEATHER_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..251e6ac24 --- /dev/null +++ b/kits/supply-chain-risk-oracle/README.md @@ -0,0 +1,110 @@ +# 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` + +### 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..7aec9f285 --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/.env.example @@ -0,0 +1,14 @@ +# 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= + +# NewsAPI — free key at https://newsapi.org (click Get API Key) +NEWS_API_KEY= + +# OpenWeatherMap — free key at https://openweathermap.org (Sign up, 60 calls/min free) +WEATHER_API_KEY= diff --git a/kits/supply-chain-risk-oracle/apps/.gitignore b/kits/supply-chain-risk-oracle/apps/.gitignore new file mode 100644 index 000000000..0b9056138 --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/.gitignore @@ -0,0 +1,4 @@ +.next/ +node_modules/ +.env +.env.local 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..c2d0c160e --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/actions/orchestrate.ts @@ -0,0 +1,100 @@ +"use server"; + +import { lamaticClient } from "@/lib/lamatic-client"; +import { config } from "@/lib/config"; +import type { ActionResult, ScanResult, 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."; +} + +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(), + news_api_key: process.env.NEWS_API_KEY ?? "", + weather_api_key: process.env.WEATHER_API_KEY ?? "", + }); + + 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."); + } + + return { + success: true, + data: { + risk_matrix: result.risk_matrix ?? [], + high_risk_suppliers: result.high_risk_suppliers ?? [], + 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.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..624a254b3 --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/app/globals.css @@ -0,0 +1,749 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +: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..0cdaa5fee --- /dev/null +++ b/kits/supply-chain-risk-oracle/apps/app/page.tsx @@ -0,0 +1,429 @@ +"use client"; + +import { useState } 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); + + 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)}> +
+
+
+ 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 +
+ +
+ + +
+ +
+ +