diff --git a/.env.local.example b/.env.local.example index e52eec0c..e116efa3 100644 --- a/.env.local.example +++ b/.env.local.example @@ -20,6 +20,10 @@ NEXT_PUBLIC_COMPOSIO_USER_ID=user@example.com # Gemini 3.1 Pro (Google Generative AI) GEMINI_3_PRO_API_KEY=your_gemini_3_pro_api_key_here +# Search API (https://tavily.com) +# If not set, the `search` tool is skipped and the LLM answers from its own knowledge. +TAVILY_API_KEY=your_tavily_api_key + # Supabase Credentials NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL_HERE NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY_HERE diff --git a/GEMINI_3.1_PRO_INTEGRATION.md b/GEMINI_3.1_PRO_INTEGRATION.md index 08f51ab4..697c5f62 100644 --- a/GEMINI_3.1_PRO_INTEGRATION.md +++ b/GEMINI_3.1_PRO_INTEGRATION.md @@ -10,8 +10,8 @@ This document describes the integration of Google's Gemini 3.1 Pro model into th Added Gemini 3.1 Pro as a provider option in the `getModel()` function with the following priority order: -1. **Gemini 3.1 Pro** - Primary choice if `GEMINI_3_PRO_API_KEY` is configured *(UPDATED PRIORITY)* -2. **xAI (Grok)** - Secondary choice if `XAI_API_KEY` is configured +1. **xAI (Grok)** - Primary choice if `XAI_API_KEY` is configured *(UPDATED PRIORITY)* +2. **Gemini 3.1 Pro** - Secondary choice if `GEMINI_3_PRO_API_KEY` is configured 3. **AWS Bedrock** - Tertiary choice if AWS credentials are configured 4. **OpenAI** - Default fallback if `OPENAI_API_KEY` is configured @@ -59,10 +59,10 @@ Gemini 3.1 Pro (`gemini-3.1-pro-preview`) supports: The provider selection follows this priority order: ``` -GEMINI_3_PRO_API_KEY exists? → Use Gemini 3.1 Pro - ↓ No XAI_API_KEY exists? → Use Grok ↓ No +GEMINI_3_PRO_API_KEY exists? → Use Gemini 3.1 Pro + ↓ No AWS credentials exist? → Use AWS Bedrock ↓ No OPENAI_API_KEY exists? → Use OpenAI (default) diff --git a/app/actions.tsx b/app/actions.tsx index 50e985bf..167d23d4 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -114,11 +114,30 @@ async function submit(formData?: FormData, skip?: boolean) { const analysisResult = await streamResult.object; summaryStream.done(analysisResult.summary || 'Analysis complete.'); - if (analysisResult.geoJson) { + // Reconstruct standard GeoJSON from flattened schema if present + let geoJson: FeatureCollection | null = null; + if (analysisResult.geoJson && analysisResult.geoJson.features) { + geoJson = { + type: 'FeatureCollection', + features: analysisResult.geoJson.features.map(f => ({ + type: 'Feature', + geometry: { + type: f.geometryType as any, + coordinates: f.coordinates as any + }, + properties: { + name: f.name, + description: f.description + } + })) + }; + } + + if (geoJson) { uiStream.append( ); } @@ -171,6 +190,7 @@ async function submit(formData?: FormData, skip?: boolean) { role: 'assistant', content: JSON.stringify({ ...analysisResult, + geoJson: geoJson, // Use reconstructed GeoJSON for storage/UI image: dataUrl, mapboxImage: mapboxDataUrl, googleImage: googleDataUrl diff --git a/bun.lock b/bun.lock index f101e5d7..bca9d9e3 100644 --- a/bun.lock +++ b/bun.lock @@ -36,6 +36,7 @@ "@tailwindcss/typography": "^0.5.16", "@tavily/core": "^0.6.4", "@turf/turf": "^7.2.0", + "@types/jspdf": "^2.0.0", "@types/mapbox__mapbox-gl-draw": "^1.4.8", "@types/pg": "^8.15.4", "@upstash/redis": "^1.35.0", @@ -56,11 +57,13 @@ "framer-motion": "^12.23.24", "geotiff": "^2.1.4-beta.1", "glassmorphic": "^0.0.3", + "jspdf": "^4.2.1", "katex": "^0.16.22", "lodash": "^4.17.21", "lottie-react": "^2.4.1", "lucide-react": "^0.507.0", "mapbox-gl": "^3.11.0", + "markdown-to-txt": "^2.0.1", "next": "15.3.8", "next-themes": "^0.3.0", "open-codex": "^0.1.30", @@ -199,7 +202,7 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], - "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], @@ -941,6 +944,8 @@ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + "@types/jspdf": ["@types/jspdf@2.0.0", "", { "dependencies": { "jspdf": "*" } }, "sha512-oonYDXI4GegGaG7FFVtriJ+Yqlh4YR3L3NVDiwCEBVG7sbya19SoGx4MW4kg1MCMRPgkbbFTck8YKJL8PrkDfA=="], + "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], @@ -959,12 +964,16 @@ "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + "@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="], + "@types/pbf": ["@types/pbf@3.0.5", "", {}, "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="], "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], "@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="], + "@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="], + "@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -977,6 +986,8 @@ "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/tz-lookup": ["@types/tz-lookup@6.1.2", "", {}, "sha512-9y31Xf/8FHXrCHjvVjGZLcsayAa6ABNc8bZlk6MPOQLLlr41tICSqW3TRPRIx2nodbzdKs5N7ipHWBrUsWUiAA=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -1131,6 +1142,8 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], @@ -1169,6 +1182,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="], + "canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -1237,6 +1252,8 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], @@ -1245,6 +1262,8 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], + "csscolorparser": ["csscolorparser@1.0.3", "", {}, "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], @@ -1309,6 +1328,8 @@ "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + "dompurify": ["dompurify@3.4.2", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], @@ -1433,6 +1454,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-png": ["fast-png@6.4.0", "", { "dependencies": { "@types/pako": "^2.0.3", "iobuffer": "^5.3.2", "pako": "^2.1.0" } }, "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], @@ -1585,6 +1608,8 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], @@ -1617,6 +1642,8 @@ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "iobuffer": ["iobuffer@5.4.0", "", {}, "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "is-absolute-url": ["is-absolute-url@4.0.1", "", {}, "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A=="], @@ -1739,6 +1766,8 @@ "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], + "jspdf": ["jspdf@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.28.6", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.3.1", "html2canvas": "^1.0.0-rc.5" } }, "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ=="], + "jsts": ["jsts@2.7.1", "", {}, "sha512-x2wSZHEBK20CY+Wy+BPE7MrFQHW6sIsdaGUMEqmGAio+3gFzQaBYPwLRonUfQf9Ak8pBieqj9tUofX1+WtAEIg=="], "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], @@ -1773,8 +1802,12 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.escape": ["lodash.escape@4.0.1", "", {}, "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "lodash.unescape": ["lodash.unescape@4.0.1", "", {}, "sha512-DhhGRshNS1aX6s5YdBE3njCCouPgnG29ebyHvImlZzXZf2SHgt+J08DHgytTPnpywNbO1Y8mNUFyQuIDBq2JZg=="], + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], @@ -1795,7 +1828,9 @@ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "markdown-to-txt": ["markdown-to-txt@2.0.1", "", { "dependencies": { "lodash.escape": "^4.0.1", "lodash.unescape": "^4.0.1", "marked": "^4.0.14" } }, "sha512-Hsj7KTN8k1gutlLum3vosHwVZGnv8/cbYKWVkUyo/D1rzOYddbDesILebRfOsaVfjIBJank/AVOySBlHAYqfZw=="], + + "marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="], "marked-terminal": ["marked-terminal@7.3.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "ansi-regex": "^6.1.0", "chalk": "^5.4.1", "cli-highlight": "^2.1.11", "cli-table3": "^0.6.5", "node-emoji": "^2.2.0", "supports-hyperlinks": "^3.1.0" }, "peerDependencies": { "marked": ">=1 <16" } }, "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw=="], @@ -2021,6 +2056,8 @@ "pbf": ["pbf@4.0.1", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": { "pbf": "bin/pbf" } }, "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA=="], + "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], + "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], @@ -2113,6 +2150,8 @@ "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="], + "raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="], + "ramda": ["ramda@0.29.1", "", {}, "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -2153,6 +2192,8 @@ "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], "rehype-external-links": ["rehype-external-links@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-is-element": "^3.0.0", "is-absolute-url": "^4.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw=="], @@ -2189,6 +2230,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="], + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], "robust-predicates": ["robust-predicates@2.0.4", "", {}, "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg=="], @@ -2275,6 +2318,8 @@ "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + "stackblur-canvas": ["stackblur-canvas@2.7.0", "", {}, "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -2335,6 +2380,8 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "svg-pathdata": ["svg-pathdata@6.0.3", "", {}, "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="], + "sweepline-intersections": ["sweepline-intersections@1.5.0", "", { "dependencies": { "tinyqueue": "^2.0.0" } }, "sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ=="], "swr": ["swr@2.3.8", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w=="], @@ -2349,6 +2396,8 @@ "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="], + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], @@ -2465,6 +2514,8 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], + "uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], @@ -2723,6 +2774,8 @@ "open-codex/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "open-codex/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "open-codex/openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="], "open-codex/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], @@ -2745,6 +2798,8 @@ "react-reconciler/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "react-textarea-autosize/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/components/chat.tsx b/components/chat.tsx index d3560a77..8b36f89f 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -21,6 +21,7 @@ import { MapDataProvider, useMapData } from './map/map-data-context'; // Add thi import { updateDrawingContext } from '@/lib/actions/chat'; // Import the server action import dynamic from 'next/dynamic' import { HeaderSearchButton } from './header-search-button' +import { ReportButton } from './report-button' type ChatProps = { id?: string // This is the chatId diff --git a/components/report-button.tsx b/components/report-button.tsx new file mode 100644 index 00000000..f8f0c95c --- /dev/null +++ b/components/report-button.tsx @@ -0,0 +1,64 @@ +'use client' + +import React, { useState } from 'react' +import { Button } from '@/components/ui/button' +import { FileDown, Loader2 } from 'lucide-react' +import { useAIState } from 'ai/rsc' +import { useMapData } from '@/components/map/map-data-context' +import { useMap } from '@/components/map/map-context' +import { generateReport } from '@/lib/utils/report-generator' +import { toast } from 'sonner' + +interface ReportButtonProps { + inline?: boolean +} + +export const ReportButton = ({ inline = false }: ReportButtonProps) => { + const [aiState] = useAIState() + const { mapData } = useMapData() + const { map } = useMap() + const [isGenerating, setIsGenerating] = useState(false) + + const handleDownloadReport = async () => { + if (isGenerating) return + + setIsGenerating(true) + try { + const mapSnapshot = map ? map.getCanvas().toDataURL('image/png') : '' + + const chatTitle = aiState.chatId ? `Chat-${aiState.chatId.substring(0, 8)}` : 'QCX-Analysis' + + await generateReport({ + messages: aiState.messages, + drawnFeatures: mapData.drawnFeatures || [], + mapSnapshot, + chatTitle + }) + + // Removed success toast as per user request + } catch (error) { + console.error('Failed to generate report:', error) + toast.error('Failed to generate report') + } finally { + setIsGenerating(false) + } + } + + return ( + + ) +} diff --git a/components/settings/components/settings.tsx b/components/settings/components/settings.tsx index 8806664d..d7f8c037 100644 --- a/components/settings/components/settings.tsx +++ b/components/settings/components/settings.tsx @@ -23,6 +23,7 @@ import { getSystemPrompt, saveSystemPrompt } from "../../../lib/actions/chat" import { getSelectedModel, saveSelectedModel } from "../../../lib/actions/users" import { useCurrentUser } from "@/lib/auth/use-current-user" import { SettingsSkeleton } from './settings-skeleton' +import { ReportButton } from '@/components/report-button' // Define the form schema with enum validation for roles const settingsFormSchema = z.object({ @@ -166,7 +167,7 @@ export function Settings({ initialTab = "system-prompt" }: SettingsProps) { System Prompt Model Selection User Management - Map + Reports - + - Map Provider - Choose the map provider to use in the application. + Report Generation + Generate and download a PDF report of your current analysis. - - setMapProvider(value as MapProvider)} - className="space-y-2" - > -
- - -
-
- - -
-
+ +
+

