diff --git a/kits/home-maintenance-triage/.env.example b/kits/home-maintenance-triage/.env.example new file mode 100644 index 000000000..c806eea68 --- /dev/null +++ b/kits/home-maintenance-triage/.env.example @@ -0,0 +1,7 @@ +# Lamatic credentials (from Settings → API Keys in Lamatic Studio) +LAMATIC_PROJECT_ENDPOINT= +LAMATIC_PROJECT_ID= +LAMATIC_PROJECT_API_KEY= + +# Your deployed flow ID (from Flow → Details Panel → Flow ID) +NEXT_PUBLIC_LAMATIC_FLOW_ID= diff --git a/kits/home-maintenance-triage/README.md b/kits/home-maintenance-triage/README.md new file mode 100644 index 000000000..a9293fae7 --- /dev/null +++ b/kits/home-maintenance-triage/README.md @@ -0,0 +1,164 @@ +# Home Maintenance Triage + +![Built with Lamatic](https://img.shields.io/badge/Built%20with-Lamatic-5B21B6?style=flat-square) +![Type: Kit](https://img.shields.io/badge/type-kit-0EA5E9?style=flat-square) +![Challenge](https://img.shields.io/badge/challenge-agentkit-F59E0B?style=flat-square) + +> Describe any household issue and get a structured AI assessment — how serious it is, whether you need a professional, and what to do right now. + +--- + +## The Problem + +When something breaks at home, most people face the same uncertainty. Is this a small thing I can handle myself, or do I need to call someone today? Is it safe to wait a few days, or is this actually an emergency? + +Getting this wrong in either direction has real consequences. Ignoring a slow gas leak or a sparking outlet can be dangerous. Panicking over a nail hole or a dripping faucet wastes time and money. + +This kit gives homeowners a fast, structured way to understand what they are dealing with. + +--- + +## What It Does + +You describe the issue — a water stain, a strange smell, a noise from the AC — and the agent returns a structured report: + +| Field | Description | +|-------|-------------| +| Severity | low, moderate, high, or emergency | +| Urgency | Plain-language timeframe for action | +| Professional needed | Yes or no, and which type | +| Safe next steps | Specific actions to take right now | +| Do not do | Things to avoid attempting yourself | +| Reasoning | Why the agent assessed it this way | +| Disclaimer | Always included — this is informational, not an inspection | + +### Safety-first design + +The agent is built around a few hard rules: + +- It always rounds up on severity when the situation is ambiguous +- For emergencies, safe next steps are hazard-specific: **gas smell** — evacuate and call emergency services only (never touch switches or breakers); **fire or smoke** — evacuate and call emergency services; **electrical fault** — evacuate, call emergency services, then isolate the breaker only if safely reachable, then contact a licensed electrician +- It never gives DIY instructions for electrical, gas, or structural problems +- It never claims to replace a licensed inspector or professional + +--- + +## How It Works + +``` +User describes the issue (text + optional photo URL or uploaded image) + | + API Request node (Lamatic trigger) + | + Generate Text node (vision LLM) + — analyzes description and image with a safety-first system prompt + | + API Response node + | + Next.js frontend renders the structured result +``` + +One flow, one LLM call, clean JSON output. Public HTTPS photo URLs and base64 image data URIs (`data:image/...`) are supported directly. + +--- + +## Setup + +### Prerequisites + +- A [Lamatic.ai](https://lamatic.ai) account (free tier works) +- A vision-capable model configured in Lamatic Studio — GPT-4o, Gemini 1.5 Pro, or Claude 3.5 Sonnet work well +- Node.js 20.9.0 or later + +### Step 1 — Build the Lamatic Flow + +1. Log in to [studio.lamatic.ai](https://studio.lamatic.ai) +2. Create a new project and a new flow +3. Add three nodes: + - **API Request** — trigger node, accepts `issueDescription` (string) and `imageUrl` (string, optional) + - **Generate Text** — LLM node, select a vision-capable model, paste the system prompt from `prompts/home-maintenance-triage_generate-text_system.md` + - **API Response** — maps `output` to `{{LLMNode_501.output.generatedResponse}}` (or your Studio-assigned LLM node output) +4. Connect them in order: API Request > Generate Text > API Response +5. Deploy the flow and copy the Flow ID from the details panel + +### Step 2 — Configure the App + +```bash +cd apps +cp .env.example .env.local +``` + +Open `.env.local` and fill in your four credentials: + +``` +LAMATIC_PROJECT_ENDPOINT=https://your-project.lamatic.ai +LAMATIC_PROJECT_ID=your-project-id +LAMATIC_PROJECT_API_KEY=your-api-key +NEXT_PUBLIC_LAMATIC_FLOW_ID=your-flow-id +``` + +### Step 3 — Run Locally + +```bash +npm install +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) + +### Step 4 — Deploy (Optional) + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/sage106/AgentKit/tree/main/kits/home-maintenance-triage/apps) + +Set the same four environment variables in your Vercel project settings. + +--- + +## Example + +**Input:** +```json +{ + "issueDescription": "Wall outlet sparked when I plugged something in. I can smell something burning near it.", + "imageUrl": "https://example.com/outlet-photo.jpg" +} +``` + +**Output:** +```json +{ + "category": "electrical", + "severity": "emergency", + "urgency": "Stop and act immediately", + "professionalNeeded": true, + "professionalType": "licensed electrician", + "safeNextSteps": [ + "Evacuate or step back from the immediate area if smoke or burning smell is active", + "Call emergency services immediately", + "Turn off power to that circuit at the breaker only if you can safely reach the panel without crossing the hazard", + "Contact a licensed electrician once the immediate hazard is addressed" + ], + "doNotDo": [ + "Do not touch the outlet", + "Do not attempt to inspect or repair this yourself", + "Do not ignore the burning smell" + ], + "reasoning": "Sparking combined with a burning smell indicates an active electrical fault and a fire risk. This needs immediate professional attention.", + "disclaimer": "This is an informational assessment, not a professional inspection. For anything electrical, gas-related, or structural, or if you are unsure, contact a licensed professional." +} +``` + +--- + +## Disclaimer + +This tool provides informational triage only. It is not a substitute for a licensed inspector, electrician, plumber, or structural engineer. For any electrical, gas, or structural concern — or if you are unsure — contact a qualified professional. + +--- + +## Notes + +- The flow uses a single LLM call. No RAG, no multi-step pipeline. For a triage task like this, keeping it simple is the right call. +- The LLM node must be a vision-capable model. Text-only models will still work when no image is provided but cannot analyze photos. +- Image URLs are passed directly to the vision model. No file storage is needed. +- The system prompt instructs the model to round up on severity when uncertain. This is intentional — false reassurance is more dangerous than over-caution. diff --git a/kits/home-maintenance-triage/agent.md b/kits/home-maintenance-triage/agent.md new file mode 100644 index 000000000..70ecd74e7 --- /dev/null +++ b/kits/home-maintenance-triage/agent.md @@ -0,0 +1,94 @@ +# Home Maintenance Triage + +## What this agent does + +Home Maintenance Triage takes a description of a household problem — a leak, a strange smell, a noise from the AC, a discoloration on the wall — and returns a structured, cautious assessment of how serious it is and what the person should do next. + +The output is always honest about what the agent can and cannot determine. When evidence is unclear, it defaults to the more cautious interpretation. It never gives repair instructions for anything electrical, gas-related, or structural. + +## What it returns + +Given an issue description and an optional photo URL, the agent produces: + +- **category** — the type of problem: water damage, electrical, structural, mold, pest, cosmetic, or other +- **severity** — low, moderate, high, or emergency +- **urgency** — a single sentence explaining the timeframe for action +- **professionalNeeded** — whether a licensed professional is required +- **professionalType** — which kind, if applicable (plumber, electrician, HVAC technician, structural engineer, etc.) +- **safeNextSteps** — two to five concrete actions the person can take right now +- **doNotDo** — things they should explicitly avoid attempting themselves +- **reasoning** — one or two sentences explaining the assessment +- **disclaimer** — a fixed note that this is informational, not a professional inspection + +## What it will not do + +- Give confident DIY repair instructions for electrical, gas, or structural problems +- Downplay a potential hazard to seem more helpful or reassuring +- Fabricate details that are not visible in the image or stated in the description +- Act as a substitute for a licensed inspector, electrician, plumber, or structural engineer + +For emergencies, safe next steps are hazard-specific: + +- **Active gas smell:** (1) Evacuate the area immediately; (2) Contact emergency services or your gas utility provider. Do not touch any switches, appliances, or breakers — any spark can ignite gas. +- **Fire or smoke:** (1) Evacuate immediately; (2) Contact emergency services (fire department). +- **Active sparking or electrical fault:** (1) Evacuate or step back from the area; (2) Contact emergency services; (3) Turn off the breaker for that circuit only if you can safely reach the panel without crossing the hazard; (4) Contact a licensed electrician. + +## Example — water damage + +**Input:** photo of a brown stain spreading across a ceiling, description: "noticed it a week ago, seems to be growing" + +**Output:** +```json +{ + "category": "water damage", + "severity": "moderate", + "urgency": "Address within the next few days", + "professionalNeeded": true, + "professionalType": "plumber", + "safeNextSteps": [ + "Check the room or floor above for a leaking pipe or fixture", + "Place a container under the stain if it is actively dripping", + "Take photos to track whether the stain grows" + ], + "doNotDo": ["Do not cut into the ceiling to inspect it yourself"], + "reasoning": "A growing stain suggests an active, ongoing leak rather than a one-time incident. The source needs to be found and stopped.", + "disclaimer": "This is an informational assessment, not a professional inspection. For anything electrical, gas-related, or structural, or if you are unsure, contact a licensed professional." +} +``` + +## Example — electrical emergency + +**Input:** description: "wall outlet is sparking and I can smell something burning" + +**Output:** +```json +{ + "category": "electrical", + "severity": "emergency", + "urgency": "Stop and act immediately", + "professionalNeeded": true, + "professionalType": "licensed electrician", + "safeNextSteps": [ + "Evacuate or step back from the immediate area if smoke or burning smell is active", + "Call emergency services immediately", + "Turn off power to that circuit at the breaker only if you can safely reach the panel without crossing the hazard", + "Contact a licensed electrician once the immediate hazard is addressed" + ], + "doNotDo": [ + "Do not touch the outlet", + "Do not attempt to inspect or repair this yourself" + ], + "reasoning": "Sparking combined with a burning smell indicates an active electrical fault and a fire risk.", + "disclaimer": "This is an informational assessment, not a professional inspection. For anything electrical, gas-related, or structural, or if you are unsure, contact a licensed professional." +} +``` + +## How the flow works + +The flow has three nodes: + +1. **API Request** — trigger node that receives `issueDescription` (required) and `imageUrl` (optional) +2. **Generate Text** — a vision-capable LLM node that analyzes the input using the system prompt in `prompts/home-maintenance-triage_generate-text_system.md` +3. **API Response** — returns the generated JSON string as the `output` field + +The system prompt encodes the severity logic, safety escalation rules, and output contract directly. The model is instructed to return only valid JSON with no surrounding text. diff --git a/kits/home-maintenance-triage/apps/.env.example b/kits/home-maintenance-triage/apps/.env.example new file mode 100644 index 000000000..c806eea68 --- /dev/null +++ b/kits/home-maintenance-triage/apps/.env.example @@ -0,0 +1,7 @@ +# Lamatic credentials (from Settings → API Keys in Lamatic Studio) +LAMATIC_PROJECT_ENDPOINT= +LAMATIC_PROJECT_ID= +LAMATIC_PROJECT_API_KEY= + +# Your deployed flow ID (from Flow → Details Panel → Flow ID) +NEXT_PUBLIC_LAMATIC_FLOW_ID= diff --git a/kits/home-maintenance-triage/apps/.gitignore b/kits/home-maintenance-triage/apps/.gitignore new file mode 100644 index 000000000..0d83b4331 --- /dev/null +++ b/kits/home-maintenance-triage/apps/.gitignore @@ -0,0 +1,34 @@ +# dependencies +node_modules/ +.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/kits/home-maintenance-triage/apps/next.config.mjs b/kits/home-maintenance-triage/apps/next.config.mjs new file mode 100644 index 000000000..b108e1a2e --- /dev/null +++ b/kits/home-maintenance-triage/apps/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/kits/home-maintenance-triage/apps/package-lock.json b/kits/home-maintenance-triage/apps/package-lock.json new file mode 100644 index 000000000..389b3b5ef --- /dev/null +++ b/kits/home-maintenance-triage/apps/package-lock.json @@ -0,0 +1,970 @@ +{ + "name": "home-maintenance-triage", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "home-maintenance-triage", + "version": "0.1.0", + "dependencies": { + "lamatic": "^0.3.2", + "next": "15.5.23", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.23.tgz", + "integrity": "sha512-Mv3Z9hVbFcPnoLevsZ6rnX1TBtyHb5E17yN7HTPDXSXxeNsGBjUFrdbjRXKKXIOhfth7/cg6Ay7PZ2UFawaWsQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.23.tgz", + "integrity": "sha512-SrEwOROH/rhA03F59hHtdhgtfZMWGzr5duDBWgRQt2rS3mJhqMKOcnNx6txOd0/i3E3D3uFKYFvyHsEiwQxzag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.23.tgz", + "integrity": "sha512-f0FpFbG2EhDCuptBGcfrLcYMDuQAhe6m1QA4VVfXFrIBoFXvXt/olGbBkYkloKlXQtmhuzvtdYyuu/6zf07GIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.23.tgz", + "integrity": "sha512-WlNtfepUXKX2u2ZsJZ8c3c8+tJSRZqsYzoMwLOY72A8ucKCCgxgNhiePA3qzFYahVWrwcQd8jOeJmBinc+VFVQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.23.tgz", + "integrity": "sha512-W/6qKk7UG93mg14PmQC+2urt69MIdwTBLNQ6MJyeC4wOCIHCjz+VfgssvS1pK7mgYBtLC1g6VKNoHD9xB0WWGg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.23.tgz", + "integrity": "sha512-vzefI32mi6VMk96RaTAyxApgfGbiFzQBXVsekEjsDv1fr48mlABTWx0sUYhaYCBHWqCalxmz3DxbxFcbFvzNtw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.23.tgz", + "integrity": "sha512-qppK/3dTGOTI+aoWWBZc3DshFIhrzgL8guATlaN9V6M1QJxbkP/rhEZ22tdICsQ/2WWXopMZ2Jokzj2u3uKY3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.23.tgz", + "integrity": "sha512-Wc29KFOdT7XBcII3Vtmw7aoU8Uk3Mes/FNJfhFeSHdYBFJWMcR/DsI8U9BCPUhq/uycsUVuqSKGthW15tLsigA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.23.tgz", + "integrity": "sha512-/C7wRW4fa9s/PKA18zGPPpVmx8ycgVpP8yOxro4gzGTzjPJdscbAP3ODeFvgiIovxD176Z2J/SXO9t8PJKHLeQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/lamatic": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/lamatic/-/lamatic-0.3.2.tgz", + "integrity": "sha512-oOIpnJmjOxlMuViFsmI3LsbEMFxB7unZXplqgzKeu9hy87kqxP1/K1gU6NMQU+98iy1A3XbW7aQSfSLxvYq3sA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.23", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.23.tgz", + "integrity": "sha512-Gvd2WKgvxIXCGotxcI1im/Uf3rS3J3oZGw0g/uskg6AVBZhyE3aAbujkYWzS3xLmEPEtTLfkaVQUKK0KMTSIkA==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.23", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.23", + "@next/swc-darwin-x64": "15.5.23", + "@next/swc-linux-arm64-gnu": "15.5.23", + "@next/swc-linux-arm64-musl": "15.5.23", + "@next/swc-linux-x64-gnu": "15.5.23", + "@next/swc-linux-x64-musl": "15.5.23", + "@next/swc-win32-arm64-msvc": "15.5.23", + "@next/swc-win32-x64-msvc": "15.5.23", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/kits/home-maintenance-triage/apps/package.json b/kits/home-maintenance-triage/apps/package.json new file mode 100644 index 000000000..99b0a2e67 --- /dev/null +++ b/kits/home-maintenance-triage/apps/package.json @@ -0,0 +1,23 @@ +{ + "name": "home-maintenance-triage", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "lamatic": "^0.3.2", + "next": "15.5.23", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "typescript": "^5" + } +} diff --git a/kits/home-maintenance-triage/apps/src/app/api/triage/route.ts b/kits/home-maintenance-triage/apps/src/app/api/triage/route.ts new file mode 100644 index 000000000..424795cda --- /dev/null +++ b/kits/home-maintenance-triage/apps/src/app/api/triage/route.ts @@ -0,0 +1,226 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Lamatic } from "lamatic"; + +/** + * POST /api/triage + * + * Accepts a home maintenance issue description (and optional image URL, home type, + * and issue location), forwards the payload to the deployed Lamatic flow via the + * official SDK, and returns a structured triage assessment containing category, + * severity, urgency, professional recommendation, safe next steps, and a disclaimer. + * + * @param req - Incoming Next.js request with JSON body containing: + * - issueDescription {string} Required. Text description of the home issue. + * - imageUrl {string} Optional. Public HTTPS URL or base64 data URI of an issue photo. + * - homeType {string} Optional. Type of home (e.g. "apartment", "house"). + * - issueLocation {string} Optional. Location in the home (e.g. "kitchen", "roof"). + * @returns NextResponse with { result } on success, or { error } with an appropriate HTTP status. + */ +export async function POST(req: NextRequest) { + // 1. Parse and validate request body + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json( + { error: "Invalid JSON in request body." }, + { status: 400 } + ); + } + + if (typeof body !== "object" || body === null) { + return NextResponse.json( + { error: "Request body must be a JSON object." }, + { status: 400 } + ); + } + + const { issueDescription, imageUrl, homeType, issueLocation } = body as Record; + + if (!issueDescription || typeof issueDescription !== "string" || !issueDescription.trim()) { + return NextResponse.json( + { error: "issueDescription is required and must be a non-empty string." }, + { status: 400 } + ); + } + + if (imageUrl !== undefined && typeof imageUrl !== "string") { + return NextResponse.json({ error: "imageUrl must be a string." }, { status: 400 }); + } + + if (homeType !== undefined && typeof homeType !== "string") { + return NextResponse.json({ error: "homeType must be a string." }, { status: 400 }); + } + + if (issueLocation !== undefined && typeof issueLocation !== "string") { + return NextResponse.json({ error: "issueLocation must be a string." }, { status: 400 }); + } + + // Validate imageUrl: + // Accept: public HTTPS URLs, or base64 data URIs from device uploads (data:image/...) + // Reject: non-HTTPS URLs, private/loopback hosts + if (imageUrl) { + const isDataUri = (imageUrl as string).startsWith("data:image/"); + + if (isDataUri) { + // Validate it is a well-formed image data URI + if (!/^data:image\/(jpeg|jpg|png|webp|gif|bmp|svg\+xml);base64,/.test(imageUrl as string)) { + return NextResponse.json( + { error: "imageUrl data URI must be a valid base64-encoded image." }, + { status: 400 } + ); + } + } else { + // Validate as a public HTTPS URL + try { + const parsed = new URL(imageUrl as string); + if (parsed.protocol !== "https:") { + return NextResponse.json( + { error: "imageUrl must use HTTPS or be a base64 data URI." }, + { status: 400 } + ); + } + const hostname = parsed.hostname.toLowerCase(); + const blocked = ["localhost", "127.0.0.1", "0.0.0.0", "::1", "169.254."]; + if (blocked.some((b) => hostname.startsWith(b) || hostname === b)) { + return NextResponse.json( + { error: "imageUrl must point to a public host." }, + { status: 400 } + ); + } + } catch { + return NextResponse.json({ error: "imageUrl is not a valid URL." }, { status: 400 }); + } + } + } + + // 2. Check env config + const endpoint = process.env.LAMATIC_PROJECT_ENDPOINT || "https://mohdsorganization618-homemaintenancetriage432.lamatic.dev/graphql"; + const projectId = process.env.LAMATIC_PROJECT_ID; + const apiKey = process.env.LAMATIC_PROJECT_API_KEY; + const flowId = process.env.NEXT_PUBLIC_LAMATIC_FLOW_ID; + + if (!projectId || !apiKey || !flowId) { + return NextResponse.json( + { error: "Lamatic credentials are not configured. Check your .env file." }, + { status: 500 } + ); + } + + const payload: Record = { + issueDescription: issueDescription.trim(), + }; + if (imageUrl) payload.imageUrl = imageUrl; + if (homeType) payload.homeType = homeType as string; + if (issueLocation) payload.issueLocation = issueLocation as string; + + // 3. Execute Lamatic Flow via SDK (Official utils.ts pattern) + let rawOutput: any; + + try { + const lamaticClient = new Lamatic({ + endpoint, + projectId, + apiKey, + }); + + const flowPromise = lamaticClient.executeFlow(flowId, payload); + let timeoutId: ReturnType | undefined; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error("TimeoutError")), 30000); + }); + + try { + const response = (await Promise.race([flowPromise, timeoutPromise])) as any; + console.log("[Lamatic SDK raw response]:", JSON.stringify(response)); + + if (response?.status === "error") { + console.error("[Lamatic SDK flow error]:", response?.message); + return NextResponse.json( + { error: `Flow returned an error: ${response?.message || "Unknown error"}` }, + { status: 502 } + ); + } + + // SDK returns { status, result: { output: { ...fields } }, statusCode } + // Unwrap: result.output first, then result, then response itself + const resultObj = response?.result; + const raw = resultObj?.output ?? resultObj ?? response?.output ?? response; + rawOutput = typeof raw === "string" ? raw : JSON.stringify(raw); + } finally { + if (timeoutId !== undefined) clearTimeout(timeoutId); + } + } catch (err: unknown) { + if (err instanceof Error && err.message === "TimeoutError") { + return NextResponse.json( + { error: "The AI flow timed out. Please try again." }, + { status: 504 } + ); + } + console.error("Lamatic SDK Execution Error:", err); + return NextResponse.json( + { error: "Failed to execute Lamatic flow. Check your credentials and flow deployment." }, + { status: 502 } + ); + } + + if (!rawOutput) { + return NextResponse.json( + { error: "No result returned from the flow. Ensure it is deployed in Lamatic Studio." }, + { status: 500 } + ); + } + + // 4. Parse and validate the LLM JSON output + let parsed: Record; + try { + const candidate = typeof rawOutput === "object" ? rawOutput : JSON.parse(rawOutput); + if (typeof candidate !== "object" || candidate === null) { + throw new Error("Output is not an object"); + } + parsed = candidate as Record; + } catch { + console.error("Failed to parse flow output as JSON:", rawOutput); + return NextResponse.json( + { error: "The AI returned an unexpected response format. Please try again." }, + { status: 500 } + ); + } + + // Normalize: some flow versions emit 'warning' instead of 'disclaimer' — map it to 'disclaimer' + if (!("disclaimer" in parsed) && "warning" in parsed) { + parsed = { ...parsed, disclaimer: parsed.warning }; + } + + // Validate required triage fields + const required = ["category", "severity", "urgency", "professionalNeeded", "safeNextSteps", "disclaimer"]; + const missing = required.filter((f) => !(f in parsed)); + if (missing.length > 0) { + console.error("Triage output missing fields:", missing); + return NextResponse.json( + { error: "The AI response was incomplete. Please try again." }, + { status: 500 } + ); + } + + // Validate severity is a known enum value + const validSeverities = ["low", "moderate", "high", "emergency"]; + if (!validSeverities.includes(parsed.severity as string)) { + console.error("Triage output has invalid severity:", parsed.severity); + return NextResponse.json( + { error: "The AI response was incomplete. Please try again." }, + { status: 500 } + ); + } + + // Require professionalType when professionalNeeded is true + if (parsed.professionalNeeded === true && !parsed.professionalType) { + console.error("Triage output missing professionalType when professionalNeeded is true"); + return NextResponse.json( + { error: "The AI response was incomplete. Please try again." }, + { status: 500 } + ); + } + + return NextResponse.json({ result: parsed }); +} diff --git a/kits/home-maintenance-triage/apps/src/app/globals.css b/kits/home-maintenance-triage/apps/src/app/globals.css new file mode 100644 index 000000000..6689fd819 --- /dev/null +++ b/kits/home-maintenance-triage/apps/src/app/globals.css @@ -0,0 +1,640 @@ +/* Inter font is loaded via next/font/google in layout.tsx */ + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +:root { + --bg-primary: #0a0a0f; + --bg-secondary: #12121a; + --bg-card: #1a1a26; + --border: rgba(255, 255, 255, 0.08); + --border-hover: rgba(255, 255, 255, 0.15); + + --text-primary: #f0f0ff; + --text-secondary: #9090b0; + --text-muted: #5a5a7a; + + --accent: #f59e0b; + --accent-light: #fbbf24; + --accent-dim: rgba(245, 158, 11, 0.12); + + --emergency: #ef4444; + --emergency-dim: rgba(239, 68, 68, 0.12); + --urgent: #f97316; + --urgent-dim: rgba(249, 115, 22, 0.12); + --soon: #eab308; + --soon-dim: rgba(234, 179, 8, 0.12); + --low: #22c55e; + --low-dim: rgba(34, 197, 94, 0.12); + + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 20px; + + --shadow: 0 4px 24px rgba(0, 0, 0, 0.35); + --transition: 0.18s ease; +} + +html, body { + min-height: 100vh; + background-color: var(--bg-primary); + color: var(--text-primary); + font-family: var(--font-inter), system-ui, sans-serif; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +.page { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Header */ +.header { + padding: 1.1rem 2rem; + border-bottom: 1px solid var(--border); + background: rgba(10, 10, 15, 0.85); + backdrop-filter: blur(12px); + position: sticky; + top: 0; + z-index: 100; +} + +.header-inner { + max-width: 860px; + margin: 0 auto; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.header-title { + font-size: 1rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.02em; +} + +.header-badge { + margin-left: auto; + font-size: 0.68rem; + font-weight: 600; + color: var(--accent); + background: var(--accent-dim); + border: 1px solid rgba(245, 158, 11, 0.25); + padding: 0.2rem 0.6rem; + border-radius: 100px; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +/* Main */ +.main { + flex: 1; + max-width: 860px; + margin: 0 auto; + width: 100%; + padding: 3rem 2rem 4rem; +} + +/* Hero */ +.hero { + margin-bottom: 2.5rem; +} + +.hero h1 { + font-size: clamp(1.8rem, 4vw, 2.6rem); + font-weight: 800; + letter-spacing: -0.04em; + line-height: 1.15; + margin-bottom: 0.75rem; + background: linear-gradient(130deg, #f0f0ff 0%, var(--accent-light) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero p { + font-size: 1rem; + color: var(--text-secondary); + max-width: 500px; + line-height: 1.7; +} + +/* Input card */ +.input-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + padding: 1.75rem; + box-shadow: var(--shadow); + margin-bottom: 1.75rem; + transition: border-color var(--transition); +} + +.input-card:focus-within { + border-color: var(--border-hover); +} + +.form-group { + margin-bottom: 1.1rem; +} + +.form-label { + display: block; + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 0.45rem; +} + +.form-label span { + color: var(--text-muted); + font-weight: 400; + text-transform: none; + letter-spacing: 0; +} + +textarea.form-input { + width: 100%; + min-height: 110px; + resize: vertical; + font-family: inherit; +} + +.form-input { + width: 100%; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 0.8rem 0.95rem; + color: var(--text-primary); + font-size: 0.9rem; + font-family: inherit; + line-height: 1.5; + transition: border-color var(--transition), box-shadow var(--transition); + outline: none; +} + +.form-input::placeholder { + color: var(--text-muted); +} + +.form-input:focus { + border-color: rgba(245, 158, 11, 0.45); + box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.07); +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +/* Submit button */ +.submit-btn { + width: 100%; + padding: 0.9rem; + background: linear-gradient(135deg, var(--accent) 0%, #d97706 100%); + color: #0a0a0f; + border: none; + border-radius: var(--radius-md); + font-size: 0.95rem; + font-weight: 700; + font-family: inherit; + cursor: pointer; + transition: opacity var(--transition), transform var(--transition), box-shadow var(--transition); + margin-top: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + letter-spacing: -0.01em; +} + +.submit-btn:hover:not(:disabled) { + opacity: 0.9; + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(245, 158, 11, 0.28); +} + +.submit-btn:active:not(:disabled) { + transform: translateY(0); +} + +.submit-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Spinner */ +.spinner { + width: 16px; + height: 16px; + border: 2px solid rgba(0, 0, 0, 0.25); + border-top-color: #0a0a0f; + border-radius: 50%; + animation: spin 0.65s linear infinite; + flex-shrink: 0; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Error */ +.error-box { + background: rgba(239, 68, 68, 0.09); + border: 1px solid rgba(239, 68, 68, 0.28); + border-radius: var(--radius-md); + padding: 0.9rem 1.1rem; + color: #fca5a5; + font-size: 0.88rem; + margin-bottom: 1.5rem; +} + +/* Result */ +.result-section { + animation: fadeUp 0.35s ease; +} + +@keyframes fadeUp { + from { opacity: 0; transform: translateY(14px); } + to { opacity: 1; transform: translateY(0); } +} + +.result-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.6rem; + flex-wrap: wrap; +} + +.result-header h2 { + font-size: 1.2rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.category-tag { + font-size: 0.75rem; + color: var(--text-muted); + font-weight: 500; + text-transform: capitalize; +} + +.urgency-text { + font-size: 1rem; + color: var(--text-secondary); + margin-bottom: 1.4rem; + line-height: 1.6; +} + +/* Urgency badge */ +.urgency-badge { + display: inline-flex; + align-items: center; + padding: 0.3rem 0.8rem; + border-radius: 100px; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + border: 1px solid; +} + +.urgency-emergency { + color: var(--emergency); + background: var(--emergency-dim); + border-color: rgba(239, 68, 68, 0.3); +} + +.urgency-high { + color: var(--urgent); + background: var(--urgent-dim); + border-color: rgba(249, 115, 22, 0.3); +} + +.urgency-moderate { + color: var(--soon); + background: var(--soon-dim); + border-color: rgba(234, 179, 8, 0.3); +} + +.urgency-low { + color: var(--low); + background: var(--low-dim); + border-color: rgba(34, 197, 94, 0.3); +} + +/* Info grid */ +.info-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.875rem; + margin-bottom: 1.25rem; +} + +.info-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.1rem 1.2rem; + transition: border-color var(--transition); +} + +.info-card:hover { + border-color: var(--border-hover); +} + +.info-card-label { + font-size: 0.68rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.07em; + margin-bottom: 0.3rem; +} + +.info-card-value { + font-size: 0.92rem; + font-weight: 600; + color: var(--text-primary); + line-height: 1.4; +} + +.info-card-value.diy-yes { color: var(--low); } +.info-card-value.diy-no { color: var(--urgent); } + +/* List cards */ +.list-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.1rem 1.4rem; + margin-bottom: 0.875rem; +} + +.list-card-header { + margin-bottom: 0.75rem; +} + +.list-card-title { + font-size: 0.82rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.01em; +} + +.list-card ul { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.45rem; +} + +.list-card li { + font-size: 0.88rem; + color: var(--text-secondary); + padding-left: 1.1rem; + position: relative; + line-height: 1.5; +} + +.list-card li::before { + content: '–'; + position: absolute; + left: 0; + color: var(--accent); +} + +.list-card.danger li::before { + color: var(--emergency); + content: '×'; +} + +/* Reasoning */ +.reasoning-box { + background: var(--bg-card); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + border-radius: var(--radius-md); + padding: 0.9rem 1.1rem; + margin-bottom: 0.875rem; +} + +.reasoning-label { + font-size: 0.68rem; + font-weight: 700; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.07em; + margin-bottom: 0.3rem; +} + +.reasoning-text { + font-size: 0.88rem; + color: var(--text-secondary); + line-height: 1.6; +} + +/* Disclaimer */ +.disclaimer-box { + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 0.8rem 1.1rem; +} + +.disclaimer-text { + font-size: 0.75rem; + color: var(--text-muted); + line-height: 1.6; +} + +/* Examples */ +.examples-section { + margin-bottom: 2rem; +} + +.examples-label { + font-size: 0.72rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.07em; + margin-bottom: 0.65rem; + display: block; +} + +.examples-grid { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.example-chip { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 100px; + padding: 0.35rem 0.8rem; + font-size: 0.78rem; + color: var(--text-secondary); + cursor: pointer; + transition: all var(--transition); + font-family: inherit; + line-height: 1.4; +} + +.example-chip:hover { + border-color: rgba(245, 158, 11, 0.35); + color: var(--accent-light); + background: var(--accent-dim); +} + +/* Footer */ +.footer { + text-align: center; + padding: 1.4rem 2rem; + border-top: 1px solid var(--border); +} + +.footer p { + font-size: 0.75rem; + color: var(--text-muted); +} + +.footer a { + color: var(--accent); + text-decoration: none; +} + +.footer a:hover { + text-decoration: underline; +} + +/* Responsive */ +@media (max-width: 600px) { + .main { padding: 1.75rem 1rem 3rem; } + .header { padding: 1rem; } + .input-card { padding: 1.25rem; } + .form-row { grid-template-columns: 1fr; } + .info-grid { grid-template-columns: 1fr; } + .hero h1 { font-size: 1.7rem; } +} + +/* ── Image upload UI ── */ +.image-mode-toggle { + display: flex; + gap: 0.5rem; + margin-bottom: 0.75rem; +} + +.mode-btn { + padding: 0.4rem 0.9rem; + border-radius: 6px; + border: 1px solid var(--border); + background: transparent; + color: var(--text-secondary); + font-size: 0.82rem; + cursor: pointer; + transition: all 0.18s; +} + +.mode-btn:hover { + border-color: var(--border-hover); + color: var(--text-primary); +} + +.mode-btn.active { + background: var(--accent-dim); + border-color: var(--accent); + color: var(--accent-light); + font-weight: 600; +} + +.upload-area { + width: 100%; +} + +.upload-btn { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + padding: 1.5rem 1rem; + border: 2px dashed var(--border-hover); + border-radius: 10px; + background: transparent; + color: var(--text-secondary); + font-size: 0.95rem; + cursor: pointer; + transition: all 0.2s; +} + +.upload-btn:hover { + border-color: var(--accent); + color: var(--text-primary); + background: var(--accent-dim); +} + +.upload-icon { + font-size: 1.8rem; +} + +.upload-hint { + font-size: 0.75rem; + color: var(--text-muted); +} + +.image-preview-wrap { + display: flex; + flex-direction: column; + gap: 0.6rem; + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; + background: var(--bg-secondary); +} + +.image-preview { + width: 100%; + max-height: 220px; + object-fit: cover; + display: block; +} + +.image-preview-info { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 0.9rem; + font-size: 0.82rem; + color: var(--text-secondary); +} + +.remove-image-btn { + background: transparent; + border: 1px solid var(--border); + color: var(--text-secondary); + padding: 0.25rem 0.6rem; + border-radius: 5px; + font-size: 0.78rem; + cursor: pointer; + transition: all 0.15s; +} + +.remove-image-btn:hover { + border-color: var(--emergency); + color: var(--emergency); +} + diff --git a/kits/home-maintenance-triage/apps/src/app/layout.tsx b/kits/home-maintenance-triage/apps/src/app/layout.tsx new file mode 100644 index 000000000..873d28acd --- /dev/null +++ b/kits/home-maintenance-triage/apps/src/app/layout.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); + +export const metadata: Metadata = { + title: "Home Maintenance Triage | AI-Powered Home Issue Diagnosis", + description: + "Describe any home issue and get an instant AI triage: urgency level, DIY feasibility, safety hazards, which professional to call, and immediate action steps.", + keywords: [ + "home maintenance", + "AI triage", + "home repair", + "DIY", + "emergency", + "plumber", + "electrician", + ], + openGraph: { + title: "Home Maintenance Triage Agent", + description: + "Instantly diagnose any home problem with AI — know if it's an emergency, who to call, and what to do right now.", + type: "website", + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/kits/home-maintenance-triage/apps/src/app/page.tsx b/kits/home-maintenance-triage/apps/src/app/page.tsx new file mode 100644 index 000000000..9407125e4 --- /dev/null +++ b/kits/home-maintenance-triage/apps/src/app/page.tsx @@ -0,0 +1,432 @@ +"use client"; + +import { useState, useRef, useCallback } from "react"; + +interface TriageResult { + category?: string; + severity?: "low" | "moderate" | "high" | "emergency"; + urgency?: string; + professionalNeeded?: boolean; + professionalType?: string | null; + safeNextSteps?: string[]; + doNotDo?: string[]; + reasoning?: string; + disclaimer?: string; +} + +const EXAMPLES = [ + "My ceiling has a brown water stain that has been slowly growing for a week", + "A wall outlet sparked when I plugged something in and I can smell burning", + "There is a strong smell of rotten eggs near the stove", + "My AC unit is making a loud grinding noise and blowing warm air", + "I noticed black mold spots in the bathroom corner behind the toilet", + "My toilet keeps running and does not stop after flushing", +]; + +const SEVERITY_CONFIG: Record = { + emergency: { label: "Emergency", className: "urgency-emergency" }, + high: { label: "Urgent", className: "urgency-high" }, + moderate: { label: "Soon", className: "urgency-moderate" }, + low: { label: "Low", className: "urgency-low" }, +}; + +export default function HomePage() { + const [issueDescription, setIssueDescription] = useState(""); + const [imageMode, setImageMode] = useState<"url" | "upload">("upload"); + const [imageUrl, setImageUrl] = useState(""); + const [imageFile, setImageFile] = useState(null); + const [imagePreview, setImagePreview] = useState(null); + const [homeType, setHomeType] = useState(""); + const [issueLocation, setIssueLocation] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [result, setResult] = useState(null); + const resultRef = useRef(null); + const fileInputRef = useRef(null); + const fileReadGenRef = useRef(0); + + const handleFileChange = useCallback((e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Only accept images + if (!file.type.startsWith("image/")) { + setError("Please select a valid image file (JPG, PNG, WEBP, etc)."); + return; + } + + // Max 5 MB to keep base64 payload reasonable + if (file.size > 5 * 1024 * 1024) { + setError("Image must be smaller than 5 MB."); + return; + } + + const currentGen = ++fileReadGenRef.current; + setImageFile(file); + setError(null); + + const reader = new FileReader(); + reader.onload = (ev) => { + if (currentGen === fileReadGenRef.current) { + setImagePreview(ev.target?.result as string); + } + }; + reader.readAsDataURL(file); + }, []); + + function clearImage() { + fileReadGenRef.current++; + setImageFile(null); + setImagePreview(null); + if (fileInputRef.current) fileInputRef.current.value = ""; + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!issueDescription.trim()) return; + + setLoading(true); + setError(null); + setResult(null); + + try { + // Resolve the image value: uploaded file (base64) or typed URL + let resolvedImageUrl: string | undefined; + if (imageMode === "upload" && imagePreview) { + resolvedImageUrl = imagePreview; // base64 data URI + } else if (imageMode === "url" && imageUrl.trim()) { + resolvedImageUrl = imageUrl.trim(); + } + + const res = await fetch("/api/triage", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + issueDescription: issueDescription.trim(), + imageUrl: resolvedImageUrl, + homeType: homeType || undefined, + issueLocation: issueLocation.trim() || undefined, + }), + }); + + const data = await res.json(); + + if (!res.ok || data.error) { + setError(data.error ?? "Something went wrong. Please try again."); + } else { + setResult(data.result); + setTimeout(() => + resultRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }), + 100 + ); + } + } catch { + setError("Network error. Please check your connection and try again."); + } finally { + setLoading(false); + } + } + + function fillExample(example: string) { + setIssueDescription(example); + setResult(null); + setError(null); + } + + function switchMode(mode: "url" | "upload") { + setImageMode(mode); + setImageUrl(""); + clearImage(); + setError(null); + } + + const severityCfg = result?.severity ? SEVERITY_CONFIG[result.severity] : null; + + return ( +
+
+
+ Home Maintenance Triage + Powered by Lamatic +
+
+ +
+
+

What is going wrong at home?

+

+ Describe any household issue and get a structured assessment — how serious it is, + whether you need a professional, and what to do right now. +

+
+ +
+ Try an example +
+ {EXAMPLES.map((ex) => ( + + ))} +
+
+ +
+
+
+ +