+ Your report will include the conversation history, current map view, analysis results, and any drawn features or measurements. +

+ +
+ +
+

Map Provider Settings

+ setMapProvider(value as MapProvider)} + className="space-y-2" + > +
+ + +
+
+ + +
+
+
diff --git a/lib/agents/resolution-search.tsx b/lib/agents/resolution-search.tsx index d456fe49..9985712d 100644 --- a/lib/agents/resolution-search.tsx +++ b/lib/agents/resolution-search.tsx @@ -1,45 +1,10 @@ import { CoreMessage, streamObject } from 'ai' import { getModel } from '@/lib/utils' -import { z } from 'zod' import { tavily } from '@tavily/core' +import { resolutionSearchSchema } from '@/lib/schema/resolution-search' // This agent is now a pure data-processing module, with no UI dependencies. -// Define the schema for the structured response from the AI. -const resolutionSearchSchema = z.object({ - summary: z.string().describe('A detailed text summary of the analysis, including land feature classification, points of interest, relevant current news, and temporal context.'), - geoJson: z.object({ - type: z.literal('FeatureCollection'), - features: z.array(z.object({ - type: z.literal('Feature'), - geometry: z.object({ - type: z.string(), // e.g., 'Point', 'Polygon' - coordinates: z.any(), - }), - properties: z.object({ - name: z.string(), - description: z.string().optional(), - }), - })), - }).describe('A GeoJSON object containing points of interest and classified land features to be overlaid on the map.'), - extractedCoordinates: z.object({ - latitude: z.number(), - longitude: z.number() - }).optional().describe('The extracted geocoordinates of the center of the image.'), - cogInfo: z.object({ - applicable: z.boolean(), - description: z.string().optional() - }).optional().describe('Information about whether Cloud Optimized GeoTIFF (COG) data is applicable or available for this area.'), - newsContext: z.object({ - hasRecentNews: z.boolean(), - newsItems: z.array(z.object({ - title: z.string(), - summary: z.string(), - relevance: z.string() - })).optional() - }).optional().describe('Recent news and events relevant to the analyzed location.') -}) - export interface DrawnFeature { id: string; type: 'Polygon' | 'LineString'; @@ -162,7 +127,7 @@ Use these user-drawn areas/lines as primary areas of interest for your analysis. 4. **Coordinate Extraction:** If possible, confirm or refine the geocoordinates (latitude/longitude) of the center of the image. 5. **COG Applicability:** Determine if this location would benefit from Cloud Optimized GeoTIFF (COG) analysis for high-precision temporal or spectral data. 6. **News Integration:** Reference any recent news or events that may be relevant to the current state of the location. -7. **Structured Output:** Return your findings in a structured JSON format including summary, geoJson, and newsContext. +7. **Structured Output:** Return your findings in a structured JSON format including summary, geoJson (if any), news context, and any extracted coordinates or COG information. Use the provided schema. Your analysis should be based on the visual information in the image, the temporal context provided, and your general knowledge. Do not attempt to access external websites or perform web searches beyond what has been provided. diff --git a/lib/agents/tools/geospatial.tsx b/lib/agents/tools/geospatial.tsx index 33099975..1690f94d 100644 --- a/lib/agents/tools/geospatial.tsx +++ b/lib/agents/tools/geospatial.tsx @@ -346,12 +346,27 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g // Build arguments const toolArgs = (() => { switch (queryType) { - case 'directions': return { waypoints: [params.origin, params.destination], includeMapPreview: includeMap, profile: params.mode }; - case 'distance': return { places: [params.origin, params.destination], includeMapPreview: includeMap, mode: params.mode || 'driving' }; - case 'reverse': return { searchText: `${params.coordinates.latitude},${params.coordinates.longitude}`, includeMapPreview: includeMap, maxResults: params.maxResults || 5 }; - case 'search': return { searchText: params.query, includeMapPreview: includeMap, maxResults: params.maxResults || 5, ...(params.coordinates && { proximity: `${params.coordinates.latitude},${params.coordinates.longitude}` }), ...(params.radius && { radius: params.radius }) }; - case 'geocode': - case 'map': return { searchText: params.location, includeMapPreview: includeMap, maxResults: queryType === 'geocode' ? params.maxResults || 5 : undefined }; + case 'directions': { + if (!params.origin || !params.destination) throw new Error("'directions' query requires origin and destination"); + return { waypoints: [params.origin, params.destination], includeMapPreview: includeMap, profile: params.mode }; + } + case 'distance': { + if (!params.origin || !params.destination) throw new Error("'distance' query requires origin and destination"); + return { places: [params.origin, params.destination], includeMapPreview: includeMap, mode: params.mode || 'driving' }; + } + case 'reverse': { + if (!params.coordinates) throw new Error("'reverse' query requires coordinates"); + return { searchText: `${params.coordinates.latitude},${params.coordinates.longitude}`, includeMapPreview: includeMap, maxResults: params.maxResults || 5 }; + } + case 'search': { + if (!params.query) throw new Error("'search' query requires query"); + return { searchText: params.query, includeMapPreview: includeMap, maxResults: params.maxResults || 5, ...(params.coordinates && { proximity: `${params.coordinates.latitude},${params.coordinates.longitude}` }), ...(params.radius && { radius: params.radius }) }; + } + case 'geocode': + case 'map': { + if (!params.location) throw new Error(`'${queryType}' query requires location`); + return { searchText: params.location, includeMapPreview: includeMap, maxResults: queryType === 'geocode' ? params.maxResults || 5 : undefined }; + } } })(); diff --git a/lib/agents/tools/index.tsx b/lib/agents/tools/index.tsx index 4c22b887..2491cf9c 100644 --- a/lib/agents/tools/index.tsx +++ b/lib/agents/tools/index.tsx @@ -14,10 +14,6 @@ export interface ToolProps { export const getTools = ({ uiStream, fullResponse, mapProvider }: ToolProps) => { const tools: any = { - search: searchTool({ - uiStream, - fullResponse - }), retrieve: retrieveTool({ uiStream, fullResponse @@ -28,6 +24,13 @@ export const getTools = ({ uiStream, fullResponse, mapProvider }: ToolProps) => }) } + if (process.env.TAVILY_API_KEY) { + tools.search = searchTool({ + uiStream, + fullResponse + }) + } + if (process.env.SERPER_API_KEY) { tools.videoSearch = videoSearchTool({ uiStream, diff --git a/lib/agents/tools/search.tsx b/lib/agents/tools/search.tsx index 7510a795..d9518b6e 100644 --- a/lib/agents/tools/search.tsx +++ b/lib/agents/tools/search.tsx @@ -1,5 +1,4 @@ import { createStreamableValue } from 'ai/rsc' -import Exa from 'exa-js' import { tavily } from '@tavily/core' import { searchSchema } from '@/lib/schema/search' import { Card } from '@/components/ui/card' @@ -39,22 +38,18 @@ export const searchTool = ({ uiStream, fullResponse }: ToolProps) => ({ const filledQuery = query.length < 5 ? query + ' '.repeat(5 - query.length) : query let searchResult - const searchAPI: 'tavily' | 'exa' = 'tavily' try { - searchResult = - searchAPI === 'tavily' - ? await tavilySearch( - filledQuery, - max_results, - search_depth, - include_answer, - topic, - time_range, - include_images, - include_image_descriptions, - include_raw_content - ) - : await exaSearch(query) + searchResult = await tavilySearch( + filledQuery, + max_results, + search_depth, + include_answer, + topic, + time_range, + include_images, + include_image_descriptions, + include_raw_content + ) } catch (error) { console.error('Search API error:', error) hasError = true @@ -101,12 +96,3 @@ async function tavilySearch( return { ...response, results: response.results.reverse() } } - -async function exaSearch(query: string, maxResults: number = 10): Promise { - const apiKey = process.env.EXA_API_KEY - const exa = new Exa(apiKey) - return exa.searchAndContents(query, { - highlights: true, - numResults: maxResults - }) -} diff --git a/lib/schema/geospatial.tsx b/lib/schema/geospatial.tsx index 98059711..c58b7f0a 100644 --- a/lib/schema/geospatial.tsx +++ b/lib/schema/geospatial.tsx @@ -1,124 +1,64 @@ import { z } from 'zod'; -// Improved schema using discriminatedUnion for better type safety and conditional requirements -// - Enforces required fields based on queryType (e.g., destination for directions/distance) -// - Renames 'query' to 'location' for clarity in most cases, but uses 'origin' and 'destination' for directions/distance -// - Makes 'coordinates' required for 'reverse' and optional for 'search' (as proximity) -// - Adds 'mode' for directions (e.g., driving, walking) assuming tool support can be added -// - Integrates 'radius' and 'maxResults' for 'search', assuming future tool arg expansion -// - Keeps 'includeMap' consistent across all -// - Defaults queryType removed; now required to encourage explicit typing -// - For 'map', treats as general query similar to geocode/search +// Flat schema. JSON Schema output is a single object with optional fields, +// so OpenAI-compatible endpoints (xAI) accept it. Field requirements per +// queryType are conveyed to the LLM via the queryType description; runtime +// behavior in the tool's execute() already tolerates missing fields per +// queryType, so loosening the schema introduces no new failure modes. -export const geospatialQuerySchema = z.discriminatedUnion('queryType', [ - z.object({ - queryType: z.literal('search'), - query: z.string() - .min(1, "Query cannot be empty") - .describe("Search term for places/POIs"), - coordinates: z.object({ - latitude: z.number().min(-90).max(90), - longitude: z.number().min(-180).max(180) - }) - .optional() - .describe("Optional reference point for proximity search"), - radius: z.number() - .positive() - .optional() - .describe("Search radius in kilometers"), - maxResults: z.number() - .int() - .positive() - .max(20) - .optional() - .default(5) - .describe("Maximum number of results to return"), - includeMap: z.boolean() - .optional() - .default(true) - .describe("Whether to include a map preview/URL in the response"), - }), - z.object({ - queryType: z.literal('geocode'), - location: z.string() - .min(1, "Location cannot be empty") - .describe("The location to geocode - address, place name, or landmark"), - includeMap: z.boolean() - .optional() - .default(true) - .describe("Whether to include a map preview/URL in the response"), - maxResults: z.number() - .int() - .positive() - .max(20) - .optional() - .default(5) - .describe("Maximum number of results to return"), - }), - z.object({ - queryType: z.literal('reverse'), - coordinates: z.object({ - latitude: z.number().min(-90).max(90), - longitude: z.number().min(-180).max(180) - }) - .describe("Coordinates for reverse geocoding"), - includeMap: z.boolean() - .optional() - .default(true) - .describe("Whether to include a map preview/URL in the response"), - maxResults: z.number() - .int() - .positive() - .max(20) - .optional() - .default(5) - .describe("Maximum number of results to return"), - }), - z.object({ - queryType: z.literal('directions'), - origin: z.string() - .min(1, "Origin cannot be empty") - .describe("Starting location for directions"), - destination: z.string() - .min(1, "Destination cannot be empty") - .describe("Ending location for directions"), - mode: z.enum(['driving', 'walking', 'cycling', 'transit']) - .optional() - .default('driving') - .describe("Transportation mode for directions"), - includeMap: z.boolean() - .optional() - .default(true) - .describe("Whether to include a map preview/URL in the response"), - }), - z.object({ - queryType: z.literal('distance'), - origin: z.string() - .min(1, "Origin cannot be empty") - .describe("Starting location for distance calculation"), - destination: z.string() - .min(1, "Destination cannot be empty") - .describe("Ending location for distance calculation"), - mode: z.enum(['driving', 'walking', 'cycling', 'transit']) - .optional() - .default('driving') - .describe("Transportation mode for distance"), - includeMap: z.boolean() - .optional() - .default(true) - .describe("Whether to include a map preview/URL in the response"), - }), - z.object({ - queryType: z.literal('map'), - location: z.string() - .min(1, "Location cannot be empty") - .describe("Location or area for map request"), - includeMap: z.boolean() - .optional() - .default(true) - .describe("Whether to include a map preview/URL in the response"), +export const geospatialQuerySchema = z.object({ + queryType: z.enum(['search', 'geocode', 'reverse', 'directions', 'distance', 'map']) + .describe( + "Type of geospatial query. Set the corresponding fields: " + + "'search' → query (optionally coordinates, radius, maxResults); " + + "'geocode' → location (optionally maxResults); " + + "'reverse' → coordinates (optionally maxResults); " + + "'directions' → origin + destination (optionally mode); " + + "'distance' → origin + destination (optionally mode); " + + "'map' → location." + ), + query: z.string() + .min(1) + .optional() + .describe("Search term for places/POIs (used by 'search')"), + location: z.string() + .min(1) + .optional() + .describe("Location to geocode or render as a map (used by 'geocode' and 'map')"), + coordinates: z.object({ + latitude: z.number().min(-90).max(90), + longitude: z.number().min(-180).max(180) }) -]); + .optional() + .describe("Coordinates (required for 'reverse', optional proximity hint for 'search')"), + origin: z.string() + .min(1) + .optional() + .describe("Starting location (used by 'directions' and 'distance')"), + destination: z.string() + .min(1) + .optional() + .describe("Ending location (used by 'directions' and 'distance')"), + mode: z.enum(['driving', 'walking', 'cycling', 'transit']) + .optional() + .default('driving') + .describe("Transportation mode (used by 'directions' and 'distance')"), + radius: z.number() + .positive() + .optional() + .describe("Search radius in kilometers (used by 'search')"), + maxResults: z.number() + .int() + .positive() + .max(20) + .optional() + .default(5) + .describe("Maximum number of results to return"), + includeMap: z.boolean() + .optional() + .default(true) + .describe("Whether to include a map preview/URL in the response"), +}); export type GeospatialQuery = z.infer; diff --git a/lib/schema/resolution-search.ts b/lib/schema/resolution-search.ts new file mode 100644 index 00000000..e7908abd --- /dev/null +++ b/lib/schema/resolution-search.ts @@ -0,0 +1,41 @@ +import { z } from 'zod' + +// This schema is designed to be compatible with xAI's OpenAI-compatible endpoint. +// We use a flattened structure and avoid z.literal (which generates JSON Schema 'const') +// and z.any() to ensure maximum compatibility with xAI's schema validation. +// This follows the pattern established in lib/schema/geospatial.tsx. + +export const resolutionSearchSchema = z.object({ + summary: z.string().describe('A detailed text summary of the analysis, including land feature classification, points of interest, relevant current news, and temporal context.'), + + // geoJson is optional so the model is not forced to produce features when none are found. + geoJson: z.object({ + type: z.string().describe("Must be 'FeatureCollection'"), + features: z.array(z.object({ + type: z.string().describe("Must be 'Feature'"), + geometryType: z.string().describe("The type of geometry, e.g., 'Point', 'Polygon'"), + coordinates: z.array(z.number()) + .or(z.array(z.array(z.number()))) + .or(z.array(z.array(z.array(z.number())))) + .describe('Coordinates for the geometry'), + name: z.string().describe('Name of the feature or point of interest'), + description: z.string().optional().describe('Description of the feature') + })) + }).optional().describe('A collection of points of interest and classified land features to be overlaid on the map.'), + + // Flattened top-level fields for better xAI compatibility + extractedLatitude: z.number().optional().describe('The extracted latitude of the center of the image.'), + extractedLongitude: z.number().optional().describe('The extracted longitude of the center of the image.'), + + cogApplicable: z.boolean().optional().describe('Whether Cloud Optimized GeoTIFF (COG) data is applicable for this area.'), + cogDescription: z.string().optional().describe('Description of COG data availability or benefits.'), + + hasRecentNews: z.boolean().optional().describe('Whether there is recent news relevant to the location.'), + newsItems: z.array(z.object({ + title: z.string(), + summary: z.string(), + relevance: z.string() + })).optional().describe('List of recent news items relevant to the location.') +}) + +export type ResolutionSearch = z.infer; diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 15565281..b2f6d47f 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -79,18 +79,7 @@ export async function getModel(requireVision: boolean = false) { } } - // Default behavior: Gemini -> Grok -> Bedrock -> OpenAI - if (gemini3ProApiKey) { - const google = createGoogleGenerativeAI({ - apiKey: gemini3ProApiKey, - }); - try { - return google('gemini-3.1-pro-preview'); - } catch (error) { - console.warn('Gemini 3.1 Pro API unavailable, falling back to next provider:', error); - } - } - + // Default behavior: Grok -> Gemini -> Bedrock -> OpenAI if (xaiApiKey) { const xai = createXai({ apiKey: xaiApiKey, @@ -103,6 +92,17 @@ export async function getModel(requireVision: boolean = false) { } } + if (gemini3ProApiKey) { + const google = createGoogleGenerativeAI({ + apiKey: gemini3ProApiKey, + }); + try { + return google('gemini-3.1-pro-preview'); + } catch (error) { + console.warn('Gemini 3.1 Pro API unavailable, falling back to next provider:', error); + } + } + if (awsAccessKeyId && awsSecretAccessKey) { const bedrock = createAmazonBedrock({ bedrockOptions: { diff --git a/lib/utils/report-generator.ts b/lib/utils/report-generator.ts new file mode 100644 index 00000000..cfde829a --- /dev/null +++ b/lib/utils/report-generator.ts @@ -0,0 +1,169 @@ +import { jsPDF } from 'jspdf'; +import { AIMessage } from '@/lib/types'; +import { MapData } from '@/components/map/map-data-context'; +import markdownToTxt from 'markdown-to-txt'; + +export interface ReportData { + messages: AIMessage[]; + drawnFeatures: MapData['drawnFeatures']; + mapSnapshot: string; + chatTitle: string; +} + +export async function generateReport({ + messages, + drawnFeatures, + mapSnapshot, + chatTitle +}: ReportData) { + const doc = new jsPDF(); + const pageWidth = doc.internal.pageSize.getWidth(); + const pageHeight = doc.internal.pageSize.getHeight(); + const margin = 15; + const contentWidth = pageWidth - 2 * margin; + let yOffset = margin; + + const checkPageBreak = (neededHeight: number) => { + if (yOffset + neededHeight > pageHeight - margin) { + doc.addPage(); + yOffset = margin; + return true; + } + return false; + }; + + const addTextWithAutoPageBreak = (text: string, fontSize: number, style: 'normal' | 'bold' = 'normal', color: [number, number, number] = [0, 0, 0]) => { + doc.setFontSize(fontSize); + doc.setFont('helvetica', style); + doc.setTextColor(color[0], color[1], color[2]); + + const lines: string[] = doc.splitTextToSize(text, contentWidth); + for (const line of lines) { + if (yOffset + 7 > pageHeight - margin) { + doc.addPage(); + yOffset = margin; + } + doc.text(line, margin, yOffset); + yOffset += 7; + } + yOffset += 3; // Small gap after text blocks + }; + + // --- Cover Page --- + doc.setFontSize(24); + doc.setTextColor(40, 40, 40); + doc.text('QCX Analysis Report', margin, yOffset); + yOffset += 15; + + doc.setFontSize(18); + doc.text(chatTitle || 'Untitled Chat', margin, yOffset); + yOffset += 10; + + doc.setFontSize(12); + doc.setTextColor(100, 100, 100); + doc.text(`Generated on: ${new Date().toLocaleString()}`, margin, yOffset); + yOffset += 20; + + if (mapSnapshot) { + try { + const imgHeight = (contentWidth * 9) / 16; + checkPageBreak(imgHeight); + doc.addImage(mapSnapshot, 'PNG', margin, yOffset, contentWidth, imgHeight); + yOffset += imgHeight + 20; + } catch (e) { + console.error('Error adding map snapshot to PDF:', e); + } + } + + // --- Q&A Section --- + doc.setFontSize(16); + doc.setTextColor(40, 40, 40); + checkPageBreak(10); + doc.text('Conversation History', margin, yOffset); + yOffset += 10; + + const userMessages = messages.filter(m => m.type === 'input' || m.type === 'input_related'); + + for (const userMsg of userMessages) { + let userContent = ''; + try { + const json = JSON.parse(userMsg.content as string); + userContent = userMsg.type === 'input' ? json.input : json.related_query; + } catch (e) { + userContent = userMsg.content as string; + } + + addTextWithAutoPageBreak(`User: ${userContent}`, 12, 'bold', [60, 60, 60]); + + const userIdx = messages.indexOf(userMsg); + const nextUserIdx = messages.findIndex((m, i) => i > userIdx && (m.type === 'input' || m.type === 'input_related')); + const turnMessages = messages.slice(userIdx + 1, nextUserIdx === -1 ? undefined : nextUserIdx); + + const aiResponse = turnMessages.find(m => m.type === 'response'); + if (aiResponse) { + // Render markdown as plain text + const plainTextAI = markdownToTxt(aiResponse.content as string); + addTextWithAutoPageBreak(`QCX: ${plainTextAI}`, 12, 'normal', [80, 80, 80]); + } + + const searchResult = turnMessages.find(m => m.type === 'resolution_search_result'); + if (searchResult) { + try { + const data = JSON.parse(searchResult.content as string); + + // GeoJSON Summary + if (data.summary) { + addTextWithAutoPageBreak(`Analysis Summary: ${data.summary}`, 11, 'normal', [80, 80, 80]); + } + + const images = [data.mapboxImage, data.googleImage, data.image].filter(Boolean); + if (images.length > 0) { + const imgWidth = (contentWidth - 10) / 2; + const imgHeight = (imgWidth * 3) / 4; + + checkPageBreak(imgHeight + 10); + + for (let i = 0; i < Math.min(images.length, 2); i++) { + doc.addImage(images[i], 'JPEG', margin + (i * (imgWidth + 10)), yOffset, imgWidth, imgHeight); + } + yOffset += imgHeight + 10; + } + } catch (e) { + console.error('Error parsing resolution search result for PDF:', e); + } + } + + yOffset += 5; + } + + // --- Drawings Appendix --- + if (drawnFeatures && drawnFeatures.length > 0) { + doc.addPage(); + yOffset = margin; + + doc.setFontSize(16); + doc.setTextColor(40, 40, 40); + doc.setFont('helvetica', 'bold'); + doc.text('Drawings & Measurements', margin, yOffset); + yOffset += 15; + + drawnFeatures.forEach((feature, index) => { + checkPageBreak(25); + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text(`${index + 1}. ${feature.type}`, margin, yOffset); + yOffset += 7; + + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + doc.text(`Measurement: ${feature.measurement}`, margin + 5, yOffset); + yOffset += 7; + + const coords = JSON.stringify(feature.geometry.coordinates).substring(0, 100) + '...'; + doc.text(`Coordinates: ${coords}`, margin + 5, yOffset); + yOffset += 10; + }); + } + + doc.save(`${chatTitle || 'QCX-Report'}.pdf`); +} diff --git a/package.json b/package.json index 87066925..bb02e298 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@tailwindcss/typography": "^0.5.16", "@tavily/core": "^0.6.4", "@turf/turf": "^7.2.0", + "@types/jspdf": "^2.0.0", "@types/mapbox__mapbox-gl-draw": "^1.4.8", "@types/pg": "^8.15.4", "@upstash/redis": "^1.35.0", @@ -68,11 +69,13 @@ "framer-motion": "^12.23.24", "geotiff": "^2.1.4-beta.1", "glassmorphic": "^0.0.3", + "jspdf": "^4.2.1", "katex": "^0.16.22", "lodash": "^4.17.21", "lottie-react": "^2.4.1", "lucide-react": "^0.507.0", "mapbox-gl": "^3.11.0", + "markdown-to-txt": "^2.0.1", "next": "15.3.8", "next-themes": "^0.3.0", "open-codex": "^0.1.30",