[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"extends\": [\"next/core-web-vitals\", \"next/typescript\"],\n  \"rules\": {\n    \"@typescript-eslint/no-unused-vars\": \"warn\",\n    \"@typescript-eslint/no-explicit-any\": \"warn\",\n    \"react/display-name\": \"warn\",\n    \"react-hooks/exhaustive-deps\": \"warn\",\n    \"prefer-const\": \"warn\"\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n.version\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Chat2Geo\nThank you for considering contributing to Chat2Geo!\nHere are the instructions on how to contribute to this project to ensure consistency throughout the development.\n\n## 1. Fork and Clone\n\n1. Fork this repo (click the \"Fork\" button on top-right).\n2. Clone your fork:\n   ```bash\n   https://github.com/GeoRetina/chat2geo.git\n\n## 3. Pick an issue or be assigned one\n  - You can either pick an issue to work on or be assigned one.\n  - If there is an issue not listed, please create one.\n\n## 4. Create a new branch off of `main` for each issue or feature and name it based on the following pattern\n  ```bash\n   <type>/<issue-id>-<short-description>\n\n    where:\n     - <type>: The purpose of the branch. Common types:\n          feat: For new features.\n          fix: For bug fixes.\n          chore: For maintenance or non-functional changes.\n          refactor: For code refactoring.\n          docs: For documentation updates.\n          test: For testing-related changes.\n    - <issue-id>: The issue/feature ID from your issue tracker (e.g., GitHub, Jira). This helps link branches to specific tickets.\n    - <short-description>: A concise, kebab-case description of the work being done.\n```\n## 5. Create a Pull Request (PR) for merging to the main\n  - Merging to the main is only possible by PR and review.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 GeoRetina Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "> [!IMPORTANT]  \n> **This repository is archived and no longer maintained.**  \n> Please use the hosted version at [https://chat2geo.georetina.ai](https://chat2geo.georetina.ai), which is production-ready, more feature-rich, and actively maintained.\n\n\n# Chat2Geo: A ChatGPT-Like Web App for Remote-Sensing-Based Geospatial Analysis\n\nChat2Geo is a Next.js 15 application providing a chatbot-like user interface for performing remote-sensing-based geospatial analyses. It leverages Google Earth Engine (GEE) in the backend to process and analyze various remote sensing datasets in real time. Users can upload their own vector data, run advanced geospatial queries, and integrate the results with an AI Assistant for specialized tasks such as **land cover mapping**, **change detection**, and **air pollutant monitoring**. \n\nChat2Geo also has advanced knowledge retrieval based on Retrieval-augmented generation (RAG), which can integrate geospatial analysis with non-geospatial/textual information. \n\nThe app also has authentication and database integrations, making it almost a complete package. \n\nChat2Geo inherits a large portion of its building blocks from the GRAI 2.0 app that is under development at GeoRetina (www.georetina.com). In parallel with GRAI 2.0 (which will be merged to Chat2Geo once it's stable), we will also keep Chat2Geo updated for the community.\n\n### 🌍 Try Chat2Geo: \nhttps://chat2geo.georetina.ai\n\n----\n\nhttps://github.com/user-attachments/assets/d9940a0e-10c8-4d0e-9ec9-3dfd0966c664\n\n\n\n## Contributing 🛠️\n\n - If you're interested in contributing to this project, please contact us at `shahabj.github@gmail.com`.\n - If you are a new contributor, please first check out our [Contributing Guidelines](./CONTRIBUTING.md) to get started.\n\n\n## Table of Contents\n\n- [Features](#features-)\n- [Tech Stack](#tech-stack-)\n- [Getting Started](#getting-started-)\n- [Current Analyses](#available-geospatial-analyses-)\n- [Considerations](#considerations)\n\n---\n\n## Features ✨\n\n1. **Chat-Style Interface**\n\n   - Interact with the system using natural language.\n   - The AI Assistant can execute various **geospatial functions** on your behalf.\n\n2. **Google Earth Engine Integration**\n\n   - Real-time access to satellite imagery and remote sensing datasets.\n   - Seamless backend processing for large-scale geospatial computations.\n\n3. **Import Your Own Vector Data**\n\n   - Upload and manage personal vector layers.\n   - Integrate your data with Earth Engine operations for advanced queries.\n\n4. **Analysis Toolkit**\n\n   - **Air Pollutants**\n   - **Urban Heat Island (UHI)** metrics\n   - **Land Cover** mapping & **Change Detection**\n   - Custom AI models deployed on **Vertex AI** for certain land cover tasks\n\n5. **RAG & Knowledge Base**\n   - Enables a Retrieval-Augmented Generation (RAG) workflow.\n   - Upload documents to build a local knowledge base.\n   - The AI Assistant can then combine geospatial insights with custom document knowledge.\n\n\n## Tech Stack 💻\n- Next.js\n- Google Cloud Platform (GCP):\n   - Google Earth Engine (remote-sensing data invocation and processing)\n   - Vertex AI (custom AI vision models)\n   - Cloud Run\n- Vercel AI\n- OpenAI (ChatGPT API)\n- Supabase (database and authentication)\n- LangChain (RAG)\n- Turf (for spatial operations)\n- Maplibre GL (for displaying maps)\n\n## Getting Started 🚀\n\n1. Clone the repo\n\n2. Install dependencies\n\n   ```bash\n   npm install\n   ```\n\n3. Create a Google Earth Engine (GEE) account and project, otherwise no analysis can be done. Note that GEE is currently only free for non-commercial use:\n   - https://earthengine.google.com\n   \n5. Set up the environment variables\n\n- Create a `.env.local` file (or similar) with the required credentials for:\n\n  - Your base url:\n     ```\n     BASE_URL=http://localhost:3000   # Change it if you're using a different port. In production, you should set it to the url of the deployment. \n     ```\n  - Google Cloud Platform (GCP):\n    ```\n      GOOGLE_MAPS_API_KEY=           # API key for Google maps. You can replace Google Maps with OSM if you want.\n      VERTEXTAI_ENDPOINT_BASE_URL=   # Base URL if you use your own custom models.\n      GEE_CLOUD_RUN_URL=             # URL for invoking a model hosted on VertexAI using cloud functions.\n      GCP_BUCKET_NAME=               # Bucket name to store the land-cover map generated by your custom model (if applicable).\n      GCP_SERVICE_ACCOUNT_KEY=       # Service Account key needed for GEE functions, depending on your GCP configurations. Make sure it has all the required permissions to use GEE.\n    ```\n\n  - Large Language Model (LLM) API Key:\n    ```\n    OPENAI_API_KEY=            # It shouldn't be necessarily OpenAI, thought. You can change it to any other API supported by Vercel AI SDK. However, you need to make some changes to the Chat API route.\n    ```\n  - For the database & authentication, the app uses Supabase. So you need the Supabase API keys as well:\n\n        NEXT_PUBLIC_SUPABASE_URL=\n        NEXT_PUBLIC_SUPABASE_ANON_KEY=\n\n  - If you want Esri integration, you also need the following keys in your env (skip this part if you don't want this integration):\n\n        ARCGIS_CLIENT_ID=\n        ARCGIS_CLIENT_SECRET=\n        ARCGIS_REDIRECT_URI=\n\n- For feedback submission, I just used a simple email-based pipeline based on Mailgun (skip this part if you don't want this feature):\n\n        MAILGUN_API_KEY=\n        MAILGUN_DOMAIN=\n        RECIPIENT_EMAIL=\n        SENDER_EMAIL=\n\n5. Run the develpment server\n\n       npm run dev\n\n\nVisit http://localhost:3000 to view the application.\n\n\n<a name=\"custom_anchor_name\"></a>\n## How to Set up Supabase Database, Storage Bucket, & Authentication 🛢️\n\nSupabase has a free-tier, generous plan that you can use to work with the app.\n\nAs mentioned above, the database (PostgreSQL) and authentication are both hosted on Supabase. To set up them, you can either use the local dev (https://supabase.com/docs/guides/local-development/cli/getting-started) or online (https://supabase.com/docs/guides/database/overview).\nTo set up the database and Supabase auth online, you need to create a supabase project & create the required databases and auth. \nYou can find the database schema of the app in the `db-schema` folder.\n\nIf you want to also use the Knowledge Base feature, you need to create a storage bucket on Supabase as well. The name of the bucket should be `documents_bucket`. This is where the PDF docs you upload to the Knowledge Base are stored. You can set up the bucket by going to the following link:\n - https://supabase.com/dashboard/project/_/storage/buckets\n\n## Available Geospatial Analyses 📊\n\nThe app includes the following geospatial analyses:\n\n| #  | Analysis Type                                    | Description |\n|----|------------------------------------------------|-------------|\n| 1  | **Urban Heat Island (UHI) Analysis**           | Evaluates temperature variations in urban areas compared to rural surroundings. |\n| 2  | **Land-Use/Land-Cover Mapping**                | Uses Google DynamicWorld to classify land cover types. |\n| 3  | **Land-Use/Land-Cover Change Mapping**         | Detects changes in land use over time using Google DynamicWorld. |\n| 4  | **Air Pollution Analysis** *(Not fully implemented)* | Analyzes air pollution patterns and trends. |\n\n\n\n\n## Considerations💡\n- Note that all remote-sensing geospatial analyses, at least for now, are based on GEE in this app. So, if you don't set up your GEE environment correctly, no analysis can be done.\n- It should be noted that this app is not yet ready for production. The app has known bugs, and perhaps unknown ones 😁 Some functionalities have not been implemented yet.\n- I may have forgotten to include some steps in setting up the app! 😅 If there's missing information in the instructions, please open an issue and let me know to update the instructions accordingly.\n- GEE-based geospatial analyses are just simple examples of how such analyses can be implemented and added. Some of them are using data that may not be up-to-date. As a result, care should be taken while interpreting the results.\n- There are parts that should be refactored or re-designed either because they could have been used/invoked in a better place, or because they should've been implemented in a much better manner.\n\n\n## Frequently Asked Questions (FAQ) 📌\n\n<details>\n  <summary>🔹 General Questions</summary>\n  \n  **❓ Is this project free to use?**  \n  *Yes! This open-source version is free to use under the terms of its license. However, note that Google Earth Engine has restrictions on commercial usage.*\n  \n</details>\n\n<details>\n  <summary>🔹 Support &amp; Contributions</summary>\n  \n  **❓ How can I get support for issues?**  \n  - *If you encounter a bug or have a feature request, please [open an issue](../../issues) on GitHub.*  \n  - *Be as detailed as possible when describing your issue (include screenshots, step-by-step explanations, error logs, and any relevant details). Abstract or vague questions will not be answered.*  \n  - *For other questions, feel free to reach out at [shahabj.github@gmail.com](mailto:shahabj.github@gmail.com).*\n  \n  **❓ How can I contribute?**  \n  *We welcome contributions! Please check out the [Contributing Guidelines](./CONTRIBUTING.md) before submitting a pull request or opening an issue. Your help in improving this project is greatly appreciated.*\n  \n</details>\n\n<details>\n  <summary>🔹 Features &amp; Customization</summary>\n  \n  **❓ Can I request additional analyses or features?**  \n  *Absolutely! You can:*\n  - *Suggest a feature by opening an issue.*\n  - *Fork the repository and implement your own changes.*  \n  *For advanced or custom solutions, please see [GRAI 2.0 (Enterprise Version)](#enterprise-version-grai-20) below.*\n  \n  **❓ Can I use my own geospatial datasets?**  \n  *Yes! The app allows you to import vector data and integrate it with Google Earth Engine for custom analyses. For raster data, at least for now, you need to either host them on GEE or a GCP bucket.*\n  \n</details>\n\n<details>\n  <summary>🔹 Enterprise Version: GRAI 2.0</summary>\n  \n  **❓ What is GRAI 2.0?**  \n  *GRAI 2.0 is the enterprise version of this project, offering:*\n  - *Custom-built solutions tailored to specific client needs.*\n  - *Additional analyses &amp; AI models not included in the open-source version.*\n  - *Continuous updates &amp; premium support.*\n  \n  **❓ How do I get access to GRAI 2.0?**  \n  *For enterprise inquiries, please visit the [GeoRetina Contact Page](https://www.georetina.com/contact).*\n  \n</details>\n\n<details>\n  <summary>🔹 Technical &amp; Setup Questions</summary>\n  \n  **❓ I'm facing issues with setup. What should I do?**  \n  1. *Check that your environment variables are properly set in `.env.local`.*\n  2. *To get past the login page, you need to first set up a Supabase Auth as described in [Supabase Setup](#how-to-set-up-supabase-database-storage-bucket--authentication-%EF%B8%8F).*\n  3. *Check your database configurations.*\n  4. *Confirm your Google Earth Engine configuration.*\n  5. *Refer to the [Getting Started](#getting-started) section in this README.*\n  6. *If issues persist, [open an issue](../../issues).*\n  \n</details>\n\n*Have a question not listed here? Feel free to [open an issue](../../issues) or reach out via email!* 🚀\n\n\n\n\n"
  },
  {
    "path": "app/(auth)/api/auth/confirm/route.ts",
    "content": "import { type EmailOtpType } from \"@supabase/supabase-js\";\nimport { type NextRequest, NextResponse } from \"next/server\";\n\nimport { createClient } from \"@/utils/supabase/server\";\n\n// Creating a handler to a GET request to route /auth/confirm\nexport async function GET(request: NextRequest) {\n  const { searchParams } = new URL(request.url);\n  const token_hash = searchParams.get(\"token_hash\");\n  const type = searchParams.get(\"type\") as EmailOtpType | null;\n  const next = \"/\";\n\n  // Create redirect link without the secret token\n  const redirectTo = request.nextUrl.clone();\n  redirectTo.pathname = next;\n  redirectTo.searchParams.delete(\"token_hash\");\n  redirectTo.searchParams.delete(\"type\");\n\n  if (token_hash && type) {\n    const supabase = await createClient();\n\n    const { error } = await supabase.auth.verifyOtp({\n      type,\n      token_hash,\n    });\n    if (!error) {\n      redirectTo.searchParams.delete(\"next\");\n      return NextResponse.redirect(redirectTo);\n    }\n  }\n\n  // return the user to an error page with some instructions\n  redirectTo.pathname = \"/login\";\n  return NextResponse.redirect(redirectTo);\n}\n"
  },
  {
    "path": "app/(auth)/api/auth/esri/authorize/route.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport { createClient } from \"@/utils/supabase/server\";\n\nexport async function GET() {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data.user) {\n    return NextResponse.json({ error: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const { ARCGIS_CLIENT_ID, ARCGIS_REDIRECT_URI } = process.env;\n\n  const authorizationUrl = `https://www.arcgis.com/sharing/rest/oauth2/authorize?client_id=${ARCGIS_CLIENT_ID}&response_type=code&redirect_uri=${encodeURIComponent(\n    ARCGIS_REDIRECT_URI!\n  )}`;\n\n  return NextResponse.redirect(authorizationUrl);\n}\n"
  },
  {
    "path": "app/(auth)/api/auth/esri/callback/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\nimport { createClient } from \"@/utils/supabase/server\";\nimport cookie from \"cookie\";\n\nexport async function GET(req: NextRequest) {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data?.user) {\n    return NextResponse.json({ error: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const { searchParams } = new URL(req.url);\n  const code = searchParams.get(\"code\");\n\n  if (!code) {\n    return NextResponse.json(\n      { error: \"Authorization code is missing\" },\n      { status: 400 }\n    );\n  }\n\n  const params = new URLSearchParams();\n  params.append(\"client_id\", process.env.ARCGIS_CLIENT_ID!);\n  params.append(\"client_secret\", process.env.ARCGIS_CLIENT_SECRET!);\n  params.append(\"grant_type\", \"authorization_code\");\n  params.append(\"code\", code);\n  params.append(\"redirect_uri\", process.env.ARCGIS_REDIRECT_URI!);\n  params.append(\"f\", \"json\");\n\n  const baseUrl = process.env.BASE_URL;\n\n  try {\n    const tokenResponse = await fetch(\n      \"https://www.arcgis.com/sharing/rest/oauth2/token\",\n      {\n        method: \"POST\",\n        body: params,\n      }\n    );\n\n    const tokenData = await tokenResponse.json();\n\n    if (tokenData.error) {\n      throw new Error(tokenData.error.message);\n    }\n\n    const response = NextResponse.redirect(\n      `${baseUrl}/services/esri/fetch-layers`\n    );\n\n    // Store the token securely in an HttpOnly, Secure cookie\n    response.cookies.set(\"arcgis_access_token\", tokenData.access_token, {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === \"production\",\n      sameSite: \"strict\",\n      maxAge: tokenData.expires_in,\n    });\n\n    return response;\n  } catch (error) {\n    console.error(\"Error during OAuth2 callback:\", error);\n    return NextResponse.json(\n      { error: \"Failed to exchange authorization code for token\" },\n      { status: 500 }\n    );\n  }\n}\n"
  },
  {
    "path": "app/(auth)/layout.tsx",
    "content": "import localFont from \"next/font/local\";\nimport \"./styles.css\";\nimport { Toaster } from \"react-hot-toast\";\nimport ToastMessage from \"@/features/ui/toast-message\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { Analytics } from \"@vercel/analytics/next\";\n\nconst geistSans = localFont({\n  src: \"./fonts/GeistVF.woff\",\n  variable: \"--font-geist-sans\",\n  weight: \"100 900\",\n});\nconst geistMono = localFont({\n  src: \"./fonts/GeistMonoVF.woff\",\n  variable: \"--font-geist-mono\",\n  weight: \"100 900\",\n});\n\n// Define metadata for the root layout\nexport const metadata = {\n  title: \"Login to Chat2Geo\",\n  description: \"Login to access AI-powered geospatial analytics\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body className={`${geistSans.variable} ${geistMono.variable} font-sans`}>\n        <ThemeProvider\n          attribute=\"class\"\n          defaultTheme=\"light\"\n          enableSystem\n          disableTransitionOnChange\n        >\n          <Toaster />\n          <ToastMessage />\n          {children}\n        </ThemeProvider>\n        <Analytics />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/(auth)/login/actions.ts",
    "content": "\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\nimport { redirect } from \"next/navigation\";\nimport { createClient } from \"@/utils/supabase/server\";\nimport { cookies } from \"next/headers\";\nimport { z } from \"zod\";\n\n// Define the validation schema using Zod\nconst loginSchema = z.object({\n  email: z.string().email(\"Invalid email format.\"),\n  password: z.string(),\n});\n\nexport async function login(formData: FormData) {\n  const supabase = await createClient();\n\n  // Parse and validate the input data\n  const formDataObject = {\n    email: formData.get(\"email\") as string,\n    password: formData.get(\"password\") as string,\n  };\n\n  const validation = loginSchema.safeParse(formDataObject);\n  if (!validation.success) {\n    // Return validation errors\n    return {\n      error: validation.error.errors.map((err) => err.message).join(\", \"),\n    };\n  }\n\n  const { email, password } = validation.data;\n\n  const { data: authData, error: authError } =\n    await supabase.auth.signInWithPassword({ email, password });\n\n  if (authError) {\n    return { error: authError.message };\n  }\n\n  const { data: userRoles, error: roleError } = await supabase\n    .from(\"user_roles\")\n    .select(\"name, role, organization, license_start, license_end\")\n    .eq(\"email\", email)\n    .single();\n\n  if (roleError || !userRoles) {\n    return { error: \"User not found or error occurred.\" };\n  }\n\n  const { name, role, organization, license_start, license_end } = userRoles;\n\n  const licenseStartString = license_start;\n  const licenseEndString = license_end;\n  const currentDate = new Date();\n  const licenseStartDate = new Date(license_start);\n  const licenseEndDate = new Date(license_end);\n\n  if (currentDate < licenseStartDate || currentDate > licenseEndDate) {\n    return {\n      error:\n        \"Your license has expired or not yet started. Please contact support.\",\n    };\n  }\n\n  return {\n    success: true,\n    userEmail: email,\n    userName: name,\n    userRole: role,\n    userOrganization: organization,\n    licenseStartString,\n    licenseEndString,\n  };\n}\n\nexport async function logout() {\n  const supabase = await createClient();\n\n  await supabase.auth.signOut();\n\n  (await cookies()).delete(\"arcgis_access_token\");\n\n  revalidatePath(\"/\");\n  redirect(\"/login\");\n}\n"
  },
  {
    "path": "app/(auth)/login/layout.tsx",
    "content": "export default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"relative flex min-h-screen\">\n      <main className=\"flex-grow\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(auth)/login/page.tsx",
    "content": "\"use client\";\n\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { useState, FormEvent, useTransition } from \"react\";\n\nimport { login } from \"@/app/(auth)/login/actions\";\nimport { useUserStore } from \"@/stores/use-user-profile-store\";\nimport PrivacyPolicy from \"@/components/notices/privacy-policy\";\nimport TermsOfService from \"@/components/notices/terms-of-services\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Loader2 } from \"lucide-react\";\n\nexport default function Login() {\n  const [error, setError] = useState<string | null>(null);\n  const [isPending, startTransition] = useTransition();\n  const [loading, setLoading] = useState<boolean>(false);\n  const router = useRouter();\n  const { setUserData } = useUserStore();\n  const [openTerms, setOpenTerms] = useState(false);\n  const [openPrivacy, setOpenPrivacy] = useState(false);\n\n  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    setError(null);\n    setLoading(true);\n\n    const formData = new FormData(event.currentTarget);\n    const result = await login(formData);\n\n    if (result?.error) {\n      setError(result.error);\n    } else {\n      // Update user store with returned data\n      setUserData(\n        result.userName!,\n        result.userEmail!,\n        result.userRole!,\n        result.userOrganization!,\n        result.licenseStartString!,\n        result.licenseEndString!\n      );\n      startTransition(() => {\n        router.push(\"/\");\n      });\n    }\n\n    setLoading(false);\n  };\n\n  return (\n    <>\n      <TermsOfService open={openTerms} onOpenChange={setOpenTerms} />\n      <PrivacyPolicy open={openPrivacy} onOpenChange={setOpenPrivacy} />\n\n      {/* Main container for md+ screens */}\n      <div className=\"container relative hidden h-full flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0\">\n        {/* Left column with background and testimonial */}\n        <div className=\"relative hidden h-full flex-col bg-muted p-10 text-white dark:border-r lg:flex\">\n          <div className=\"absolute inset-0 bg-gray-950\" />\n          <div className=\"relative z-20 mt-auto\">\n            <blockquote className=\"space-y-2\">\n              <p className=\"text-lg\">\n                Let powerful AI solutions help you better address various\n                environmental challenges.\n              </p>\n            </blockquote>\n          </div>\n        </div>\n\n        {/* Right column with the actual login form */}\n        <div className=\"lg:p-8\">\n          <div className=\"mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]\">\n            <div className=\"flex flex-col space-y-2 text-center\">\n              <h1 className=\"text-2xl font-semibold tracking-tight\">\n                Sign in to your account\n              </h1>\n              <p className=\"text-sm text-muted-foreground\">\n                Enter your credentials below\n              </p>\n            </div>\n\n            <form onSubmit={handleSubmit} className=\"space-y-4\">\n              {/* Email Field */}\n              <div className=\"flex flex-col space-y-1\">\n                <Label htmlFor=\"email\">Email</Label>\n                <Input\n                  id=\"email\"\n                  name=\"email\"\n                  type=\"email\"\n                  placeholder=\"name@company.com\"\n                  autoComplete=\"email\"\n                  required\n                />\n              </div>\n\n              {/* Password Field */}\n              <div className=\"flex flex-col space-y-1\">\n                <Label htmlFor=\"password\">Password</Label>\n                <Input\n                  id=\"password\"\n                  name=\"password\"\n                  type=\"password\"\n                  placeholder=\"••••••••\"\n                  autoComplete=\"current-password\"\n                  required\n                />\n              </div>\n\n              {/* Error Alert */}\n              {error && (\n                <p className=\"text-destructive text-sm\">\n                  <strong>Error:</strong> {error}\n                </p>\n              )}\n\n              {/* Submit Button */}\n              <Button\n                type=\"submit\"\n                disabled={loading || isPending}\n                className=\"w-full\"\n              >\n                {(loading || isPending) && <Loader2 className=\"animate-spin\" />}\n                Sign in\n              </Button>\n            </form>\n\n            <p className=\"px-8 text-center text-sm text-muted-foreground\">\n              By clicking continue, you agree to our{\" \"}\n              <button\n                className=\"underline underline-offset-4 hover:text-primary\"\n                onClick={() => setOpenTerms(true)}\n              >\n                Terms of Service\n              </button>{\" \"}\n              and{\" \"}\n              <button\n                className=\"underline underline-offset-4 hover:text-primary\"\n                onClick={() => setOpenPrivacy(true)}\n              >\n                Privacy Policy\n              </button>\n              .\n            </p>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "app/(auth)/styles.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 47.4% 11.2%;\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 47.4% 11.2%;\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 47.4% 11.2%;\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n    --primary-blue: 220 90% 56%;\n    --primary-blue-foreground: 210 40% 98%;\n    --primary-green: 145 63% 42%;\n    --primary-green-foreground: 210 40% 98%;\n    --secondary: 220 13% 92%; /* Neutral light gray */\n    --secondary-foreground: 220 10% 40%; /* Neutral medium gray */\n    --accent: 220 15% 85%; /* Neutral soft gray */\n    --accent-foreground: 220 20% 12%; /* Neutral deep gray */\n    --destructive: 0 100% 50%;\n    --destructive-foreground: 210 40% 98%;\n    --warning: 45 100% 50%;\n    --warning-foreground: 45 100% 15%;\n    --info: 200 98% 48%;\n    --info-foreground: 210 40% 98%;\n    --ring: 215 20.2% 65.1%;\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    /* Base */\n    --background: 240 2% 12%; /* #1f1f21 */\n    --foreground: 0 0% 95%; /* Very light neutral */\n\n    /* Darker variant for cards/popovers */\n    --card: 220 3% 10%; /* #18191a */\n    --card-foreground: 0 0% 95%;\n    --popover: 220 3% 10%; /* #18191a */\n    --popover-foreground: 0 0% 95%;\n\n    /* Muted */\n    --muted: 240 2% 16%; /* Slightly lighter than background */\n    --muted-foreground: 0 0% 60%; /* Subdued text */\n\n    /* Interactive elements */\n    --accent: 240 2% 18%; /* Subtle highlight */\n    --accent-foreground: 0 0% 98%;\n\n    /* Borders and rings */\n    --border: 240 2% 20%; /* Visible but subtle */\n    --input: 240 2% 20%;\n    --ring: 240 2% 20%;\n\n    /* Primary */\n    --primary: 0 0% 98%; /* Almost white */\n    --primary-foreground: 240 2% 12%; /* Same as background */\n\n    /* Secondary */\n    --secondary: 240 2% 22%; /* Lighter than accent */\n    --secondary-foreground: 0 0% 98%;\n\n    /* Semantic colors - coordinated with our neutral theme */\n    --primary-blue: 220 90% 56%;\n    --primary-blue-foreground: 210 40% 98%;\n\n    --primary-green: 145 63% 42%;\n    --primary-green-foreground: 210 40% 98%;\n\n    --destructive: 0 50% 35%; /* More muted red */\n    --destructive-foreground: 0 0% 98%;\n\n    --warning: 45 100% 50%;\n    --warning-foreground: 45 100% 15%;\n    --info: 200 98% 48%;\n    --info-foreground: 210 40% 98%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply font-sans antialiased bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "app/(broadcast)/api/services/esri/fetch-layers-list/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\nimport { createClient } from \"@/utils/supabase/server\";\n\nexport async function GET(req: NextRequest) {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data?.user) {\n    return NextResponse.json({ error: \"Unauthenticated!\" }, { status: 401 });\n  }\n  const accessToken = req.cookies.get(\"arcgis_access_token\")?.value;\n\n  if (!accessToken) {\n    return NextResponse.json(\n      {\n        error: \"No access token found. User needs to authenticate with ArcGIS.\",\n      },\n      { status: 401 }\n    );\n  }\n\n  const portalUrl = `https://www.arcgis.com/sharing/rest/portals/self?f=json&token=${accessToken}`;\n\n  try {\n    const portalResponse = await fetch(portalUrl);\n    const portalData = await portalResponse.json();\n\n    if (!portalResponse.ok) {\n      throw new Error(\"Failed to fetch organization ID\");\n    }\n    const orgId = portalData.id;\n    if (!orgId) {\n      throw new Error(\"Organization ID not found in portalData\");\n    }\n\n    const searchUrl = `https://www.arcgis.com/sharing/rest/search?q=orgid:${orgId} (type:\"Feature Service\")&f=json&token=${accessToken}`;\n    const servicesResponse = await fetch(searchUrl);\n\n    if (!servicesResponse.ok) {\n      throw new Error(\n        `Failed to fetch services from ArcGIS: ${servicesResponse.statusText}`\n      );\n    }\n\n    const servicesData = await servicesResponse.json();\n\n    return NextResponse.json(servicesData);\n  } catch (error) {\n    console.error(\"Error fetching layers or feature services:\", error);\n    return NextResponse.json(\n      { error: \"Failed to retrieve services or organization information\" },\n      { status: 500 }\n    );\n  }\n}\n"
  },
  {
    "path": "app/(broadcast)/api/services/esri/fetch-selected-layer/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\nimport { createClient } from \"@/utils/supabase/server\";\n\nexport async function GET(req: NextRequest) {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data?.user) {\n    return NextResponse.json({ error: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const token = req.cookies.get(\"arcgis_access_token\")?.value;\n\n  if (!token) {\n    return NextResponse.json(\n      { error: \"Access token is missing\" },\n      { status: 401 }\n    );\n  }\n\n  const { searchParams } = new URL(req.url);\n  const layerUrl = searchParams.get(\"layerUrl\");\n\n  if (!layerUrl) {\n    return NextResponse.json(\n      { error: \"Layer URL is missing\" },\n      { status: 400 }\n    );\n  }\n\n  try {\n    const queryUrl = `${layerUrl}/0/query?f=pgeojson&where=1=1`;\n    const response = await fetch(queryUrl, {\n      headers: {\n        Authorization: `Bearer ${token}`,\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`Failed to import layer: ${response.statusText}`);\n    }\n\n    const layerData = await response.json();\n    return NextResponse.json(layerData);\n  } catch (error) {\n    console.error(\"Error importing AGOL layer:\", error);\n    return NextResponse.json(\n      { error: \"Failed to import layer\" },\n      { status: 500 }\n    );\n  }\n}\n"
  },
  {
    "path": "app/(broadcast)/layout.tsx",
    "content": "import localFont from \"next/font/local\";\nimport \"./styles.css\";\nimport { Toaster } from \"react-hot-toast\";\nimport ToastMessage from \"@/features/ui/toast-message\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\n\nconst geistSans = localFont({\n  src: \"./fonts/GeistVF.woff\",\n  variable: \"--font-geist-sans\",\n  weight: \"100 900\",\n});\nconst geistMono = localFont({\n  src: \"./fonts/GeistMonoVF.woff\",\n  variable: \"--font-geist-mono\",\n  weight: \"100 900\",\n});\n\n// Define metadata for the root layout\nexport const metadata = {\n  title: \"Chat2Geo\",\n  description: \"AI-powered geospatial analytics\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <body className={`${geistSans.variable} ${geistMono.variable} font-sans`}>\n        <ThemeProvider\n          attribute=\"class\"\n          defaultTheme=\"light\"\n          enableSystem\n          disableTransitionOnChange\n        >\n          <Toaster />\n          <ToastMessage />\n          {children}\n        </ThemeProvider>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/(broadcast)/services/esri/fetch-layers/page.tsx",
    "content": "import { createClient } from \"@/utils/supabase/server\";\nimport { redirect } from \"next/navigation\";\nimport AddArcGisLayerClient from \"@/components/services/esri/add-arcgis-layers\";\n\nexport default async function AddArcGisLayerPage() {\n  const supabase = await createClient();\n  const { data: authResults, error } = await supabase.auth.getUser();\n\n  if (error || !authResults?.user) {\n    redirect(\"/login\");\n  }\n\n  return <AddArcGisLayerClient />;\n}\n"
  },
  {
    "path": "app/(broadcast)/styles.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 47.4% 11.2%;\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 47.4% 11.2%;\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 47.4% 11.2%;\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n    --primary-blue: 220 90% 56%;\n    --primary-blue-foreground: 210 40% 98%;\n    --primary-green: 145 63% 42%;\n    --primary-green-foreground: 210 40% 98%;\n    --secondary: 220 13% 92%; /* Neutral light gray */\n    --secondary-foreground: 220 10% 40%; /* Neutral medium gray */\n    --accent: 220 15% 85%; /* Neutral soft gray */\n    --accent-foreground: 220 20% 12%; /* Neutral deep gray */\n    --destructive: 0 100% 50%;\n    --destructive-foreground: 210 40% 98%;\n    --warning: 45 100% 50%;\n    --warning-foreground: 45 100% 15%;\n    --info: 200 98% 48%;\n    --info-foreground: 210 40% 98%;\n    --ring: 215 20.2% 65.1%;\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    /* Base */\n    --background: 240 2% 12%; /* #1f1f21 */\n    --foreground: 0 0% 95%; /* Very light neutral */\n\n    /* Darker variant for cards/popovers */\n    --card: 220 3% 10%; /* #18191a */\n    --card-foreground: 0 0% 95%;\n    --popover: 220 3% 10%; /* #18191a */\n    --popover-foreground: 0 0% 95%;\n\n    /* Muted */\n    --muted: 240 2% 16%; /* Slightly lighter than background */\n    --muted-foreground: 0 0% 60%; /* Subdued text */\n\n    /* Interactive elements */\n    --accent: 240 2% 18%; /* Subtle highlight */\n    --accent-foreground: 0 0% 98%;\n\n    /* Borders and rings */\n    --border: 240 2% 20%; /* Visible but subtle */\n    --input: 240 2% 20%;\n    --ring: 240 2% 20%;\n\n    /* Primary */\n    --primary: 0 0% 98%; /* Almost white */\n    --primary-foreground: 240 2% 12%; /* Same as background */\n\n    /* Secondary */\n    --secondary: 240 2% 22%; /* Lighter than accent */\n    --secondary-foreground: 0 0% 98%;\n\n    /* Semantic colors - coordinated with our neutral theme */\n    --primary-blue: 220 90% 56%;\n    --primary-blue-foreground: 210 40% 98%;\n\n    --primary-green: 145 63% 42%;\n    --primary-green-foreground: 210 40% 98%;\n\n    --destructive: 0 50% 35%; /* More muted red */\n    --destructive-foreground: 0 0% 98%;\n\n    --warning: 45 100% 50%;\n    --warning-foreground: 45 100% 15%;\n    --info: 200 98% 48%;\n    --info-foreground: 210 40% 98%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply font-sans antialiased bg-background text-foreground;\n  }\n}\n"
  },
  {
    "path": "app/(main)/actions/get-user-profile.ts",
    "content": "\"use server\";\nimport { createClient } from \"@/utils/supabase/server\";\n\nexport async function getUserProfile() {\n  const supabase = await createClient();\n\n  const {\n    data: { user },\n    error: authError,\n  } = await supabase.auth.getUser();\n\n  // If no user or no email, return null\n  if (authError || !user || !user.email) {\n    return null;\n  }\n\n  const { data: userData, error: userError } = await supabase\n    .from(\"user_roles\")\n    .select(\"name, role, organization, license_start, license_end\")\n    .eq(\"email\", user.email)\n    .single();\n\n  if (userError || !userData) {\n    return null;\n  }\n\n  // Now we know user.email is a string\n  return {\n    email: user.email,\n    name: userData.name,\n    role: userData.role,\n    organization: userData.organization,\n    licenseStart: userData.license_start,\n    licenseEnd: userData.license_end,\n  };\n}\n"
  },
  {
    "path": "app/(main)/api/chat/chat-history/route.ts",
    "content": "import { createClient } from \"@/utils/supabase/server\";\nimport { getChatsByUser } from \"@/lib/database/chat/queries\";\nimport { NextResponse } from \"next/server\";\n\ninterface Chat {\n  id: string;\n}\n\nexport async function GET() {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  const userId = data.user?.id;\n\n  if (error || !data?.user) {\n    return NextResponse.json({ error: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const chats = (await getChatsByUser(userId as string)) as Chat[];\n\n  return NextResponse.json(chats);\n}\n"
  },
  {
    "path": "app/(main)/api/chat/route.ts",
    "content": "import { openai } from \"@ai-sdk/openai\";\nimport { azure } from \"@ai-sdk/azure\"; // You can also use Azure's hosted GPT models. More info: https://sdk.vercel.ai/providers/ai-sdk-providers\nimport {\n  type Message,\n  type CoreUserMessage,\n  streamText,\n  convertToCoreMessages,\n} from \"ai\";\n\nimport { createClient } from \"@/utils/supabase/server\";\nimport { z } from \"zod\";\n\nimport { NextResponse } from \"next/server\";\n\nimport {\n  getChatById,\n  saveChat,\n  saveMessages,\n  searchGeeDatasets,\n} from \"@/lib/database/chat/queries\";\nimport {\n  getUsageForUser,\n  getUserRoleAndTier,\n  incrementRequestCount,\n} from \"@/lib/database/usage\";\nimport { getPermissionSet } from \"@/lib/auth\";\nimport {\n  requestGeospatialAnalysis,\n  requestLoadingGeospatialData,\n  requestRagQuery,\n  draftReport,\n  requestWebScraping,\n} from \"@/lib/database/chat/tools\";\nimport {\n  generateUUID,\n  sanitizeResponseMessages,\n  getMostRecentUserMessage,\n  generateTitleFromUserMessage,\n  getFormattedDate,\n} from \"@/features/chat/utils/general-utils\";\n\n// export const maxDuration = 30;\n\nexport async function POST(request: Request) {\n  const {\n    id,\n    messages,\n    selectedRoiGeometryInChat,\n    mapLayersNames,\n  }: {\n    id: string;\n    messages: Array<Message>;\n    modelId: string;\n    selectedRoiGeometryInChat: any;\n    mapLayersNames: string[];\n  } = await request.json();\n\n  const supabase = await createClient();\n  const { data: authResult, error: authError } = await supabase.auth.getUser();\n  if (authError || !authResult?.user) {\n    return NextResponse.json({ error: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const userId = authResult.user.id;\n  // Fetch the user's role + subscription\n  const userRoleRecord = await getUserRoleAndTier(userId);\n  if (!userRoleRecord) {\n    return NextResponse.json(\n      { error: \"Failed to get role/subscription\" },\n      { status: 403 }\n    );\n  }\n\n  const { role, subscription_tier: subscriptionTier } = userRoleRecord;\n  const { maxRequests, maxArea } = await getPermissionSet(\n    role,\n    subscriptionTier\n  );\n  const usage = await getUsageForUser(userId);\n  if (usage.requests_count >= maxRequests) {\n    return NextResponse.json(\n      { error: \"Request limit exceeded\" },\n      { status: 403 }\n    );\n  }\n\n  const cookieStore = request.headers.get(\"cookie\");\n  const chat = await getChatById(id);\n\n  const coreMessages = convertToCoreMessages(messages);\n  const userMessage = getMostRecentUserMessage(coreMessages);\n\n  // Increment usage count\n  await incrementRequestCount(userId);\n\n  if (!chat) {\n    const generatedTitle = await generateTitleFromUserMessage({\n      message: messages[0] as CoreUserMessage,\n    });\n    await saveChat({ id: id, title: generatedTitle });\n  }\n\n  const userMessageId = generateUUID();\n  await saveMessages({\n    messages: [\n      {\n        ...userMessage,\n        id: userMessageId,\n        createdAt: new Date(),\n        chatId: id,\n      },\n    ],\n  });\n\n  // System instructions\n  const systemInstructions = `Today is ${getFormattedDate()}. You are an AI Assistant specializing in geospatial analytics. \n  Be kind, warm, and professional. Use emojis where appropriate to enhance user experience. \n  When user asks for a geospatial analysis or data, never ask for the location unless you run the analysis and you get a corresponding error. Users provide the name of their region of interest (ROI) data when requesting an analysis.\n  Always highlight important outputs and provide help in interpreting results. NEVER include map URLs or map legends/palette (like classes) in your responses.\n  Refuse to answer questions irrelevant to geospatial analytics or the platform's context. You have access to several tools. If running a tool fails, and you thought you would be to fix it with a change, try 3 times until you fix it.\n  IF USER ASKS FOR DRAFTING REPORTS, YOU SHOULD RUN THE \"draftReport\" TOOL, AND JUST CONFIRM THE DRAFTING OF THE REPORT. YOU SHOULD NOT EVER DRAFT REPORT IN THE CHAT.\"\n  You also have access to a tool that can load geospatial data. First, run the tool that searches the database containing GEE datasets information to find the datasets best match user's request. Afterwards, run the web scraper tool to find extra info such as how to set the visualization parameter (pay attention to the code snippet from the official doc you will recieve). After that provide a short summary of what data with what parameters you're going to load to make sure if it's exactly what the user needs. After everything goes well and the user confirmed the details of the analysis to run, use all the information to load the dataset. \n  Another tool you have access to is a RAG query tool that you can use to answer questions you don't know the answer to.\n  Before running any geospatial analysis, make sure the layer name doesn't already exist in the map layers. No geospatial analysis is available for the year 2025, so you SHOULD NOT run analysis for 2025 even if the user asks for it.\n  When executing analyes (not ragQueryRetrieval, though):\n  1. Always provide a clear summary of what was analyzed\n  2. Highlight key findings and patterns in the data,\n  3. Try to tabulate some parts of the results/descriptions for the sake of clarity.`;\n\n  // Prepend system instructions to the conversation as a separate message for the AI\n  const systemMessage = {\n    role: \"assistant\", // Change role to \"assistant\" to avoid unhandled role errors\n    content: systemInstructions,\n  };\n\n  // Add the system message at the beginning of the conversation\n  const processedMessages = [\n    systemMessage,\n    ...messages.filter((msg: any) => msg.role !== \"system\"),\n  ] as Array<Message>;\n\n  const result = await streamText({\n    model: openai(\"gpt-4o\"),\n    // model: azure(\"gpt-4o\"),  // You can also use Azure's hosted GPT models\n    maxSteps: 5,\n    messages: convertToCoreMessages(processedMessages),\n    onFinish: async ({ response }) => {\n      if (userId) {\n        try {\n          const responseMessagesWithoutIncompleteToolCalls =\n            sanitizeResponseMessages(response.messages);\n\n          await saveMessages({\n            messages: responseMessagesWithoutIncompleteToolCalls.map(\n              (message) => {\n                const messageId = generateUUID();\n\n                return {\n                  id: messageId,\n                  chatId: id,\n                  draftedReportId: null,\n                  role: message.role,\n                  content: message.content,\n                  createdAt: new Date(),\n                };\n              }\n            ),\n          });\n        } catch (error) {\n          console.error(\"Failed to save chat\");\n        }\n      }\n    },\n\n    tools: {\n      requestGeospatialAnalysis: {\n        description: `Today is ${getFormattedDate()}, so you should be able to help the user with requests by up to this date. No analysis should be done for the year of 2025 as analyses are not yet ready for the new year.\n          After running an analysis: 1. Provide a clear summary of what was analyzed and why, 2. Explain the key findings and their significance. NEVER PROVIDE MAP URLs or MAP LEGENDS FROM THE ANALYSES IN THE RESPONSE. Also the maximum area the user can request analysis for is ${maxArea} sq km. per request.\n          It should be noted that the land cover map (start date: 2015) and bi-temporal land cover change map (start date: 2015) are based on Sentinel-2 imagery, UHI (start date: 2015) is based on Landsat imagery. For all \"CHANGE\" maps, the user must provide \"startDate2 and endDate2\". If in doubt about an analysis (e.g., it may not exactly match the analysis we have), you have to double check with the user.`,\n        parameters: z.object({\n          functionType: z.string()\n            .describe(`The type of analysis to execute. It can be one of the following:\n            'Urban Heat Island (UHI) Analysis',\n            'Land Use/Land Cover Maps',\n            'Land Use/Land Cover Change Maps'.`),\n          startDate1String: z\n            .string()\n            .describe(\n              \"The start date for the first period. The date format should be 'YYYY-MM-DD'. But convert any other date format the user gives you to that one.\"\n            ),\n          endDate1String: z\n            .string()\n            .describe(\n              \"The end date for the first period. The date format should be 'YYYY-MM-DD'. But convert any other date format the user gives you to that one.\"\n            ),\n          startDate2String: z\n            .string()\n            .optional()\n            .describe(\n              \"The start date for the second period. The date format should be 'YYYY-MM-DD'. But convert any other date format the user gives you to that one.\"\n            ),\n          endDate2String: z\n            .string()\n            .optional()\n            .describe(\n              \"The end date for the second period. The date format should be 'YYYY-MM-DD'. But convert any other date format the user gives you to that one.\"\n            ),\n          aggregationMethod: z.string().describe(\n            `The method to use for aggregating the data. It means that in a time-series, what method is used to aggregate data for a given point/pixel in the final map/analysis delivered. For land use/land cover mapping, it's always \"Median\", and thus you don't need to ask user for that. It can be one of the following:\n            'Mean',\n            'Median',\n            'Min',\n            'Max',\n            . Note that the user may not provide it, so by default its value should be 'Max', and you should not ask the user to tell you what method to use. If the default value is used, make sure to mention it in the response to user that your analysis is based on the maximum va.\n          `\n          ),\n          layerName: z\n            .string()\n            .describe(\n              \"The name of the layer to be displayed. You ask the user about it if they don't provide it. Otherwise, use a name based on the function type, but make sure the name is concise and descriptive. \"\n            ),\n          title: z\n            .string()\n            .optional()\n            .describe(\n              \"Briefly describe the title of the analysis in one sentence confirming you're working on the user's request.\"\n            ),\n        }),\n        execute: async (args) =>\n          requestGeospatialAnalysis({\n            ...args,\n            cookieStore,\n            selectedRoiGeometryInChat,\n            maxArea,\n          }),\n      },\n      requestLoadingGeospatialData: {\n        description: `The user has requested loading and visualizing geospatial data. You should load the data based on the user's request.`,\n        parameters: z.object({\n          geospatialDataType: z.string().describe(\n            `The type of geospatial data to load. It can be one of the following:\n      'Load GEE Data'`\n          ),\n          selectedRoiGeometry: z\n            .object({\n              type: z.string().optional(),\n              coordinates: z.array(z.array(z.array(z.number()))).optional(),\n            })\n            .optional()\n            .describe(\n              \"The selected region of interest (ROI) geometry. You should run the analysis based on the user's request.\"\n            ),\n          dataType: z\n            .string()\n            .describe(\n              `The type of data to load. It can be one of the following: 'Image', 'ImageCollection'.`\n            ),\n\n          divideValue: z\n            .number()\n            .describe(\n              `The value to divide the image by. If based on the scraped data you didn't find it, use your logic to see if it should be set based on the dataset. Sometimes, the division is done within a \"cloud mask\" function, so you should extract its value from there in that case. If you decide not to set it, set it to 1.`\n            ),\n          datasetId: z.string().describe(\"The ID of the GEE dataset to load.\"),\n          startDate: z\n            .string()\n            .describe(\n              \"The start date for the data to load. The date format should be 'YYYY-MM-DD'. But convert any other date format the user gives you to that one.\"\n            ),\n          endDate: z\n            .string()\n            .describe(\n              \"The end date for the data to load. The date format should be 'YYYY-MM-DD'. But convert any other date format the user gives you to that one.\"\n            ),\n          visParams: z.union([\n            // single-band case\n            z.object({\n              bands: z.array(z.string()).length(1),\n              palette: z.array(z.string()),\n              min: z.number().optional(),\n              max: z.number().optional(),\n            }),\n            // multi-band case\n            z.object({\n              bands: z.array(z.string()),\n              min: z.number().optional(),\n              max: z.number().optional(),\n            }),\n          ])\n            .describe(`You should set the visualization parameters best matching user's request for the data to load and best way of visualization:\n            1) If you want to combine more than one band for visualization, set visParams using the bands: [...] attribute.\n            2) Otherwise, use the palette: [...] attribute (and do not include bands).\n            As an example, RGB visualization should be set as: {bands: ['red', 'green', 'blue']}. Forest loss should be using pellete if it's one band.\n      `),\n          labelNames: z\n            .array(z.string())\n            .describe(\n              \"The label names for the data to load. You should run the analysis based on the user's request. Choose the closet label names even if it doesn't 100% match what you already know. Infer it.\"\n            ),\n          layerName: z\n            .string()\n            .describe(\n              \"The name of the layer to be displayed. You ask the user about it if they don't provide it. Otherwise, use a name based on the function type, but make sure the name is concise and descriptive. \"\n            ),\n          title: z\n            .string()\n            .optional()\n            .describe(\n              \"Briefly describe the title of the analysis in one sentence confirming you're working on the user's request.\"\n            ),\n        }),\n        execute: async (args) => {\n          return requestLoadingGeospatialData({\n            ...args,\n            cookieStore,\n            selectedRoiGeometryInChat,\n          });\n        },\n      },\n      searchGeeDatasets: {\n        description: `Find the datasets available in Google Earth Engine (GEE) that best match the user's query.`,\n        parameters: z.object({\n          query: z.string().describe(\"The name of the dataset to search.\"),\n          startDate: z\n            .string()\n            .optional()\n            .describe(\n              \"The start date for the data to load based on the scraping results. This could be the year or the date in a format. This shows the start date the data is available.\"\n            ),\n          endDate: z\n            .string()\n            .optional()\n            .describe(\n              \"The end date for the data to load based on the scraping result. This could be the year or the date in a format. This shows the end date the data is available.\"\n            ),\n          title: z\n            .string()\n            .optional()\n            .describe(\n              \"Briefly describe the title of the analysis in one sentence confirming you're working on the user's request.\"\n            ),\n        }),\n        execute: async (args) => {\n          const result = searchGeeDatasets(args.query);\n          return result;\n        },\n      },\n\n      scrapeWebpage: {\n        description:\n          \"Scrape the webpage of the GEE dataset to learn what dataset_id, how data is visualized, legends, any division by a value, etc. you should use for the the requested dataset. For example, one of the things you should learn is whether you need to have a band combination (e.g., [b1, b2, b3]) or a palette (e.g., ['red', 'green', 'blue']) to visualize the image.\",\n        parameters: z.object({\n          url: z\n            .string()\n            .describe(\n              \"The asset URL of the webpage to scrape. The name of the column you're scraping for this parameter should be 'asset_url'.\"\n            ),\n          title: z\n            .string()\n            .optional()\n            .describe(\n              \"Briefly describe the title of the analysis in one sentence confirming you're working on the user's request.\"\n            ),\n        }),\n\n        execute: async (args) => {\n          return requestWebScraping(args);\n        },\n      },\n      requestRagQuery: {\n        description: `The user has some documents with which a RAG has been built. If you're asked a question that you didn't know the answer, run the requestRagQuery tool that is based on user's documents to get the answer.`,\n        parameters: z.object({\n          query: z.string().describe(\"The user's query text.\"),\n          title: z\n            .string()\n            .optional()\n            .describe(\n              \"Briefly describe the title of the analysis in one sentence confirming you're working on the user's request.\"\n            ),\n        }),\n        execute: async (args) => requestRagQuery({ ...args, cookieStore }),\n      },\n      draftReport: {\n        description: `When this tool is called, draft a report that summarizes the analyses and their results. The report should be concise and easy to understand, highlighting the key findings and insights. Markdown is supported.`,\n        parameters: z.object({\n          messages: z\n            .array(z.object({}))\n            .describe(\n              \"The messages exchanged between the user and the you. You should use relevant messages in the chat to generate the report the user requested. Make sure you format the report in a standard way with all the common structures.\"\n            ),\n          title: z\n            .string()\n            .optional()\n            .describe(\n              \"Briefly describe the title of the report to be drafted in one sentence confirming you're working on the user's request.\"\n            ),\n          reportFileName: z\n            .string()\n            .optional()\n            .describe(\"Provide a concise name for the report file.\"),\n        }),\n        execute: async (args) =>\n          draftReport({ ...args, messages: processedMessages }),\n      },\n      checkMapLayersNames: {\n        description:\n          \"Here are the the names of the current map layers. If you run a geospatial analysis, and you select a name for the layer, you should should first check the layer names to make sure the name you selected is not already in use. You shouldn't output any message regarding the name you select.\",\n        parameters: z.object({\n          layerName: z\n            .string()\n            .describe(\"The name of the layer to be displayed.\"),\n          title: z\n            .string()\n            .optional()\n            .describe(\n              \"Briefly describe the title of the analysis in one sentence confirming you're working on the user's request.\"\n            ),\n        }),\n\n        execute: async (args) => {\n          return mapLayersNames;\n        },\n      },\n    },\n  });\n\n  return result.toDataStreamResponse();\n}\n"
  },
  {
    "path": "app/(main)/api/gee/request-geospatial-analysis/route.ts",
    "content": "import { createClient } from \"@/utils/supabase/server\";\nimport { NextResponse } from \"next/server\";\nimport { NextRequest } from \"next/server\";\nimport { geeAuthenticate } from \"@/features/maps/utils/authentication-utils/gee-auth\";\nimport { urbanHeatIslandAnalysis } from \"@/lib/geospatial/gee/analysis-functions/heat-analysis/urban-heat-island-analysis\";\nimport { airPollutionAnalysis } from \"@/lib/geospatial/gee/analysis-functions/pollution-analysis/air-pollution-analysis\";\nimport { sentinelLandcoverLanduseMapping } from \"@/lib/geospatial/gee/analysis-functions/lancover-landuse-mapping/sentinel-landcover-landuse-mapping\";\nimport { convertToEeGeometry } from \"@/features/maps/utils/geometry-utils\";\nimport googleDynamicWorldMapping from \"@/lib/geospatial/gee/analysis-functions/lancover-landuse-mapping/google-dynamic-world-landcover-mapping\";\nimport landcoverChangeMapping from \"@/lib/geospatial/gee/analysis-functions/lancover-landuse-mapping/landcover-change-mapping\";\n\nconst validAnalysisOptionsForVulnerabilityMapBuilder: MultiAnalysisOptionsTypeForVulnerabilityMapBuilderType[] =\n  [\"Air Pollutants\", \"Flood Risk\", \"Urban Heat Island (UHI)\"];\nconst validAnalysisOptionsForAtmosphericGasAnalysis: MultiAnalysisOptionsTypeForAirPollutantsAnalysisType[] =\n  [\"CO\", \"NO2\", \"CH4\", \"Aerosols\"];\n\nexport async function POST(req: NextRequest) {\n  const supabase = await createClient();\n\n  const { data, error } = await supabase.auth.getUser();\n\n  if (error || !data?.user) {\n    return NextResponse.json({ error: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  // Parse the request body as JSON\n  let body;\n  try {\n    body = await req.json();\n  } catch (err) {\n    console.error(err);\n    return NextResponse.json({ error: \"Invalid JSON body\" }, { status: 400 });\n  }\n\n  const {\n    functionType,\n    selectedRoiGeometry,\n    aggregationMethod,\n    startDate1,\n    endDate1,\n    startDate2,\n    endDate2,\n    multiAnalysisOptions,\n  } = body;\n\n  // Validate the ROI geometry\n  if (!selectedRoiGeometry) {\n    return NextResponse.json(\n      {\n        error:\n          \"No Region of Interest (ROI) was provided. Please provide an ROI.\",\n      },\n      { status: 400 }\n    );\n  }\n\n  // If the geometry comes in as a string, decode and parse it\n  let geometry = selectedRoiGeometry;\n  if (typeof selectedRoiGeometry === \"string\") {\n    try {\n      const geometryString = decodeURIComponent(selectedRoiGeometry);\n      geometry = JSON.parse(geometryString);\n    } catch (err) {\n      return NextResponse.json(\n        { error: \"Invalid geometry JSON\" },\n        { status: 400 }\n      );\n    }\n  }\n\n  try {\n    await initializeGee();\n\n    const eeReadyGeometry = convertToEeGeometry(geometry);\n\n    let result;\n    switch (functionType) {\n      case \"Air Pollution Analysis\":\n        if (\n          multiAnalysisOptions.every((option: any) =>\n            validAnalysisOptionsForAtmosphericGasAnalysis.includes(\n              option as MultiAnalysisOptionsTypeForAirPollutantsAnalysisType\n            )\n          )\n        ) {\n          result = await airPollutionAnalysis(\n            geometry,\n            multiAnalysisOptions as MultiAnalysisOptionsTypeForAirPollutantsAnalysisType[],\n            startDate1,\n            endDate1,\n            aggregationMethod as AggregationMethodTypeNumerical,\n            startDate2,\n            endDate2\n          );\n        } else {\n          throw new Error(\n            \"Invalid analysis option for Air Pollutants Analysis\"\n          );\n        }\n        break;\n      case \"Land Use/Land Cover Maps\":\n        result = await googleDynamicWorldMapping(\n          eeReadyGeometry,\n          startDate1,\n          endDate1\n        );\n        break;\n\n      case \"Land Use/Land Cover Change Maps\":\n        result = await landcoverChangeMapping(\n          eeReadyGeometry,\n          startDate1,\n          endDate1,\n          startDate2,\n          endDate2\n        );\n        break;\n\n      case \"Urban Heat Island (UHI) Analysis\":\n        result = await urbanHeatIslandAnalysis(\n          eeReadyGeometry,\n          startDate1,\n          endDate1,\n          aggregationMethod as AggregationMethodTypeNumerical\n        );\n        break;\n\n      default:\n        throw new Error(\"Invalid function type\");\n    }\n\n    return NextResponse.json(result, { status: 200 });\n  } catch (error: any) {\n    console.error(error);\n    return NextResponse.json({ result: error.message }, { status: 404 });\n  }\n}\n\n// Function to initialize Google Earth Engine\nconst initializeGee = async () => {\n  await geeAuthenticate();\n};\n"
  },
  {
    "path": "app/(main)/api/gee/request-loading-geospatial-data/route.ts",
    "content": "import { createClient } from \"@/utils/supabase/server\";\nimport { NextResponse } from \"next/server\";\nimport { NextRequest } from \"next/server\";\nimport { geeAuthenticate } from \"@/features/maps/utils/authentication-utils/gee-auth\";\nimport { convertToEeGeometry } from \"@/features/maps/utils/geometry-utils\";\nimport { loadRasterData } from \"@/lib/geospatial/gee/load-data/load-raster-data\";\n\nexport async function POST(req: NextRequest) {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data?.user) {\n    return NextResponse.json({ error: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  let body;\n  try {\n    body = await req.json();\n  } catch (err) {\n    console.error(\"Error parsing request body:\", err);\n    return NextResponse.json(\n      { error: \"Invalid request format\" },\n      { status: 400 }\n    );\n  }\n\n  const {\n    geospatialDataType,\n    dataType,\n    selectedRoiGeometry,\n    divideValue,\n    datasetId,\n    startDate,\n    endDate,\n    visParams,\n    labelNames,\n  } = body;\n\n  if (!selectedRoiGeometry) {\n    return NextResponse.json(\n      {\n        error:\n          \"No Region of Interest (ROI) was provided. Please provide an ROI.\",\n      },\n      { status: 400 }\n    );\n  }\n\n  let geometry = selectedRoiGeometry;\n\n  if (typeof selectedRoiGeometry === \"string\") {\n    try {\n      const geometryString = decodeURIComponent(selectedRoiGeometry);\n      geometry = JSON.parse(geometryString);\n    } catch (err) {\n      return NextResponse.json(\n        { error: \"Invalid geometry format\" },\n        { status: 400 }\n      );\n    }\n  }\n\n  try {\n    await initializeGee();\n    const eeReadyGeometry = convertToEeGeometry(geometry);\n\n    switch (geospatialDataType) {\n      case \"Load GEE Data\": {\n        if (!datasetId || !startDate || !endDate || !visParams || !labelNames) {\n          return NextResponse.json(\n            { error: \"Missing required parameters\" },\n            { status: 400 }\n          );\n        }\n\n        const result = await loadRasterData(\n          datasetId,\n          dataType,\n          eeReadyGeometry,\n          startDate,\n          endDate,\n          divideValue,\n          visParams,\n          labelNames\n        );\n\n        if (!result) {\n          return NextResponse.json(\n            { error: \"Failed to load raster data\" },\n            { status: 500 }\n          );\n        }\n\n        return NextResponse.json(result);\n      }\n\n      default:\n        return NextResponse.json(\n          { error: \"Invalid geospatial data type\" },\n          { status: 400 }\n        );\n    }\n  } catch (error: any) {\n    console.error(\"Error processing request:\", error);\n    return NextResponse.json(\n      { error: error.message || \"Failed to process geospatial data\" },\n      { status: 500 }\n    );\n  }\n}\n\nconst initializeGee = async () => {\n  await geeAuthenticate();\n};\n"
  },
  {
    "path": "app/(main)/api/sendfeedback/route.ts",
    "content": "// It's a simple email-based feedback form. The user sends a message, and it gets sent to a recipient email address.\n\nimport { createClient } from \"@/utils/supabase/server\";\nimport FormData from \"form-data\";\nimport Mailgun from \"mailgun.js\";\nimport { NextResponse } from \"next/server\";\n\nconst API_KEY = process.env.MAILGUN_API_KEY || \"\";\nconst DOMAIN = process.env.MAILGUN_DOMAIN || \"\";\nconst ALLOWED_ORIGIN = process.env.NEXT_PUBLIC_APP_URL!;\nconst RECIPIENT_EMAIL = process.env.RECIPIENT_EMAIL;\nconst SENDER_EMAIL = process.env.SENDER_EMAIL;\n\nexport async function OPTIONS() {\n  return new NextResponse(null, {\n    status: 200,\n    headers: {\n      \"Access-Control-Allow-Origin\": ALLOWED_ORIGIN,\n      \"Access-Control-Allow-Methods\": \"POST, OPTIONS\",\n      \"Access-Control-Allow-Headers\": \"Content-Type, Authorization\",\n    },\n  });\n}\n\nexport async function POST(req: Request) {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  const user = data?.user;\n  if (error || !data.user) {\n    return new NextResponse(JSON.stringify({ error: \"Unauthenticated!\" }), {\n      status: 401,\n    });\n  }\n\n  const contentType = req.headers.get(\"content-type\") || \"\";\n  if (!contentType.includes(\"application/json\")) {\n    return new NextResponse(\n      JSON.stringify({ error: \"Unsupported media type. Please send JSON.\" }),\n      { status: 415 }\n    );\n  }\n\n  let body;\n  try {\n    body = await req.json();\n  } catch (err) {\n    console.error(\"Error parsing JSON:\", err);\n    return new NextResponse(\n      JSON.stringify({ error: \"Invalid JSON payload.\" }),\n      { status: 400 }\n    );\n  }\n  const { message } = body || {};\n  if (!message) {\n    return new NextResponse(\n      JSON.stringify({\n        error: \"All fields (message) are required.\",\n      }),\n      { status: 400 }\n    );\n  }\n\n  try {\n    const mailgun = new Mailgun(FormData);\n    const client = mailgun.client({ username: \"api\", key: API_KEY });\n\n    const messageData = {\n      from: `Chat2Geo ${SENDER_EMAIL}`,\n      to: RECIPIENT_EMAIL,\n      subject: \"New Feedback for Chat2Geo!\",\n      text: `Hello,\n  \n      You have a new form entry from: ${user?.email}.\n  \n      ${message}\n      `,\n    };\n    await client.messages.create(DOMAIN, messageData);\n    return new NextResponse(JSON.stringify({ submitted: true }), {\n      status: 200,\n    });\n  } catch (mailErr: any) {\n    console.error(\"Error sending email:\", mailErr);\n    return new NextResponse(JSON.stringify({ error: \"Failed to send email\" }), {\n      status: 500,\n    });\n  }\n}\n"
  },
  {
    "path": "app/(main)/api/services/google-maps/basemaps/roadmap/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\nimport { createClient } from \"@/utils/supabase/server\";\n\nexport async function GET(request: NextRequest) {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data.user) {\n    return NextResponse.json({ error: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const { searchParams } = new URL(request.url);\n  const x = searchParams.get(\"x\");\n  const y = searchParams.get(\"y\");\n  const z = searchParams.get(\"z\");\n  const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY;\n\n  if (!GOOGLE_MAPS_API_KEY) {\n    return NextResponse.json(\n      { error: \"API key is not configured\" },\n      { status: 500 }\n    );\n  }\n\n  if (!x || !y || !z) {\n    return NextResponse.json({ error: \"Missing parameters\" }, { status: 400 });\n  }\n\n  const tileUrl = `https://maps.googleapis.com/maps/vt?lyrs=r&x=${x}&y=${y}&z=${z}&key=${GOOGLE_MAPS_API_KEY}`;\n  return NextResponse.redirect(tileUrl);\n}\n"
  },
  {
    "path": "app/(main)/api/services/google-maps/basemaps/satellite/route.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\nimport { createClient } from \"@/utils/supabase/server\";\n\nexport async function GET(request: NextRequest) {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data.user) {\n    return NextResponse.json({ error: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const { searchParams } = new URL(request.url);\n  const x = searchParams.get(\"x\");\n  const y = searchParams.get(\"y\");\n  const z = searchParams.get(\"z\");\n  const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY;\n\n  if (!GOOGLE_MAPS_API_KEY) {\n    return NextResponse.json(\n      { error: \"API key is not configured\" },\n      { status: 500 }\n    );\n  }\n\n  if (!x || !y || !z) {\n    return NextResponse.json({ error: \"Missing parameters\" }, { status: 400 });\n  }\n\n  const tileUrl = `https://maps.googleapis.com/maps/vt?lyrs=s&x=${x}&y=${y}&z=${z}&key=${GOOGLE_MAPS_API_KEY}`;\n  return NextResponse.redirect(tileUrl);\n}\n"
  },
  {
    "path": "app/(main)/api/services/google-maps/geocode/route.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport { Client } from \"@googlemaps/google-maps-services-js\";\n\nconst client = new Client({});\n\nexport async function POST(request: Request) {\n  const { address } = await request.json();\n\n  if (!address) {\n    return NextResponse.json({ error: \"Address is required\" }, { status: 400 });\n  }\n\n  try {\n    const response = await client.geocode({\n      params: {\n        address,\n        key: process.env.GOOGLE_MAPS_API_KEY || \"\",\n      },\n    });\n\n    return NextResponse.json(response.data);\n  } catch (error) {\n    console.error(\"Geocoding error:\", error);\n    return NextResponse.json(\n      { error: \"Failed to geocode address\" },\n      { status: 500 }\n    );\n  }\n}\n"
  },
  {
    "path": "app/(main)/api/services/google-maps/places/route.ts",
    "content": "import { NextResponse } from \"next/server\";\n\nexport async function GET() {\n  const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY;\n\n  if (!GOOGLE_MAPS_API_KEY) {\n    return NextResponse.json({ error: \"API key is missing\" }, { status: 500 });\n  }\n\n  const scriptUrl = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`;\n\n  return NextResponse.json({ scriptUrl });\n}\n"
  },
  {
    "path": "app/(main)/api/user-usage/route.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport { createClient } from \"@/utils/supabase/server\";\nimport { getUsageForUser, getUserRoleAndTier } from \"@/lib/database/usage\";\nimport { getPermissionSet } from \"@/lib/auth\";\n\nexport async function GET() {\n  const supabase = await createClient();\n\n  const { data: authData, error: authError } = await supabase.auth.getUser();\n  if (authError || !authData?.user) {\n    return NextResponse.json({ error: \"Unauthenticated\" }, { status: 401 });\n  }\n  const userId = authData.user.id;\n\n  const usage = await getUsageForUser(userId);\n\n  const userRoleRecord = await getUserRoleAndTier(userId);\n  if (!userRoleRecord) {\n    return NextResponse.json(\n      { error: \"Role or subscription not found\" },\n      { status: 404 }\n    );\n  }\n\n  const { role, subscription_tier } = userRoleRecord;\n  const { maxRequests, maxDocs, maxArea } = await getPermissionSet(\n    role,\n    subscription_tier\n  );\n\n  return NextResponse.json({\n    requests_count: usage.requests_count,\n    knowledge_base_docs_count: usage.knowledge_base_docs_count,\n    maxRequests,\n    maxDocs,\n    maxArea,\n  });\n}\n"
  },
  {
    "path": "app/(main)/api/web-scraper/route.ts",
    "content": "import { NextResponse } from \"next/server\";\nimport * as cheerio from \"cheerio\";\n\nexport async function GET(request: Request) {\n  try {\n    const { searchParams } = new URL(request.url);\n    const url = searchParams.get(\"url\");\n\n    if (!url) {\n      return NextResponse.json(\n        { error: \"URL parameter is required\" },\n        { status: 400 }\n      );\n    }\n\n    const response = await fetch(url, {\n      headers: {\n        \"User-Agent\":\n          \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\",\n        Accept:\n          \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n        \"Accept-Language\": \"en-US,en;q=0.9\",\n        \"Cache-Control\": \"no-cache\",\n        Pragma: \"no-cache\",\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n\n    const html = await response.text();\n    const $ = cheerio.load(html);\n\n    // Initialize content structure focused on code snippets\n    const content = {\n      title: $(\"title\").text().trim(),\n      codeSnippets: [] as Array<{\n        language?: string;\n        code: string;\n        context?: string; // Optional heading or context where the code was found\n      }>,\n    };\n\n    // Look for code blocks in pre and code tags\n    $(\"pre, code\").each((_, element) => {\n      const $el = $(element);\n      const code = $el.text().trim();\n\n      // Skip empty code blocks\n      if (!code) return;\n\n      // Try to determine the language\n      const language = $el.attr(\"class\")?.match(/language-(\\w+)/)?.[1];\n\n      // Get surrounding context (e.g., nearest heading)\n      const context = $el\n        .closest(\"section\")\n        .find(\"h1, h2, h3, h4, h5, h6\")\n        .first()\n        .text()\n        .trim();\n\n      // Avoid duplicates\n      if (!content.codeSnippets.some((snippet) => snippet.code === code)) {\n        content.codeSnippets.push({\n          language,\n          code,\n          context: context || undefined,\n        });\n      }\n    });\n\n    return NextResponse.json({\n      success: true,\n      data: content,\n      debug: {\n        url: response.url,\n        snippetsFound: content.codeSnippets.length,\n      },\n    });\n  } catch (error) {\n    console.error(\"Scraping error:\", error);\n    return NextResponse.json(\n      {\n        error: \"Failed to scrape the webpage\",\n        details: error instanceof Error ? error.message : \"Unknown error\",\n      },\n      { status: 500 }\n    );\n  }\n}\n"
  },
  {
    "path": "app/(main)/chat/[id]/layout.tsx",
    "content": "import React from \"react\";\nimport { createClient } from \"@/utils/supabase/server\";\nimport { redirect } from \"next/navigation\";\n\nexport default async function ChatLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error,\n  } = await supabase.auth.getUser();\n\n  if (error || !user) {\n    redirect(\"/login\");\n  }\n\n  return (\n    <div className=\"relative flex min-h-screen\">\n      <main className=\"flex-grow\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(main)/chat/[id]/loading.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nexport default function Loading() {\n  return (\n    <div className=\"page-loading-overlay\">\n      <div className=\"page-loading-spinner\"></div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(main)/chat/[id]/page.tsx",
    "content": "import MainChatPage from \"@/features/chat/components/chat\";\nimport { getChatById, getMessagesByChatId } from \"@/lib/database/chat/queries\";\nimport { notFound } from \"next/navigation\";\n\nimport { convertToUIMessages } from \"@/features/chat/utils/general-utils\";\n\nexport default async function Page(props: { params: Promise<{ id: string }> }) {\n  const params = await props.params;\n  const { id } = params;\n  const chat = await getChatById(id);\n\n  if (!chat) {\n    notFound();\n  }\n\n  const initialMessages = await getMessagesByChatId(id);\n\n  return (\n    <MainChatPage\n      chatId={id}\n      initialMessages={convertToUIMessages(initialMessages as any)}\n    />\n  );\n}\n"
  },
  {
    "path": "app/(main)/chat-history/layout.tsx",
    "content": "import React from \"react\";\nimport { createClient } from \"@/utils/supabase/server\";\nimport { redirect } from \"next/navigation\";\n\nexport default async function ChatHistoryLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error,\n  } = await supabase.auth.getUser();\n\n  if (error || !user) {\n    redirect(\"/login\");\n  }\n\n  return (\n    <div className=\"relative flex min-h-screen\">\n      <main className=\"flex-grow\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(main)/chat-history/loading.tsx",
    "content": "import ChatHistoryTableSkeleton from \"@/features/chat-history/components/chat-history-table-skeleton\";\n\nconst loading = () => {\n  return <ChatHistoryTableSkeleton />;\n};\n\nexport default loading;\n"
  },
  {
    "path": "app/(main)/chat-history/page.tsx",
    "content": "export const dynamic = \"force-dynamic\";\n\nimport React from \"react\";\nimport ChatHistory from \"@/features/chat-history/components/chat-history\";\n\nconst ChatHistoryPage = async () => {\n  return <ChatHistory />;\n};\n\nexport default ChatHistoryPage;\n"
  },
  {
    "path": "app/(main)/integrations/layout.tsx",
    "content": "import { createClient } from \"@/utils/supabase/server\";\nimport { redirect } from \"next/navigation\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error,\n  } = await supabase.auth.getUser();\n\n  if (error || !user) {\n    redirect(\"/login\");\n  }\n\n  return (\n    <div className=\"relative flex min-h-screen\">\n      <main className=\"flex-grow\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(main)/integrations/loading.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nexport default function Loading() {\n  return (\n    <div className=\"page-loading-overlay\">\n      <div className=\"page-loading-spinner\"></div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(main)/integrations/page.tsx",
    "content": "export const dynamic = \"force-dynamic\";\n\nimport React from \"react\";\nimport IntegrationsPage from \"@/features/integrations/components/integrations-page\";\n\nconst Integrations = async () => {\n  return <IntegrationsPage />;\n};\n\nexport default Integrations;\n"
  },
  {
    "path": "app/(main)/knowledge-base/layout.tsx",
    "content": "import { createClient } from \"@/utils/supabase/server\";\nimport { redirect } from \"next/navigation\";\n\nexport default async function Layout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error,\n  } = await supabase.auth.getUser();\n\n  if (error || !user) {\n    redirect(\"/login\");\n  }\n\n  return (\n    <div className=\"relative flex min-h-screen\">\n      <main className=\"flex-grow\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(main)/knowledge-base/loading.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nexport default function Loading() {\n  return (\n    <div className=\"page-loading-overlay\">\n      <div className=\"page-loading-spinner\"></div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(main)/knowledge-base/page.tsx",
    "content": "export const dynamic = \"force-dynamic\";\n\nimport KnowledgeBase from \"@/features/knowledge-base/components/knolwedge-base\";\nimport { fetchDocumentFiles } from \"@/features/knowledge-base/actions/document-actions\";\n\nconst KnowledgeBasePage = async () => {\n  const documents = await fetchDocumentFiles();\n\n  return <KnowledgeBase initialDocuments={documents} />;\n};\n\nexport default KnowledgeBasePage;\n"
  },
  {
    "path": "app/(main)/layout.tsx",
    "content": "import localFont from \"next/font/local\";\nimport \"./styles.css\";\nimport \"maplibre-gl/dist/maplibre-gl.css\";\nimport \"@blocknote/mantine/style.css\";\nimport { createClient } from \"@/utils/supabase/server\";\nimport { redirect } from \"next/navigation\";\nimport { Analytics } from \"@vercel/analytics/next\";\nimport { getUserProfile } from \"./actions/get-user-profile\";\nimport ClientWrapper from \"@/components/client-wrapper\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\n\nconst geistSans = localFont({\n  src: \"./fonts/GeistVF.woff\",\n  variable: \"--font-geist-sans\",\n  weight: \"100 900\",\n});\nconst geistMono = localFont({\n  src: \"./fonts/GeistMonoVF.woff\",\n  variable: \"--font-geist-mono\",\n  weight: \"100 900\",\n});\n\n// Define metadata for the root layout\nexport const metadata = {\n  title: \"Chat2Geo\",\n  description: \"AI-powered geospatial analyticse\",\n};\n\nexport default async function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  const supabase = await createClient();\n  const { data: authResults, error } = await supabase.auth.getUser();\n  if (error || !authResults?.user) {\n    redirect(\"/login\");\n  }\n  const userProfile = await getUserProfile();\n  return (\n    <html lang=\"en\" suppressHydrationWarning>\n      <TooltipProvider>\n        <body\n          className={`${geistSans.variable} ${geistMono.variable} font-sans`}\n        >\n          <ClientWrapper userProfile={userProfile}>{children}</ClientWrapper>\n          <Analytics />\n        </body>\n      </TooltipProvider>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/(main)/loading.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\n\nexport default function Loading() {\n  return (\n    <div className=\"page-loading-overlay\">\n      <div className=\"page-loading-spinner\"></div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(main)/page.tsx",
    "content": "export const dynamic = \"force-dynamic\";\n\nimport MainChatPage from \"@/features/chat/components/chat\";\nimport \"@blocknote/mantine/style.css\";\nimport { generateUUID } from \"@/features/chat/utils/general-utils\";\n\nexport default async function Home() {\n  const chatId = generateUUID();\n\n  return (\n    <div>\n      <MainChatPage chatId={chatId} initialMessages={[]} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/(main)/styles.css",
    "content": "/* -------------------------------------------\n  Tailwind Base Imports\n------------------------------------------- */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* -------------------------------------------\n  Theme Variables & Base Styles\n------------------------------------------- */\n@layer base {\n  :root {\n    /* Light Theme Variables */\n    --background: 0 0% 100%;\n    --foreground: 222.2 47.4% 11.2%;\n\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 47.4% 11.2%;\n\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 47.4% 11.2%;\n\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n\n    --primary-blue: 220 90% 56%;\n    --primary-blue-foreground: 210 40% 98%;\n\n    --primary-green: 145 63% 42%;\n    --primary-green-foreground: 210 40% 98%;\n\n    --secondary: 220 13% 92%;\n    --secondary-foreground: 220 10% 40%;\n\n    --accent: 220 15% 85%;\n    --accent-foreground: 220 20% 12%;\n\n    --destructive: 0 100% 50%;\n    --destructive-foreground: 210 40% 98%;\n\n    --warning: 45 100% 50%;\n    --warning-foreground: 45 100% 15%;\n\n    --info: 200 98% 48%;\n    --info-foreground: 210 40% 98%;\n\n    --ring: 215 20.2% 65.1%;\n    --radius: 0.5rem;\n\n    --shimmer-highlight: rgba(255, 255, 255, 0.6);\n  }\n\n  .dark {\n    /* Dark Theme Variables */\n\n    /* Base */\n    --background: 240 2% 12%; /* #1f1f21 */\n    --foreground: 0 0% 95%; /* Very light neutral */\n\n    /* Cards/Popovers */\n    --card: 220 3% 10%; /* #18191a */\n    --card-foreground: 0 0% 95%;\n    --popover: 220 3% 10%;\n    --popover-foreground: 0 0% 95%;\n\n    /* Muted */\n    --muted: 240 2% 16%;\n    --muted-foreground: 0 0% 60%;\n\n    /* Interactive elements */\n    --accent: 240 2% 18%;\n    --accent-foreground: 0 0% 98%;\n\n    /* Borders & Rings */\n    --border: 240 2% 20%;\n    --input: 240 2% 20%;\n    --ring: 240 2% 20%;\n\n    /* Primary */\n    --primary: 0 0% 98%;\n    --primary-foreground: 240 2% 12%;\n\n    /* Secondary */\n    --secondary: 240 2% 22%;\n    --secondary-foreground: 0 0% 98%;\n\n    /* Semantic Colors */\n    --primary-blue: 220 90% 56%;\n    --primary-blue-foreground: 210 40% 98%;\n\n    --primary-green: 145 63% 42%;\n    --primary-green-foreground: 210 40% 98%;\n\n    --destructive: 0 50% 35%;\n    --destructive-foreground: 0 0% 98%;\n\n    --warning: 45 100% 50%;\n    --warning-foreground: 45 100% 15%;\n\n    --info: 200 98% 48%;\n    --info-foreground: 210 40% 98%;\n\n    --shimmer-highlight: rgba(255, 255, 255, 0.1);\n  }\n\n  /* Universal defaults */\n  * {\n    @apply border-border;\n  }\n\n  body {\n    @apply font-sans antialiased bg-background text-foreground;\n  }\n}\n\n/* -------------------------------------------\n   Keyframes & Animation Classes\n------------------------------------------- */\n@keyframes shimmer {\n  0% {\n    background-position: 200% 0;\n  }\n  100% {\n    background-position: 0% 0;\n  }\n}\n\n.animate-shimmer {\n  display: inline-block;\n  color: transparent;\n  background: linear-gradient(\n    90deg,\n    #1e90ff,\n    #34d399,\n    #a3e635,\n    #ffd700,\n    #ff8c00,\n    #ff69b4,\n    #ff007f,\n    #7928ca,\n    #1e90ff\n  );\n  background-size: 200% 100%;\n  -webkit-background-clip: text;\n  background-clip: text;\n  animation: shimmer 3s linear infinite;\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n    transform: translateY(20px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.fade-in {\n  animation: fadeIn 0.6s ease-in-out;\n}\n\n/* -------------------------------------------\n  MapLibre GL Styles\n------------------------------------------- */\n\n/* Remove default focus ring for MapLibre canvas */\n.maplibregl-canvas:focus {\n  outline: none !important;\n  box-shadow: none !important;\n}\n\n/* Attribution text */\n.maplibregl-ctrl-attrib-inner {\n  font-size: 12px !important;\n  white-space: nowrap;\n  padding: 2px 2px;\n}\n\n/* Popup content for light theme (default) */\n.maplibregl-popup-content {\n  background-color: hsl(var(--background));\n  color: hsl(var(--foreground));\n  border-radius: var(--radius);\n  padding: 8px 10px;\n  box-shadow: 0 2px 6px hsl(var(--foreground) / 0.2);\n}\n\n.maplibregl-popup-tip {\n  border-top-color: hsl(var(--background)) !important;\n}\n\n/* Popup content for dark theme */\n:is([data-theme=\"dark\"], .dark) .maplibregl-popup-content {\n  background-color: hsl(var(--background));\n  color: hsl(var(--foreground));\n}\n\n:is([data-theme=\"dark\"], .dark) .maplibregl-popup-tip {\n  border-top-color: hsl(var(--background)) !important;\n}\n\n/* Scale control (light) */\n.maplibregl-ctrl.maplibregl-ctrl-scale {\n  background-color: hsl(var(--background) / 0.75);\n  border-color: hsl(var(--muted-foreground));\n  color: hsl(var(--foreground));\n}\n\n/* Base for maplibre control buttons */\n.maplibregl-ctrl button {\n  background-color: hsl(var(--background));\n}\n\n/* Control group styling (light) */\n.maplibregl-ctrl-group {\n  background: hsl(var(--background));\n  display: inline-flex !important;\n  flex-direction: row !important;\n}\n\n/* Reset button styles, remove default borders */\n.maplibregl-ctrl-group > button {\n  border: none !important;\n  border-radius: 0 !important;\n}\n\n/* Add divider between adjacent buttons */\n.maplibregl-ctrl-group > button + button {\n  border-left: 2px solid hsl(var(--border)) !important;\n}\n\n/* Round left/right edges of group */\n.maplibregl-ctrl-group > button:first-of-type {\n  border-top-left-radius: var(--radius) !important;\n  border-bottom-left-radius: var(--radius) !important;\n}\n.maplibregl-ctrl-group > button:last-of-type {\n  border-top-right-radius: var(--radius) !important;\n  border-bottom-right-radius: var(--radius) !important;\n}\n\n/* Add subtle box-shadow for group */\n.maplibregl-ctrl-group:not(:empty) {\n  box-shadow: 0 0 0 2px hsl(var(--border) / 0.1);\n}\n\n/* Dark theme overrides for scale control & buttons */\n:is([data-theme=\"dark\"], .dark) .maplibregl-ctrl.maplibregl-ctrl-scale {\n  background-color: hsl(var(--background) / 0.75);\n  border-color: hsl(var(--muted-foreground));\n  color: hsl(var(--foreground));\n}\n:is([data-theme=\"dark\"], .dark) .maplibregl-ctrl button {\n  background-color: hsl(var(--background));\n}\n:is([data-theme=\"dark\"], .dark) .maplibregl-ctrl-group {\n  background: hsl(var(--background));\n}\n:is([data-theme=\"dark\"], .dark) .maplibregl-ctrl-group:not(:empty) {\n  box-shadow: 0 0 0 2px hsl(var(--border) / 0.1);\n}\n:is([data-theme=\"dark\"], .dark) .maplibregl-ctrl button .maplibregl-ctrl-icon {\n  filter: invert(1) brightness(100);\n}\n:is([data-theme=\"dark\"], .dark) .maplibregl-ctrl button:hover {\n  background-color: hsl(var(--muted));\n}\n\n/* Attribution control (light) */\n.maplibregl-ctrl.maplibregl-ctrl-attrib {\n  background-color: hsl(var(--background) / 0.6);\n  border-radius: var(--radius);\n}\n.maplibregl-ctrl-attrib-inner {\n  background-color: transparent;\n  color: hsl(var(--foreground));\n  border-radius: var(--radius);\n}\n\n/* Attribution control (dark) */\n:is([data-theme=\"dark\"], .dark) .maplibregl-ctrl.maplibregl-ctrl-attrib {\n  background-color: hsl(var(--background) / 0.6);\n  color: hsl(var(--foreground));\n  border-radius: var(--radius);\n}\n:is([data-theme=\"dark\"], .dark) .maplibregl-ctrl-attrib-inner {\n  background-color: transparent;\n  color: hsl(var(--foreground));\n  border-radius: var(--radius);\n}\n\n/* -------------------------------------------\n  BN Container Overrides\n------------------------------------------- */\n.bn-container[data-color-scheme=\"light\"] {\n  --bn-colors-editor-background: hsl(var(--background)) !important;\n}\n\n.bn-container[data-color-scheme=\"dark\"] {\n  --bn-colors-editor-background: hsl(var(--background)) !important;\n}\n\n/* -------------------------------------------\n  Misc. Utilities\n------------------------------------------- */\n.custom-scrollbar {\n  scrollbar-width: thin; /* Firefox */\n}\n\n/* Loading overlay */\n.page-loading-overlay {\n  @apply fixed inset-0 z-[9999] flex items-center justify-center bg-transparent;\n}\n\n/* Loading spinner */\n.page-loading-spinner {\n  @apply inline-block h-10 w-10 animate-spin rounded-full border-4 border-blue-400 border-t-transparent;\n}\n"
  },
  {
    "path": "app/actions/rag-actions.ts",
    "content": "\"use server\";\nimport { createClient } from \"@/utils/supabase/server\";\nimport { WebPDFLoader } from \"@langchain/community/document_loaders/web/pdf\";\nimport { SupabaseVectorStore } from \"@langchain/community/vectorstores/supabase\";\nimport { generateEmbeddings } from \"@/features/knowledge-base/lib/generate-embeddings\";\nimport { generateChunks } from \"@/features/knowledge-base/lib/generate-embeddings\";\nimport { cleanString } from \"@/utils/general/general-utils\";\n\nexport async function saveRagDocument(\n  file: any,\n  numberOfPages: number,\n  folderId: string | null\n) {\n  const supabase = await createClient();\n  const { data: authResult, error: userError } = await supabase.auth.getUser();\n  if (userError || !authResult?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  const bucketName = \"documents_bucket\";\n  const filePath = `${authResult.user.id}/${file.name}`;\n\n  const { error: uploadError } = await supabase.storage\n    .from(bucketName)\n    .upload(filePath, file);\n\n  if (uploadError)\n    throw new Error(`Failed to upload file: ${uploadError.message}`);\n\n  const { data: signedUrlData, error: signedUrlError } = await supabase.storage\n    .from(bucketName)\n    .createSignedUrl(filePath, 60 * 60 * 1); // Signed URL valid for 1 hour\n\n  if (signedUrlError)\n    throw new Error(`Failed to create signed URL: ${signedUrlError.message}`);\n\n  const fileSignedURL = signedUrlData?.signedUrl;\n\n  if (!fileSignedURL) {\n    throw new Error(\"Failed to retrieve the file's signed URL.\");\n  }\n\n  const { data: fileData, error: fileError } = await supabase\n    .from(\"document_files\")\n    .insert({\n      name: file.name,\n      owner: authResult.user.id,\n      number_of_pages: numberOfPages,\n      file_path: fileSignedURL,\n      folder_id: folderId ?? null,\n    })\n    .select()\n    .single<DocumentFile>();\n\n  if (fileError) throw fileError;\n\n  const loader = new WebPDFLoader(file);\n  const output = await loader.load();\n\n  const docs = output.map((d) => ({\n    ...d,\n    metadata: {\n      ...d.metadata,\n      fileName: file.name,\n      fileId: fileData.id,\n      ownerId: authResult.user.id,\n    },\n  }));\n\n  const splittedDocs = await generateChunks.splitDocuments(docs);\n\n  const contents = splittedDocs.map((doc) => doc.pageContent);\n  const embeddings = await generateEmbeddings.embedDocuments(contents);\n\n  const sanitizedEmbeddingsData = splittedDocs.map((doc, index) => ({\n    content: cleanString(doc.pageContent),\n    metadata: doc.metadata,\n    embedding: embeddings[index],\n    file_id: fileData.id,\n  }));\n\n  const { error: embeddingsError } = await supabase\n    .from(\"embeddings\")\n    .insert(sanitizedEmbeddingsData);\n\n  if (embeddingsError) {\n    console.error(\"Insert Error Details:\", embeddingsError);\n    throw new Error(`Failed to insert embeddings: ${embeddingsError.message}`);\n  }\n\n  return fileData;\n}\n\nexport async function answerQuery(query: string) {\n  const supabase = await createClient();\n  const { data: authResult, error: userError } = await supabase.auth.getUser();\n  if (userError || !authResult?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  const userId = authResult.user.id;\n\n  const vectorStore = await SupabaseVectorStore.fromExistingIndex(\n    generateEmbeddings,\n    {\n      client: supabase,\n      tableName: \"embeddings\",\n      queryName: \"search_documents_by_similarity\",\n    }\n  );\n\n  const retriever = vectorStore.asRetriever({\n    k: 5,\n    filter: { owner: userId },\n  });\n\n  let topMatches = await retriever._getRelevantDocuments(query);\n\n  topMatches = topMatches.filter((match) => match.metadata.similarity > 0.4);\n\n  const matchesByPage = topMatches.reduce((acc, doc) => {\n    const pageNumber = doc.metadata.loc.pageNumber;\n    const currentBest = acc[pageNumber];\n    if (\n      !currentBest ||\n      doc.metadata.similarity > currentBest.metadata.similarity\n    ) {\n      acc[pageNumber] = doc;\n    }\n    return acc;\n  }, {} as Record<number, (typeof topMatches)[number]>);\n\n  return Object.values(matchesByPage);\n}\n"
  },
  {
    "path": "components/changelog-modal.tsx",
    "content": "\"use client\";\nimport React, { useState, useEffect } from \"react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport ReactMarkdown from \"react-markdown\";\nimport { changelog } from \"@/lib/changelog\";\n\nexport default function ChangelogModal() {\n  const [hasMounted, setHasMounted] = useState(false);\n  const [open, setOpen] = useState(false);\n\n  useEffect(() => {\n    setHasMounted(true);\n    if (changelog.length > 0) {\n      const newest = changelog[0];\n      const storedVersion = localStorage.getItem(\"changelog_last_seen_version\");\n      if (storedVersion !== newest.version) {\n        setOpen(true);\n      }\n    }\n  }, []);\n\n  if (!hasMounted) return null;\n\n  const latestEntry = changelog[0];\n  if (!latestEntry) return null;\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(value) => {\n        setOpen(value);\n        if (!value) {\n          localStorage.setItem(\n            \"changelog_last_seen_version\",\n            latestEntry.version\n          );\n        }\n      }}\n    >\n      <DialogContent className=\"max-w-xl\">\n        <DialogHeader>\n          <DialogTitle>\n            Chat2Geo Updated (Version {latestEntry.version}) -{\" \"}\n            {latestEntry.date}\n          </DialogTitle>\n        </DialogHeader>\n\n        <ReactMarkdown className=\"prose dark:prose-invert max-w-none\">\n          {latestEntry.content}\n        </ReactMarkdown>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "components/client-hydrator.tsx",
    "content": "\"use client\";\nimport { useEffect } from \"react\";\nimport { useUserStore } from \"@/stores/use-user-profile-store\";\n\ninterface ClientHydratorProps {\n  userProfile: {\n    email: string;\n    name: string;\n    role: string;\n    organization: string;\n    licenseStart: string;\n    licenseEnd: string;\n  } | null;\n}\n\nexport default function ClientHydrator({ userProfile }: ClientHydratorProps) {\n  const { setUserData } = useUserStore();\n\n  useEffect(() => {\n    if (userProfile) {\n      setUserData(\n        userProfile.name,\n        userProfile.email,\n        userProfile.role,\n        userProfile.organization,\n        userProfile.licenseStart,\n        userProfile.licenseEnd\n      );\n    }\n  }, [userProfile, setUserData]);\n\n  return null;\n}\n"
  },
  {
    "path": "components/client-wrapper.tsx",
    "content": "// components/client-wrapper.tsx\n\"use client\";\n\nimport React from \"react\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { Toaster } from \"react-hot-toast\";\nimport ToastMessage from \"@/features/ui/toast-message\";\nimport MainSidebar from \"@/components/main-sidebar/main-sidebar\";\nimport ClientHydrator from \"@/components/client-hydrator\";\nimport ChangelogModal from \"@/components/changelog-modal\";\n\nexport default function ClientWrapper({\n  userProfile,\n  children,\n}: {\n  userProfile: any;\n  children: React.ReactNode;\n}) {\n  return (\n    <ThemeProvider\n      attribute=\"class\"\n      defaultTheme=\"light\"\n      enableSystem\n      disableTransitionOnChange\n    >\n      <ClientHydrator userProfile={userProfile} />\n      <ChangelogModal />\n      <Toaster />\n      <ToastMessage />\n\n      <MainSidebar />\n      {children}\n    </ThemeProvider>\n  );\n}\n"
  },
  {
    "path": "components/document-viewer.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { useDocumentViewer } from \"@/hooks/docs-hooks/use-document-viewer\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface DocumentViewerProps {\n  documentName: string;\n  pageNumber: number;\n  onClose: () => void;\n}\n\nexport function DocumentViewer({\n  documentName,\n  pageNumber,\n  onClose,\n}: DocumentViewerProps) {\n  const { pdfUrl, error, isLoading } = useDocumentViewer(documentName);\n\n  // Render any error in a Dialog\n  if (error) {\n    return (\n      <Dialog open={true} onOpenChange={() => onClose()}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle className=\"text-red-600\">Error</DialogTitle>\n            <DialogDescription>{error}</DialogDescription>\n          </DialogHeader>\n          <DialogFooter>\n            <Button onClick={onClose}>Close</Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    );\n  }\n\n  // Otherwise, show the PDF in a Dialog\n  return (\n    <Dialog open={!!pdfUrl} onOpenChange={() => onClose()}>\n      <DialogContent className=\"max-w-5xl w-full h-[95vh] flex flex-col\">\n        <DialogHeader>\n          <DialogTitle>\n            {documentName} (Page {pageNumber})\n          </DialogTitle>\n        </DialogHeader>\n\n        <div className=\"flex-1 overflow-hidden\">\n          {/* If pdfUrl is present, show an iframe with #page=pageNumber */}\n          {pdfUrl && (\n            <iframe\n              src={`${pdfUrl}#page=${pageNumber}`}\n              width=\"100%\"\n              height=\"100%\"\n            />\n          )}\n        </div>\n\n        <DialogFooter></DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "components/feedback.tsx",
    "content": "\"use client\";\n\nimport React, { useState } from \"react\";\nimport { Rnd } from \"react-rnd\";\nimport { Card } from \"@/components/ui/card\";\nimport {\n  CardHeader,\n  CardTitle,\n  CardContent,\n  CardFooter,\n} from \"@/components/ui/card\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\n// Lucide icons\nimport { X, Send, Loader2 } from \"lucide-react\";\n\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\n\ninterface FeedbackFloatingProps {\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nexport function FeedbackFloating({ isOpen, onClose }: FeedbackFloatingProps) {\n  const [feedback, setFeedback] = useState(\"\");\n\n  // Track submit/loading state\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const setToastMessage = useToastMessageStore(\n    (state) => state.setToastMessage\n  );\n\n  // Draggable + Resizable state for the popup\n  const [panelPosition, setPanelPosition] = useState({ x: 300, y: 120 });\n  const [panelSize, setPanelSize] = useState({ width: 400, height: 300 });\n\n  if (!isOpen) return null;\n\n  async function handleSubmit() {\n    setIsSubmitting(true);\n    try {\n      const res = await fetch(\"/api/sendfeedback\", {\n        method: \"POST\",\n        headers: { \"content-type\": \"application/json\" },\n        body: JSON.stringify({ message: feedback }),\n      });\n\n      if (res.ok) {\n        setToastMessage(\"Thanks for your feedback!\", \"success\");\n        setFeedback(\"\");\n        onClose();\n      } else {\n        setToastMessage(\"Something went wrong. Please try again!\", \"error\");\n      }\n    } catch (err) {\n      console.error(err);\n      setToastMessage(\"Error sending feedback.\", \"error\");\n    } finally {\n      setIsSubmitting(false);\n    }\n  }\n\n  return (\n    <Rnd\n      position={panelPosition}\n      size={{ width: panelSize.width, height: panelSize.height }}\n      onDragStop={(e, d) => setPanelPosition({ x: d.x, y: d.y })}\n      onResizeStop={(e, dir, ref, delta, pos) => {\n        setPanelSize({\n          width: parseInt(ref.style.width, 10),\n          height: parseInt(ref.style.height, 10),\n        });\n        setPanelPosition({ x: pos.x, y: pos.y });\n      }}\n      minWidth={320}\n      minHeight={200}\n      bounds=\"parent\"\n      dragHandleClassName=\"drag-handle\"\n      className=\"fixed z-[9999]\"\n    >\n      <Card className=\"w-full h-full flex flex-col\">\n        <CardHeader\n          className=\"\n            drag-handle\n            border-b border-stone-300 dark:border-stone-600\n            px-4 py-2\n          \"\n        >\n          <div className=\"flex w-full items-center justify-between cursor-move\">\n            <CardTitle>Feedback</CardTitle>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              onClick={onClose}\n              className=\"hover:bg-transparent text-muted-foreground hover:text-foreground\"\n            >\n              <X size={16} />\n            </Button>\n          </div>\n        </CardHeader>\n\n        <CardContent className=\"flex-1 p-4 overflow-hidden flex flex-col min-h-0\">\n          <Textarea\n            placeholder=\"Please let use know your requests or feedback, or any issues/bugs you came across while using the app.\"\n            value={feedback}\n            onChange={(e) => setFeedback(e.target.value)}\n            className=\"\n              flex-1 h-full w-full resize-none\n              border-stone-300 dark:border-stone-600\n            \"\n          />\n        </CardContent>\n\n        <CardFooter className=\"border-t border-stone-300 dark:border-stone-600 p-3 flex justify-end\">\n          <Button\n            onClick={handleSubmit}\n            disabled={isSubmitting}\n            className=\"flex items-center gap-2\"\n          >\n            {isSubmitting ? (\n              // Spinner icon\n              <Loader2 size={16} className=\"animate-spin\" />\n            ) : (\n              // Normal send icon\n              <Send size={16} />\n            )}\n            {isSubmitting ? \"Sending...\" : \"Submit\"}\n          </Button>\n        </CardFooter>\n      </Card>\n    </Rnd>\n  );\n}\n"
  },
  {
    "path": "components/loading-widgets/loading-for-widget.tsx",
    "content": "import React from \"react\";\n\ninterface LoadingWidgetForContainerProps {\n  loadingSize: \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n  color?:\n    | \"white\"\n    | \"blue\"\n    | \"red\"\n    | \"green\"\n    | \"yellow\"\n    | \"gray\"\n    | \"purple\"\n    | \"pink\";\n}\n\nconst LoadingWidgetForContainer: React.FC<LoadingWidgetForContainerProps> = ({\n  loadingSize = \"sm\",\n  color = \"white\",\n}) => {\n  // Define size mappings\n  const sizeClasses = {\n    xs: \"w-4 h-4 border-2\",\n    sm: \"w-6 h-6 border-2\",\n    md: \"w-8 h-8 border-4\",\n    lg: \"w-10 h-10 border-4\",\n    xl: \"w-12 h-12 border-4\",\n  };\n\n  // Define color mappings\n  const colorClasses = {\n    white: \"border-white\",\n    blue: \"border-blue-500\",\n    red: \"border-red-500\",\n    green: \"border-green-500\",\n    yellow: \"border-yellow-500\",\n    gray: \"border-gray-500\",\n    purple: \"border-purple-500\",\n    pink: \"border-pink-500\",\n  };\n\n  return (\n    <div\n      className={`inline-block rounded-full border-t-transparent animate-spin ${sizeClasses[loadingSize]} ${colorClasses[color]}`}\n    ></div>\n  );\n};\n\nexport default LoadingWidgetForContainer;\n"
  },
  {
    "path": "components/loading-widgets/loading-primary.tsx",
    "content": "import React from \"react\";\nimport useLoadingStore from \"@/stores/use-loading-store\";\n\nconst LoadingWidget = () => {\n  const { isLoading, isLocal } = useLoadingStore();\n\n  if (!isLoading) return null;\n\n  return (\n    <>\n      {/* Background overlay */}\n      <div className=\"absolute inset-0 bg-muted opacity-70 z-[1000] h-full w-full\"></div>\n      {/* Spinner */}\n      <div className=\"absolute inset-0 flex items-center justify-center z-[2000] h-full w-full\">\n        <div className=\"inline-block w-8 h-8 border-4 border-blue-400 border-t-transparent rounded-full animate-spin\"></div>\n      </div>\n    </>\n  );\n};\n\nexport default LoadingWidget;\n"
  },
  {
    "path": "components/main-sidebar/app-setttings.tsx",
    "content": "import React from \"react\";\nimport { IconSettings } from \"@tabler/icons-react\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { ThemeModeToggle } from \"@/components/ui/theme-mode-toggle\";\nimport { Label } from \"@/components/ui/label\";\nimport { Tooltip } from \"react-tooltip\";\n\nconst appVersion = process.env.NEXT_PUBLIC_APP_VERSION;\n\nconst AppSettings = () => {\n  const isSidebarCollapsed = useButtonsStore(\n    (state) => state.isSidebarCollapsed\n  );\n\n  return (\n    <section className=\"z-[5000]\">\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <div className=\"px-4 py-2 pb-6 text-accent-foreground cursor-pointer text-sm font-normal\">\n            <div\n              className={`flex items-center px-3 py-2 gap-4 w-full rounded-xl text-gray-100 hover:bg-muted dark:hover:bg-muted-foreground/20 hover:text-foreground ${\n                isSidebarCollapsed ? \"justify-center\" : \"justify-start\"\n              }`}\n              data-tooltip-content=\"Open settings\"\n              data-tooltip-id=\"settings\"\n            >\n              <button className=\"\">\n                <IconSettings stroke={1.5} className=\"h-7 w-7 flex-shrink-0\" />\n              </button>\n              {!isSidebarCollapsed && <span>Settings</span>}\n            </div>\n          </div>\n        </DropdownMenuTrigger>\n\n        <DropdownMenuContent\n          side=\"left\"\n          align=\"end\"\n          className=\"w-64 dark:text-accent-foreground bg-background dark:bg-accent\"\n        >\n          <DropdownMenuLabel>Settings</DropdownMenuLabel>\n          <DropdownMenuSeparator />\n\n          <DropdownMenuGroup>\n            <div className=\"p-2\">\n              <h3 className=\"text-sm font-medium mb-1\">Appearance</h3>\n              <p className=\"text-xs text-muted-foreground mb-2\">\n                Customize how the application looks\n              </p>\n              <div className=\"flex items-center justify-between\">\n                <Label htmlFor=\"theme-toggle\" className=\"text-sm\">\n                  Theme\n                </Label>\n                <ThemeModeToggle />\n              </div>\n            </div>\n          </DropdownMenuGroup>\n\n          <DropdownMenuSeparator />\n\n          <div className=\"p-2 pt-1 text-xs text-muted-foreground\">\n            <p className=\"mb-1\">\n              Version: <strong>{appVersion}</strong>\n            </p>\n          </div>\n        </DropdownMenuContent>\n      </DropdownMenu>\n      <Tooltip\n        id=\"settings\"\n        place=\"right\"\n        style={{\n          backgroundColor: \"white\",\n          color: \"black\",\n          position: \"fixed\",\n          zIndex: 9999,\n          padding: \"8px\",\n          borderRadius: \"4px\",\n          boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.2)\",\n          fontWeight: \"500\",\n        }}\n        hidden={!isSidebarCollapsed}\n      />\n    </section>\n  );\n};\n\nexport default AppSettings;\n"
  },
  {
    "path": "components/main-sidebar/main-sidebar.tsx",
    "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport { Tooltip } from \"react-tooltip\";\nimport useSidebarButtonStores, {\n  Pages,\n} from \"@/stores/use-sidebar-button-stores\";\nimport {\n  IconEdit,\n  IconBook,\n  IconDatabaseImport,\n  IconCirclesRelation,\n  IconCircleChevronLeft,\n  IconCircleChevronRight,\n  IconMessage,\n} from \"@tabler/icons-react\";\n\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport { useRouter } from \"next/navigation\";\nimport { Separator } from \"@/components/ui/separator\";\nimport AppSettings from \"./app-setttings\";\nimport UserProfile from \"@/features/user-profile/components/user-profile-modal\";\nimport { resetChatStores } from \"@/utils/reset-chat-stores\";\nimport { FeedbackFloating } from \"../feedback\";\n\nconst MainSidebar = () => {\n  const setPageToOpen = useSidebarButtonStores((state) => state.setPageToOpen);\n  const pageToOpen = useSidebarButtonStores((state) => state.pageToOpen);\n\n  const isSidebarCollapsed = useButtonsStore(\n    (state) => state.isSidebarCollapsed\n  );\n  const toggleSidebarCollapse = useButtonsStore(\n    (state) => state.toggleSidebarCollapse\n  );\n\n  const [isFeedbackOpen, setIsFeedbackOpen] = useState(false);\n\n  const router = useRouter();\n\n  function handleOpenPage(page: Pages) {\n    setPageToOpen(page);\n    if (page === Pages.NewChat) {\n      resetChatStores();\n    }\n    router.push(`/${page}`);\n  }\n\n  // Helper to build class names conditionally\n  function getButtonClasses(page: Pages, extraClasses?: string) {\n    const base =\n      `flex items-center gap-4 px-3 py-2 rounded-xl w-full cursor-pointer ${\n        isSidebarCollapsed ? \"justify-center\" : \"justify-start\"\n      } ${extraClasses || \"\"}`.trim();\n\n    // If this button is for the active page, use \"active\" styles only:\n    if (pageToOpen === page && page !== Pages.NewChat) {\n      return `${base} bg-stone-300 text-gray-800`;\n    }\n\n    return `${base} text-gray-100 hover:bg-muted dark:hover:bg-muted-foreground/20 hover:text-foreground`;\n  }\n\n  return (\n    <>\n      <div\n        className={`fixed top-0 left-0 h-screen bg-[#6C7782] z-[2000] dark:bg-accent ${\n          isSidebarCollapsed ? \"w-20\" : \"w-64\"\n        } flex flex-col shadow-lg transition-all duration-300 ease-in-out overflow-hidden`}\n      >\n        {/* Navigation Links */}\n        <nav className=\"flex-grow px-4 py-6 space-y-5 pt-14 text-sm font-normal\">\n          <button\n            className={getButtonClasses(Pages.NewChat, \"mb-10\")}\n            data-tooltip-content=\"Start a new session\"\n            data-tooltip-id=\"new-session\"\n            onClick={() => handleOpenPage(Pages.NewChat)}\n          >\n            <IconEdit stroke={1.5} className=\"h-7 w-7 flex-shrink-0\" />\n            {!isSidebarCollapsed && (\n              <span className=\"whitespace-nowrap\">New Session</span>\n            )}\n          </button>\n\n          <button\n            className={getButtonClasses(Pages.ChatHistory)}\n            data-tooltip-content=\"View session history\"\n            data-tooltip-id=\"session-history\"\n            onClick={() => handleOpenPage(Pages.ChatHistory)}\n          >\n            <IconBook stroke={1.5} className=\"h-7 w-7 flex-shrink-0\" />\n            {!isSidebarCollapsed && (\n              <span className=\"whitespace-nowrap\">Session History</span>\n            )}\n          </button>\n          <button\n            className={getButtonClasses(Pages.KnowledgeBase)}\n            data-tooltip-content=\"Manage knowledge base documents\"\n            data-tooltip-id=\"knowledge-base\"\n            onClick={() => handleOpenPage(Pages.KnowledgeBase)}\n          >\n            <IconDatabaseImport\n              stroke={1.5}\n              className=\"h-7 w-7 flex-shrink-0\"\n            />\n            {!isSidebarCollapsed && (\n              <span className=\"whitespace-nowrap\">Knowledge Base</span>\n            )}\n          </button>\n\n          <button\n            className={getButtonClasses(Pages.Integrations)}\n            data-tooltip-content=\"Manage integrations\"\n            data-tooltip-id=\"integrations\"\n            onClick={() => handleOpenPage(Pages.Integrations)}\n          >\n            <IconCirclesRelation\n              stroke={1.5}\n              className=\"h-7 w-7 flex-shrink-0\"\n            />\n            {!isSidebarCollapsed && (\n              <span className=\"whitespace-nowrap\">Integrations</span>\n            )}\n          </button>\n\n          {/* Feedback Button (placed inside nav, using same style) */}\n          <button\n            onClick={() => setIsFeedbackOpen(true)}\n            className={getButtonClasses(Pages.NewChat, \"mb-3 translate-y-10\")}\n            data-tooltip-content=\"Send feedback\"\n            data-tooltip-id=\"feedback\"\n          >\n            <IconMessage stroke={1.5} className=\"h-7 w-7 flex-shrink-0\" />\n            {!isSidebarCollapsed && <span>Feedback</span>}\n          </button>\n        </nav>\n\n        {/* Footer */}\n        <UserProfile />\n        <AppSettings />\n\n        <Separator className=\"my-0 bg-gray-200 dark:bg-gray-200\" />\n\n        <div\n          className=\"px-4 py-2 pt-1 cursor-pointer text-sm font-normal\"\n          onClick={toggleSidebarCollapse}\n        >\n          <div\n            className={`flex items-center text-gray-100 px-3 py-2 gap-4 w-full rounded-xl hover:bg-muted dark:hover:bg-muted-foreground/20 hover:text-foreground ${\n              isSidebarCollapsed ? \"justify-center\" : \"justify-start\"\n            }`}\n            data-tooltip-content=\"Toggle sidebar\"\n            data-tooltip-id=\"toggle-sidebar\"\n          >\n            <div>\n              {isSidebarCollapsed ? (\n                <IconCircleChevronRight\n                  stroke={1.5}\n                  className=\"h-7 w-7 flex-shrink-0\"\n                />\n              ) : (\n                <IconCircleChevronLeft\n                  stroke={1.5}\n                  className=\"h-7 w-7 flex-shrink-0\"\n                />\n              )}\n            </div>\n            {!isSidebarCollapsed && <span>Collapse</span>}\n          </div>\n        </div>\n\n        {/* Tooltips */}\n        <Tooltip\n          id=\"new-session\"\n          place=\"right\"\n          style={{\n            backgroundColor: \"white\",\n            color: \"black\",\n            position: \"fixed\",\n            zIndex: 9999,\n            padding: \"8px\",\n            borderRadius: \"4px\",\n            boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.2)\",\n            fontWeight: \"500\",\n          }}\n          hidden={!isSidebarCollapsed}\n        />\n        <Tooltip\n          id=\"session-history\"\n          place=\"right\"\n          style={{\n            backgroundColor: \"white\",\n            color: \"black\",\n            position: \"fixed\",\n            zIndex: 9999,\n            padding: \"8px\",\n            borderRadius: \"4px\",\n            boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.2)\",\n            fontWeight: \"500\",\n          }}\n          hidden={!isSidebarCollapsed}\n        />\n        <Tooltip\n          id=\"build-center\"\n          place=\"right\"\n          style={{\n            backgroundColor: \"white\",\n            color: \"black\",\n            position: \"fixed\",\n            zIndex: 9999,\n            padding: \"8px\",\n            borderRadius: \"4px\",\n            boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.2)\",\n            fontWeight: \"500\",\n          }}\n          hidden={!isSidebarCollapsed}\n        />\n        <Tooltip\n          id=\"knowledge-base\"\n          place=\"right\"\n          style={{\n            backgroundColor: \"white\",\n            color: \"black\",\n            position: \"fixed\",\n            zIndex: 9999,\n            padding: \"8px\",\n            borderRadius: \"4px\",\n            boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.2)\",\n            fontWeight: \"500\",\n          }}\n          hidden={!isSidebarCollapsed}\n        />\n        <Tooltip\n          id=\"integrations\"\n          place=\"right\"\n          style={{\n            backgroundColor: \"white\",\n            color: \"black\",\n            position: \"fixed\",\n            zIndex: 9999,\n            padding: \"8px\",\n            borderRadius: \"4px\",\n            boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.2)\",\n            fontWeight: \"500\",\n          }}\n          hidden={!isSidebarCollapsed}\n        />\n        <Tooltip\n          id=\"feedback\"\n          place=\"right\"\n          style={{\n            backgroundColor: \"white\",\n            color: \"black\",\n            position: \"fixed\",\n            zIndex: 9999,\n            padding: \"8px\",\n            borderRadius: \"4px\",\n            boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.2)\",\n            fontWeight: \"500\",\n          }}\n          hidden={!isSidebarCollapsed}\n        />\n        <Tooltip\n          id=\"toggle-sidebar\"\n          place=\"right\"\n          style={{\n            backgroundColor: \"white\",\n            color: \"black\",\n            position: \"fixed\",\n            zIndex: 9999,\n            padding: \"8px\",\n            borderRadius: \"4px\",\n            boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.2)\",\n            fontWeight: \"500\",\n          }}\n          hidden={!isSidebarCollapsed}\n        />\n      </div>\n      {/* Feedback Floating Panel */}\n      <FeedbackFloating\n        isOpen={isFeedbackOpen}\n        onClose={() => setIsFeedbackOpen(false)}\n      />\n    </>\n  );\n};\n\nexport default MainSidebar;\n"
  },
  {
    "path": "components/notices/privacy-policy.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface PrivacyPolicyModalProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nexport default function PrivacyPolicy({\n  open,\n  onOpenChange,\n}: PrivacyPolicyModalProps) {\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-h-[80vh] overflow-y-auto\">\n        <DialogHeader>\n          <DialogTitle>Privacy Policy</DialogTitle>\n        </DialogHeader>\n\n        <div className=\"space-y-4 text-sm leading-relaxed\">\n          <p>\n            <strong>Effective Date:</strong> January 06, 2025\n          </p>\n          <p>\n            Your privacy is important to us. This Privacy Policy explains how we\n            collect, use, share, and protect your personal information when you\n            use the Chat2Geo platform (“the Service”).\n          </p>\n\n          <h2 className=\"font-semibold\">1. Information We Collect</h2>\n          <p>\n            <strong>1.1 Personal Information</strong>\n            <br />\n            We may collect personal information such as your name, email\n            address, and any additional information you provide when you create\n            an account or use the Service.\n            <br />\n            <strong>Note:</strong> We do not record or collect IP addresses.\n          </p>\n          <p>\n            <strong>1.2 Uploaded Content</strong>\n            <br />\n            We store the content you upload to the Service (e.g., text data,\n            vector files, or documents) to provide you with geospatial analyses\n            and related features. This includes data processed in your chats and\n            any documents you integrate with the Service.\n          </p>\n          <p>\n            <strong>1.3 Interaction Data</strong>\n            <br />\n            We may collect basic information about how you navigate or interact\n            with the Service (e.g., feature usage, timestamps of actions) solely\n            for improving user experience and maintaining platform stability. We\n            do not collect IP addresses or other network identifiers.\n          </p>\n\n          <h2 className=\"font-semibold\">2. How We Use Your Information</h2>\n          <p>\n            <strong>2.1 Providing the Service</strong>\n            <br />\n            We use your information exclusively to operate and maintain the\n            Service, including running analyses, generating reports, and\n            displaying results for your use. We do not share or sell your data\n            to any third party for their own use.\n          </p>\n          <p>\n            <strong>2.2 Improvement and Development</strong>\n            <br />\n            We may use aggregated or anonymized information about overall\n            feature usage for research and development to enhance and refine our\n            services. This data will not identify you or your specific User\n            Data.\n          </p>\n          <p>\n            <strong>2.3 Communication</strong>\n            <br />\n            We may use your contact information to send you administrative or\n            technical notices, updates, and other information directly relevant\n            to your use of the Service.\n          </p>\n\n          <h2 className=\"font-semibold\">3. Sharing and Disclosure</h2>\n          <p>\n            <strong>3.1 Limited Disclosure to Service Providers</strong>\n            <br />\n            We may share minimal information with trusted service providers who\n            help us operate and improve the Service. Such providers are bound by\n            confidentiality and are prohibited from using the information for\n            any purpose other than providing services to us.\n          </p>\n          <p>\n            <strong>3.2 Legal Requirements</strong>\n            <br />\n            We may disclose your information if required by law or in response\n            to valid legal processes, such as a subpoena or court order.\n          </p>\n          <p>\n            <strong>3.3 Business Transfers</strong>\n            <br />\n            In the event of a merger, acquisition, or sale of assets, your\n            information may be transferred as part of that transaction. We will\n            notify you if any such transfer occurs.\n          </p>\n\n          <h2 className=\"font-semibold\">4. Data Retention and Deletion</h2>\n          <p>\n            <strong>4.1 Storage Period</strong>\n            <br />\n            We retain your data as long as you have an active account or as\n            needed to provide you with the Service. For beta testers, data and\n            history may be removed upon completion of the beta period, as stated\n            in our Terms of Service.\n          </p>\n          <p>\n            <strong>4.2 Deletion Requests</strong>\n            <br />\n            You can delete your data at any time by following the instructions\n            within the Service or by contacting us directly. Once deleted, your\n            data may not be recoverable.\n          </p>\n\n          <h2 className=\"font-semibold\">5. Security Measures</h2>\n          <p>\n            We take reasonable measures to protect your information from\n            unauthorized access, alteration, disclosure, or destruction.\n            However, no method of data transmission or storage is 100% secure,\n            and we cannot guarantee absolute security.\n          </p>\n\n          <h2 className=\"font-semibold\">6. Children’s Privacy</h2>\n          <p>\n            Because this Service provides specialized geospatial analysis tools\n            intended for professional or academic use, it is not marketed toward\n            or intended for use by individuals under the age of 18. We do not\n            knowingly collect personal information from minors. If you are a\n            parent or guardian and believe we may have inadvertently collected\n            information from a minor, please contact us immediately.\n          </p>\n\n          <h2 className=\"font-semibold\">7. Changes to This Privacy Policy</h2>\n          <p>\n            We may update this Privacy Policy from time to time to reflect\n            changes in our practices. We will notify you by updating the\n            “Effective Date” at the top of this page. Your continued use of the\n            Service after any changes indicate your acceptance of the new\n            Privacy Policy.\n          </p>\n          <p className=\"text-xs mt-4\">Last Updated: January 06, 2025</p>\n        </div>\n\n        <DialogFooter></DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "components/notices/terms-of-services.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface TermsOfServiceModalProps {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}\n\nexport default function TermsOfService({\n  open,\n  onOpenChange,\n}: TermsOfServiceModalProps) {\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className=\"max-h-[80vh] overflow-y-auto\">\n        <DialogHeader>\n          <DialogTitle>Terms of Service</DialogTitle>\n        </DialogHeader>\n\n        <div className=\"space-y-4 text-sm leading-relaxed\">\n          <p>\n            <strong>Effective Date:</strong> January 06, 2025\n          </p>\n          <p>\n            Welcome to the Chat2Geo platform (“the Service”). By accessing or\n            using the Service, you agree to be bound by these Terms of Service\n            (“Terms”). If you do not agree with any part of these Terms, you\n            must not use the Service.\n          </p>\n\n          <h2 className=\"font-semibold\">1. Beta Testing Program</h2>\n          <p>\n            <strong>1.1 Limited Access</strong>\n            <br />\n            Access to the beta version of the Service is granted by selection\n            only. Each license is personal to the individual user and is\n            non-transferable.\n          </p>\n          <p>\n            <strong>1.2 Beta Features</strong>\n            <br />\n            Because this is a beta version, certain features may be in\n            development or subject to change without notice. The Service may not\n            operate as intended, and you may encounter bugs, errors, or other\n            issues.\n          </p>\n          <p>\n            <strong>1.3 Feedback</strong>\n            <br />\n            We welcome feedback about your experience with the Service. You\n            grant us a non-exclusive, perpetual, irrevocable, royalty-free\n            license to use, modify, and incorporate any feedback you provide\n            into our products or services.\n          </p>\n\n          <h2 className=\"font-semibold\">2. User Accounts</h2>\n          <p>\n            <strong>2.1 Account Creation</strong>\n            <br />\n            To use certain features of the Service, you may be required to\n            create an account. You agree to provide accurate, current, and\n            complete information during the registration process and to update\n            such information to keep it accurate and complete.\n          </p>\n          <p>\n            <strong>2.2 Account Security</strong>\n            <br />\n            You are responsible for maintaining the confidentiality of your\n            account credentials and for all activities that occur under your\n            account. You must immediately notify us of any unauthorized use of\n            your account or any other breach of security.\n          </p>\n\n          <h2 className=\"font-semibold\">3. User Content and Data</h2>\n          <p>\n            <strong>3.1 Data Storage</strong>\n            <br />\n            Your chats, analyses, and documents uploaded to the Service (“User\n            Data”) are stored in our systems so that you can review or reload\n            them at a later time.\n          </p>\n          <p>\n            <strong>3.2 Ownership of User Data</strong>\n            <br />\n            You retain all ownership rights to content you upload. By uploading\n            content to the Service, you grant us a limited, non-exclusive right\n            to store, reproduce, and process your content solely for the purpose\n            of providing the Service to you.\n          </p>\n          <p>\n            <strong>3.3 Exclusive Use</strong>\n            <br />\n            Your session history and any documents you upload are only used for\n            your own purposes, namely to facilitate and enhance the geospatial\n            analyses you conduct. We do not share or use your content for any\n            other purpose.\n          </p>\n\n          <h2 className=\"font-semibold\">4. License and Usage</h2>\n          <p>\n            <strong>4.1 License Grant</strong>\n            <br />\n            Subject to these Terms, we grant you a limited, non-transferable,\n            non-exclusive, revocable license to use the Service for lawful\n            purposes.\n          </p>\n          <p>\n            <strong>4.2 Prohibited Conduct</strong>\n            <br />\n            You agree not to use the Service to:\n          </p>\n          <ul className=\"list-disc list-inside ml-4\">\n            <li>\n              Violate any local, state, national, or international law or\n              regulation.\n            </li>\n            <li>\n              Infringe or misappropriate the intellectual property rights of any\n              third party.\n            </li>\n            <li>\n              Upload harmful or disruptive materials, such as malware or\n              viruses.\n            </li>\n            <li>\n              Perform analyses or operations you are not authorized to execute.\n            </li>\n          </ul>\n\n          <h2 className=\"font-semibold\">5. Intellectual Property</h2>\n          <p>\n            All intellectual property rights in the Service, including any\n            trademarks, logos, designs, or underlying technology, are owned or\n            licensed by us. Nothing in these Terms grants you any right, title,\n            or interest in our intellectual property except as expressly set\n            forth herein.\n          </p>\n\n          <h2 className=\"font-semibold\">6. Disclaimers</h2>\n          <p>\n            <strong>6.1 Beta Disclaimer</strong>\n            <br />\n            The Service is provided on an “as is” and “as available” basis. As\n            this is a beta version, no warranties or guarantees of performance,\n            reliability, or availability are provided.\n          </p>\n          <p>\n            <strong>6.2 No Warranty</strong>\n            <br />\n            We disclaim any and all warranties, express or implied, including\n            but not limited to merchantability, fitness for a particular\n            purpose, and non-infringement.\n          </p>\n\n          <h2 className=\"font-semibold\">7. Limitation of Liability</h2>\n          <p>\n            To the maximum extent permitted by law, we shall not be liable for\n            any indirect, incidental, special, consequential, or exemplary\n            damages arising out of or in connection with the use or inability to\n            use the Service, even if we have been advised of the possibility of\n            such damages.\n          </p>\n\n          <h2 className=\"font-semibold\">8. Termination and Data Removal</h2>\n          <p>\n            <strong>8.1 Termination</strong>\n            <br />\n            We may terminate or suspend access to the Service at any time\n            without prior notice or liability for any reason. You may also\n            discontinue use of the Service at any time.\n          </p>\n          <p>\n            <strong>8.2 Data Removal</strong>\n            <br />\n            Upon the end of your beta testing, all of your data and history will\n            be permanently removed if not already deleted by you, and this\n            removal is irreversible.\n          </p>\n\n          <h2 className=\"font-semibold\">\n            9. Governing Law and Dispute Resolution\n          </h2>\n          <p>\n            These Terms shall be governed by and construed in accordance with\n            the laws of the jurisdiction in which our company is registered,\n            without regard to its conflict of law provisions. Any dispute\n            arising under these Terms shall be resolved exclusively in the\n            courts within that jurisdiction.\n          </p>\n\n          <h2 className=\"font-semibold\">10. Changes to the Terms</h2>\n          <p>\n            We reserve the right to modify these Terms at any time. If we make\n            material changes, we will notify you by updating the “Effective\n            Date” at the top of this page. Your continued use of the Service\n            after such changes constitute acceptance of the modified Terms.\n          </p>\n\n          <p className=\"text-xs mt-4\">Last Updated: January 06, 2025</p>\n        </div>\n\n        <DialogFooter></DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "components/services/esri/add-arcgis-layers.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { fetchAgolLayersList } from \"@/lib/fetchers/services/esri/fetch-layers-list\";\nimport { useAgolLayersStore } from \"@/features/maps/stores/use-agol-layers-store\";\n\nexport default function AddArcGisLayerClient() {\n  const setAvailableAgolLayers = useAgolLayersStore(\n    (state) => state.setAvailableAgolLayers\n  );\n\n  useEffect(() => {\n    const broadcastChannel = new BroadcastChannel(\"esriChannel\");\n\n    const fetchAndSetLayers = async () => {\n      try {\n        const layers = await fetchAgolLayersList();\n        if (!layers || layers.length === 0) {\n          throw new Error(\"No layers found.\");\n        }\n\n        const connectionStatus = \"connected\";\n        broadcastChannel.postMessage({ layers, connectionStatus });\n      } catch (error) {\n        console.error(\"Error fetching AGOL layers:\", error);\n      } finally {\n        broadcastChannel.close();\n        if (window.opener) {\n          window.close();\n        }\n      }\n    };\n\n    fetchAndSetLayers();\n\n    return () => {\n      broadcastChannel.close();\n    };\n  }, [setAvailableAgolLayers]);\n\n  return (\n    <div className=\"flex items-center justify-center h-screen\">\n      <h1 className=\"text-lg font-bold\">Loading Layers...</h1>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nconst NextThemesProvider = dynamic(\n  () => import(\"next-themes\").then((e) => e.ThemeProvider),\n  {\n    ssr: false,\n  }\n);\n\nexport type ThemeProviderProps = React.ComponentProps<\n  typeof NextThemesProvider\n>;\nimport dynamic from \"next/dynamic\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "components/ui/alert-dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\n\nimport { cn } from \"@/lib/utils\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\nconst AlertDialog = AlertDialogPrimitive.Root;\n\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\n\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst AlertDialogOverlay = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Overlay\n    className={cn(\n      \"fixed inset-0 z-[9999] bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n    ref={ref}\n  />\n));\nAlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;\n\nconst AlertDialogContent = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPortal>\n    <AlertDialogOverlay />\n    <AlertDialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-[9999] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-gray-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-stone-600 dark:bg-secondary\",\n        className\n      )}\n      {...props}\n    />\n  </AlertDialogPortal>\n));\nAlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;\n\nconst AlertDialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-2 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n);\nAlertDialogHeader.displayName = \"AlertDialogHeader\";\n\nconst AlertDialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n);\nAlertDialogFooter.displayName = \"AlertDialogFooter\";\n\nconst AlertDialogTitle = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Title\n    ref={ref}\n    className={cn(\"text-lg font-semibold\", className)}\n    {...props}\n  />\n));\nAlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;\n\nconst AlertDialogDescription = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-primary/90\", className)}\n    {...props}\n  />\n));\nAlertDialogDescription.displayName =\n  AlertDialogPrimitive.Description.displayName;\n\nconst AlertDialogAction = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Action>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Action\n    ref={ref}\n    className={cn(buttonVariants(), className)}\n    {...props}\n  />\n));\nAlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;\n\nconst AlertDialogCancel = React.forwardRef<\n  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,\n  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>\n>(({ className, ...props }, ref) => (\n  <AlertDialogPrimitive.Cancel\n    ref={ref}\n    className={cn(\n      buttonVariants({ variant: \"outline\" }),\n      \"mt-2 sm:mt-0\",\n      className\n    )}\n    {...props}\n  />\n));\nAlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;\n\nexport {\n  AlertDialog,\n  AlertDialogPortal,\n  AlertDialogOverlay,\n  AlertDialogTrigger,\n  AlertDialogContent,\n  AlertDialogHeader,\n  AlertDialogFooter,\n  AlertDialogTitle,\n  AlertDialogDescription,\n  AlertDialogAction,\n  AlertDialogCancel,\n};\n"
  },
  {
    "path": "components/ui/alert.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-background text-foreground\",\n        destructive:\n          \"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nconst Alert = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n  <div\n    ref={ref}\n    role=\"alert\"\n    className={cn(alertVariants({ variant }), className)}\n    {...props}\n  />\n))\nAlert.displayName = \"Alert\"\n\nconst AlertTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  <h5\n    ref={ref}\n    className={cn(\"mb-1 font-medium leading-none tracking-tight\", className)}\n    {...props}\n  />\n))\nAlertTitle.displayName = \"AlertTitle\"\n\nconst AlertDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-sm [&_p]:leading-relaxed\", className)}\n    {...props}\n  />\n))\nAlertDescription.displayName = \"AlertDescription\"\n\nexport { Alert, AlertTitle, AlertDescription }\n"
  },
  {
    "path": "components/ui/badge.tsx",
    "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2 dark:border-gray-800 dark:focus:ring-gray-300\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-gray-900 text-gray-50 hover:bg-gray-900/80 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/80\",\n        secondary:\n          \"border-transparent bg-gray-100 text-gray-900 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80\",\n        destructive:\n          \"border-transparent bg-red-500 text-gray-50 hover:bg-red-500/80 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/80\",\n        outline: \"text-gray-950 dark:text-gray-50\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  }\n)\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  )\n}\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-30 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        warning: \"bg-warning text-warning-foreground hover:bg-warning/90\",\n        info: \"bg-info text-info-foreground hover:bg-info/90\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n        \"primary-blue\":\n          \"bg-primary-blue text-primary-blue-foreground hover:bg-primary-blue/90\",\n        \"primary-green\":\n          \"bg-primary-green text-primary-green-foreground hover:bg-primary-green/90\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        xs: \"h-7 rounded-sm px-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  }\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "components/ui/card.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-lg border border-stone-300 bg-secondary shadow-sm dark:border-stone-600\",\n      className\n    )}\n    {...props}\n  />\n));\nCard.displayName = \"Card\";\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n));\nCardHeader.displayName = \"CardHeader\";\n\nconst CardTitle = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"text-2xl font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n));\nCardTitle.displayName = \"CardTitle\";\n\nconst CardDescription = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-sm text-gray-500 dark:text-gray-400\", className)}\n    {...props}\n  />\n));\nCardDescription.displayName = \"CardDescription\";\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n));\nCardContent.displayName = \"CardContent\";\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "components/ui/checkbox.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Check } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Checkbox = React.forwardRef<\n  React.ElementRef<typeof CheckboxPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <CheckboxPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n      className\n    )}\n    {...props}\n  >\n    <CheckboxPrimitive.Indicator\n      className={cn(\"flex items-center justify-center text-current\")}\n    >\n      <Check className=\"h-4 w-4\" />\n    </CheckboxPrimitive.Indicator>\n  </CheckboxPrimitive.Root>\n))\nCheckbox.displayName = CheckboxPrimitive.Root.displayName\n\nexport { Checkbox }\n"
  },
  {
    "path": "components/ui/confirmation-modal.tsx",
    "content": "import { Loader2 } from \"lucide-react\";\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface ConfirmationModalProps {\n  isOpen?: boolean;\n  title?: string;\n  message?: string;\n  cancelText?: string;\n  confirmText?: string;\n  onCancel?: () => void;\n  onConfirm?: () => Promise<void> | void;\n  confirmButtonClassName?: string;\n  /** NEW: Add an isDeleting or loading prop */\n  isDeleting?: boolean;\n}\n\nconst ConfirmationModal = ({\n  isOpen = false,\n  title = \"Confirm Action\",\n  message = \"Are you sure you want to proceed? This action cannot be undone.\",\n  cancelText = \"Cancel\",\n  confirmText = \"Confirm\",\n  onCancel = () => {},\n  onConfirm = () => {},\n  confirmButtonClassName = \"bg-red-500 hover:bg-red-600\",\n  isDeleting = false,\n}: ConfirmationModalProps) => {\n  return (\n    <AlertDialog\n      open={isOpen}\n      onOpenChange={(open) => {\n        // If user tries to close while isDeleting, ignore that attempt\n        if (!open && !isDeleting) {\n          onCancel();\n        }\n      }}\n    >\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          <AlertDialogDescription>{message}</AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel onClick={onCancel} disabled={isDeleting}>\n            {cancelText}\n          </AlertDialogCancel>\n          {/* Replace AlertDialogAction with a normal Button */}\n          <Button\n            onClick={onConfirm}\n            disabled={isDeleting}\n            className={cn(confirmButtonClassName)}\n          >\n            {isDeleting && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n            {confirmText}\n          </Button>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n};\n\nexport default ConfirmationModal;\n"
  },
  {
    "path": "components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-[9999] bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-[9999] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-gray-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-gray-800 dark:bg-secondary\",\n        className\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 disabled:pointer-events-none data-[state=open]:bg-gray-100 data-[state=open]:text-gray-500 dark:ring-offset-gray-950 dark:focus:ring-gray-300 dark:data-[state=open]:bg-secondary-800 dark:data-[state=open]:text-gray-400\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-gray-500 dark:text-gray-400\", className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "components/ui/dropdown-menu.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { Check, ChevronRight, Circle } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto\" />\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-[1000] min-w-[8rem] overflow-hidden rounded-xl border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-[1000] min-w-[8rem] overflow-hidden rounded-xl border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"h-2 w-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className\n    )}\n    {...props}\n  />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  );\n};\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\";\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
  },
  {
    "path": "components/ui/form.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport {\n  Controller,\n  ControllerProps,\n  FieldPath,\n  FieldValues,\n  FormProvider,\n  useFormContext,\n} from \"react-hook-form\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Label } from \"@/components/ui/label\"\n\nconst Form = FormProvider\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n> = {\n  name: TName\n}\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n  {} as FormFieldContextValue\n)\n\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  )\n}\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext)\n  const itemContext = React.useContext(FormItemContext)\n  const { getFieldState, formState } = useFormContext()\n\n  const fieldState = getFieldState(fieldContext.name, formState)\n\n  if (!fieldContext) {\n    throw new Error(\"useFormField should be used within <FormField>\")\n  }\n\n  const { id } = itemContext\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  }\n}\n\ntype FormItemContextValue = {\n  id: string\n}\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n  {} as FormItemContextValue\n)\n\nconst FormItem = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const id = React.useId()\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div ref={ref} className={cn(\"space-y-2\", className)} {...props} />\n    </FormItemContext.Provider>\n  )\n})\nFormItem.displayName = \"FormItem\"\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField()\n\n  return (\n    <Label\n      ref={ref}\n      className={cn(error && \"text-destructive\", className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  )\n})\nFormLabel.displayName = \"FormLabel\"\n\nconst FormControl = React.forwardRef<\n  React.ElementRef<typeof Slot>,\n  React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()\n\n  return (\n    <Slot\n      ref={ref}\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    />\n  )\n})\nFormControl.displayName = \"FormControl\"\n\nconst FormDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n  const { formDescriptionId } = useFormField()\n\n  return (\n    <p\n      ref={ref}\n      id={formDescriptionId}\n      className={cn(\"text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  )\n})\nFormDescription.displayName = \"FormDescription\"\n\nconst FormMessage = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n  const { error, formMessageId } = useFormField()\n  const body = error ? String(error?.message) : children\n\n  if (!body) {\n    return null\n  }\n\n  return (\n    <p\n      ref={ref}\n      id={formMessageId}\n      className={cn(\"text-sm font-medium text-destructive\", className)}\n      {...props}\n    >\n      {body}\n    </p>\n  )\n})\nFormMessage.displayName = \"FormMessage\"\n\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n}\n"
  },
  {
    "path": "components/ui/input-text-confirm.tsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\nimport { Button } from \"./button\";\nimport { Input } from \"@/components/ui/input\";\n\ninterface InputTextConfirmProps {\n  isOpen: boolean;\n  onClose: () => void;\n  onSubmit: (value: string) => void;\n  title: string;\n  placeholder?: string;\n  initialValue?: string;\n}\n\nconst InputTextConfirm: React.FC<InputTextConfirmProps> = ({\n  isOpen,\n  onClose,\n  onSubmit,\n  title,\n  placeholder = \"Enter ROI name...\",\n  initialValue = \"\",\n}) => {\n  const [inputValue, setInputValue] = useState(initialValue);\n  const popoverRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (isOpen) {\n      setInputValue(initialValue);\n    }\n  }, [isOpen, initialValue]);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        popoverRef.current &&\n        !popoverRef.current.contains(event.target as Node)\n      ) {\n        onClose();\n      }\n    };\n\n    if (isOpen) {\n      document.addEventListener(\"mousedown\", handleClickOutside);\n    }\n\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, [isOpen, onClose]);\n\n  if (!isOpen) return null;\n\n  const handleSubmit = () => {\n    onSubmit(inputValue);\n    onClose();\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"Enter\") {\n      handleSubmit();\n    } else if (e.key === \"Escape\") {\n      onClose();\n    }\n  };\n\n  return (\n    <div\n      ref={popoverRef}\n      className=\"absolute bottom-full mb-2 bg-background rounded-lg p-4 w-64 shadow-xl border border-stone-300 dark:border-stone-600\"\n    >\n      <h2 className=\"text-md font-bold mb-3 text-foreground\">{title}</h2>\n\n      <Input\n        type=\"text\"\n        value={inputValue}\n        onChange={(e) => setInputValue(e.target.value)}\n        onKeyDown={handleKeyDown}\n        placeholder={placeholder}\n      />\n\n      <div className=\"flex justify-end space-x-2 pt-4\">\n        <Button\n          onClick={onClose}\n          variant=\"ghost\"\n          size=\"xs\"\n          className=\"text-foreground\"\n        >\n          Cancel\n        </Button>\n        <Button onClick={handleSubmit} variant=\"primary-blue\" size=\"xs\">\n          Confirm\n        </Button>\n      </div>\n    </div>\n  );\n};\n\nexport default InputTextConfirm;\n"
  },
  {
    "path": "components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Input = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-10 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-base dark:text-primary ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-gray-950 placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:border-gray-800 dark:bg-accent dark:ring-offset-accent dark:file:text-gray-50 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  }\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
  },
  {
    "path": "components/ui/label.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\n)\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n))\nLabel.displayName = LabelPrimitive.Root.displayName\n\nexport { Label }\n"
  },
  {
    "path": "components/ui/popover.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-[9999] w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent };\n"
  },
  {
    "path": "components/ui/scroll-area.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root\n    ref={ref}\n    className={cn(\"relative overflow-hidden\", className)}\n    {...props}\n  >\n    <ScrollAreaPrimitive.Viewport className=\"h-full w-full rounded-[inherit]\">\n      {children}\n    </ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n));\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" &&\n        \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" &&\n        \"h-2.5 flex-col border-t border-t-transparent p-[1px]\",\n      className\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-gray-200 dark:bg-muted-foreground\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n));\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "components/ui/select.tsx",
    "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Select = SelectPrimitive.Root\n\nconst SelectGroup = SelectPrimitive.Group\n\nconst SelectValue = SelectPrimitive.Value\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n))\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollUpButton>\n))\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"h-4 w-4\" />\n  </SelectPrimitive.ScrollDownButton>\n))\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\"\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n))\nSelectContent.displayName = SelectPrimitive.Content.displayName\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"py-1.5 pl-8 pr-2 text-sm font-semibold\", className)}\n    {...props}\n  />\n))\nSelectLabel.displayName = SelectPrimitive.Label.displayName\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n))\nSelectItem.displayName = SelectPrimitive.Item.displayName\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n))\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n}\n"
  },
  {
    "path": "components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = \"horizontal\", decorative = true, ...props },\n    ref\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-stone-300 dark:bg-stone-700\",\n        orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n        className\n      )}\n      {...props}\n    />\n  )\n);\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "components/ui/table.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Table = React.forwardRef<\n  HTMLTableElement,\n  React.HTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n  // <div className=\"relative w-full overflow-auto rounded-2xl border border-stone-300 dark:border-stone-600\">\n  <table\n    ref={ref}\n    className={cn(\"w-full caption-bottom text-sm \", className)}\n    {...props}\n  />\n  // </div>\n));\nTable.displayName = \"Table\";\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead ref={ref} className={cn(\"[&_tr]:border-b\", className)} {...props} />\n));\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn(\"[&_tr:last-child]:border-0\", className)}\n    {...props}\n  />\n));\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn(\n      \"border-t bg-gray-100/50 font-medium [&>tr]:last:border-b-0 dark:bg-gray-800/50\",\n      className\n    )}\n    {...props}\n  />\n));\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n  <tr\n    ref={ref}\n    className={cn(\n      \"border-b transition-colors data-[state=selected]:bg-muted hover:bg-muted/50 dark:hover:bg-muted/30 dark:data-[state=selected]:bg-muted\",\n      className\n    )}\n    {...props}\n  />\n));\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      \"h-12 px-4 text-left align-middle font-bold bg-gray-200 dark:bg-accent text-primary/80 [&:has([role=checkbox])]:pr-0\",\n      className\n    )}\n    {...props}\n  />\n));\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn(\"p-4 align-middle [&:has([role=checkbox])]:pr-0\", className)}\n    {...props}\n  />\n));\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption\n    ref={ref}\n    className={cn(\"mt-4 text-sm text-gray-500 dark:text-gray-400\", className)}\n    {...props}\n  />\n));\nTableCaption.displayName = \"TableCaption\";\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "components/ui/textarea.tsx",
    "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Textarea = React.forwardRef<\n  HTMLTextAreaElement,\n  React.ComponentProps<\"textarea\">\n>(({ className, ...props }, ref) => {\n  return (\n    <textarea\n      className={cn(\n        \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n        className\n      )}\n      ref={ref}\n      {...props}\n    />\n  )\n})\nTextarea.displayName = \"Textarea\"\n\nexport { Textarea }\n"
  },
  {
    "path": "components/ui/theme-mode-toggle.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Moon, Sun } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\nexport function ThemeModeToggle() {\n  const { setTheme } = useTheme();\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          className=\"bg-forground border border-stone-300 dark:border-stone-600\"\n        >\n          <Sun className=\"h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0\" />\n          <Moon className=\"absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100\" />\n          <span className=\"sr-only\">Toggle theme</span>\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"center\" side=\"right\">\n        <DropdownMenuItem onClick={() => setTheme(\"light\")}>\n          Light\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setTheme(\"dark\")}>\n          Dark\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={() => setTheme(\"system\")}>\n          System\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst Tooltip = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-[9999] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className\n    )}\n    {...props}\n  />\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"gray\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\",\n    \"ui\": \"@/components/ui\",\n    \"lib\": \"@/lib\",\n    \"hooks\": \"@/hooks\"\n  },\n  \"iconLibrary\": \"lucide\"\n}\n"
  },
  {
    "path": "custom-configs/ai-assistants.ts",
    "content": "export const AI_ASSISTANTS: AI_ASSISTANTS_TYPE = {\n  default: [\"GPT-4o\"],\n  custom: [],\n  icons: {\n    \"GPT-4o\": \"/vendors-logos/openai.svg\",\n  },\n};\n"
  },
  {
    "path": "custom-configs/charts-config.ts",
    "content": "export const chartColors = {\n  timeSeriesChart: [\"#00A8E8\"],\n  barChartNumerical: [\"#00A8E8\"],\n  stackedBarChartStats: [\"#00A8E8\", \"#D1495B\"],\n  dualBarChartNumerical: [\"#00A8E8\", \"#D1495B\"],\n  dualTimeSeriesChart: [\"#00A8E8\", \"#D1495B\"],\n};\n"
  },
  {
    "path": "custom-configs/integrations.ts",
    "content": "export const INTEGRATION_SERVICES: IntegrationService[] = [\n  {\n    id: \"arcgis\",\n    name: \"Esri Feature Server\",\n    description:\n      \"Connect to ArcGIS Feature Servers to import and manage spatial data\",\n    icon: \"/vendors-logos/esri.svg\",\n    status: \"not_connected\",\n  },\n];\n"
  },
  {
    "path": "custom-configs/project-config.ts",
    "content": "const projectConfigs = {\n  initialFlyToCoords: { lat: 44.235128, lng: -76.592403 },\n} as const;\n\nexport default projectConfigs;\n"
  },
  {
    "path": "db-schema/schema.sql",
    "content": "\n\nSET statement_timeout = 0;\nSET lock_timeout = 0;\nSET idle_in_transaction_session_timeout = 0;\nSET client_encoding = 'UTF8';\nSET standard_conforming_strings = on;\nSELECT pg_catalog.set_config('search_path', '', false);\nSET check_function_bodies = false;\nSET xmloption = content;\nSET client_min_messages = warning;\nSET row_security = off;\n\n\nCREATE SCHEMA IF NOT EXISTS \"public\";\n\nCREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public;\n\n\nALTER SCHEMA \"public\" OWNER TO \"pg_database_owner\";\n\n\nCOMMENT ON SCHEMA \"public\" IS 'standard public schema';\n\n\n\nCREATE TYPE \"public\".\"subscription_tier_enum\" AS ENUM (\n    'Essentials',\n    'Pro',\n    'Enterprise'\n);\n\n\nALTER TYPE \"public\".\"subscription_tier_enum\" OWNER TO \"postgres\";\n\n\nCREATE TYPE \"public\".\"user_role_enum\" AS ENUM (\n    'ADMIN',\n    'USER',\n    'TRIAL',\n    'VIEWER'\n);\n\n\nALTER TYPE \"public\".\"user_role_enum\" OWNER TO \"postgres\";\n\n\nCREATE OR REPLACE FUNCTION \"public\".\"create_user_usage_row\"() RETURNS \"trigger\"\n    LANGUAGE \"plpgsql\"\n    AS $$\nBEGIN\n  -- Insert a matching row in user_usage\n  INSERT INTO public.user_usage (user_id)\n  VALUES (NEW.id);\n\n  RETURN NEW;\nEND;\n$$;\n\n\nALTER FUNCTION \"public\".\"create_user_usage_row\"() OWNER TO \"postgres\";\n\n\nCREATE OR REPLACE FUNCTION \"public\".\"handle_new_auth_user\"() RETURNS \"trigger\"\n    LANGUAGE \"plpgsql\" SECURITY DEFINER\n    AS $$\nBEGIN\n  INSERT INTO public.user_roles (\n    id,\n    name,\n    email,\n    organization,\n    role,\n    license_start,\n    license_end,\n    subscription_tier\n  )\n  VALUES (\n    NEW.id,\n    COALESCE(NEW.raw_user_meta_data->>'full_name', 'New User'),\n    NEW.email,\n    '',\n    'TRIAL',\n    CURRENT_DATE,\n    CURRENT_DATE + 14,\n    'Essentials'\n  );\n  RETURN NEW;\nEND;\n$$;\n\n\nALTER FUNCTION \"public\".\"handle_new_auth_user\"() OWNER TO \"postgres\";\n\n\nCREATE OR REPLACE FUNCTION \"public\".\"search_documents_by_similarity\"(\"query_embedding\" \"public\".\"vector\", \"match_count\" integer DEFAULT NULL::integer, \"filter\" \"jsonb\" DEFAULT '{}'::\"jsonb\") RETURNS TABLE(\"id\" bigint, \"content\" \"text\", \"metadata\" \"jsonb\")\n    LANGUAGE \"plpgsql\"\n    AS $$\nBEGIN\n  RETURN QUERY\n  SELECT\n    e.id,\n    e.content,\n    e.metadata\n      || jsonb_build_object('similarity', 1 - (e.embedding <=> query_embedding)) AS metadata\n  FROM embeddings e\n  JOIN document_files f ON f.id = e.file_id\n  WHERE\n f.owner = (filter->>'owner')::uuid\n\n  ORDER BY\n    e.embedding <=> query_embedding\n  LIMIT match_count;\nEND;\n$$;\n\n\nALTER FUNCTION \"public\".\"search_documents_by_similarity\"(\"query_embedding\" \"public\".\"vector\", \"match_count\" integer, \"filter\" \"jsonb\") OWNER TO \"postgres\";\n\n\nCREATE OR REPLACE FUNCTION \"public\".\"search_documents_by_similarity\"(\"query_embedding\" \"public\".\"vector\", \"match_count\" integer DEFAULT NULL::integer, \"owner_uuid\" \"uuid\" DEFAULT NULL::\"uuid\", \"metadata_filter\" \"jsonb\" DEFAULT '{}'::\"jsonb\") RETURNS TABLE(\"id\" bigint, \"content\" \"text\", \"metadata\" \"jsonb\")\n    LANGUAGE \"plpgsql\"\n    AS $$\nBEGIN\n  RETURN QUERY\n  SELECT\n    e.id,\n    e.content,\n    e.metadata\n      || jsonb_build_object('similarity', 1 - (e.embedding <=> query_embedding)) AS metadata\n  FROM embeddings e\n  JOIN document_files f ON f.id = e.file_id\n  WHERE\n    -- If owner_uuid is provided, filter on that\n    (owner_uuid IS NULL OR f.owner = owner_uuid)\n    \n    -- If metadata_filter is not empty, check that e.metadata contains it\n    AND (\n      metadata_filter = '{}' \n      OR e.metadata @> metadata_filter\n    )\n  ORDER BY\n    e.embedding <=> query_embedding\n  LIMIT match_count;\nEND;\n$$;\n\n\nALTER FUNCTION \"public\".\"search_documents_by_similarity\"(\"query_embedding\" \"public\".\"vector\", \"match_count\" integer, \"owner_uuid\" \"uuid\", \"metadata_filter\" \"jsonb\") OWNER TO \"postgres\";\n\n\nCREATE OR REPLACE FUNCTION \"public\".\"search_gee_datasets_ft\"(\"query\" \"text\") RETURNS TABLE(\"id\" integer, \"dataset_id\" \"text\", \"asset_url\" \"text\", \"type\" \"text\", \"start_date\" \"date\", \"end_date\" \"date\", \"title\" \"text\", \"rank\" real)\n    LANGUAGE \"plpgsql\"\n    AS $$\nBEGIN\n  RETURN QUERY\n  SELECT\n    gd.id,\n    gd.dataset_id,\n    gd.asset_url,\n    gd.type,\n    gd.start_date,\n    gd.end_date,\n    gd.title,\n    ts_rank(gd.search_vector, to_tsquery('simple', query)) AS rank\n  FROM public.gee_datasets gd\n  WHERE gd.search_vector @@ to_tsquery('simple', query)\n  ORDER BY rank DESC;\nEND;\n$$;\n\n\nALTER FUNCTION \"public\".\"search_gee_datasets_ft\"(\"query\" \"text\") OWNER TO \"postgres\";\n\n\nCREATE OR REPLACE FUNCTION \"public\".\"update_user_usage_docs_count\"() RETURNS \"trigger\"\n    LANGUAGE \"plpgsql\"\n    AS $$\nDECLARE\n  affected_user UUID;\nBEGIN\n  -- Coalesce to figure out which user was affected\n  affected_user := COALESCE(NEW.owner, OLD.owner);\n\n  -- Recalculate total docs for that user\n  UPDATE public.user_usage\n  SET knowledge_base_docs_count = (\n    SELECT COUNT(*) \n    FROM public.document_files\n    WHERE owner = affected_user\n  ),\n  updated_at = now()\n  WHERE user_id = affected_user;\n\n  RETURN NULL;  -- For an AFTER trigger, we can return NULL\nEND;\n$$;\n\n\nALTER FUNCTION \"public\".\"update_user_usage_docs_count\"() OWNER TO \"postgres\";\n\nSET default_tablespace = '';\n\nSET default_table_access_method = \"heap\";\n\n\nCREATE TABLE IF NOT EXISTS \"public\".\"chats\" (\n    \"id\" \"uuid\" DEFAULT \"extensions\".\"uuid_generate_v4\"() NOT NULL,\n    \"userId\" \"uuid\",\n    \"chatTitle\" character varying(255),\n    \"createdAt\" timestamp without time zone DEFAULT \"now\"()\n);\n\n\nALTER TABLE \"public\".\"chats\" OWNER TO \"postgres\";\n\n\nCREATE TABLE IF NOT EXISTS \"public\".\"document_files\" (\n    \"id\" bigint NOT NULL,\n    \"name\" \"text\" NOT NULL,\n    \"owner\" \"uuid\" NOT NULL,\n    \"number_of_pages\" integer,\n    \"created_at\" timestamp without time zone DEFAULT \"now\"() NOT NULL,\n    \"file_path\" \"text\" NOT NULL,\n    \"folder_id\" \"text\"\n);\n\n\nALTER TABLE \"public\".\"document_files\" OWNER TO \"postgres\";\n\n\nALTER TABLE \"public\".\"document_files\" ALTER COLUMN \"id\" ADD GENERATED ALWAYS AS IDENTITY (\n    SEQUENCE NAME \"public\".\"document_files_id_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1\n);\n\n\n\nCREATE TABLE IF NOT EXISTS \"public\".\"drafted_reports\" (\n    \"id\" \"uuid\" DEFAULT \"extensions\".\"uuid_generate_v4\"() NOT NULL,\n    \"userId\" \"uuid\",\n    \"title\" character varying(255) NOT NULL,\n    \"content\" \"text\",\n    \"createdAt\" timestamp without time zone DEFAULT \"now\"()\n);\n\n\nALTER TABLE \"public\".\"drafted_reports\" OWNER TO \"postgres\";\n\n\nCREATE TABLE IF NOT EXISTS \"public\".\"embeddings\" (\n    \"id\" bigint NOT NULL,\n    \"content\" \"text\",\n    \"metadata\" \"jsonb\",\n    \"embedding\" \"public\".\"vector\"(3072),\n    \"file_id\" bigint NOT NULL\n);\n\n\nALTER TABLE \"public\".\"embeddings\" OWNER TO \"postgres\";\n\n\nCREATE SEQUENCE IF NOT EXISTS \"public\".\"embeddings_id_seq\"\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE \"public\".\"embeddings_id_seq\" OWNER TO \"postgres\";\n\n\nALTER SEQUENCE \"public\".\"embeddings_id_seq\" OWNED BY \"public\".\"embeddings\".\"id\";\n\n\n\nCREATE TABLE IF NOT EXISTS \"public\".\"gee_datasets\" (\n    \"id\" integer NOT NULL,\n    \"dataset_id\" \"text\",\n    \"asset_url\" \"text\",\n    \"provider\" \"text\",\n    \"title\" \"text\",\n    \"start_date\" \"date\",\n    \"end_date\" \"date\",\n    \"tags\" \"text\",\n    \"type\" \"text\",\n    \"thumbnail_url\" \"text\",\n    \"search_vector\" \"tsvector\"\n);\n\n\nALTER TABLE \"public\".\"gee_datasets\" OWNER TO \"postgres\";\n\n\nCREATE SEQUENCE IF NOT EXISTS \"public\".\"gee_datasets_id_seq\"\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE \"public\".\"gee_datasets_id_seq\" OWNER TO \"postgres\";\n\n\nALTER SEQUENCE \"public\".\"gee_datasets_id_seq\" OWNED BY \"public\".\"gee_datasets\".\"id\";\n\n\n\nCREATE TABLE IF NOT EXISTS \"public\".\"messages\" (\n    \"id\" \"uuid\" DEFAULT \"extensions\".\"uuid_generate_v4\"() NOT NULL,\n    \"chatId\" \"uuid\",\n    \"draftedReportId\" \"uuid\",\n    \"role\" character varying(50) NOT NULL,\n    \"content\" \"json\" NOT NULL,\n    \"createdAt\" timestamp without time zone DEFAULT \"now\"(),\n    \"toolResult\" \"jsonb\"\n);\n\n\nALTER TABLE \"public\".\"messages\" OWNER TO \"postgres\";\n\n\nCREATE TABLE IF NOT EXISTS \"public\".\"user_roles\" (\n    \"id\" \"uuid\" NOT NULL,\n    \"name\" character varying(255) NOT NULL,\n    \"email\" character varying(255) NOT NULL,\n    \"organization\" character varying(255),\n    \"role\" \"public\".\"user_role_enum\" NOT NULL,\n    \"license_start\" \"date\",\n    \"license_end\" \"date\",\n    \"created_at\" timestamp without time zone DEFAULT \"now\"(),\n    \"subscription_tier\" \"public\".\"subscription_tier_enum\" DEFAULT 'Essentials'::\"public\".\"subscription_tier_enum\" NOT NULL\n);\n\n\nALTER TABLE \"public\".\"user_roles\" OWNER TO \"postgres\";\n\n\nCREATE TABLE IF NOT EXISTS \"public\".\"user_usage\" (\n    \"id\" \"uuid\" DEFAULT \"gen_random_uuid\"() NOT NULL,\n    \"user_id\" \"uuid\" NOT NULL,\n    \"requests_count\" integer DEFAULT 0 NOT NULL,\n    \"knowledge_base_docs_count\" integer DEFAULT 0 NOT NULL,\n    \"updated_at\" timestamp without time zone DEFAULT \"now\"()\n);\n\n\nALTER TABLE \"public\".\"user_usage\" OWNER TO \"postgres\";\n\n\nALTER TABLE ONLY \"public\".\"embeddings\" ALTER COLUMN \"id\" SET DEFAULT \"nextval\"('\"public\".\"embeddings_id_seq\"'::\"regclass\");\n\n\n\nALTER TABLE ONLY \"public\".\"gee_datasets\" ALTER COLUMN \"id\" SET DEFAULT \"nextval\"('\"public\".\"gee_datasets_id_seq\"'::\"regclass\");\n\n\n\nALTER TABLE ONLY \"public\".\"chats\"\n    ADD CONSTRAINT \"chats_pkey\" PRIMARY KEY (\"id\");\n\n\n\nALTER TABLE ONLY \"public\".\"document_files\"\n    ADD CONSTRAINT \"document_files_file_path_key\" UNIQUE (\"file_path\");\n\n\n\nALTER TABLE ONLY \"public\".\"document_files\"\n    ADD CONSTRAINT \"document_files_owner_name_key\" UNIQUE (\"owner\", \"name\");\n\n\n\nALTER TABLE ONLY \"public\".\"document_files\"\n    ADD CONSTRAINT \"document_files_pkey\" PRIMARY KEY (\"id\");\n\n\n\nALTER TABLE ONLY \"public\".\"drafted_reports\"\n    ADD CONSTRAINT \"drafted_reports_pkey\" PRIMARY KEY (\"id\");\n\n\n\nALTER TABLE ONLY \"public\".\"embeddings\"\n    ADD CONSTRAINT \"embeddings_pkey\" PRIMARY KEY (\"id\");\n\n\n\nALTER TABLE ONLY \"public\".\"gee_datasets\"\n    ADD CONSTRAINT \"gee_datasets_pkey\" PRIMARY KEY (\"id\");\n\n\n\nALTER TABLE ONLY \"public\".\"messages\"\n    ADD CONSTRAINT \"messages_pkey\" PRIMARY KEY (\"id\");\n\n\n\nALTER TABLE ONLY \"public\".\"user_roles\"\n    ADD CONSTRAINT \"user_roles_email_key\" UNIQUE (\"email\");\n\n\n\nALTER TABLE ONLY \"public\".\"user_roles\"\n    ADD CONSTRAINT \"user_roles_pkey\" PRIMARY KEY (\"id\");\n\n\n\nALTER TABLE ONLY \"public\".\"user_usage\"\n    ADD CONSTRAINT \"user_usage_pkey\" PRIMARY KEY (\"id\");\n\n\n\nCREATE INDEX \"gee_datasets_dataset_id_idx\" ON \"public\".\"gee_datasets\" USING \"btree\" (\"lower\"(\"dataset_id\"));\n\n\n\nCREATE INDEX \"gee_datasets_search_idx\" ON \"public\".\"gee_datasets\" USING \"gin\" (\"search_vector\");\n\n\n\nCREATE INDEX \"gee_datasets_title_idx\" ON \"public\".\"gee_datasets\" USING \"btree\" (\"lower\"(\"title\"));\n\n\n\nCREATE OR REPLACE TRIGGER \"trigger_update_docs_count\" AFTER INSERT OR DELETE OR UPDATE OF \"owner\" ON \"public\".\"document_files\" FOR EACH ROW EXECUTE FUNCTION \"public\".\"update_user_usage_docs_count\"();\n\n\n\nCREATE OR REPLACE TRIGGER \"user_roles_after_insert\" AFTER INSERT ON \"public\".\"user_roles\" FOR EACH ROW EXECUTE FUNCTION \"public\".\"create_user_usage_row\"();\n\n\n\nALTER TABLE ONLY \"public\".\"chats\"\n    ADD CONSTRAINT \"chats_user_id_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"user_roles\"(\"id\") ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY \"public\".\"document_files\"\n    ADD CONSTRAINT \"document_files_owner_fkey\" FOREIGN KEY (\"owner\") REFERENCES \"public\".\"user_roles\"(\"id\") ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY \"public\".\"drafted_reports\"\n    ADD CONSTRAINT \"drafted_reports_user_id_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"user_roles\"(\"id\") ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY \"public\".\"embeddings\"\n    ADD CONSTRAINT \"embeddings_file_id_fkey\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"document_files\"(\"id\") ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY \"public\".\"messages\"\n    ADD CONSTRAINT \"messages_chat_id_fkey\" FOREIGN KEY (\"chatId\") REFERENCES \"public\".\"chats\"(\"id\") ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY \"public\".\"messages\"\n    ADD CONSTRAINT \"messages_drafted_report_id_fkey\" FOREIGN KEY (\"draftedReportId\") REFERENCES \"public\".\"drafted_reports\"(\"id\") ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY \"public\".\"user_roles\"\n    ADD CONSTRAINT \"user_roles_id_fkey\" FOREIGN KEY (\"id\") REFERENCES \"auth\".\"users\"(\"id\") ON DELETE CASCADE;\n\n\n\nALTER TABLE ONLY \"public\".\"user_usage\"\n    ADD CONSTRAINT \"user_usage_user_id_fkey\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"user_roles\"(\"id\") ON DELETE CASCADE;\n\n\n\nCREATE POLICY \"AUTHENTICATED\" ON \"public\".\"chats\" TO \"authenticated\" USING ((\"userId\" = \"auth\".\"uid\"())) WITH CHECK ((\"userId\" = \"auth\".\"uid\"()));\n\n\n\nCREATE POLICY \"AUTHENTICATED\" ON \"public\".\"document_files\" TO \"authenticated\" USING ((EXISTS ( SELECT 1\n   FROM \"public\".\"user_roles\"\n  WHERE ((\"user_roles\".\"id\" = \"document_files\".\"owner\") AND (\"user_roles\".\"id\" = \"auth\".\"uid\"()))))) WITH CHECK ((EXISTS ( SELECT 1\n   FROM \"public\".\"user_roles\"\n  WHERE ((\"user_roles\".\"id\" = \"document_files\".\"owner\") AND (\"user_roles\".\"id\" = \"auth\".\"uid\"())))));\n\n\n\nCREATE POLICY \"AUTHENTICATED\" ON \"public\".\"embeddings\" TO \"authenticated\" USING ((EXISTS ( SELECT 1\n   FROM \"public\".\"document_files\"\n  WHERE ((\"document_files\".\"id\" = \"embeddings\".\"file_id\") AND (EXISTS ( SELECT 1\n           FROM \"public\".\"user_roles\"\n          WHERE ((\"user_roles\".\"id\" = \"document_files\".\"owner\") AND (\"user_roles\".\"id\" = \"auth\".\"uid\"())))))))) WITH CHECK ((EXISTS ( SELECT 1\n   FROM \"public\".\"document_files\"\n  WHERE ((\"document_files\".\"id\" = \"embeddings\".\"file_id\") AND (EXISTS ( SELECT 1\n           FROM \"public\".\"user_roles\"\n          WHERE ((\"user_roles\".\"id\" = \"document_files\".\"owner\") AND (\"user_roles\".\"id\" = \"auth\".\"uid\"()))))))));\n\n\n\nCREATE POLICY \"AUTHENTICATED\" ON \"public\".\"messages\" TO \"authenticated\" USING ((EXISTS ( SELECT 1\n   FROM \"public\".\"chats\"\n  WHERE ((\"chats\".\"id\" = \"messages\".\"chatId\") AND (\"chats\".\"userId\" = \"auth\".\"uid\"()))))) WITH CHECK ((EXISTS ( SELECT 1\n   FROM \"public\".\"chats\"\n  WHERE ((\"chats\".\"id\" = \"messages\".\"chatId\") AND (\"chats\".\"userId\" = \"auth\".\"uid\"())))));\n\n\n\nCREATE POLICY \"Allow authenticated users to access their own roles\" ON \"public\".\"user_roles\" FOR SELECT USING (((\"auth\".\"uid\"() IS NOT NULL) AND ((\"email\")::\"text\" = \"auth\".\"email\"())));\n\n\n\nCREATE POLICY \"INSERT\" ON \"public\".\"user_roles\" FOR INSERT TO \"service_role\", \"postgres\" WITH CHECK (true);\n\n\n\nCREATE POLICY \"INSERT\" ON \"public\".\"user_usage\" FOR INSERT TO \"authenticated\" WITH CHECK ((\"user_id\" = \"auth\".\"uid\"()));\n\n\n\nCREATE POLICY \"SELECT\" ON \"public\".\"gee_datasets\" FOR SELECT TO \"authenticated\" USING (true);\n\n\n\nCREATE POLICY \"SELECT\" ON \"public\".\"user_usage\" FOR SELECT TO \"authenticated\" USING ((\"user_id\" = \"auth\".\"uid\"()));\n\n\n\nCREATE POLICY \"UPDATE\" ON \"public\".\"user_usage\" FOR UPDATE TO \"authenticated\" USING ((\"user_id\" = \"auth\".\"uid\"())) WITH CHECK ((\"user_id\" = \"auth\".\"uid\"()));\n\n\n\nALTER TABLE \"public\".\"chats\" ENABLE ROW LEVEL SECURITY;\n\n\nALTER TABLE \"public\".\"document_files\" ENABLE ROW LEVEL SECURITY;\n\n\nALTER TABLE \"public\".\"drafted_reports\" ENABLE ROW LEVEL SECURITY;\n\n\nALTER TABLE \"public\".\"embeddings\" ENABLE ROW LEVEL SECURITY;\n\n\nALTER TABLE \"public\".\"gee_datasets\" ENABLE ROW LEVEL SECURITY;\n\n\nALTER TABLE \"public\".\"messages\" ENABLE ROW LEVEL SECURITY;\n\n\nALTER TABLE \"public\".\"user_roles\" ENABLE ROW LEVEL SECURITY;\n\n\nALTER TABLE \"public\".\"user_usage\" ENABLE ROW LEVEL SECURITY;\n\n\nGRANT USAGE ON SCHEMA \"public\" TO \"postgres\";\nGRANT USAGE ON SCHEMA \"public\" TO \"anon\";\nGRANT USAGE ON SCHEMA \"public\" TO \"authenticated\";\nGRANT USAGE ON SCHEMA \"public\" TO \"service_role\";\n\n\n\nGRANT ALL ON FUNCTION \"public\".\"create_user_usage_row\"() TO \"anon\";\nGRANT ALL ON FUNCTION \"public\".\"create_user_usage_row\"() TO \"authenticated\";\nGRANT ALL ON FUNCTION \"public\".\"create_user_usage_row\"() TO \"service_role\";\n\n\n\nGRANT ALL ON FUNCTION \"public\".\"handle_new_auth_user\"() TO \"anon\";\nGRANT ALL ON FUNCTION \"public\".\"handle_new_auth_user\"() TO \"authenticated\";\nGRANT ALL ON FUNCTION \"public\".\"handle_new_auth_user\"() TO \"service_role\";\n\n\n\nGRANT ALL ON FUNCTION \"public\".\"search_documents_by_similarity\"(\"query_embedding\" \"public\".\"vector\", \"match_count\" integer, \"filter\" \"jsonb\") TO \"anon\";\nGRANT ALL ON FUNCTION \"public\".\"search_documents_by_similarity\"(\"query_embedding\" \"public\".\"vector\", \"match_count\" integer, \"filter\" \"jsonb\") TO \"authenticated\";\nGRANT ALL ON FUNCTION \"public\".\"search_documents_by_similarity\"(\"query_embedding\" \"public\".\"vector\", \"match_count\" integer, \"filter\" \"jsonb\") TO \"service_role\";\n\n\n\nGRANT ALL ON FUNCTION \"public\".\"search_documents_by_similarity\"(\"query_embedding\" \"public\".\"vector\", \"match_count\" integer, \"owner_uuid\" \"uuid\", \"metadata_filter\" \"jsonb\") TO \"anon\";\nGRANT ALL ON FUNCTION \"public\".\"search_documents_by_similarity\"(\"query_embedding\" \"public\".\"vector\", \"match_count\" integer, \"owner_uuid\" \"uuid\", \"metadata_filter\" \"jsonb\") TO \"authenticated\";\nGRANT ALL ON FUNCTION \"public\".\"search_documents_by_similarity\"(\"query_embedding\" \"public\".\"vector\", \"match_count\" integer, \"owner_uuid\" \"uuid\", \"metadata_filter\" \"jsonb\") TO \"service_role\";\n\n\n\nGRANT ALL ON FUNCTION \"public\".\"search_gee_datasets_ft\"(\"query\" \"text\") TO \"anon\";\nGRANT ALL ON FUNCTION \"public\".\"search_gee_datasets_ft\"(\"query\" \"text\") TO \"authenticated\";\nGRANT ALL ON FUNCTION \"public\".\"search_gee_datasets_ft\"(\"query\" \"text\") TO \"service_role\";\n\n\n\nGRANT ALL ON FUNCTION \"public\".\"update_user_usage_docs_count\"() TO \"anon\";\nGRANT ALL ON FUNCTION \"public\".\"update_user_usage_docs_count\"() TO \"authenticated\";\nGRANT ALL ON FUNCTION \"public\".\"update_user_usage_docs_count\"() TO \"service_role\";\n\n\n\nGRANT ALL ON TABLE \"public\".\"chats\" TO \"anon\";\nGRANT ALL ON TABLE \"public\".\"chats\" TO \"authenticated\";\nGRANT ALL ON TABLE \"public\".\"chats\" TO \"service_role\";\n\n\n\nGRANT ALL ON TABLE \"public\".\"document_files\" TO \"anon\";\nGRANT ALL ON TABLE \"public\".\"document_files\" TO \"authenticated\";\nGRANT ALL ON TABLE \"public\".\"document_files\" TO \"service_role\";\n\n\n\nGRANT ALL ON SEQUENCE \"public\".\"document_files_id_seq\" TO \"anon\";\nGRANT ALL ON SEQUENCE \"public\".\"document_files_id_seq\" TO \"authenticated\";\nGRANT ALL ON SEQUENCE \"public\".\"document_files_id_seq\" TO \"service_role\";\n\n\n\nGRANT ALL ON TABLE \"public\".\"drafted_reports\" TO \"anon\";\nGRANT ALL ON TABLE \"public\".\"drafted_reports\" TO \"authenticated\";\nGRANT ALL ON TABLE \"public\".\"drafted_reports\" TO \"service_role\";\n\n\n\nGRANT ALL ON TABLE \"public\".\"embeddings\" TO \"anon\";\nGRANT ALL ON TABLE \"public\".\"embeddings\" TO \"authenticated\";\nGRANT ALL ON TABLE \"public\".\"embeddings\" TO \"service_role\";\n\n\n\nGRANT ALL ON SEQUENCE \"public\".\"embeddings_id_seq\" TO \"anon\";\nGRANT ALL ON SEQUENCE \"public\".\"embeddings_id_seq\" TO \"authenticated\";\nGRANT ALL ON SEQUENCE \"public\".\"embeddings_id_seq\" TO \"service_role\";\n\n\n\nGRANT ALL ON TABLE \"public\".\"gee_datasets\" TO \"anon\";\nGRANT ALL ON TABLE \"public\".\"gee_datasets\" TO \"authenticated\";\nGRANT ALL ON TABLE \"public\".\"gee_datasets\" TO \"service_role\";\n\n\n\nGRANT ALL ON SEQUENCE \"public\".\"gee_datasets_id_seq\" TO \"anon\";\nGRANT ALL ON SEQUENCE \"public\".\"gee_datasets_id_seq\" TO \"authenticated\";\nGRANT ALL ON SEQUENCE \"public\".\"gee_datasets_id_seq\" TO \"service_role\";\n\n\n\nGRANT ALL ON TABLE \"public\".\"messages\" TO \"anon\";\nGRANT ALL ON TABLE \"public\".\"messages\" TO \"authenticated\";\nGRANT ALL ON TABLE \"public\".\"messages\" TO \"service_role\";\n\n\n\nGRANT ALL ON TABLE \"public\".\"user_roles\" TO \"anon\";\nGRANT ALL ON TABLE \"public\".\"user_roles\" TO \"authenticated\";\nGRANT ALL ON TABLE \"public\".\"user_roles\" TO \"service_role\";\n\n\n\nGRANT ALL ON TABLE \"public\".\"user_usage\" TO \"anon\";\nGRANT ALL ON TABLE \"public\".\"user_usage\" TO \"authenticated\";\nGRANT ALL ON TABLE \"public\".\"user_usage\" TO \"service_role\";\n\n\n\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON SEQUENCES  TO \"postgres\";\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON SEQUENCES  TO \"anon\";\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON SEQUENCES  TO \"authenticated\";\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON SEQUENCES  TO \"service_role\";\n\n\n\n\n\n\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON FUNCTIONS  TO \"postgres\";\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON FUNCTIONS  TO \"anon\";\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON FUNCTIONS  TO \"authenticated\";\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON FUNCTIONS  TO \"service_role\";\n\n\n\n\n\n\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON TABLES  TO \"postgres\";\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON TABLES  TO \"anon\";\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON TABLES  TO \"authenticated\";\nALTER DEFAULT PRIVILEGES FOR ROLE \"postgres\" IN SCHEMA \"public\" GRANT ALL ON TABLES  TO \"service_role\";\n\n\n\n\n\n\nRESET ALL;\n"
  },
  {
    "path": "features/charts/components/charts-display.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect, useState } from \"react\";\nimport selectChartType from \"@/features/charts/utils/select-chart-type\";\nimport usePlotReadyDataFromVectorLayerStore from \"@/features/maps/stores/plots-stores/usePlotReadyFromVectorLayerStore\";\nimport Chart from \"./charts\";\nimport useLayerSelectionStore from \"@/features/maps/stores/use-layer-selection-store\";\nimport useMapLayersStore from \"@/features/maps/stores/use-map-layer-store\";\nimport isEqual from \"lodash.isequal\";\nimport useFunctionStore from \"@/features/maps/stores/use-function-store\";\nimport useMapDisplayStore from \"@/features/maps/stores/use-map-display-store\";\n\n/////////////// *****For Stats Display***** //////////////////////\ninterface ChartStatsDisplayProps {\n  layerName: string;\n  mapStats: any;\n  uhiMetrics?: any;\n  legendConfig: any;\n  functionType: string;\n}\nexport const ChartStatsDisplay = ({\n  layerName,\n  mapStats,\n  functionType,\n  legendConfig,\n}: ChartStatsDisplayProps) => {\n  const handleSelectChartType = selectChartType();\n  const [palette, setPalette] = useState<any>(null);\n\n  if (layerName && mapStats) {\n    const chartType = handleSelectChartType(functionType);\n    // Set palette based on the chart type if necessary\n    if (\n      chartType.queryChart === \"barChartPercentage\" ||\n      chartType.queryChart === \"stackedBarChartForLandcoverChangeMaps\"\n    ) {\n      const newPalette = {\n        labels: legendConfig?.labelNamesStats || legendConfig?.labelNames,\n        palette: legendConfig?.statsPalette || legendConfig?.palette,\n      };\n\n      if (!isEqual(palette, newPalette)) {\n        setPalette(newPalette);\n      }\n    } else {\n      if (palette !== null) {\n        setPalette(null);\n      }\n    }\n  }\n\n  return (\n    <div\n      className=\"flex flex-col justify-center items-center h-full w-full overflow-hidden gap-2 bg-secondary/20 rounded-xl border border-stone-300 dark:border-stone-600\n    \"\n    >\n      {(() => {\n        const chartType = handleSelectChartType(functionType);\n\n        switch (chartType.statsChart) {\n          case \"boxplotTimeseries\":\n            return (\n              <Chart\n                data={mapStats}\n                chartType=\"boxplotTimeseries\"\n                chartTitle={layerName}\n                palette={palette}\n              />\n            );\n          case \"pieChartPercentage\":\n            return (\n              <Chart\n                data={mapStats}\n                chartType=\"pieChartStats\"\n                chartTitle={layerName}\n                palette={palette}\n              />\n            );\n          case \"barChartStats\":\n            return (\n              <Chart\n                data={mapStats}\n                chartType=\"barChartStats\"\n                chartTitle={layerName}\n                palette={palette}\n              />\n            );\n          case \"stackedBarChartStats\":\n            return (\n              <Chart\n                data={mapStats}\n                chartType=\"stackedBarChartStats\"\n                chartTitle={layerName}\n                palette={palette}\n              />\n            );\n          case \"stackedPercentageBarChartStats\":\n            return (\n              <Chart\n                data={mapStats}\n                chartType=\"stackedPercentageBarChartStats\"\n                chartTitle={layerName}\n                palette={palette}\n              />\n            );\n          case \"combinedStackedBarChartStats\":\n            return (\n              <Chart\n                data={mapStats}\n                chartType=\"combinedStackedBarChartStats\"\n                chartTitle={layerName}\n                palette={palette}\n              />\n            );\n          case \"stackedBarChartForLandcoverChangeMaps\":\n            return (\n              <Chart\n                data={{\n                  year1Distribution: mapStats.year1Distribution,\n                  year2Distribution: mapStats.year2Distribution,\n                }}\n                chartType=\"stackedBarChartForLandcoverChangeMaps\"\n                chartTitle={layerName}\n                palette={palette}\n              />\n            );\n          default:\n            return <div />;\n        }\n      })()}\n    </div>\n  );\n};\n\nChartStatsDisplay.displayName = \"ChartStatsDisplay\";\n\n/////////////// *****For Query Display***** //////////////////////\nexport const ChartQueryDisplay = React.memo(() => {\n  const handleSelectChartType = selectChartType();\n  const [selectedChartType, setSelectedChartType] = useState(\"\");\n  const [selectedChartUnit, setSelectedChartUnit] = useState(\"\");\n  const {\n    plotReadyDataForSelectedAreaOnMap,\n    setPlotReadyDataForSelectedAreaOnMap,\n  } = usePlotReadyDataFromVectorLayerStore();\n  const { removeLayerSignal } = useMapLayersStore();\n  const selectedRasterLayer = useLayerSelectionStore(\n    (state) => state.selectedRasterLayer\n  );\n  const getFunctionConfig = useFunctionStore(\n    (state) => state.getFunctionConfig\n  );\n  const [queryIsReady, setQueryIsReady] = useState(false);\n  const [data, setData] = useState<any | null>(null);\n\n  const [isSelectedAreaOnMapUpdated, setIsSelectedAreaOnMapUpdated] =\n    useState(false);\n\n  const [palette, setPalette] = useState<any>(null);\n\n  const toggleMapChartPanel = useMapDisplayStore(\n    (state) => state.toggleMapChartPanel\n  );\n  const isMapChartPanelOpen = useMapDisplayStore(\n    (state) => state.isMapChartPanelOpen\n  );\n\n  useEffect(() => {\n    if (isMapChartPanelOpen) {\n      setQueryIsReady(true);\n    } else {\n      setQueryIsReady(false);\n      setData(null);\n    }\n  }, [isMapChartPanelOpen]);\n\n  // For Selected Point on Map\n  useEffect(() => {\n    if (\n      plotReadyDataForSelectedAreaOnMap.data &&\n      selectedRasterLayer.layerName\n    ) {\n      const functionConfig = getFunctionConfig(selectedRasterLayer.layerName);\n      const legendConfig = functionConfig?.legendConfig;\n\n      setData(plotReadyDataForSelectedAreaOnMap.data);\n      const functionType = functionConfig?.functionType;\n      const newChartType = handleSelectChartType(functionType as string);\n      setPalette({\n        labels: legendConfig?.labelNamesStats || legendConfig?.labelNames,\n        palette: legendConfig?.statsPalette || legendConfig?.palette,\n      });\n      setSelectedChartType(newChartType.queryChart);\n      setSelectedChartUnit(newChartType?.unit || \"\");\n\n      if (!isMapChartPanelOpen) {\n        toggleMapChartPanel();\n      }\n\n      if (isSelectedAreaOnMapUpdated) {\n        setIsSelectedAreaOnMapUpdated(false);\n      }\n    }\n  }, [isSelectedAreaOnMapUpdated, plotReadyDataForSelectedAreaOnMap.data]);\n\n  useEffect(() => {\n    if (removeLayerSignal === selectedRasterLayer.layerName) {\n      setPlotReadyDataForSelectedAreaOnMap(null, \"\");\n      setData(null);\n    }\n  }, [removeLayerSignal]);\n\n  return (\n    <>\n      {(() => {\n        if (\n          (data?.outputsFromMap?.monoTemporalQueryValues?.length > 0 ||\n            data?.outputsFromMap?.biTemporalQueryValues?.length > 0 ||\n            (data?.outputsFromMap?.monoTemporalQueryValues &&\n              Object.values(data?.outputsFromMap?.monoTemporalQueryValues).some(\n                (value) => value != null\n              )) ||\n            data?.outputsFromMap?.length > 0) &&\n          queryIsReady\n        ) {\n          return (\n            <div className=\"flex h-full w-full overflow-hidden \">\n              <Chart\n                data={\n                  data?.outputsFromMap?.biTemporalQueryValues?.length > 0\n                    ? {\n                        year1Distribution:\n                          data?.outputsFromMap?.biTemporalQueryValues[0]\n                            .year1Distribution,\n                        year2Distribution:\n                          data?.outputsFromMap?.biTemporalQueryValues[0]\n                            .year2Distribution,\n                      }\n                    : data?.outputsFromMap\n                }\n                chartType={selectedChartType}\n                chartTitle={\"Aggregated Query\"}\n                palette={palette}\n                chartUnit={selectedChartUnit}\n              />\n            </div>\n          );\n        } else if (data?.outputsFromMap?.length === 0 && queryIsReady) {\n          return (\n            <div className=\"flex items-center justify-center w-full h-full\">\n              <p>No Data Available for this location</p>\n            </div>\n          );\n        }\n      })()}\n    </>\n  );\n});\n\nChartQueryDisplay.displayName = \"ChartQueryDisplay\";\n\n/////////////// *****For Time-Series Query Display***** //////////////////////\n\nexport const ChartTimeseriesQueryDisplay = React.memo(() => {\n  const handleSelectChartType = selectChartType();\n  const [selectedChartType, setSelectedChartType] = useState(\"\");\n  const [selectedChartUnit, setSelectedChartUnit] = useState(\"\");\n  const {\n    plotReadyDataForSelectedAreaOnMap,\n    setPlotReadyDataForSelectedAreaOnMap,\n  } = usePlotReadyDataFromVectorLayerStore();\n  const { removeLayerSignal } = useMapLayersStore();\n  const selectedRasterLayer = useLayerSelectionStore(\n    (state) => state.selectedRasterLayer\n  );\n  const [queryIsReady, setQueryIsReady] = useState(false);\n\n  const [data, setData] = useState<any | null>(null);\n\n  const [isSelectedAreaOnMapUpdated, setIsSelectedAreaOnMapUpdated] =\n    useState(false);\n\n  const isMapChartPanelOpen = useMapDisplayStore(\n    (state) => state.isMapChartPanelOpen\n  );\n\n  const [palette, setPalette] = useState<any>(null);\n  const getFunctionConfig = useFunctionStore(\n    (state) => state.getFunctionConfig\n  );\n\n  // For Selected Point on Map\n  useEffect(() => {\n    if (\n      plotReadyDataForSelectedAreaOnMap.data &&\n      selectedRasterLayer.layerName\n    ) {\n      setData(plotReadyDataForSelectedAreaOnMap.data);\n      const functionConfig = getFunctionConfig(selectedRasterLayer.layerName);\n      const legendConfig = functionConfig?.legendConfig;\n\n      setData(plotReadyDataForSelectedAreaOnMap.data);\n      const functionType = functionConfig?.functionType;\n      const newChartType = handleSelectChartType(functionType as string);\n      setPalette({\n        labels: legendConfig.labelNames,\n        palette: legendConfig.palette,\n      });\n      setSelectedChartType(\n        newChartType.customTimeseriesChart || \"timeSeriesNumericalQuery\"\n      );\n      setSelectedChartUnit(newChartType.unit || \"\");\n\n      if (isSelectedAreaOnMapUpdated) {\n        setIsSelectedAreaOnMapUpdated(false);\n      }\n    }\n  }, [isSelectedAreaOnMapUpdated, plotReadyDataForSelectedAreaOnMap.data]);\n\n  useEffect(() => {\n    if (removeLayerSignal === selectedRasterLayer.layerName) {\n      setPlotReadyDataForSelectedAreaOnMap(null, \"\");\n      setData(null);\n    }\n  }, [removeLayerSignal]);\n\n  useEffect(() => {\n    if (isMapChartPanelOpen) {\n      setQueryIsReady(true);\n    } else {\n      setQueryIsReady(false);\n      setData(null);\n    }\n  }, [isMapChartPanelOpen]);\n\n  if (!data?.outputsFromMap?.timeSeriesQueryValues) {\n    return;\n  }\n\n  return (\n    <>\n      {(() => {\n        if (\n          (data?.outputsFromMap?.timeSeriesQueryValues?.length > 0 ||\n            Object.values(data?.outputsFromMap?.timeSeriesQueryValues).some(\n              (value) => Array.isArray(value) && value.length > 0\n            )) &&\n          queryIsReady\n        ) {\n          return (\n            <div className=\"flex h-full w-full overflow-hidden\">\n              <Chart\n                data={data.outputsFromMap.timeSeriesQueryValues}\n                chartType={selectedChartType}\n                chartTitle=\"Time Series Query\"\n                palette={palette}\n                chartUnit={selectedChartUnit}\n              />\n            </div>\n          );\n        } else if (data?.outputsFromMap?.length === 0 && queryIsReady) {\n          return (\n            <div className=\"flex items-center justify-center w-full h-full\">\n              <p>No Data Available for this location</p>\n            </div>\n          );\n        }\n      })()}\n    </>\n  );\n});\n\nChartTimeseriesQueryDisplay.displayName = \"ChartTimeseriesQueryDisplay\";\n"
  },
  {
    "path": "features/charts/components/charts.tsx",
    "content": "\"use client\";\nimport React, { useRef, useEffect, useMemo } from \"react\";\nimport * as echarts from \"echarts\";\nimport { chartColors } from \"@/custom-configs/charts-config\";\nimport EChartsReact from \"echarts-for-react\";\nimport { useTheme } from \"next-themes\";\n\nconst getHslVariable = (variableName: any) => {\n  const value = getComputedStyle(document.documentElement)\n    .getPropertyValue(variableName)\n    .trim();\n  return `hsl(${value})`;\n};\n\ninterface ChartProps {\n  chartType: string;\n  data: any;\n  chartTitle: string;\n  palette?: any;\n  chartUnit?: string;\n  extraInfo?: string;\n}\n\nexport default function Chart({\n  chartType,\n  data,\n  chartTitle,\n  palette,\n  chartUnit,\n  extraInfo,\n}: ChartProps) {\n  const { theme } = useTheme();\n  const labelColor =\n    theme === \"dark\"\n      ? getHslVariable(\"--foreground\") || \"#e5e7eb\" // Fallback to light gray\n      : getHslVariable(\"--secondary-foreground\") || \"#1f2937\"; // Fallback to dark gray\n\n  echarts.registerTheme(\"custom_dark\", {\n    labelColor: labelColor,\n  });\n\n  switch (chartType) {\n    case \"scoresBarChart\":\n      return <ScoresBarChart data={data} chartTitle={chartTitle} />;\n    case \"timeSeries\":\n      return (\n        <TimeSeriesChart\n          data={data}\n          chartTitle={chartTitle}\n          theme={theme}\n          labelColor={labelColor}\n          chartUnit={chartUnit}\n        />\n      );\n\n    case \"stackedBarChartForLandcoverChangeMaps\":\n      return (\n        <StackedBarChartForLandcoverChangeMap\n          data={data}\n          chartTitle={chartTitle}\n          theme={theme}\n          palette={palette}\n        />\n      );\n\n    case \"timeSeriesNumericalQuery\":\n      return (\n        <TimeSeriesNumericalQueryChart\n          data={data}\n          chartTitle={chartTitle}\n          chartUnit={chartUnit}\n          theme={theme}\n          labelColor={labelColor}\n        />\n      );\n    case \"dualTimeSeriesChart\":\n      return (\n        <DualTimeSeriesNumericalQueryChart\n          data={data}\n          chartTitle={chartTitle}\n          theme={theme}\n          labelColor={labelColor}\n        />\n      );\n    case \"barChartStats\":\n      return (\n        <BarChartStatistics\n          data={data}\n          chartTitle={chartTitle}\n          theme={theme}\n          labelColor={labelColor}\n        />\n      );\n    case \"stackedBarChartStats\":\n      return (\n        <StackedBarChartStatistics\n          data={data}\n          chartTitle={chartTitle}\n          theme={theme}\n          labelColor={labelColor}\n        />\n      );\n    case \"stackedPercentageBarChartStats\":\n      return (\n        <StackedPercentageBarChartStatistics\n          data={data}\n          chartTitle={chartTitle}\n          theme={theme}\n          labelColor={labelColor}\n        />\n      );\n    case \"combinedStackedBarChartStats\":\n      return (\n        <CombinedStackedBarChartStatistics\n          data={data}\n          chartTitle={chartTitle}\n          theme={theme}\n          labelColor={labelColor}\n        />\n      );\n    case \"boxplotTimeseries\":\n      return (\n        <BoxPlotTimeseriesChart\n          data={data}\n          chartTitle={chartTitle}\n          extraInfo={extraInfo}\n          theme={theme}\n          labelColor={labelColor}\n        />\n      );\n    case \"barChartPercentage\":\n      return (\n        <BarChartPercentage\n          data={data}\n          chartTitle={chartTitle}\n          palette={palette}\n          theme={theme}\n          labelColor={labelColor}\n        />\n      );\n    case \"barChartNumerical\":\n      return (\n        <BarChartNumerical\n          data={data}\n          chartTitle={chartTitle}\n          theme={theme}\n          labelColor={labelColor}\n          chartUnit={chartUnit}\n        />\n      );\n    case \"dualBarChartNumerical\":\n      return (\n        <DualBarChartNumerical\n          data={data}\n          chartTitle={chartTitle}\n          theme={theme}\n          labelColor={labelColor}\n        />\n      );\n    case \"pieChartStats\":\n      return (\n        <PieChartStats\n          data={data}\n          chartTitle={chartTitle}\n          palette={palette}\n          theme={theme}\n          labelColor={labelColor}\n        />\n      );\n    case \"histogram\":\n      return <HistogramChart data={data} />;\n    default:\n      return <div></div>;\n  }\n}\n\nconst extractChartName = (chartTitle: string) => {\n  const regex = /-\\d+/;\n  const match = chartTitle.match(regex);\n  if (match) {\n    return chartTitle.substring(0, chartTitle.indexOf(match[0]));\n  } else {\n    return chartTitle;\n  }\n};\n\ninterface TimeseriesNumericalQueryChartProps {\n  data: { year: number; value: number }[];\n  chartTitle: string;\n}\n\ntype ScoreData = { name: string; score: number | string };\n\ntype ScoresBarChartProps = {\n  data: ScoreData[];\n  chartTitle: string;\n};\n\nexport const ScoresBarChart: React.FC<ScoresBarChartProps> = ({\n  data,\n  chartTitle,\n}) => {\n  if (!data) {\n    data = [\n      { name: \"Surface Materials\", score: \"N/A\" },\n      { name: \"Vegetation\", score: \"N/A\" },\n      { name: \"Traffic\", score: \"N/A\" },\n      { name: \"Impervious\", score: \"N/A\" },\n      { name: \"Shading\", score: \"N/A\" },\n    ];\n  }\n\n  const chartRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (chartRef.current) {\n      const chartInstance = echarts.init(chartRef.current);\n\n      const option = {\n        title: {\n          text: chartTitle,\n          left: \"center\",\n          textStyle: {\n            color: \"#B2BEB5\",\n            fontSize: 20,\n            fontWeight: \"bold\",\n          },\n        },\n        tooltip: {\n          trigger: \"item\",\n          formatter: function (params: any) {\n            return `${params.name}: ${params.value}`;\n          },\n        },\n        xAxis: {\n          show: false, // Hide the x-axis\n        },\n        yAxis: {\n          type: \"category\",\n          data: data.map((d) => d.name), // Use only the names for the labels\n          axisLine: {\n            lineStyle: {\n              color: \"#B2BEB5\",\n              width: 2,\n            },\n          },\n          axisLabel: {\n            color: \"#B2BEB5\",\n            // fontWeight: \"bold\",\n            overflow: \"break\", // Prevent overflow\n            fontSize: 14, // Adjust font size to fit labels\n          },\n          splitLine: { show: false }, // Remove grid lines\n        },\n        series: [\n          {\n            type: \"bar\",\n            data: data.map((d) => (d.score === \"N/A\" ? 0 : d.score)), // Use scores for the values, 0 for \"N/A\"\n            label: {\n              show: true,\n              position: \"right\", // Move labels to the right\n              formatter: function (params: any) {\n                return data[params.dataIndex].score;\n              },\n              color: \"white\",\n              fontSize: 14,\n              // fontWeight: \"bold\",\n            },\n            itemStyle: {\n              borderColor: \"#16a34a\",\n              borderWidth: 2,\n              color: \"rgba(0, 0, 0, 0)\", // Fully transparent fill\n            },\n          },\n        ],\n        backgroundColor: \"transparent\",\n        grid: {\n          left: 130, // Increase left margin to make space for y-axis labels\n          bottom: 20, // Adjust bottom margin\n          top: 60, // Adjust top margin to make space for title\n          right: 20,\n        },\n      };\n\n      chartInstance.setOption(option);\n\n      return () => {\n        chartInstance.dispose();\n      };\n    }\n  }, [data, chartTitle]);\n\n  return <div ref={chartRef} style={{ width: \"100%\", height: \"100%\" }}></div>;\n};\n\n////////////////////////////////////////\n\ninterface TimeseriesChartProps {\n  data: { year: number; value: number }[];\n  chartTitle: string;\n  chartUnit: string | undefined;\n  theme: string | undefined;\n  labelColor: string;\n}\n\nexport const TimeSeriesChart = ({\n  data,\n  chartTitle,\n  chartUnit,\n  theme,\n}: TimeseriesChartProps) => {\n  // Prepare the chart option using useMemo for performance optimization\n  const option = useMemo(() => {\n    const validData = data.filter((point) => point.value !== 0);\n    const zeroValueData = data.filter((point) => point.value === 0);\n    const gridLineColor = \"rgba(255, 255, 255, 0.2)\";\n\n    // Replace this with your actual color palette\n    const colorPalette = [\"#5470C6\", \"#EE6666\"]; // Example colors\n\n    if (validData.length === 1) {\n      const singlePoint = validData[0];\n\n      // Prepare series data\n      const seriesData = [\n        { value: [0, singlePoint.value], symbol: \"none\" },\n        { value: [1, singlePoint.value], symbol: \"circle\" },\n      ];\n\n      const series = {\n        type: \"line\",\n        smooth: false,\n        lineStyle: {\n          color: \"#16a34a\",\n        },\n        itemStyle: {\n          // color: \"#72d60f\",\n        },\n        symbolSize: 10,\n        data: seriesData,\n      };\n\n      return {\n        title: {\n          text: chartTitle,\n          left: \"center\",\n          top: 10,\n          textStyle: {\n            // color: \"#E5E7EB\",\n            fontSize: 16,\n            fontWeight: \"bold\",\n          },\n        },\n        tooltip: {\n          trigger: \"axis\",\n          formatter: function (params: any) {\n            const index = params[0].dataIndex;\n            if (index === 1) {\n              const value = params[0].value[1];\n              return `Year: ${singlePoint.year}<br/>Value: ${value} ${\n                chartUnit || \"\"\n              }`;\n            }\n            return \"\";\n          },\n        },\n        xAxis: {\n          type: \"value\",\n          min: 0,\n          max: 1,\n          interval: 1,\n          axisLine: {\n            lineStyle: {\n              // color: \"#B2BEB5\",\n              width: 2,\n            },\n          },\n          axisLabel: {\n            formatter: function (value: any) {\n              if (value === 1) return singlePoint.year.toString();\n              return \"\";\n            },\n            // color: \"#B2BEB5\",\n            fontSize: 12,\n          },\n          splitLine: {\n            show: false,\n          },\n          axisTick: {\n            show: false,\n          },\n        },\n        yAxis: {\n          type: \"value\",\n          min: singlePoint.value - 1,\n          max: singlePoint.value + 1,\n          name: chartUnit || \"\",\n          nameTextStyle: { fontSize: 14, align: \"center\" },\n          axisLine: {\n            lineStyle: {\n              // color: \"#B2BEB5\",\n              width: 2,\n            },\n          },\n          axisLabel: {\n            // color: \"#B2BEB5\",\n            fontSize: 12,\n          },\n          splitLine: {\n            show: true,\n            lineStyle: {\n              color: gridLineColor,\n            },\n          },\n        },\n        series: [series],\n        backgroundColor: \"transparent\",\n        grid: {\n          left: \"15%\",\n          right: \"10%\",\n          top: 60,\n          bottom: 70,\n        },\n      };\n    } else {\n      // Original code for multiple data points\n      const years = data.map((d) => d.year);\n      let minYear = Math.min(...years);\n      let maxYear = Math.max(...years);\n\n      const values = validData.map((d) => d.value);\n      let minValue = Math.min(...values) - 1;\n      let maxValue = Math.max(...values) + 1;\n\n      const seriesData = validData.map((point) => [point.year, point.value]);\n\n      const series = {\n        type: \"line\",\n        smooth: false,\n        lineStyle: {\n          color: colorPalette[0],\n        },\n        itemStyle: {\n          // color: \"#72d60f\",\n        },\n        symbolSize: 10,\n        data: seriesData,\n        markPoint: {\n          data: zeroValueData.map((point) => ({\n            coord: [point.year, point.value],\n            name: \"Low-Quality LST\",\n            value: \"Low-Quality LST\",\n            itemStyle: {\n              color: \"red\",\n            },\n            label: {\n              color: \"red\",\n              fontSize: 12,\n              formatter: \"Low-Quality LST\",\n              position: \"top\",\n            },\n          })),\n        },\n      };\n\n      return {\n        title: {\n          text: chartTitle,\n          left: \"center\",\n          top: 10,\n          textStyle: {\n            // color: \"#E5E7EB\",\n            fontSize: 16,\n            fontWeight: \"bold\",\n          },\n        },\n        tooltip: {\n          trigger: \"axis\",\n          formatter: function (params: any) {\n            const year = params[0].value[0];\n            const value = params[0].value[1];\n            return `Year: ${year}<br/>Value: ${value} ${chartUnit || \"\"}`;\n          },\n        },\n        xAxis: {\n          type: \"value\",\n          name: \"\",\n          min: minYear,\n          max: maxYear,\n          nameLocation: \"middle\",\n          nameGap: 25,\n          nameTextStyle: { color: \"#B2BEB5\", fontSize: 14 },\n          axisLine: {\n            lineStyle: {\n              // color: \"#B2BEB5\",\n              width: 2,\n            },\n          },\n          axisLabel: {\n            // color: \"#B2BEB5\",\n            fontSize: 12,\n            formatter: (value: any) => value.toString(),\n          },\n          splitLine: {\n            show: true,\n            lineStyle: {\n              color: gridLineColor,\n            },\n          },\n          axisTick: {\n            alignWithLabel: true,\n          },\n          interval: 1,\n          offset: 10,\n        },\n        yAxis: {\n          type: \"value\",\n          name: chartUnit || \"\",\n          min: minValue,\n          max: maxValue,\n          nameTextStyle: { fontSize: 14 },\n          axisLine: {\n            lineStyle: {\n              // color: \"#B2BEB5\",\n              width: 2,\n            },\n          },\n          axisLabel: {\n            // color: \"#B2BEB5\",\n            fontSize: 12,\n          },\n          splitLine: {\n            show: true,\n            lineStyle: {\n              color: gridLineColor,\n            },\n          },\n        },\n        series: [series],\n        backgroundColor: \"transparent\",\n        grid: {\n          top: 60,\n          bottom: 40,\n          left: \"13%\",\n          right: \"8%\",\n        },\n      };\n    }\n  }, [data, chartTitle, chartUnit]);\n\n  return (\n    <div className=\"py-2\" style={{ width: \"100%\", height: \"100%\" }}>\n      <EChartsReact\n        theme={theme === \"dark\" ? \"dark\" : \"light\"}\n        option={option}\n        style={{ width: \"100%\", height: \"100%\" }}\n        notMerge={true}\n        lazyUpdate={true}\n      />\n    </div>\n  );\n};\n\n////////////////////////////////////////\n// Time-series chart for query results\nexport const TimeSeriesNumericalQueryChart: React.FC<{\n  data: any[];\n  chartTitle: string;\n  chartUnit: string | undefined;\n  theme: string | undefined;\n  labelColor: string;\n}> = ({ data, chartTitle, chartUnit, theme, labelColor }) => {\n  // Find the first non-undefined field (e.g., CO, NO2)\n  const dataField = Object.keys(\n    data.find((d) =>\n      Object.keys(d).some((key) => key !== \"date\" && d[key] !== undefined)\n    ) || {}\n  ).find((key) => key !== \"date\" && data.some((d) => d[key] !== undefined));\n\n  // Filter out undefined values for the selected data field\n  const validData = data.filter(\n    (d) => (dataField && d[dataField] !== undefined) || d.values !== undefined\n  );\n\n  // Prepare series data (with sequential index on x-axis)\n  const seriesData = validData.map((d, index) => [\n    index,\n    (dataField && d[dataField]) || d.values,\n  ]);\n\n  const valuesArray = validData.map(\n    (d) => (dataField && d[dataField]) || d.values\n  );\n\n  const minValue = Math.min(...valuesArray);\n  const maxValue = Math.max(...valuesArray);\n\n  // Add some margin (e.g., 5% of the data range)\n  const margin = (maxValue - minValue) * 0.05;\n  const adjustedMinValue = minValue - margin;\n  const adjustedMaxValue = maxValue + margin;\n\n  // Set up eCharts options\n  const option = {\n    title: {\n      text: chartTitle,\n      top: 10,\n      left: \"center\",\n      show: true,\n      textStyle: {\n        // color: \"#E5E7EB\",\n        fontSize: 16,\n        fontWeight: \"bold\",\n      },\n    },\n    tooltip: {\n      trigger: \"axis\",\n      formatter: function (params: any) {\n        const index = params[0].value[0];\n        const value = params[0].value[1].toExponential(4);\n        const date = validData[index].date;\n        return `Date: ${date}<br/>Value: ${value}${\n          chartUnit ? ` ${chartUnit}` : \"\"\n        }`;\n      },\n    },\n    xAxis: {\n      type: \"category\",\n      name: \" \",\n      data: validData.map((_, index) => \" \"), // Use index for unique x-axis\n      axisLine: {\n        lineStyle: {\n          color: \"#B2BEB5\",\n          width: 2,\n        },\n      },\n      axisLabel: {\n        // color: \"#B2BEB5\",\n        fontSize: 12,\n      },\n      splitLine: {\n        show: true,\n        lineStyle: {\n          color: \"rgba(255, 255, 255, 0.2)\",\n        },\n      },\n      axisTick: {\n        alignWithLabel: true,\n        show: false,\n      },\n    },\n    yAxis: {\n      type: \"value\",\n      name: chartUnit ? `${dataField} (${chartUnit})` : dataField,\n      min: adjustedMinValue.toExponential(2),\n      max: adjustedMaxValue.toExponential(2),\n      axisLine: {\n        lineStyle: {\n          // color: \"#B2BEB5\",\n          width: 2,\n        },\n      },\n      axisLabel: {\n        // color: \"#B2BEB5\",\n        fontSize: 12,\n        formatter: (value: number) => value.toExponential(2),\n      },\n      splitLine: {\n        show: true,\n        lineStyle: {\n          color: \"rgba(255, 255, 255, 0.2)\",\n        },\n      },\n      splitNumber: 3,\n    },\n    series: [\n      {\n        type: \"line\",\n        smooth: false,\n        lineStyle: {\n          // color: \"#16a34a\",\n        },\n        itemStyle: {\n          // color: \"#72d60f\",\n        },\n        symbolSize: 10,\n        data: seriesData,\n      },\n    ],\n    // backgroundColor: \"transparent\",\n    grid: {\n      left: \"5%\", // Adjust the left spacing\n      right: \"5%\", // Adjust the right spacing to center the chart\n      top: 60,\n      bottom: 0,\n      containLabel: true, // Ensures labels don't overflow the chart's boundary\n    },\n  };\n\n  return (\n    <div className=\"pt-2\" style={{ width: \"100%\", height: \"100%\" }}>\n      <EChartsReact\n        theme={theme === \"dark\" ? \"dark\" : \"light\"}\n        option={option}\n        style={{ height: \"100%\", width: \"100%\" }}\n        notMerge={true}\n        lazyUpdate={true}\n      />\n    </div>\n  );\n};\n\n////////////////////////////////////////\n// Dual Time-series chart for query results when two data fields are available (e.g., NO2 and CO)\n\nexport const DualTimeSeriesNumericalQueryChart: React.FC<{\n  data: { [gas: string]: any[] };\n  chartTitle: string;\n  theme: string | undefined;\n  labelColor: string;\n}> = ({ data, chartTitle, theme, labelColor }) => {\n  // Extract the gas names dynamically from the data object keys\n  const gases = Object.keys(data);\n\n  const colorPalette = chartColors[\"dualBarChartNumerical\"]; // Ensure this is defined\n\n  // Find all unique dates across all gases\n  const allDates = [\n    ...new Set(gases.flatMap((gas) => data[gas].map((d) => d.date))),\n  ];\n\n  // Prepare valid dates - only dates for which at least one gas has valid data\n  const validDates = allDates.filter((date) =>\n    gases.some((gas) => {\n      const gasData = data[gas].find((d) => d.date === date);\n      return gasData && !isNaN(parseFloat(gasData[gas]));\n    })\n  );\n\n  // Prepare series data filtered by valid dates\n  const seriesData = gases.map((gas, index) => {\n    const gasData = validDates.map((date) => {\n      const entry = data[gas].find((d) => d.date === date);\n      return entry ? parseFloat(entry[gas]) : NaN;\n    });\n\n    const validGasData = gasData.filter((value) => !isNaN(value));\n    const minValue = Math.min(...validGasData);\n    const maxValue = Math.max(...validGasData);\n    const margin = (maxValue - minValue) * 0.05;\n    return {\n      gas,\n      data: gasData,\n      minValue: minValue - margin,\n      maxValue: maxValue + margin,\n      yAxisIndex: index, // Index will map each gas to a separate y-axis\n    };\n  });\n\n  // Define the y-axes for the chart\n  const yAxes = seriesData.map((series, index) => ({\n    type: \"value\",\n    name: `${series.gas}`,\n    min: isFinite(series.minValue) ? series.minValue : null,\n    max: isFinite(series.maxValue) ? series.maxValue : null,\n    axisLine: {\n      lineStyle: {\n        color: index === 0 ? colorPalette[0] : colorPalette[1], // Different color for each y-axis\n        width: 2,\n      },\n    },\n    axisLabel: {\n      color: \"#B2BEB5\",\n      fontSize: 12,\n      formatter: (value: number) => value.toExponential(2),\n    },\n    splitLine: {\n      show: index === 0, // Show split lines only for the first y-axis\n      lineStyle: {\n        color: \"rgba(255, 255, 255, 0.2)\",\n      },\n    },\n    position: index === 0 ? \"left\" : \"right\", // First y-axis on the left, others on the right\n  }));\n\n  // Define the series for the chart\n  const series = seriesData.map((series, index) => ({\n    name: series.gas,\n    type: \"line\",\n    smooth: false,\n    lineStyle: {\n      color: index === 0 ? colorPalette[0] : colorPalette[1], // Different color for each line\n    },\n    itemStyle: {\n      color: index === 0 ? colorPalette[0] : colorPalette[1],\n    },\n    yAxisIndex: index,\n    symbolSize: 10,\n    data: series.data,\n  }));\n\n  const option = {\n    title: {\n      text: chartTitle,\n      top: 10,\n      left: \"center\",\n      textStyle: {\n        color: \"#E5E7EB\",\n        fontSize: 16,\n        fontWeight: \"bold\",\n      },\n    },\n    tooltip: {\n      trigger: \"axis\",\n      formatter: function (params: any) {\n        let tooltipText = `Date: ${params[0]?.axisValue || \"N/A\"}<br/>`;\n        params.forEach((param: any) => {\n          const value =\n            param.value && !isNaN(param.value)\n              ? parseFloat(param.value).toExponential(4)\n              : \"N/A\";\n          tooltipText += `${param.seriesName}: ${value}<br/>`;\n        });\n        return tooltipText;\n      },\n    },\n    legend: {\n      data: gases, // Show gas names dynamically in the legend\n      top: 40,\n      textStyle: {\n        color: \"#E5E7EB\",\n      },\n    },\n    xAxis: {\n      type: \"category\",\n      name: \"\",\n      data: validDates, // Use only valid dates\n      axisLine: {\n        show: false,\n        lineStyle: {\n          color: \"#B2BEB5\",\n          width: 2,\n        },\n      },\n      axisLabel: {\n        color: \"#B2BEB5\",\n        fontSize: 12,\n      },\n      axisTick: {\n        show: false, // Disable any ticks on the x-axis\n      },\n      splitLine: {\n        show: true,\n        lineStyle: {\n          color: \"rgba(255, 255, 255, 0.2)\",\n        },\n      },\n    },\n    yAxis: yAxes,\n    series: series,\n    backgroundColor: \"transparent\",\n    grid: {\n      left: \"5%\", // Adjust the left spacing\n      right: \"5%\", // Adjust the right spacing to center the chart\n      top: 70,\n      bottom: 10,\n      containLabel: true,\n    },\n  };\n\n  return (\n    <div className=\"pt-2\" style={{ width: \"100%\", height: \"100%\" }}>\n      <EChartsReact\n        theme={theme === \"dark\" ? \"dark\" : \"light\"}\n        option={option}\n        style={{ width: \"100%\", height: \"100%\" }}\n        notMerge={true}\n        lazyUpdate={true}\n      />\n    </div>\n  );\n};\n\n////////////////////////////////////////\n// Box plot component for time series statistics\nexport type TimeSeriesStats = {\n  [year: string]: {\n    Min: number;\n    Median: number; // You can replace this with Q2 if you have it\n    Mean: number;\n    Max: number;\n    Q1: number;\n    Q3: number;\n  };\n};\n\nexport type BoxPlotTimeseriesChartProps = {\n  data: TimeSeriesStats;\n  chartTitle: string;\n  extraInfo?: string;\n  theme: string | undefined;\n  labelColor: string;\n};\n\nexport const BoxPlotTimeseriesChart = React.memo<BoxPlotTimeseriesChartProps>(\n  ({ data, chartTitle, extraInfo, theme, labelColor }) => {\n    const years = Object.keys(data);\n    const generateColors = (numColors: number) => {\n      const colors = [];\n      for (let i = 0; i < numColors; i++) {\n        const hue = (i * 360) / numColors;\n        // Adjusted saturation and lightness for better visibility on light background\n        colors.push(`hsl(${hue}, 80%, 45%)`);\n      }\n      return colors;\n    };\n    const colors = generateColors(years.length);\n\n    const candlestickData = years.map((year, index) => {\n      const stats = data[year];\n      return {\n        name: year,\n        value: [\n          Number(stats.Q1.toFixed(2)),\n          Number(stats.Q3.toFixed(2)),\n          Number(stats.Min.toFixed(2)),\n          Number(stats.Max.toFixed(2)),\n          Number(stats.Median.toFixed(2)),\n          Number(stats.Mean.toFixed(2)),\n        ],\n        itemStyle: {\n          color: colors[index],\n          // Changed border color to be more visible on light background\n          // borderColor: \"#666666\",\n        },\n      };\n    });\n\n    const option = {\n      backgroundColor: \"transparent\",\n      title: {\n        text: chartTitle,\n        top: 10,\n        left: \"center\",\n        // Updated text color for light background\n        // textStyle: { color: \"#1a1a1a\", fontSize: 16, fontWeight: \"bold\" },\n        textStyle: { fontSize: 16, fontWeight: \"bold\" },\n      },\n      tooltip: {\n        trigger: \"axis\",\n        axisPointer: {\n          type: \"cross\",\n          label: {\n            // Updated for better contrast\n            // backgroundColor: \"#1a1a1a\",\n            // color: \"#ffffff\",\n          },\n        },\n        formatter: (params: any) => {\n          const param = params[0];\n          const data = param.data.value;\n          return `\n          Year: ${param.name}<br/>\n          Min: ${data[3]}<br/>\n          Median: ${data[1]}<br/>\n          Mean: ${data[5]}<br/>\n          Max: ${data[4]}<br/>\n        `;\n        },\n      },\n      xAxis: {\n        type: \"category\",\n        data: years,\n        boundaryGap: true,\n        // axisLine: { lineStyle: { color: \"#333333\" } },\n        // axisLabel: { color: \"#333333\" },\n      },\n      grid: {\n        top: 60,\n        left: \"5%\",\n        right: \"10%\",\n        bottom: \"10%\",\n        containLabel: true,\n      },\n      yAxis: {\n        type: \"value\",\n        scale: true,\n        // Updated split line color for light background\n        // splitLine: { lineStyle: { color: \"rgba(0, 0, 0, 0.1)\" } },\n        // axisLabel: { color: \"#333333\" },\n        // axisLabel: { color: labelColor },\n      },\n      series: [\n        {\n          name: \"Statistics\",\n          type: \"candlestick\",\n          data: candlestickData,\n          markLine: {\n            data: [\n              {\n                yAxis: Math.min(...candlestickData.map((d) => d.value[2])),\n                name: \"Min\",\n                symbol: \"none\",\n                label: {\n                  show: true,\n                  formatter: (params: any) => `${params.value}`,\n                  fontStyle: \"italic\",\n                  fontWeight: \"bold\",\n                  fontSize: 14,\n                  // Updated colors for better visibility\n                  // color: \"#0066cc\",\n                },\n                lineStyle: { type: \"dashed\", color: \"#0066cc\" },\n              },\n              {\n                yAxis: Math.max(...candlestickData.map((d) => d.value[3])),\n                name: \"Max\",\n                symbol: \"none\",\n                label: {\n                  show: true,\n                  formatter: (params: any) => `${params.value}`,\n                  fontStyle: \"italic\",\n                  fontWeight: \"bold\",\n                  fontSize: 14,\n                  // Updated to a darker red for better contrast\n                  // color: \"#cc0000\",\n                },\n                lineStyle: { type: \"dashed\", color: \"#cc0000\" },\n              },\n            ],\n          },\n        },\n      ],\n    };\n\n    return (\n      <div\n        className=\"py-2\"\n        style={{\n          width: \"100%\",\n          height: \"100%\",\n        }}\n      >\n        <EChartsReact\n          theme={theme === \"dark\" ? \"dark\" : \"light\"}\n          option={option}\n          style={{ width: \"100%\", height: \"100%\" }}\n          notMerge={true}\n          lazyUpdate={true}\n        />\n      </div>\n    );\n  }\n);\n\n////////////////////////////////////////\n//Bar chart for percentage values\nexport type BarChartPercentageProps = {\n  data: {\n    monoTemporalQueryValues: { name: string; percentage: number }[];\n  };\n  chartTitle: string;\n  palette?: any;\n  theme: string | undefined;\n  labelColor: string;\n};\n\nexport const BarChartPercentage: React.FC<BarChartPercentageProps> = ({\n  data,\n  chartTitle,\n  palette,\n  theme,\n}) => {\n  const extractedChartTitle = extractChartName(chartTitle);\n  const { labels: paletteLabels, palette: paletteColors } = palette;\n\n  // Map the data labels to their corresponding colors in the palette\n  const mappedColors = data.monoTemporalQueryValues.map((d) => {\n    const labelIndex = paletteLabels.indexOf(d.name);\n    return labelIndex !== -1 ? paletteColors[labelIndex] : \"#cccccc\"; // Default to gray if no match is found\n  });\n\n  const option = {\n    title: {\n      text: extractedChartTitle,\n      left: \"center\",\n      top: 10,\n      textStyle: {\n        // color: \"#E5E7EB\",\n        fontSize: 16,\n      },\n    },\n    tooltip: {\n      trigger: \"item\",\n      confine: true,\n      formatter: function (params: any) {\n        return `${params.name}: ${\n          parseInt(params.value) === 0 ? \"< 1\" : params.value\n        }%`;\n      },\n    },\n    grid: {\n      left: \"10%\",\n      right: \"10%\",\n      top: 60,\n      bottom: \"5%\",\n    },\n    yAxis: {\n      type: \"category\",\n      data: data.monoTemporalQueryValues.map((d) => d.name),\n      axisLine: {\n        lineStyle: {\n          // color: \"#B2BEB5\",\n          width: 2,\n        },\n      },\n      axisLabel: {\n        show: false, // Hide the y-axis labels since they will be on the bars\n      },\n      position: \"right\", // Position the y-axis on the right side\n      inverse: true, // Reverse the order of categories (bottom to top)\n    },\n    xAxis: {\n      type: \"value\",\n      axisLine: {\n        show: false,\n      },\n      axisLabel: {\n        show: false,\n      },\n      splitLine: { show: false },\n      inverse: true, // Reverse the x-axis to make the bars start from right to left\n    },\n    series: [\n      {\n        type: \"bar\",\n        data: data.monoTemporalQueryValues.map((d, index) => ({\n          value: d.percentage,\n          name: d.name,\n          label: {\n            show: true,\n            // color: \"#333333 \",\n            color: theme === \"dark\" ? \"#e5e7eb\" : \"#1f2937\",\n            position: \"insideRight\",\n            formatter: (params: any) => {\n              const labelText =\n                params.name.length > 10\n                  ? params.name.slice(0, 10) + \"...\"\n                  : params.name; // Truncate long names\n              const valueText =\n                parseInt(params.value) === 0 ? \"< 1\" : params.value;\n              return `${labelText}       ${valueText}%`;\n            },\n\n            fontSize: 14,\n            // color: \"white\",\n            align: \"right\",\n            verticalAlign: \"middle\",\n            padding: [0, 10, 0, 0],\n            z: 1,\n          },\n          itemStyle: {\n            color: mappedColors[index],\n          },\n        })),\n        barWidth: \"80%\",\n        barGap: \"0%\",\n      },\n    ],\n    backgroundColor: \"transparent\",\n  };\n\n  return (\n    <EChartsReact\n      theme={theme === \"dark\" ? \"dark\" : \"light\"}\n      option={option}\n      style={{ height: \"100%\", width: \"100%\" }}\n      notMerge={true}\n      lazyUpdate={true}\n    />\n  );\n};\n\n////////////////////////////////////////\n// Bar chart for numerical values\nexport type BarChartNumericalProps = {\n  data: any;\n  chartTitle: string;\n  chartUnit: string | undefined;\n  theme: string | undefined;\n  labelColor: string;\n};\n\nexport const BarChartNumerical: React.FC<BarChartNumericalProps> = ({\n  data,\n  chartTitle,\n  chartUnit,\n  theme,\n}) => {\n  const colorPalette = chartColors[\"barChartNumerical\"];\n\n  if (!data || data.length === 0) {\n    return (\n      <div className=\"flex items-center justify-center\">\n        <strong className=\"text-center justify-center items-center\">\n          No data available for this location\n        </strong>\n      </div>\n    );\n  }\n\n  const extractedChartTitle = chartTitle;\n  const option = {\n    title: {\n      text: `${extractedChartTitle}`,\n      left: \"center\",\n      top: 10,\n      textStyle: {\n        fontSize: 16,\n      },\n    },\n    tooltip: {\n      show: false,\n      trigger: \"item\",\n      formatter: function (params: any) {\n        return `${params.name}: ${params.value} ${chartUnit || \"\"}`;\n      },\n    },\n    xAxis: {\n      type: \"category\",\n      data: data.monoTemporalQueryValues.map((_: any, index: any) => ``),\n      axisLine: {\n        lineStyle: {\n          width: 2,\n        },\n      },\n      axisLabel: {\n        overflow: \"break\",\n        rotate: 0,\n        fontSize: 14,\n      },\n      splitLine: { show: false },\n    },\n    yAxis: {\n      type: \"value\",\n      show: false,\n    },\n    series: [\n      {\n        type: \"bar\",\n        data: data.monoTemporalQueryValues.map((d: any) => ({\n          value: d,\n          name: `${chartTitle}`,\n          label: {\n            show: true,\n            position: \"inside\",\n            color: theme === \"dark\" ? \"#e5e7eb\" : \"#1f2937\",\n            formatter: `{c} ${chartUnit || \"\"}`,\n            fontSize: 14,\n          },\n        })),\n        itemStyle: {\n          borderColor: colorPalette[0],\n          borderWidth: 2,\n          color: \"rgba(0, 0, 0, 0)\",\n        },\n      },\n    ],\n    backgroundColor: \"transparent\",\n    grid: {\n      left: \"10%\",\n      right: \"20%\",\n      bottom: \"5%\",\n      top: 60,\n      containLabel: true,\n    },\n  };\n\n  return (\n    <div className=\"pt-2\" style={{ width: \"100%\", height: \"100%\" }}>\n      <EChartsReact\n        theme={theme === \"dark\" ? \"dark\" : \"light\"}\n        option={option}\n        style={{ height: \"100%\", width: \"100%\" }}\n        notMerge={true}\n        lazyUpdate={true}\n      />\n    </div>\n  );\n};\n\n////////////////////////////////////////\n// Dual-bar chart for numerical values\n\nexport type DualBarChartNumericalProps = {\n  data: any;\n  chartTitle: string;\n  theme: string | undefined;\n  labelColor: string;\n};\n\nexport const DualBarChartNumerical: React.FC<DualBarChartNumericalProps> = ({\n  data,\n  chartTitle,\n  theme,\n}) => {\n  const parsedData = data.values.monoTemporalQueryValues;\n\n  if (!parsedData || Object.keys(parsedData).length === 0) {\n    return (\n      <div className=\"flex items-center justify-center\">\n        <strong className=\"text-center justify-center items-center\">\n          No data available for this location\n        </strong>\n      </div>\n    );\n  }\n\n  const colorPalette = chartColors[\"dualBarChartNumerical\"];\n\n  const keys = Object.keys(parsedData);\n\n  const isPercentage = keys.some(\n    (key) => parsedData[key].percentageChange !== undefined\n  );\n\n  let option = {};\n\n  if (isPercentage) {\n    const yAxisOptions = [\n      {\n        type: \"value\",\n        name: \"\",\n        position: \"left\",\n        axisLine: {\n          lineStyle: {\n            color: \"#B2BEB5\",\n            width: 2,\n          },\n        },\n        axisLabel: {\n          color: \"#B2BEB5\",\n          fontSize: 10,\n          formatter: (value: number) => `${value.toFixed(0)}%`,\n        },\n        splitLine: {\n          show: true,\n          lineStyle: {\n            color: \"#444\",\n          },\n        },\n      },\n    ];\n\n    const seriesData = keys.map((key, index) => {\n      const gasData = parsedData[key];\n      const value = parseFloat(gasData.percentageChange);\n      return {\n        value: value,\n        name: key,\n        label: {\n          show: true,\n          position: value !== null && value < 0 ? \"top\" : \"inside\",\n          formatter: `${value.toFixed(0)}%`,\n          // color: \"white\",\n          color: theme === \"dark\" ? \"#e5e7eb\" : \"#1f2937\",\n          fontSize: 12,\n        },\n        itemStyle: {\n          color: colorPalette[index % colorPalette.length],\n        },\n      };\n    });\n\n    option = {\n      title: {\n        text: chartTitle,\n        left: \"center\",\n        top: 10,\n        textStyle: {\n          color: \"#e5e7eb\",\n          fontSize: 16,\n        },\n      },\n      tooltip: {\n        show: true,\n        trigger: \"item\",\n        confine: true,\n        formatter: function (params: any) {\n          const gasData = parsedData[params.name];\n          return `${params.name}<br/>\n          Date 1: ${gasData.gasValue1}<br/>\n          Date 2: ${gasData.gasValue2}<br/>\n          Difference: ${gasData.differenceValue}<br/>\n          Percentage Change: ${gasData.percentageChange}`;\n        },\n      },\n      xAxis: {\n        type: \"category\",\n        data: keys,\n        axisLine: {\n          lineStyle: {\n            color: \"#B2BEB5\",\n            width: 2,\n          },\n        },\n        axisLabel: {\n          color: \"#B2BEB5\",\n          fontSize: 14,\n        },\n        splitLine: {\n          show: true,\n          lineStyle: {\n            color: \"#444\",\n          },\n        },\n      },\n      yAxis: yAxisOptions,\n      series: [\n        {\n          name: \"Percentage Change\",\n          type: \"bar\",\n          data: seriesData,\n          barWidth: \"80%\",\n        },\n      ],\n      backgroundColor: \"transparent\",\n      grid: {\n        left: \"5%\",\n        right: \"5%\",\n        bottom: \"5%\",\n        top: 60,\n        containLabel: true,\n      },\n    };\n  } else {\n    const yAxisOptions = [\n      {\n        type: \"value\",\n        name: \"\",\n        position: \"left\",\n        axisLine: {\n          lineStyle: {\n            color: \"#B2BEB5\",\n            width: 2,\n          },\n        },\n        axisLabel: {\n          color: \"#B2BEB5\",\n          fontSize: 10,\n          formatter: (value: number) =>\n            value !== null && !isNaN(value) ? value.toExponential(2) : \"\",\n        },\n        splitLine: {\n          show: true,\n          lineStyle: {\n            color: \"#444\",\n          },\n        },\n      },\n      {\n        type: \"value\",\n        name: \"\",\n        position: \"right\",\n        axisLine: {\n          lineStyle: {\n            color: \"#B2BEB5\",\n            width: 2,\n          },\n        },\n        axisLabel: {\n          color: \"#B2BEB5\",\n          fontSize: 10,\n          formatter: (value: number) =>\n            value !== null && !isNaN(value) ? value.toExponential(2) : \"\",\n        },\n        splitLine: {\n          show: false,\n        },\n      },\n    ];\n\n    const values = keys.map((key) => {\n      const gasData = parsedData[key];\n      const value = parseFloat(gasData.gasValue1);\n      return value;\n    });\n\n    const validValues = values.filter(\n      (v): v is number => v !== null && !isNaN(v)\n    );\n    const max_value = Math.max(...validValues);\n    const maxThreshold = max_value / 2;\n\n    const seriesData1: any[] = new Array(keys.length).fill(null);\n    const seriesData2: any[] = new Array(keys.length).fill(null);\n\n    for (let i = 0; i < keys.length; i++) {\n      const key = keys[i];\n      const gasData = parsedData[key];\n      const value = parseFloat(gasData.gasValue1);\n      const unit = gasData.unit;\n\n      const dataPoint = {\n        value: value,\n        name: key,\n        label: {\n          show: true,\n          position: value !== null && value < 0 ? \"top\" : \"inside\",\n          formatter: `${value.toExponential(2)} \\n\\n(${unit})`,\n          // color: \"white\",\n          color: theme === \"dark\" ? \"#e5e7eb\" : \"#1f2937\",\n          fontSize: 12,\n        },\n        itemStyle: {\n          color: colorPalette[i % colorPalette.length],\n        },\n      };\n\n      if (value !== null && value >= maxThreshold) {\n        seriesData1[i] = dataPoint;\n      } else {\n        seriesData2[i] = dataPoint;\n      }\n    }\n\n    option = {\n      title: {\n        text: chartTitle,\n        left: \"center\",\n        top: 10,\n        textStyle: {\n          color: \"#e5e7eb\",\n          fontSize: 16,\n        },\n      },\n      tooltip: {\n        show: true,\n        trigger: \"item\",\n        formatter: function (params: any) {\n          const gasData = parsedData[params.name];\n          return `${params.name}<br/>\n          ${gasData.gasValue1}`;\n        },\n      },\n      xAxis: {\n        type: \"category\",\n        data: keys,\n        axisLine: {\n          lineStyle: {\n            color: \"#B2BEB5\",\n            width: 2,\n          },\n        },\n        axisLabel: {\n          color: \"#B2BEB5\",\n          fontSize: 14,\n        },\n        splitLine: {\n          show: true,\n          lineStyle: {\n            color: \"#444\",\n          },\n        },\n      },\n      yAxis: yAxisOptions,\n      series: [\n        {\n          name: \"High Values\",\n          type: \"bar\",\n          yAxisIndex: 0,\n          data: seriesData1,\n          barWidth: \"80%\",\n          barGap: \"-100%\",\n        },\n        {\n          name: \"Low Values\",\n          type: \"bar\",\n          yAxisIndex: 1,\n          data: seriesData2,\n          barWidth: \"80%\",\n          barGap: \"-100%\",\n        },\n      ],\n      backgroundColor: \"transparent\",\n      grid: {\n        left: \"5%\",\n        right: \"5%\",\n        bottom: \"5%\",\n        top: 60,\n        containLabel: true,\n      },\n    };\n  }\n\n  return (\n    <div className=\"pt-2\" style={{ width: \"100%\", height: \"100%\" }}>\n      <EChartsReact\n        option={option}\n        style={{ width: \"100%\", height: \"100%\" }}\n        notMerge={true}\n        lazyUpdate={true}\n      />\n    </div>\n  );\n};\n\ntype BarChartStatsProps = {\n  data: {\n    Mean: number | string;\n    Median: number | string;\n    Min: number | string;\n    Max: number | string;\n  };\n  chartTitle: string;\n  theme: string | undefined;\n  labelColor: string;\n};\n\nexport const BarChartStatistics: React.FC<BarChartStatsProps> = ({\n  data,\n  chartTitle,\n  theme,\n}) => {\n  const extractedChartTitle = extractChartName(chartTitle);\n\n  // Define a color palette\n  const colorPalette = [\n    \"#16a34a\", // Green for Min\n    \"#1f77b4\", // Blue for Median\n    \"#ff7f0e\", // Orange for Mean\n    \"#d62728\", // Red for Max\n  ];\n\n  const option = {\n    title: {\n      show: true, // Show the title in the chart\n      text: extractedChartTitle, // Use the chartTitle passed as a prop\n      left: \"center\", // Center the title horizontally\n      top: 10, // Adjust the vertical position of the title\n      textStyle: {\n        // color: \"#e5e7eb\",\n        fontSize: 16,\n      },\n    },\n    grid: {\n      left: \"5%\",\n      right: \"5%\",\n      top: 70,\n      bottom: \"3%\",\n      containLabel: true,\n    },\n    tooltip: {\n      trigger: \"item\",\n      formatter: function (params: any) {\n        return `${params.name}: ${params.value}`;\n      },\n    },\n    xAxis: {\n      type: \"category\",\n      data: [\"Min\", \"Median\", \"Mean\", \"Max\"],\n      axisLine: {\n        lineStyle: {\n          // color: \"#B2BEB5\",\n          width: 2,\n        },\n      },\n      axisLabel: {\n        show: true,\n      },\n    },\n    yAxis: {\n      type: \"value\",\n      axisLine: {\n        show: false,\n      },\n      axisLabel: {\n        show: false,\n      },\n      splitLine: { show: false },\n    },\n    series: [\n      {\n        type: \"bar\",\n        data: [\n          {\n            value: data.Min,\n            name: \"Min\",\n            itemStyle: { color: colorPalette[0] },\n          },\n          {\n            value: data.Median,\n            name: \"Median\",\n            itemStyle: { color: colorPalette[1] },\n          },\n          {\n            value: data.Mean,\n            name: \"Mean\",\n            itemStyle: { color: colorPalette[2] },\n          },\n          {\n            value: data.Max,\n            name: \"Max\",\n            itemStyle: { color: colorPalette[3] },\n          },\n        ],\n        label: {\n          show: true,\n          position: \"top\",\n          formatter: (params: any) => `${params.value}`,\n          fontSize: 12,\n          // color: \"white\",\n          color: theme === \"dark\" ? \"#e5e7eb\" : \"#1f2937\",\n          align: \"center\",\n          verticalAlign: \"middle\",\n          offset: [0, -10],\n          z: 1,\n        },\n        barWidth: \"50%\",\n        barGap: \"0%\",\n      },\n    ],\n    backgroundColor: \"transparent\",\n  };\n\n  return (\n    <div className=\"pt-2\" style={{ width: \"100%\", height: \"100%\" }}>\n      <EChartsReact\n        theme={theme === \"dark\" ? \"dark\" : \"light\"}\n        option={option}\n        style={{ width: \"100%\", height: \"100%\" }}\n        notMerge={true}\n        lazyUpdate={true}\n      />\n    </div>\n  );\n};\n\n////////////////////////////////////////\n// Stacked bar chart component for the query stats\n////////////////////////////////////////\n\ntype StackedBarChartStatisticsProps = {\n  data: {\n    gas: {\n      Mean: { actual: string; normalized: string };\n      Median: { actual: string; normalized: string };\n      Min: { actual: string; normalized: string };\n      Max: { actual: string; normalized: string };\n    };\n  };\n  chartTitle: string;\n  theme: string | undefined;\n  labelColor: string;\n};\n\nexport const StackedBarChartStatistics: React.FC<\n  StackedBarChartStatisticsProps\n> = ({ data, chartTitle, theme, labelColor }) => {\n  const extractedChartTitle = chartTitle;\n  const colorPalette = chartColors[\"stackedBarChartStats\"]; // Colors for Min, Median, Mean, Max\n\n  // Dynamically extract the keys from the data (CO, NO2, CH4, etc.)\n  const keys = Object.keys(data) as Array<keyof typeof data>;\n  const series: any = [];\n  const legendData: any = [];\n  const yAxis: any = [];\n\n  // Loop through each key to generate the series and y-axes dynamically\n  keys.forEach((key, index) => {\n    const actuals = {\n      Min: parseFloat(data[key as keyof typeof data].Min.actual),\n      Median: parseFloat(data[key].Median.actual),\n      Mean: parseFloat(data[key].Mean.actual),\n      Max: parseFloat(data[key].Max.actual),\n    };\n\n    // Generate unique y-axis for each gas\n    yAxis.push({\n      type: \"value\",\n      name: key.toUpperCase(),\n      position: index % 2 === 0 ? \"left\" : \"right\", // Alternate between left and right y-axis\n      axisLine: {\n        lineStyle: {\n          color: colorPalette[index % colorPalette.length], // Color to match the gas\n        },\n      },\n      splitLine: { show: false },\n      axisLabel: {\n        formatter: function (value: number) {\n          return value.toExponential(2); // Format in exponential for readability\n        },\n      },\n    });\n\n    // Push the series for each key\n    series.push({\n      name: key.toUpperCase(), // Capitalize the key name for display in the legend\n      type: \"bar\",\n      yAxisIndex: index, // Assign to the corresponding y-axis\n      stack: key, // Stack bars for the same gas type\n      itemStyle: { color: colorPalette[index % colorPalette.length] }, // Rotate colors\n      data: [\n        {\n          value: actuals.Min, // No scaling required, as each y-axis has its own scale\n          actual: actuals.Min,\n        },\n        {\n          value: actuals.Median,\n          actual: actuals.Median,\n        },\n        {\n          value: actuals.Mean,\n          actual: actuals.Mean,\n        },\n        {\n          value: actuals.Max,\n          actual: actuals.Max,\n        },\n      ],\n    });\n\n    // Add the key to the legend\n    legendData.push(key.toUpperCase());\n  });\n\n  const option = {\n    title: {\n      show: true,\n      text: extractedChartTitle,\n      left: \"center\",\n      top: 10,\n      textStyle: {\n        // color: \"#e5e7eb\",\n        fontSize: 16,\n      },\n    },\n    grid: {\n      left: \"5%\",\n      right: \"5%\",\n      top: 70,\n      bottom: \"10%\", // Increased bottom space for labels\n      containLabel: true,\n    },\n    tooltip: {\n      trigger: \"axis\",\n      containLabel: true,\n      confine: true,\n      axisPointer: {\n        type: \"shadow\",\n      },\n      formatter: function (params: any) {\n        let tooltipText = `${params[0].name}<br/>`;\n        params.forEach((param: any) => {\n          const { seriesName } = param;\n          const actual = param.data.actual.toExponential(2);\n          tooltipText += `${seriesName}: ${actual}<br/>`;\n        });\n        return tooltipText;\n      },\n    },\n    legend: {\n      data: legendData, // Use dynamically generated legend data\n      top: 40,\n      textStyle: {\n        // color: \"#e5e7eb\",\n      },\n    },\n    xAxis: {\n      type: \"category\",\n      data: [\"Min\", \"Median\", \"Mean\", \"Max\"],\n      axisLine: {\n        lineStyle: {\n          color: \"#B2BEB5\",\n          width: 2,\n        },\n      },\n      axisLabel: {\n        // color: \"#e5e7eb\",\n        interval: 0, // Ensure all labels are shown\n        rotate: 0,\n        fontSize: 12,\n      },\n    },\n    yAxis: yAxis, // Use dynamically generated y-axes\n    series: series, // Use dynamically generated series\n    backgroundColor: \"transparent\",\n  };\n\n  return (\n    <div className=\"pt-2\" style={{ width: \"100%\", height: \"100%\" }}>\n      <EChartsReact\n        theme={theme === \"dark\" ? \"dark\" : \"light\"}\n        option={option}\n        style={{ width: \"100%\", height: \"100%\" }}\n        notMerge={true}\n        lazyUpdate={true}\n      />\n    </div>\n  );\n};\n\n////////////////////////////////////////\n// Stacked percenrage bar chart component for the query stats\n////////////////////////////////////////\n\ntype StackedBarChartPercentageProps = {\n  data: {\n    gas: {\n      Mean: { actual: string; percentage: string };\n      Median: { actual: string; percentage: string };\n      Min: { actual: string; percentage: string };\n      Max: { actual: string; percentage: string };\n    };\n  };\n  chartTitle: string;\n  theme: string | undefined;\n  labelColor: string;\n};\n\nexport const StackedPercentageBarChartStatistics: React.FC<\n  StackedBarChartPercentageProps\n> = ({ data, chartTitle, theme, labelColor }) => {\n  const extractedChartTitle = chartTitle;\n  const colorPalette = chartColors[\"stackedBarChartStats\"]; // Colors for Median, Mean\n\n  // Dynamically extract the keys from the data (CO, Aerosols, etc.)\n  const keys = Object.keys(data) as Array<keyof typeof data>;\n  const series: any = [];\n  const legendData: any = [];\n\n  // Loop through each key to generate the series dynamically\n  keys.forEach((key, index) => {\n    const percentages = {\n      Median: data[key].Median.percentage\n        ? parseInt(data[key].Median.percentage)\n        : 0,\n      Mean: data[key].Mean.percentage ? parseInt(data[key].Mean.percentage) : 0,\n    };\n\n    // Push the series for each key\n    series.push({\n      name: key.toUpperCase(), // Capitalize the key name for display in the legend\n      type: \"bar\",\n      stack: key, // Stack bars for the same gas type\n      itemStyle: { color: colorPalette[index % colorPalette.length] }, // Rotate colors\n      data: [\n        { value: percentages.Median, actual: percentages.Median },\n        { value: percentages.Mean, actual: percentages.Mean },\n      ],\n    });\n\n    // Add the key to the legend\n    legendData.push(key.toUpperCase());\n  });\n\n  const option = {\n    title: {\n      show: true,\n      text: extractedChartTitle,\n      left: \"center\",\n      top: 10,\n      textStyle: {\n        // color: \"#e5e7eb\",\n        fontSize: 16,\n      },\n    },\n    grid: {\n      left: \"5%\",\n      right: \"5%\",\n      top: 70,\n      bottom: \"15%\", // Increased bottom space for labels\n      containLabel: true,\n    },\n    tooltip: {\n      trigger: \"axis\",\n      containLabel: true,\n      confine: true,\n      axisPointer: {\n        type: \"shadow\",\n      },\n      formatter: function (params: any) {\n        let tooltipText = `${params[0].name}<br/>`;\n        params.forEach((param: any) => {\n          const { seriesName, value } = param;\n          const actual = param.data.actual.toFixed(0) + \"%\";\n          tooltipText += `${seriesName}: ${actual}<br/>`;\n        });\n        return tooltipText;\n      },\n    },\n    legend: {\n      data: legendData, // Use dynamically generated legend data\n      top: 40,\n      textStyle: {\n        // color: \"#e5e7eb\",\n      },\n    },\n    xAxis: {\n      type: \"category\",\n      data: [\"Median\", \"Mean\"], // Only show Median and Mean\n      axisLine: {\n        lineStyle: {\n          // color: \"#B2BEB5\",\n          width: 2,\n        },\n      },\n      axisTick: {\n        alignWithLabel: true, // Ensures the labels align under the bars\n      },\n      axisLabel: {\n        // color: \"#e5e7eb\",\n        interval: 0, // Ensure all labels are shown, even if overlapping\n        rotate: 0, // No rotation of labels\n        fontSize: 12, // Adjust font size if labels are too large\n      },\n    },\n    yAxis: {\n      type: \"value\",\n      axisLine: {\n        lineStyle: {\n          // color: \"#e5e7eb\",\n        },\n      },\n      splitLine: { show: false },\n      axisLabel: {\n        formatter: function (value: number) {\n          return value.toFixed(0) + \"%\"; // Format as percentage for readability\n        },\n        // color: \"#e5e7eb\",\n      },\n      name: \"\", // Removes the y-axis label name\n    },\n    series: series, // Use dynamically generated series\n    backgroundColor: \"transparent\",\n  };\n\n  return (\n    <div className=\"pt-2\" style={{ width: \"100%\", height: \"100%\" }}>\n      <EChartsReact\n        theme={theme === \"dark\" ? \"dark\" : \"light\"}\n        option={option}\n        style={{ width: \"100%\", height: \"100%\" }}\n        notMerge={true}\n        lazyUpdate={true}\n      />\n    </div>\n  );\n};\n\n////////////////////////////////////////\n// Combined stacked bar chart component for the query stats\n\ntype GasData = {\n  Mean: { actual: string; percentage: string | null };\n  Median: { actual: string; percentage: string | null };\n  Min: { actual: string; percentage: string | null };\n  Max: { actual: string; percentage: string | null };\n  unit: AirPollutantsUnits;\n};\n\ntype CombinedStackedBarChartStatisticsProps = {\n  data: {\n    [gas: string]: GasData;\n  };\n  chartTitle: string;\n  theme: string | undefined;\n  labelColor: string;\n};\n\nexport const CombinedStackedBarChartStatistics: React.FC<\n  CombinedStackedBarChartStatisticsProps\n> = ({ data, chartTitle, theme, labelColor }) => {\n  const colorPalette = chartColors[\"stackedBarChartStats\"]; // Define your color palette\n\n  // Extract gases and ensure they are strings\n  const gases = Object.keys(data) as string[];\n\n  // Determine if percentages are present in the data\n  const isPercentageMode = gases.some((gas) =>\n    [\"Mean\", \"Median\"].some(\n      (stat) =>\n        data[gas][stat as AggregationMethodTypeNumerical].percentage !== null &&\n        data[gas][stat as AggregationMethodTypeNumerical].percentage !==\n          undefined\n    )\n  );\n\n  const series: any[] = [];\n  const legendData: string[] = [];\n  const yAxisOptions: any[] = [];\n  let xAxisData: AggregationMethodTypeNumerical[] = [];\n\n  if (isPercentageMode) {\n    // Percentage Mode\n    xAxisData = [\"Median\", \"Mean\"];\n\n    gases.forEach((gas, gasIndex) => {\n      const percentages = xAxisData.map((stat) => {\n        const percentageValue = data[gas][stat].percentage;\n        return percentageValue ? parseFloat(percentageValue) : 0;\n      });\n\n      series.push({\n        name: gas,\n        type: \"bar\",\n        stack: gas,\n        itemStyle: { color: colorPalette[gasIndex % colorPalette.length] },\n        data: percentages.map((value) => ({ value, actual: value })),\n      });\n\n      legendData.push(gas);\n    });\n\n    // Configure yAxis for percentage mode\n    yAxisOptions.push({\n      type: \"value\",\n      axisLine: {\n        lineStyle: {\n          // color: \"#e5e7eb\",\n        },\n      },\n      splitLine: { show: false },\n      axisLabel: {\n        formatter: function (value: number) {\n          return value.toFixed(0) + \"%\";\n        },\n        // color: \"#e5e7eb\",\n      },\n      name: \"\",\n    });\n  } else {\n    // Non-Percentage Mode\n    xAxisData = [\"Min\", \"Median\", \"Mean\", \"Max\"];\n\n    gases.forEach((gas, gasIndex) => {\n      const actuals = xAxisData.map((stat) => {\n        const actualValue = data[gas][stat].actual;\n        return actualValue ? parseFloat(actualValue) : null;\n      });\n\n      // Generate unique y-axis for each gas\n      yAxisOptions.push({\n        type: \"value\",\n        name: data[gas].unit,\n        position: gasIndex % 2 === 0 ? \"left\" : \"right\",\n        offset: gasIndex > 1 ? 50 * Math.floor(gasIndex / 2) : 0,\n        axisLine: {\n          lineStyle: {\n            color: colorPalette[gasIndex % colorPalette.length],\n          },\n        },\n        splitLine: { show: false },\n        axisLabel: {\n          formatter: function (value: number) {\n            return value.toExponential(2);\n          },\n          color: colorPalette[gasIndex % colorPalette.length],\n        },\n      });\n\n      series.push({\n        name: gas,\n        type: \"bar\",\n        yAxisIndex: gasIndex,\n        stack: gas,\n        itemStyle: { color: colorPalette[gasIndex % colorPalette.length] },\n        data: actuals.map((value) => ({\n          value,\n          actual: value,\n          unit: data[gas].unit,\n        })),\n      });\n\n      legendData.push(gas);\n    });\n  }\n\n  const option = {\n    title: {\n      show: true,\n      text: chartTitle,\n      left: \"center\",\n      top: 10,\n      textStyle: {\n        // color: \"#e5e7eb\",\n        fontSize: 16,\n      },\n    },\n    grid: {\n      left: \"5%\",\n      right: \"5%\",\n      top: 70,\n      bottom: \"15%\",\n      containLabel: true,\n    },\n    tooltip: {\n      trigger: \"axis\",\n      containLabel: true,\n      confine: true,\n      axisPointer: {\n        type: \"shadow\",\n      },\n      formatter: function (params: any) {\n        let tooltipText = `${params[0].name}<br/>`;\n        params.forEach((param: any) => {\n          const { seriesName, data } = param;\n          if (isPercentageMode) {\n            const actual = data.actual.toFixed(0) + \"%\";\n            tooltipText += `<strong>${seriesName}:</strong> ${actual}<br/>`;\n          } else {\n            const actual = data.actual ? data.actual.toExponential(2) : \"N/A\";\n            tooltipText += `<strong>${seriesName}:</strong> ${actual} (${data.unit})<br/>`;\n          }\n        });\n        return tooltipText;\n      },\n    },\n    legend: {\n      data: legendData,\n      top: 40,\n      textStyle: {\n        // color: \"#e5e7eb\",\n      },\n    },\n    xAxis: {\n      type: \"category\",\n      data: xAxisData,\n      axisLine: {\n        lineStyle: {\n          // color: \"#B2BEB5\",\n          width: 2,\n        },\n      },\n      axisTick: {\n        alignWithLabel: true,\n      },\n      axisLabel: {\n        // color: \"#e5e7eb\",\n        interval: 0,\n        rotate: 0,\n        fontSize: 12,\n      },\n    },\n    yAxis: yAxisOptions,\n    series: series,\n    backgroundColor: \"transparent\",\n  };\n\n  return (\n    <div className=\"pt-2\" style={{ width: \"100%\", height: \"100%\" }}>\n      <EChartsReact\n        theme={theme === \"dark\" ? \"dark\" : \"light\"}\n        option={option}\n        style={{ width: \"100%\", height: \"100%\" }}\n        notMerge={true}\n        lazyUpdate={true}\n      />\n    </div>\n  );\n};\n\n////////////////////////////////////////\n// Stacked bar chart for land-cover change map\n////////////////////////////////////////\ninterface LandcoverDistributions {\n  year1Distribution: Record<string, string>;\n  year2Distribution: Record<string, string>;\n}\n\ninterface StackedBarChartForLandcoverChangeMapProps {\n  /**\n   * Contains the two distributions. Example:\n   * {\n   *   year1Distribution: { Water: \"8.3\", Trees: \"31.2\", ... },\n   *   year2Distribution: { Water: \"7.5\", Trees: \"32.7\", ... }\n   * }\n   */\n  data: LandcoverDistributions;\n  chartTitle: string;\n  palette: {\n    labels: string[]; // e.g. [\"Water\", \"Trees\", \"Grass\", ...]\n    palette: string[]; // e.g. [\"#419BDF\", \"#397D49\", ...]\n  };\n  theme: string | undefined;\n}\n\n/**\n * Renders a stacked bar chart:\n * - X-axis: 2 categories => Year 1, Year 2\n * - One bar series per landcover class => stacks to show 100% total\n */\nexport const StackedBarChartForLandcoverChangeMap =\n  React.memo<StackedBarChartForLandcoverChangeMapProps>(\n    ({ data, chartTitle, palette, theme }) => {\n      const { year1Distribution, year2Distribution } = data;\n\n      const allLabels = palette.labels;\n      const colors = palette.palette;\n\n      // Build ECharts series\n      const series = useMemo(() => {\n        return allLabels.map((label, idx) => {\n          const y1Val = parseFloat(year1Distribution[label] ?? \"0\");\n          const y2Val = parseFloat(year2Distribution[label] ?? \"0\");\n\n          return {\n            name: label,\n            type: \"bar\",\n            stack: \"total\",\n            itemStyle: {\n              color: colors[idx] || \"#e2e8f0\",\n            },\n            data: [y1Val, y2Val],\n          };\n        });\n      }, [allLabels, colors, year1Distribution, year2Distribution]);\n\n      const option = useMemo(() => {\n        return {\n          backgroundColor: theme === \"dark\" ? \"#00000000\" : \"#ffffff\",\n          title: {\n            text: chartTitle,\n            top: 10,\n            left: \"center\",\n            textStyle: {\n              fontSize: 16,\n              fontWeight: \"bold\",\n              color: theme === \"dark\" ? \"#e5e7eb\" : \"#1f2937\",\n            },\n          },\n          tooltip: {\n            trigger: \"axis\",\n            confine: true,\n            formatter: (params: any) => {\n              // `params` is an array of stacked segments (one per class) for that X category\n              let content = `${params[0].axisValue}<br/>`;\n              params.forEach((item: any) => {\n                // If the value is 0, skip it\n                if (item.value > 0) {\n                  content += `${item.marker} ${item.seriesName}: ${item.value}%<br/>`;\n                }\n              });\n              return content;\n            },\n            backgroundColor: \"rgba(255, 255, 255, 0.9)\",\n            borderColor: \"#e2e8f0\",\n            textStyle: {\n              color: \"#1f2937\",\n            },\n          },\n          grid: {\n            top: 80,\n            left: \"15%\",\n            right: \"5%\",\n            bottom: \"15%\",\n          },\n          xAxis: {\n            type: \"category\",\n            data: [\"Year 1\", \"Year 2\"],\n            axisLine: {\n              lineStyle: {\n                color: theme === \"dark\" ? \"#e5e7eb\" : \"#1f2937\",\n              },\n            },\n            axisLabel: {\n              color: theme === \"dark\" ? \"#e5e7eb\" : \"#1f2937\",\n            },\n          },\n          yAxis: {\n            type: \"value\",\n            max: 100,\n            axisLine: { show: false },\n            axisLabel: {\n              color: theme === \"dark\" ? \"#e5e7eb\" : \"#1f2937\",\n              formatter: \"{value}%\",\n            },\n            splitLine: {\n              lineStyle: {\n                color: theme === \"dark\" ? \"#374151\" : \"#e2e8f0\",\n              },\n            },\n          },\n          series,\n        };\n      }, [chartTitle, theme, series]);\n\n      return (\n        <div style={{ width: \"100%\", height: \"400px\" }}>\n          <EChartsReact\n            option={option}\n            notMerge={true}\n            lazyUpdate={true}\n            style={{ width: \"100%\", height: \"100%\" }}\n            theme={theme === \"dark\" ? \"dark\" : \"light\"}\n          />\n        </div>\n      );\n    }\n  );\n\n////////////////////////////////////////\n// Pie chart component for the query stats\n////////////////////////////////////////\n\ninterface PieChartStatsProps {\n  data: Record<string, string>; // Update the type of data\n  chartTitle: string;\n  palette?: any;\n  theme: string | undefined;\n  labelColor: string;\n}\nexport const PieChartStats = React.memo<PieChartStatsProps>(\n  ({ data, chartTitle, palette, theme, labelColor }) => {\n    const processedData: {\n      name: string;\n      value: number;\n      itemStyle?: { color: string };\n    }[] = useMemo(() => {\n      const { labels, palette: paletteColors } = palette;\n\n      const dataArray = Object.entries(data).map(([name, value]) => ({\n        name,\n        value: parseFloat(value),\n      }));\n\n      return dataArray.map(({ name, value }) => {\n        const labelIndex = labels.indexOf(name);\n\n        return {\n          name,\n          value,\n          itemStyle: {\n            color: labelIndex !== -1 ? paletteColors[labelIndex] : \"#e2e8f0\",\n          },\n        };\n      });\n    }, [data, palette]);\n\n    const option = {\n      backgroundColor: theme === \"dark\" ? \"#00000000\" : \"#ffffff\",\n      title: {\n        text: chartTitle,\n        top: 10,\n        left: \"center\",\n        show: true,\n        textStyle: {\n          fontSize: 16,\n          fontWeight: \"bold\",\n        },\n      },\n      tooltip: {\n        trigger: \"item\",\n        confine: true,\n        formatter: function (params: any) {\n          return `${params.name}: ${params.percent}%`;\n        },\n        backgroundColor: \"rgba(255, 255, 255, 0.9)\",\n        borderColor: \"#e2e8f0\",\n        textStyle: {\n          color: \"#1f2937\",\n        },\n      },\n      series: [\n        {\n          name: \"Classes\",\n          type: \"pie\",\n          radius: \"65%\",\n          center: [\"50%\", \"60%\"],\n          data: processedData,\n          // emphasis: {\n          //   itemStyle: {\n          //     shadowBlur: 10,\n          //     shadowOffsetX: 0,\n          //     shadowColor: \"rgba(0, 0, 0, 0.1)\", // Lighter shadow for light theme\n          //   },\n          // },\n          label: {\n            // color: \"#1f2937\", // Dark gray for light theme\n            color: theme === \"dark\" ? \"#e5e7eb\" : \"#1f2937\",\n            formatter: \"{b}: {d}%\",\n          },\n        },\n      ],\n    };\n\n    return (\n      <EChartsReact\n        option={option}\n        theme={theme === \"dark\" ? \"dark\" : \"light\"}\n        style={{ width: \"100%\", height: \"100%\" }}\n        notMerge={true}\n        lazyUpdate={true}\n      />\n    );\n  }\n);\n\n////////////////////////////////////////\n// Histogram chart component for the query stats\n\ntype ValueType = {\n  output: number;\n  UID: number;\n  vectorLayerData: any;\n};\n\ntype InputArgsType = {\n  type: string;\n  name: string;\n  vectorLayerName: string;\n  values: ValueType[];\n};\n\ntype HistogramChartProps = {\n  data: InputArgsType;\n};\n\nexport const HistogramChart: React.FC<HistogramChartProps> = ({ data }) => {\n  const chartRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const chart = echarts.init(chartRef.current as HTMLDivElement);\n\n    const option = {\n      title: {\n        text: `${data.vectorLayerName}: ${data.name}`,\n      },\n      tooltip: {\n        trigger: \"axis\",\n        axisPointer: {\n          type: \"shadow\",\n        },\n      },\n      xAxis: {\n        type: \"category\",\n        data: data.values.map((item) => item.UID),\n        name: \"UID\",\n      },\n      yAxis: {\n        type: \"value\",\n        name: \"Output\",\n      },\n      series: [\n        {\n          type: \"bar\",\n          data: data.values.map((item) => item.output),\n          barWidth: \"50%\",\n        },\n      ],\n    };\n\n    chart.setOption(option);\n\n    return () => {\n      chart.dispose();\n    };\n  }, [data]);\n\n  return <div ref={chartRef} style={{ width: \"100%\", height: \"100%\" }} />;\n};\n"
  },
  {
    "path": "features/charts/utils/select-chart-type.ts",
    "content": "enum ChartTypes {\n  BarChartNumerical = \"barChartNumerical\",\n  DualBarChartNumerical = \"dualBarChartNumerical\",\n  BarChartPercentage = \"barChartPercentage\",\n  BarChartStats = \"barChartStats\",\n  StackedBarChartStats = \"stackedBarChartStats\",\n  StackedPercentageBarChartStats = \"stackedPercentageBarChartStats\",\n  StackedBarChartForLandcoverChangeMaps = \"stackedBarChartForLandcoverChangeMaps\",\n  CombinedStackedBarChartStats = \"combinedStackedBarChartStats\",\n  LineChart = \"lineChart\",\n  TimeSeries = \"timeSeries\",\n  DualTimeSeries = \"dualTimeSeriesChart\",\n  BoxplotTimeseries = \"boxplotTimeseries\",\n  BubbleChart = \"bubbleChart\",\n  ScatterPlot = \"scatterPlot\",\n  BoxPlot = \"boxPlot\",\n  HistogramChart = \"histogram\",\n  ViolinPlot = \"violinPlot\",\n  Heatmap = \"heatmap\",\n  RadarChart = \"radarChart\",\n  StackedBarChart = \"stackedBarChart\",\n  ParallelCoordinatesPlot = \"parallelCoordinatesPlot\",\n  PieChart = \"pieChart\",\n  PieChartPercentage = \"pieChartPercentage\",\n}\n\nconst selectChartType = () => {\n  const handleSelectChartType = (functionType: string) => {\n    switch (functionType) {\n      // Group equivalent cases\n      case \"Land Use/Land Cover Maps\":\n        return {\n          statsChart: ChartTypes.PieChartPercentage,\n          queryChart: ChartTypes.BarChartPercentage,\n        };\n\n      case \"CO Emissions Analysis\":\n      case \"CH4 Emissions Analysis\":\n      case \"NO2 Emissions Analysis\":\n      case \"PM2.5 Analysis\":\n      case \"Vulnerability Assessment\":\n        return {\n          statsChart: ChartTypes.BarChartStats,\n          queryChart: ChartTypes.BarChartNumerical,\n          unit: \"mol/m²\",\n        };\n\n      case \"Urban Heat Island (UHI) Analysis\":\n        return {\n          statsChart: ChartTypes.BoxplotTimeseries,\n          queryChart: ChartTypes.BarChartNumerical,\n          customTimeseriesChart: ChartTypes.TimeSeries,\n          unit: \"°C\",\n        };\n\n      case \"Air Pollutation Analysis\":\n        return {\n          statsChart: ChartTypes.CombinedStackedBarChartStats,\n          queryChart: ChartTypes.DualBarChartNumerical,\n          customTimeseriesChart: ChartTypes.DualTimeSeries,\n          unit: \"%\",\n        };\n\n      case \"Land Use/Land Cover Change Maps\":\n        return {\n          statsChart: ChartTypes.StackedBarChartForLandcoverChangeMaps,\n          queryChart: ChartTypes.StackedBarChartForLandcoverChangeMaps,\n        };\n\n      default:\n        return {\n          statsChart: ChartTypes.LineChart,\n          queryChart: ChartTypes.LineChart,\n        };\n    }\n  };\n\n  return handleSelectChartType;\n};\n\nexport default selectChartType;\n"
  },
  {
    "path": "features/chat/components/artifacts-sidebar/artifacts-sidebar.tsx",
    "content": "\"use client\";\nimport React, { use, useEffect } from \"react\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport MapContainer from \"@/features/maps/components/map-container\";\nimport useMapDisplayStore from \"@/features/maps/stores/use-map-display-store\";\nimport GenerateReport from \"../chat-response-box/in-response-tool-calling-results/draft-report/draft-report\";\nimport useDraftedReportStore from \"@/features/chat/stores/use-drafted-report-store\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport { Button } from \"@/components/ui/button\";\nimport { MoveRight } from \"lucide-react\";\nimport useMapLayersStore from \"@/features/maps/stores/use-map-layer-store\";\n\nconst ArtifactsSidebar = () => {\n  const isArtifactsSidebarOpen = useButtonsStore(\n    (state) => state.isArtifactsSidebarOpen\n  );\n  const toggleArtifactsSidebar = useButtonsStore(\n    (state) => state.toggleArtifactsSidebar\n  );\n\n  const draftedReport = useDraftedReportStore((state) => state.draftedReport);\n\n  const setDraftedReport = useDraftedReportStore(\n    (state) => state.setDraftedReport\n  );\n  const displayMapRequestedFromChatResponse = useMapDisplayStore(\n    (state) => state.displayMapRequestedFromChatResponse\n  );\n\n  const setMapLoaded = useMapLayersStore((state) => state.setMapLoaded);\n\n  const displayRawMapRequestedFromInsightsViewerIcon = useMapDisplayStore(\n    (state) => state.displayRawMapRequestedFromInsightsViewerIcon\n  );\n  const setDisplayMapRequestedFromChatResponse = useMapDisplayStore(\n    (state) => state.setDisplayMapRequestedFromChatResponse\n  );\n\n  const mapMaximizeRequested = useMapDisplayStore(\n    (state) => state.mapMaximizeRequested\n  );\n  const isSidebarCollapsed = useButtonsStore(\n    (state) => state.isSidebarCollapsed\n  );\n  const isROIDrawingActive = useROIStore((state) => state.isROIDrawingActive);\n  const setIsROIDrawingActive = useROIStore(\n    (state) => state.setIsROIDrawingActive\n  );\n\n  const handleToggleArtifactsSidebar = () => {\n    toggleArtifactsSidebar();\n    setDraftedReport(\"\");\n    setDisplayMapRequestedFromChatResponse(false);\n    setIsROIDrawingActive(false);\n  };\n\n  useEffect(() => {\n    if (\n      !isArtifactsSidebarOpen &&\n      (displayMapRequestedFromChatResponse || draftedReport)\n    ) {\n      toggleArtifactsSidebar();\n    }\n  }, [displayMapRequestedFromChatResponse, draftedReport]);\n\n  useEffect(() => {\n    if (isROIDrawingActive) {\n      setDraftedReport(\"\");\n      setDisplayMapRequestedFromChatResponse(true);\n    } else {\n      setDisplayMapRequestedFromChatResponse(false);\n      if (isArtifactsSidebarOpen) {\n        toggleArtifactsSidebar();\n      }\n    }\n  }, [isROIDrawingActive]);\n\n  useEffect(() => {\n    if (draftedReport) {\n      setMapLoaded(false);\n    }\n  }, [draftedReport]);\n\n  return (\n    <div\n      className={`${!isArtifactsSidebarOpen ? \"hidden\" : \"\"}\n      fixed right-0 top-0 h-full ${\n        mapMaximizeRequested\n          ? isSidebarCollapsed\n            ? \"w-[calc(100vw-5rem)]\"\n            : \"w-[calc(100vw-16rem)]\"\n          : \"w-[45vw]\"\n      } bg-background dark:bg-accent shadow-xl flex flex-col\n      border-l border-stone-300 dark:border-stone-600 transform transition-transform duration-300 \n      ease-in-out z-[2000]\n    `}\n    >\n      <div className=\"flex items-center justify-between bg-secondary px-4 py-3 bg-opacity-10 border-b border-stone-300 dark:border-stone-600\">\n        <h2 className=\"text-md font-semibold text-foreground\">\n          Insights Viewer\n        </h2>\n        <Button\n          onClick={handleToggleArtifactsSidebar}\n          className=\"p-1\"\n          aria-label=\"Close sidebar\"\n          variant=\"ghost\"\n          size={\"icon\"}\n        >\n          <MoveRight />\n        </Button>\n      </div>\n\n      <div className=\"flex-1 overflow-y-auto p-2 bg-stone-400 bg-opacity-5\">\n        <div className=\"flex flex-col items-center justify-center h-full\">\n          {draftedReport ? (\n            <GenerateReport report={draftedReport} />\n          ) : (\n            <>\n              <div\n                className={`w-full h-full ${\n                  displayMapRequestedFromChatResponse ||\n                  displayRawMapRequestedFromInsightsViewerIcon\n                    ? \"block\"\n                    : \"hidden\"\n                }`}\n              >\n                <MapContainer />\n              </div>\n            </>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default React.memo(ArtifactsSidebar);\n"
  },
  {
    "path": "features/chat/components/chat-response-box/capabilities-banner.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Alert, AlertTitle, AlertDescription } from \"@/components/ui/alert\";\nimport { useUserStore } from \"@/stores/use-user-profile-store\";\nimport { cn } from \"@/lib/utils\";\n\nexport function CapabilitiesBanner() {\n  const userName = useUserStore((state) => state.userName);\n  const maxArea = useUserStore((state) => state.maxArea);\n  const maxDocs = useUserStore((state) => state.maxDocs);\n  const userRole = useUserStore((state) => state.userRole);\n\n  return (\n    <div className=\"w-full flex justify-center my-6\">\n      <div className=\"max-w-lg w-full\">\n        <Alert\n          variant=\"default\"\n          className={cn(\n            \"border border-border rounded-lg p-4 shadow-sm\",\n            \"bg-secondary dark:bg-neutral-900\"\n          )}\n        >\n          <AlertTitle className=\"flex flex-col items-center justify-center gap-1\">\n            <div className=\"flex items-center gap-2\">\n              <span className=\"font-bold text-lg tracking-tight\">\n                Hello, {userName.split(\" \")[0] || \"there\"}!\n              </span>\n            </div>\n          </AlertTitle>\n\n          <AlertDescription className=\"mt-3 text-sm leading-relaxed text-center text-foreground/80 space-y-4\">\n            <p>\n              Here’s a quick overview of some of the capabilities of Chat2Geo:\n            </p>\n\n            {/* Group 1: Platform Capabilities */}\n            <div className=\"text-left\">\n              <p className=\"font-semibold mb-1\">Platform Capabilities:</p>\n              <ul className=\"list-none space-y-2 pl-4\">\n                <li>\n                  <span className=\"mr-2\">⚡</span>Real-time geospatial analysis\n                </li>\n                <li>\n                  <span className=\"mr-2\">📚</span>Knowledge Base queries using\n                  your docs\n                </li>\n                <li>\n                  <span className=\"mr-2\">📝</span>Drafting summary reports\n                </li>\n              </ul>\n            </div>\n\n            {/* Group 2: Geospatial Analyses */}\n            <div className=\"text-left\">\n              <p className=\"font-semibold mb-1\">Geospatial Analyses:</p>\n              <ul className=\"list-none space-y-2 pl-4\">\n                <li>\n                  <span className=\"mr-2\">🗺️</span>Land-use/land-cover mapping\n                </li>\n                <li>\n                  <span className=\"mr-2\">🔄</span>Bi-Temporal Change detection\n                </li>\n                <li>\n                  <span className=\"mr-2\">🏙️</span>Urban heat island analysis\n                </li>\n                <li>\n                  <span className=\"mr-2\">🌫️</span>\n                  Air pollution assessment\n                </li>\n              </ul>\n            </div>\n\n            {/* Group 3: Google Earth Engine Data Loading */}\n            <div className=\"text-left\">\n              <p className=\"font-semibold mb-1\">\n                Google Earth Engine (GEE) Data Loading (Experimental):\n              </p>\n              <ul className=\"list-none space-y-2 pl-4\">\n                <li>\n                  <span className=\"mr-2\">🌍</span>Load any raster dataset\n                  available on GEE\n                </li>\n\n                <li>\n                  <span className=\"mr-2\">🔍</span>Access to a wide range of GEE\n                  datasets with a single prompt!\n                </li>\n              </ul>\n            </div>\n\n            {/* Key Notes */}\n            <div className=\"pt-2 text-left\">\n              <p className=\"font-semibold\">Key Notes:</p>\n              <ul className=\"list-disc list-inside text-sm text-foreground/70 mt-1 space-y-1\">\n                <li>\n                  <strong>Role:</strong> {userRole || \"USER\"}\n                </li>\n                <li>\n                  You can store up to <strong>{maxDocs || \"N/A\"}</strong>{\" \"}\n                  documents for Knowledge Base queries.\n                </li>\n                <li>\n                  No analyses are available for <strong>2025</strong> yet.\n                </li>\n                <li>\n                  Maximum area per request is{\" \"}\n                  <strong>{maxArea || \"N/A\"}</strong> &nbsp;sq km.\n                </li>\n                <li>\n                  Analyses start from <strong>2015</strong>.\n                </li>\n              </ul>\n            </div>\n          </AlertDescription>\n        </Alert>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "features/chat/components/chat-response-box/chat-message/chat-message.tsx",
    "content": "\"use client\";\nimport React, { useEffect } from \"react\";\nimport { IconSparkles } from \"@tabler/icons-react\";\nimport ReactMarkdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\nimport { type Message, type ToolInvocation } from \"ai\";\nimport FadeInWithDelay from \"../../../ui/fadeIn-with-delay\";\nimport ToolCallingResults from \"../in-response-tool-calling-results/tool-calling-results\";\n\ninterface ChatMessageProps {\n  message: Message | ToolInvocation;\n  messageResults: ToolCallingMessageResults;\n}\n\nconst ChatMessage = React.memo(\n  ({ message, messageResults }: ChatMessageProps) => {\n    const [toolCallTitle, setToolCallTitle] = React.useState(\"\");\n\n    useEffect(() => {\n      if (\"args\" in message) {\n        setToolCallTitle(message.args.title);\n      } else {\n        setToolCallTitle(\"\");\n      }\n    }, [message]);\n\n    let isAssistant = false;\n    let messageContent = \"\";\n    let messageId = \"\";\n    if (\"role\" in message) {\n      messageId = message.id;\n      isAssistant = message.role === \"assistant\";\n      messageContent = message.content;\n    } else if (\"args\" in message) {\n      isAssistant = true;\n      messageContent = message.args.title;\n    }\n\n    const content = isAssistant ? (\n      <div className=\"flex justify-center min-w-full mb-10\">\n        <div className=\"relative w-[660px]\">\n          <div className=\"self-end flex py-3 w-full text-left\">\n            <div>\n              <IconSparkles\n                stroke={1}\n                className=\"text-blue-500 h-8 w-8 p-[.5px] mr-1\"\n              />\n            </div>\n            <div className=\"w-full\">\n              {toolCallTitle ? (\n                <div className=\"prose animate-shimmer text-blue-500 font-semibold\">\n                  {toolCallTitle}\n                </div>\n              ) : (\n                <ReactMarkdown\n                  className=\"prose prose-md leading-snug max-w-none break-words dark:prose-invert text-foreground/90\"\n                  remarkPlugins={[[remarkGfm, { singleTilde: false }]]}\n                >\n                  {messageContent}\n                </ReactMarkdown>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n    ) : (\n      <div className=\"flex justify-center\">\n        <div className=\"relative w-[660px] flex justify-end\">\n          <div className=\"w-fit max-w-[500px] bg-gray-100 dark:bg-accent/85 p-2 px-5 flex py-3 rounded-3xl text-left\">\n            {messageContent && (\n              <ReactMarkdown className=\"prose prose-md leading-snug break-words text-foreground/90\">\n                {messageContent}\n              </ReactMarkdown>\n            )}\n          </div>\n        </div>\n      </div>\n    );\n\n    return (\n      <div className=\"flex flex-col\">\n        {isAssistant ? (\n          <FadeInWithDelay delay={10} opacityDuration={1}>\n            {content}\n          </FadeInWithDelay>\n        ) : (\n          content\n        )}\n\n        {messageResults && (\n          <ToolCallingResults\n            messageId={messageId}\n            messageResults={messageResults}\n          />\n        )}\n      </div>\n    );\n  }\n);\n\nexport default ChatMessage;\n"
  },
  {
    "path": "features/chat/components/chat-response-box/chat-response-box.tsx",
    "content": "\"use client\";\n\nimport React, {\n  useEffect,\n  useRef,\n  useState,\n  useCallback,\n  startTransition,\n} from \"react\";\n\nimport { ToolInvocation } from \"ai\";\nimport { useChat } from \"@ai-sdk/react\";\nimport ChatInputBox from \"../input/chat-input-box\";\nimport { IconDeviceAnalytics } from \"@tabler/icons-react\";\nimport { useGeeOutputStore } from \"@/features/maps/stores/use-gee-ouput-store\";\nimport useChatResponseSourcesStore from \"@/features/chat/stores/use-chat-response-sources-store\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport ChatMessage from \"./chat-message/chat-message\";\nimport useFunctionStore from \"@/features/maps/stores/use-function-store\";\nimport useMapLayersStore from \"@/features/maps/stores/use-map-layer-store\";\nimport ArtifactsSidebar from \"../artifacts-sidebar/artifacts-sidebar\";\nimport { useSWRConfig } from \"swr\";\nimport { transformMetadataToCitations } from \"@/features/knowledge-base/utils/transform-metadata-to-citation\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport useMapDisplayStore from \"@/features/maps/stores/use-map-display-store\";\nimport useMapLegendStore from \"@/features/maps/stores/use-map-legend-store\";\nimport { checkUserUsageInChat, generateUUID } from \"../../utils/general-utils\";\nimport { useUserStore } from \"@/stores/use-user-profile-store\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\nimport { CapabilitiesBanner } from \"./capabilities-banner\";\nimport {\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n} from \"@/components/ui/tooltip\";\n\ninterface MessageCompletionState {\n  isComplete: boolean;\n  hasAnalysis: boolean;\n}\n\ninterface ChatResponseBoxProps {\n  chatId: string;\n  initialMessages: any;\n}\nconst ChatResponseBox = ({ chatId, initialMessages }: ChatResponseBoxProps) => {\n  const { mutate } = useSWRConfig();\n  const selectedRoiGeometryInChat = useROIStore(\n    (state) => state.selectedGeometryInChat\n  );\n\n  const addRoiGeometry = useROIStore((state) => state.addRoiGeometry);\n  const setRoiGeometryFromSessionHistory = useROIStore(\n    (state) => state.setRoiGeometryFromSessionHistory\n  );\n\n  const [isChatStarted, setIsChatStarted] = useState(false);\n  const usageRequests = useUserStore((state) => state.usageRequests);\n  const maxRequests = useUserStore((state) => state.maxRequests);\n  const maxArea = useUserStore((state) => state.maxArea);\n  const addMapLayer = useMapLayersStore((state) => state.addMapLayer);\n  const getMapLayersNames = useMapLayersStore(\n    (state) => state.getMapLayerNames\n  );\n  const addLegend = useMapLegendStore((state) => state.addLegend);\n  const setDisplayRawMapRequestedFromInsightsViewerIcon = useMapDisplayStore(\n    (state) => state.setDisplayRawMapRequestedFromInsightsViewerIcon\n  );\n\n  const [messageResults, setMessageResults] = useState<{\n    [messageId: string]: ToolCallingMessageResults;\n  }>({});\n  const [messageCompletionStates, setMessageCompletionStates] = useState<{\n    [key: string]: MessageCompletionState;\n  }>({});\n\n  const [pendingToolCallIds, setPendingToolCallIds] = useState<Set<string>>(\n    new Set()\n  );\n  const [toolResults, setToolResults] = useState<Set<string>>(new Set());\n  const [pendingResults, setPendingResults] = useState<any[]>([]);\n  const messagesEndRef = useRef<HTMLDivElement>(null);\n\n  const addNewGeeOutput = useGeeOutputStore((state) => state.addNewGeeOutput);\n  const addGeeTempMapInAssetsPath = useGeeOutputStore(\n    (state) => state.addTempCreatedMapInAssetsPath\n  );\n  const addFunctionConfig = useFunctionStore(\n    (state) => state.addFunctionConfig\n  );\n\n  const isArtifactsSidebarOpen = useButtonsStore(\n    (state) => state.isArtifactsSidebarOpen\n  );\n  const isSidebarCollapsed = useButtonsStore(\n    (state) => state.isSidebarCollapsed\n  );\n\n  const toggleArtifactsSidebar = useButtonsStore(\n    (state) => state.toggleArtifactsSidebar\n  );\n  const setChatResponseSources = useChatResponseSourcesStore(\n    (state) => state.setSources\n  );\n\n  const [toolCallTitle, setToolCallTitle] = useState<string>(\"\");\n\n  const [toolCallIdToMessageIdsMap, setToolCallIdToMessageIdsMap] = useState<{\n    [key: string]: Set<string>;\n  }>({});\n\n  const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(true);\n  const scrollContainerRef = useRef<HTMLDivElement>(null);\n  const isUserScrolling = useRef(false);\n\n  const { messages, input, handleInputChange, handleSubmit, isLoading } =\n    useChat({\n      experimental_throttle: 100,\n      onToolCall: (message) => {\n        setToolCallTitle((message.toolCall.args as any).title);\n      },\n      initialMessages: initialMessages,\n\n      body: {\n        id: chatId,\n        selectedRoiGeometryInChat: selectedRoiGeometryInChat,\n        mapLayersNames: getMapLayersNames(),\n      },\n\n      onFinish: (message) => {\n        mutate(\"/api/chat-history\");\n        useUserStore.getState().fetchAndSetUsage();\n        // When a message finishes, track its tool call IDs\n        const toolCallIds =\n          message.toolInvocations?.map(\n            (toolInvocation: ToolInvocation) => toolInvocation.toolCallId\n          ) || [];\n\n        setPendingToolCallIds((prev) => {\n          const newSet = new Set(prev);\n          toolCallIds.forEach((id) => newSet.add(id));\n          return newSet;\n        });\n\n        // Check completion state after tool results are processed\n        checkMessageCompletionState(message);\n      },\n    });\n\n  // Auto-scroll to the bottom of the chat box when new messages are added\n  useEffect(() => {\n    if (isAutoScrollEnabled && messagesEndRef.current && isChatStarted) {\n      isUserScrolling.current = true;\n      messagesEndRef.current.scrollIntoView({ behavior: \"smooth\" });\n\n      // Reset the user scrolling flag after animation completes\n      setTimeout(() => {\n        isUserScrolling.current = false;\n      }, 100);\n    }\n  }, [messages, isAutoScrollEnabled, isChatStarted]);\n\n  // Check if the user is scrolling up to disable auto-scroll\n  useEffect(() => {\n    const scrollContainer = scrollContainerRef.current;\n    if (!scrollContainer) return;\n\n    const handleScroll = () => {\n      if (!isUserScrolling.current) {\n        const { scrollTop, scrollHeight, clientHeight } = scrollContainer;\n        const isAtBottom =\n          Math.abs(scrollHeight - scrollTop - clientHeight) < 100;\n\n        if (!isAtBottom) {\n          setIsAutoScrollEnabled(false);\n        }\n      }\n    };\n\n    scrollContainer.addEventListener(\"scroll\", handleScroll);\n    return () => scrollContainer.removeEventListener(\"scroll\", handleScroll);\n  }, []);\n\n  // Handle sending a message\n  const handleSendMessage = useCallback(() => {\n    if (!isChatStarted) {\n      setIsChatStarted(true);\n    }\n    const userLimitReached = checkUserUsageInChat(\n      maxRequests,\n      maxArea,\n      selectedRoiGeometryInChat?.geometry,\n      usageRequests\n    );\n\n    if (userLimitReached?.reason) {\n      useToastMessageStore\n        .getState()\n        .setToastMessage(userLimitReached.reason, \"error\");\n      return;\n    }\n    if (input.trim()) {\n      const syntheticEvent = {\n        preventDefault: () => {},\n      } as React.FormEvent<HTMLFormElement>;\n\n      setIsAutoScrollEnabled(true);\n\n      handleSubmit(syntheticEvent);\n    }\n  }, [input, handleSubmit]);\n\n  // Handle sending a message on Enter key press\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n      if (event.key === \"Enter\" && !event.shiftKey) {\n        event.preventDefault();\n        handleSendMessage();\n      }\n    },\n    [handleSendMessage]\n  );\n\n  // Open the artifacts sidebar with the raw map\n  const handleOpenArtifactsSidebarWithRawMap = () => {\n    toggleArtifactsSidebar();\n    setDisplayRawMapRequestedFromInsightsViewerIcon(true);\n  };\n\n  const checkMessageCompletionState = useCallback(\n    (message: any) => {\n      const isToolInvocationComplete = !message.toolInvocations?.some(\n        (toolInvocation: ToolInvocation) =>\n          toolInvocation.toolName && !(toolInvocation as any).result\n      );\n\n      if (isToolInvocationComplete) {\n        // Get all tool call IDs for this message\n        const messageToolCallIds = new Set(\n          message.toolInvocations?.map(\n            (toolInvocation: ToolInvocation) => toolInvocation.toolCallId\n          ) || []\n        );\n\n        // Check if all tool results have been processed\n        const allToolResultsProcessed = Array.from(messageToolCallIds).every(\n          (toolCallId) => !pendingToolCallIds.has(toolCallId as string)\n        );\n\n        if (allToolResultsProcessed) {\n          setMessageCompletionStates((prev) => ({\n            ...prev,\n            [message.id]: {\n              isComplete: true,\n              hasAnalysis: true, // TODO: Update this because it may not be reliable for all cases.\n            },\n          }));\n        }\n      }\n    },\n    [toolCallIdToMessageIdsMap, pendingToolCallIds]\n  );\n\n  // Collect the results of the tool calls\n  useEffect(() => {\n    messages.forEach((m) => {\n      m.toolInvocations?.forEach((toolInvocation: ToolInvocation) => {\n        const extractedToolCallId = toolInvocation.toolCallId;\n\n        if (\n          toolInvocation.toolName &&\n          (toolInvocation as any).result &&\n          !toolResults.has(extractedToolCallId)\n        ) {\n          setToolResults((prev) => new Set(prev).add(extractedToolCallId));\n          const result = (toolInvocation as any).result;\n          setPendingResults((prev) => [\n            ...prev,\n            {\n              ...result,\n              toolCallId: extractedToolCallId,\n              toolName: toolInvocation.toolName,\n            },\n          ]);\n        }\n      });\n    });\n  }, [messages]);\n\n  //Process the results of the tool calls\n  // This optmized effect was given by Claude. Should be tested further\n  useEffect(() => {\n    if (pendingResults.length === 0 || isLoading) return;\n\n    const processResults = async () => {\n      for (const result of pendingResults) {\n        if (result.error) {\n          continue;\n        }\n\n        const { toolCallId, toolName } = result;\n\n        // Find the last assistant message that came after this tool call\n        const messageSequence = Array.from(\n          toolCallIdToMessageIdsMap[toolCallId] || []\n        );\n\n        const lastAssistantMessage = messages\n          .filter(\n            (m) => messageSequence.includes(m.id) && m.role === \"assistant\"\n          )\n          .pop();\n\n        if (!lastAssistantMessage) continue;\n\n        const lastMessageId = lastAssistantMessage.id;\n\n        switch (toolName) {\n          /////////////////////////////////////////////////////////////////////////\n          // 1. Handle Geospatial ANALYSIS\n          /////////////////////////////////////////////////////////////////////////\n          case \"requestGeospatialAnalysis\": {\n            const {\n              urlFormat,\n              legendConfig,\n              mapStats,\n              layerName,\n              uhiMetrics,\n              functionType,\n              startDate1,\n              endDate1,\n              startDate2,\n              endDate2,\n              aggregationMethod,\n              selectedRoiGeometry,\n              tempCreatedMapInAssetsPath,\n            } = result;\n\n            startTransition(() => {\n              // Add the result to the message state\n              setMessageResults((prev) => ({\n                ...prev,\n                [lastMessageId]: {\n                  ...prev[lastMessageId],\n                  geospatialAnalysis: {\n                    urlFormat,\n                    legendConfig,\n                    mapStats,\n                    layerName,\n                    uhiMetrics,\n                    functionType,\n                  },\n                },\n              }));\n\n              addNewGeeOutput({\n                urlFormat,\n                mapStats,\n                legendConfig,\n                layerName,\n                uhiMetrics,\n              });\n\n              if (tempCreatedMapInAssetsPath) {\n                addGeeTempMapInAssetsPath(\n                  layerName,\n                  tempCreatedMapInAssetsPath\n                );\n              }\n\n              // Save analysis config\n              addFunctionConfig({\n                functionType,\n                layerName,\n                selectedRoiGeometry: selectedRoiGeometry?.geometry || null,\n                startDate: startDate1,\n                endDate: endDate1,\n                startDate2,\n                endDate2,\n                aggregationMethod,\n                legendConfig,\n              });\n\n              // Add a map layer\n              addMapLayer({\n                id: generateUUID(),\n                layerFunctionType: functionType,\n                name: layerName,\n                visible: true,\n                type: \"raster\",\n                mapStats,\n                uhiMetrics,\n                roiName: selectedRoiGeometry?.name || \"\",\n              });\n\n              // Add legend\n              addLegend(layerName, legendConfig);\n            });\n            break;\n          }\n\n          /////////////////////////////////////////////////////////////////////////\n          // 2. Handle Geospatial DATA LOADING\n          /////////////////////////////////////////////////////////////////////////\n          case \"requestLoadingGeospatialData\": {\n            const {\n              urlFormat,\n              legendConfig,\n              mapStats,\n              layerName,\n              datasetId,\n              startDate,\n              endDate,\n              geospatialDataType,\n              selectedRoiGeometry,\n            } = result;\n\n            startTransition(() => {\n              // Update message results for data loading\n              setMessageResults((prev) => ({\n                ...prev,\n                [lastMessageId]: {\n                  ...prev[lastMessageId],\n                  geospatialData: {\n                    urlFormat,\n                    legendConfig,\n                    mapStats,\n                    layerName,\n                    datasetId,\n                    geospatialDataType,\n                  },\n                },\n              }));\n\n              // Possibly use a separate utility if you want different handling\n              // for \"data loading\" vs \"analysis.\"\n              addNewGeeOutput({\n                urlFormat,\n                mapStats,\n                legendConfig,\n                layerName,\n              });\n\n              addFunctionConfig({\n                functionType: geospatialDataType,\n                layerName,\n                selectedRoiGeometry: selectedRoiGeometry?.geometry || null,\n                startDate,\n                endDate,\n                legendConfig,\n              });\n\n              addMapLayer({\n                id: generateUUID(),\n                layerFunctionType: geospatialDataType,\n                name: layerName,\n                visible: true,\n                type: \"raster\",\n                mapStats,\n                roiName: selectedRoiGeometry?.name || \"\",\n              });\n\n              addLegend(layerName, legendConfig);\n            });\n            break;\n          }\n\n          /////////////////////////////////////////////////////////////////////////\n          // 3. Handle RAG QUERY\n          /////////////////////////////////////////////////////////////////////////\n          case \"requestRagQuery\": {\n            let sources = result.data;\n            // sources = sources.map((source: any) => {\n            //   source.metada;\n            // });\n            const citations = transformMetadataToCitations(sources);\n            startTransition(() => {\n              setMessageResults((prev) => ({\n                ...prev,\n                [lastMessageId]: {\n                  ...prev[lastMessageId],\n                  citationSources: citations,\n                  toolCallTitle,\n                },\n              }));\n              setChatResponseSources(toolCallId, sources);\n            });\n            break;\n          }\n\n          /////////////////////////////////////////////////////////////////////////\n          // 4. Handle DRAFT REPORT\n          /////////////////////////////////////////////////////////////////////////\n          case \"draftReport\": {\n            const { report, reportFileName } = result;\n            startTransition(() => {\n              setMessageResults((prev) => ({\n                ...prev,\n                [lastMessageId]: {\n                  ...prev[lastMessageId],\n                  draftedReport: report,\n                  toolCallTitle,\n                  reportFileName,\n                },\n              }));\n            });\n            break;\n          }\n        }\n      }\n      setPendingResults([]);\n    };\n\n    processResults();\n  }, [pendingResults, toolCallIdToMessageIdsMap]);\n\n  // Replaced the previous logic with a more robust one that can handle multiple tool call sequences\n  useEffect(() => {\n    let localActiveToolCallId: string | null = null;\n    const newToolCallIdToMessageIdsMap: { [key: string]: Set<string> } = {};\n\n    messages.forEach((m) => {\n      if (m.role === \"user\") {\n        // Every time user speaks, we start fresh\n        localActiveToolCallId = null;\n      }\n\n      if (m.toolInvocations && m.toolInvocations.length > 0 && !isLoading) {\n        // We have a new tool call sequence starting\n        m.toolInvocations.forEach((toolInvocation: ToolInvocation) => {\n          const toolCallId = toolInvocation.toolCallId;\n          if (!newToolCallIdToMessageIdsMap[toolCallId]) {\n            newToolCallIdToMessageIdsMap[toolCallId] = new Set();\n          }\n          newToolCallIdToMessageIdsMap[toolCallId].add(m.id);\n          localActiveToolCallId = toolCallId;\n        });\n      } else if (m.role === \"assistant\" && localActiveToolCallId) {\n        // This assistant message belongs to the currently active tool call\n        newToolCallIdToMessageIdsMap[localActiveToolCallId].add(m.id);\n        // If this is the final message in the sequence, reset\n        // Adjust this logic depending on how you know when the sequence is done\n        localActiveToolCallId = null;\n      }\n    });\n\n    setToolCallIdToMessageIdsMap(newToolCallIdToMessageIdsMap);\n  }, [messages, isLoading]);\n\n  // Add the ROI geometries from the initial messages when a history chat is loaded\n  useEffect(() => {\n    if (!initialMessages?.length) return;\n\n    initialMessages.forEach((message: any) => {\n      if (message.role === \"assistant\" && message.toolInvocations?.length) {\n        message.toolInvocations.forEach((invocation: any) => {\n          if (invocation.result && invocation.result.selectedRoiGeometry) {\n            const retrievedRoiGeometry = invocation.result.selectedRoiGeometry;\n            const { id, name, geometry, source } = retrievedRoiGeometry;\n            // Add that geometry to your ROI store\n            addRoiGeometry({ id, geometry, name, source });\n            addMapLayer({\n              id: id,\n              name: name,\n              visible: true,\n              type: \"roi\",\n              layerOpacity: 1,\n              roiName: null,\n            });\n\n            // Set the ROI from the session history: this will notify the map to display the ROI (use-map.ts)\n            setRoiGeometryFromSessionHistory(retrievedRoiGeometry);\n          }\n        });\n      }\n    });\n  }, [initialMessages, addRoiGeometry]);\n\n  const allItems = messages.flatMap((m) => {\n    // Add this message to processed messages\n\n    const items: {\n      key: string;\n      type: \"message\" | \"tool\";\n      data: any;\n      isLoading: boolean;\n    }[] = [{ key: m.id, type: \"message\", data: m, isLoading: isLoading }];\n\n    m.toolInvocations?.forEach((toolInvocation: ToolInvocation) => {\n      const toolCallId = toolInvocation.toolCallId;\n\n      if (!(toolInvocation as any).result) {\n        items.push({\n          key: toolCallId,\n          type: \"tool\",\n          data: toolInvocation,\n          isLoading: isLoading,\n        });\n      }\n    });\n\n    return items;\n  });\n\n  return (\n    <div\n      className={`flex-grow transition-all duration-300 ${\n        isSidebarCollapsed ? \"ml-20\" : \"ml-64\"\n      }`}\n    >\n      <div\n        ref={scrollContainerRef}\n        className={`\n        flex flex-col items-center justify-center h-screen\n        overflow-y-scroll w-full p-2\n        transition-all duration-500 ease-in-out\n        ${isArtifactsSidebarOpen ? \"pr-[45vw]\" : \"pr-0\"}\n      `}\n      >\n        <div className=\"flex flex-col items-center justify-start pt-10 gap-3 w-[400px] md:w-[750px] lg:w-full h-screen\">\n          {!isChatStarted && initialMessages.length === 0 && (\n            <CapabilitiesBanner />\n          )}\n          <button\n            className=\"fixed top-10 right-10 hover:bg-muted p-2 rounded-xl\"\n            onClick={handleOpenArtifactsSidebarWithRawMap}\n          />\n          {/* becomes: */}\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <button\n                className=\"fixed top-10 right-10 hover:bg-muted p-2 rounded-xl\"\n                onClick={handleOpenArtifactsSidebarWithRawMap}\n              >\n                <IconDeviceAnalytics\n                  stroke={1.5}\n                  size={30}\n                  className=\"text-foreground/80\"\n                />\n              </button>\n            </TooltipTrigger>\n            <TooltipContent side=\"left\">Open Insights viewer</TooltipContent>\n          </Tooltip>\n          {/* Messages */}\n          <div className=\"w-full\" style={{ paddingBottom: `${200}px` }}>\n            {allItems.map((item) => {\n              if (item.type === \"message\" || item.type === \"tool\") {\n                if (\n                  item.type === \"message\" &&\n                  (!item.data.content || item.data.content.trim() === \"\")\n                ) {\n                  return null;\n                }\n\n                return (\n                  <div className=\"flex w-full\" key={item.key}>\n                    <div\n                      className={`${\n                        item.data.role === \"user\"\n                          ? \"justify-center w-full\"\n                          : \"justify-center w-full\"\n                      } flex mb-3`}\n                    >\n                      <ChatMessage\n                        key={item.key}\n                        message={item.data}\n                        messageResults={messageResults[item.data.id]}\n                      />\n                    </div>\n                  </div>\n                );\n              }\n              return null;\n            })}\n            <div ref={messagesEndRef}></div>\n          </div>\n\n          {/* Input Box */}\n          <div className=\"w-full flex flex-col gap-4 items-center z-[1000] fade-in fixed bottom-5\">\n            <ChatInputBox\n              onSendMessage={handleSendMessage}\n              inputValue={input}\n              handleInputChange={handleInputChange}\n              handleKeyDown={handleKeyDown}\n              isStreaming={isLoading}\n            />\n          </div>\n\n          <div className=\"fixed flex justiy-center bottom-0 right-0 w-full z-[10] h-20 bg-background mr-10\"></div>\n        </div>\n      </div>\n      <div\n        className={`\n          fixed right-0 top-0 h-screen w-[660px] z-[2000]\n          transform transition-transform duration-500 ease-in-out\n          ${isArtifactsSidebarOpen ? \"translate-x-0\" : \"translate-x-full\"}\n        `}\n      >\n        <ArtifactsSidebar />\n      </div>\n    </div>\n  );\n};\n\nexport default React.memo(ChatResponseBox);\n"
  },
  {
    "path": "features/chat/components/chat-response-box/in-response-tool-calling-results/display-in-chat-analysis-map/display-in-chat-analysis-map-btn.tsx",
    "content": "import React from \"react\";\nimport { IconMap } from \"@tabler/icons-react\";\nimport useMapDisplayStore from \"@/features/maps/stores/use-map-display-store\";\nimport useLayerSelectionStore from \"@/features/maps/stores/use-layer-selection-store\";\nimport useZoomRequestStore from \"@/features/maps/stores/use-map-zoom-request-store\";\nimport useMapLayersStore from \"@/features/maps/stores/use-map-layer-store\";\n\nconst DisplayInChatAnalysisMapBtn = ({\n  analysisLayerName,\n}: {\n  analysisLayerName: string;\n}) => {\n  // 1) Grab your “selectedRasterLayer” setter\n  const selectedRasterLayer = useLayerSelectionStore(\n    (state) => state.selectedRasterLayer\n  );\n  const setSelectedRasterLayer = useLayerSelectionStore(\n    (state) => state.setSelectRasterLayer\n  );\n\n  // 2) Grab reorderLayers + mapLayers from the map-layers store\n  const reorderLayers = useMapLayersStore((state) => state.reorderLayers);\n  const mapLayers = useMapLayersStore((state) => state.mapLayers);\n\n  // For map display\n  const setDisplayMapRequestedFromChatResponse = useMapDisplayStore(\n    (state) => state.setDisplayMapRequestedFromChatResponse\n  );\n  const setZoomToLayerRequestWithGeometry = useZoomRequestStore(\n    (state) => state.setZoomToLayerRequestWithGeometry\n  );\n\n  const handleClick = () => {\n    // If we’re switching to a different raster, update the selection\n    if (analysisLayerName !== selectedRasterLayer.layerName) {\n      setSelectedRasterLayer(analysisLayerName);\n    }\n\n    // 3) Reorder the layers so `analysisLayerName` is last (on top in your store)\n    const layerNames = mapLayers.map((l) => l.name);\n    const index = layerNames.indexOf(analysisLayerName);\n    if (index !== -1) {\n      layerNames.splice(index, 1); // remove it\n      layerNames.push(analysisLayerName); // put it at the end\n      reorderLayers(layerNames);\n    }\n\n    // Trigger your zoom / display map\n    setZoomToLayerRequestWithGeometry(analysisLayerName);\n    setDisplayMapRequestedFromChatResponse(true);\n  };\n\n  return (\n    <div className=\"\">\n      <button\n        className=\"flex items-center bg-blue-500 border min-h-20 border-stone-300 \n                   dark:border-stone-600 bg-opacity-15 text-sm font-medium \n                   px-3 py-2 rounded-md hover:bg-opacity-20\"\n        onClick={handleClick}\n      >\n        <IconMap stroke={1.3} size={25} className=\"mr-3\" />\n        <span className=\"flex flex-col\">\n          <span>{analysisLayerName}</span>\n          <span className=\"text-xs text-muted-foreground self-start\">\n            Click to open map\n          </span>\n        </span>\n      </button>\n    </div>\n  );\n};\n\nexport default DisplayInChatAnalysisMapBtn;\n"
  },
  {
    "path": "features/chat/components/chat-response-box/in-response-tool-calling-results/draft-report/draft-report.tsx",
    "content": "\"use client\";\nimport React from \"react\";\nimport TextEditor from \"@/features/text-editor/components/text-editor\";\n\ninterface DraftReportProps {\n  report: string;\n}\nconst DraftReport = ({ report }: DraftReportProps) => {\n  return (\n    <div className=\"h-full w-full overflow-y-auto  text-black\">\n      <TextEditor inputText={report} />\n    </div>\n  );\n};\n\nexport default DraftReport;\n"
  },
  {
    "path": "features/chat/components/chat-response-box/in-response-tool-calling-results/draft-report/drafted-report-btn.tsx",
    "content": "import React, { useEffect } from \"react\";\nimport useDraftedReportStore from \"@/features/chat/stores/use-drafted-report-store\";\nimport { IconClipboardText } from \"@tabler/icons-react\";\n\ninterface DraftedReportBtnProps {\n  report: string;\n  reportFileName: string;\n}\nconst DraftedReportBtn = ({\n  report,\n  reportFileName,\n}: DraftedReportBtnProps) => {\n  const setDraftedReport = useDraftedReportStore(\n    (state) => state.setDraftedReport\n  );\n\n  const handleClick = () => {\n    setDraftedReport(report);\n  };\n\n  return (\n    <div className=\"\">\n      <button\n        className=\"flex items-center bg-green-500 border min-h-20 border-stone-300 dark:border-stone-600 bg-opacity-15 text-sm font-medium px-3 py-2 rounded-md hover:bg-opacity-20\"\n        onClick={handleClick}\n      >\n        <IconClipboardText stroke={1.3} size={25} className=\"mr-3\" />{\" \"}\n        <span className=\"flex flex-col\">\n          <span>{reportFileName}</span>\n          <span className=\"text-xs text-muted-foreground self-start\">\n            Click to open report\n          </span>\n        </span>\n      </button>\n    </div>\n  );\n};\n\nexport default DraftedReportBtn;\n"
  },
  {
    "path": "features/chat/components/chat-response-box/in-response-tool-calling-results/knowledge-base-citation/citation-badge.tsx",
    "content": "import React, { useState } from \"react\";\nimport { DocumentViewer } from \"@/components/document-viewer\";\nimport {\n  IconBrandGoogleDrive,\n  IconBrandOnedrive,\n  IconBrandNotion,\n  IconFileText,\n  IconWorld,\n} from \"@tabler/icons-react\";\nimport { Button } from \"@/components/ui/button\";\n\n// Define the colors for each source\nconst sourceColors: { [key: string]: string } = {\n  uploadedDocument: \"#4CAF50\",\n  OneDrive: \"#0078D4\",\n  GDrive: \"#4285F4\",\n  Notion: \"#000000\",\n  Confluence: \"#172B4D\",\n  Website: \"#FFA500\",\n};\n\n// Define the icons for each source\nconst sourceIcons: { [key: string]: React.ReactNode } = {\n  uploadedDocument: <IconFileText size={18} />,\n  OneDrive: <IconBrandOnedrive size={18} />,\n  GDrive: <IconBrandGoogleDrive size={18} />,\n  Notion: <IconBrandNotion size={18} />,\n  Confluence: <IconBrandNotion size={18} />,\n  Website: <IconWorld size={18} />,\n};\n\ninterface Citation {\n  documentName: string;\n  pages: { page: number }[];\n  similarity?: number;\n}\n\ninterface CitationBadgeProps {\n  citations: Citation[];\n  citationSource: string;\n}\n\nconst CitationBadge = ({ citations, citationSource }: CitationBadgeProps) => {\n  const color = sourceColors[citationSource];\n  const icon = sourceIcons[citationSource];\n\n  if (citations.length === 0) return null;\n\n  return (\n    <div className=\"space-y-4\">\n      {citations.map((citation, index) => (\n        <CitationItem\n          key={index}\n          citation={citation}\n          color={color}\n          icon={icon}\n        />\n      ))}\n    </div>\n  );\n};\n\ninterface CitationItemProps {\n  citation: Citation;\n  color: string;\n  icon: React.ReactNode;\n}\n\nconst CitationItem = ({ citation, color, icon }: CitationItemProps) => {\n  const [selectedPage, setSelectedPage] = useState<number | null>(null);\n\n  const handleDocumentClick = (pageNumber: number) => {\n    setSelectedPage(pageNumber);\n  };\n\n  const handleCloseViewer = () => {\n    setSelectedPage(null);\n  };\n\n  return (\n    <div className=\"relative flex flex-col space-y-2\">\n      {/* Document Header */}\n      <div className=\"flex items-center \">\n        <div\n          className=\"rounded-full p-2 flex items-center justify-center\"\n          style={{\n            color: color,\n          }}\n        >\n          {icon}\n        </div>\n        <div className=\"text-sm font-medium truncate\">\n          {citation.documentName}\n        </div>\n      </div>\n\n      {/* Pages */}\n      <div className=\"flex flex-wrap gap-2 pl-5\">\n        {citation.pages.map((page, index) => (\n          <Button\n            key={index}\n            style={{ backgroundColor: `${color}20` }}\n            variant=\"ghost\"\n            size={\"xs\"}\n            onClick={() => handleDocumentClick(page.page)}\n            className=\"text-xs px-3 py-1 rounded-full\"\n          >\n            Page {page.page}\n          </Button>\n        ))}\n      </div>\n\n      {/* Document Viewer */}\n      {selectedPage && (\n        <DocumentViewer\n          documentName={citation.documentName}\n          pageNumber={selectedPage}\n          onClose={handleCloseViewer}\n        />\n      )}\n    </div>\n  );\n};\n\nexport default CitationBadge;\n"
  },
  {
    "path": "features/chat/components/chat-response-box/in-response-tool-calling-results/tool-calling-results.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { ChartStatsDisplay } from \"@/features/charts/components/charts-display\";\nimport CitationBadge from \"./knowledge-base-citation/citation-badge\";\nimport DisplayInChatAnalysisMapBtn from \"./display-in-chat-analysis-map/display-in-chat-analysis-map-btn\";\nimport DraftedReportBtn from \"./draft-report/drafted-report-btn\";\nimport { validateToolCallingResults } from \"@/features/chat/utils/tool-calling-results-validation\";\nimport { Separator } from \"@/components/ui/separator\";\n\ninterface ToolCallingResultsProps {\n  messageId: string;\n  messageResults: ToolCallingMessageResults;\n}\n\nconst ToolCallingResults = React.memo(\n  ({ messageId, messageResults }: ToolCallingResultsProps) => {\n    const messageDraftedReport = messageResults?.draftedReport;\n    const messageCitationSources = messageResults?.citationSources;\n    const reportFileName = messageResults?.reportFileName;\n\n    const geospatialAnalysis = messageResults?.geospatialAnalysis;\n\n    const geospatialData = messageResults?.geospatialData;\n\n    const validationResults = validateToolCallingResults(messageResults);\n\n    if (!Object.values(validationResults).some((isValid) => isValid)) {\n      return null;\n    }\n\n    return (\n      <div className=\"flex flex-col gap-5 mb-10\">\n        {geospatialAnalysis && validationResults.geospatialAnalysis && (\n          <div className=\"flex flex-col h-full gap-8 justify-center items-center\">\n            <div className=\"flex justify-center items-center h-[350px] w-[75%]\">\n              <ChartStatsDisplay\n                functionType={geospatialAnalysis.functionType || \"\"}\n                mapStats={geospatialAnalysis.mapStats || []}\n                layerName={geospatialAnalysis.layerName || \"\"}\n                legendConfig={geospatialAnalysis.legendConfig}\n              />\n            </div>\n            <div className=\"w-full flex justify-start ml-20\">\n              <DisplayInChatAnalysisMapBtn\n                analysisLayerName={geospatialAnalysis.layerName || \"\"}\n              />\n            </div>\n          </div>\n        )}\n\n        {geospatialData && validationResults.geospatialData && (\n          <div className=\"flex flex-col h-full gap-8 justify-center items-center\">\n            <div className=\"w-full flex justify-start ml-20\">\n              <DisplayInChatAnalysisMapBtn\n                analysisLayerName={geospatialData.layerName || \"\"}\n              />\n            </div>\n          </div>\n        )}\n\n        {messageDraftedReport && validationResults.draftedReport && (\n          <div className=\"w-full flex justify-start ml-10\">\n            <DraftedReportBtn\n              report={messageDraftedReport}\n              reportFileName={reportFileName || \"\"}\n            />\n          </div>\n        )}\n        {messageCitationSources && validationResults.citationSources && (\n          <div className=\"flex flex-col gap-4 ml-10\">\n            <Separator />\n            <p className=\"font-semibold\">References:</p>\n            <CitationBadge\n              citations={messageCitationSources}\n              citationSource=\"uploadedDocument\"\n            />\n          </div>\n        )}\n      </div>\n    );\n  }\n);\n\nexport default ToolCallingResults;\n"
  },
  {
    "path": "features/chat/components/chat.tsx",
    "content": "import ChatResponseBox from \"./chat-response-box/chat-response-box\";\n\nconst MainChatPage = async ({\n  chatId,\n  initialMessages,\n}: ChatResponseBoxProps) => {\n  return <ChatResponseBox chatId={chatId} initialMessages={initialMessages} />;\n};\n\nexport default MainChatPage;\n"
  },
  {
    "path": "features/chat/components/external-assets/assets-modal.tsx",
    "content": "\"use client\";\n\nimport React, { useState } from \"react\";\nimport { useAgolLayersStore } from \"@/features/maps/stores/use-agol-layers-store\";\nimport { Button } from \"@/components/ui/button\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport {\n  isValidUrl,\n  sanitizeUrl,\n  isValidLayerName,\n  sanitizeLayerName,\n} from \"@/utils/validation-utils/validation-utils\";\nimport { fetchSelectedEsriLayer } from \"@/lib/fetchers/services/esri/fetch-selected-layer\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Input } from \"@/components/ui/input\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\n\nimport {\n  ColumnDef,\n  ColumnFiltersState,\n  SortingState,\n  VisibilityState,\n  flexRender,\n  getCoreRowModel,\n  getFilteredRowModel,\n  getSortedRowModel,\n  getPaginationRowModel,\n  useReactTable,\n  filterFns,\n} from \"@tanstack/react-table\";\n\nimport { Loader2, Search } from \"lucide-react\";\n\ninterface AssetsModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nconst columns: ColumnDef<ArcGISLayer>[] = [\n  {\n    id: \"select\",\n    header: ({ table }) => (\n      <Checkbox\n        checked={\n          table.getIsAllPageRowsSelected() ||\n          (table.getIsSomePageRowsSelected() && \"indeterminate\")\n        }\n        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}\n        aria-label=\"Select all\"\n      />\n    ),\n    cell: ({ row }) => (\n      <Checkbox\n        checked={row.getIsSelected()}\n        onCheckedChange={(value) => row.toggleSelected(!!value)}\n        aria-label=\"Select row\"\n      />\n    ),\n    enableSorting: false,\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"name\",\n    header: \"Name\",\n    cell: ({ row }) => {\n      // We run your sanitize function here\n      const rawName = row.getValue<string>(\"name\");\n      return <div>{sanitizeLayerName(rawName)}</div>;\n    },\n  },\n  {\n    accessorKey: \"type\",\n    header: \"Type\",\n  },\n];\n\nconst AssetsModal: React.FC<AssetsModalProps> = ({ isOpen, onClose }) => {\n  const [globalFilter, setGlobalFilter] = useState(\"\");\n\n  const setToastMessage = useToastMessageStore(\n    (state) => state.setToastMessage\n  );\n\n  const availableAgolLayers = useAgolLayersStore(\n    (state) => state.availableAgolLayers\n  );\n  const setAgolLayerRequestedToImport = useAgolLayersStore(\n    (state) => state.setAgolLayerRequestedToImport\n  );\n  const removeAgolLayerFromList = useAgolLayersStore(\n    (state) => state.removeAgolLayerFromList\n  );\n\n  const addRoiGeometry = useROIStore((state) => state.addRoiGeometry);\n\n  // 3. TanStack Table states\n  const [sorting, setSorting] = useState<SortingState>([]);\n  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);\n  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});\n  const [rowSelection, setRowSelection] = useState({});\n  const [isLayerImportingPending, setIsLayerImportingPending] = useState(false);\n\n  // 4. Create the table instance with your data and columns\n  const table = useReactTable({\n    data: availableAgolLayers ?? [],\n    columns,\n    state: {\n      sorting,\n      columnFilters,\n      columnVisibility,\n      rowSelection,\n      globalFilter,\n    },\n    onSortingChange: setSorting,\n    onColumnFiltersChange: setColumnFilters,\n    onColumnVisibilityChange: setColumnVisibility,\n    onRowSelectionChange: setRowSelection,\n    onGlobalFilterChange: setGlobalFilter,\n    globalFilterFn: filterFns.includesString,\n    getCoreRowModel: getCoreRowModel(),\n    getFilteredRowModel: getFilteredRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getPaginationRowModel: getPaginationRowModel(),\n  });\n\n  // Early return if modal is not open\n  if (!isOpen) return null;\n\n  const handleCloseModal = () => {\n    setRowSelection({});\n    onClose();\n  };\n\n  const handleImportLayerClick = async () => {\n    setIsLayerImportingPending(true);\n    const selectedRows = table\n      .getSelectedRowModel()\n      .rows.map((row) => row.original);\n\n    if (!selectedRows.length) {\n      setToastMessage(\"No layers selected\", \"error\");\n      setIsLayerImportingPending(false);\n      return;\n    }\n\n    // Import each selected layer\n    for (const layer of selectedRows) {\n      if (layer && isValidUrl(layer.url) && isValidLayerName(layer.name)) {\n        // 1) Trigger the ROI import\n        await fetchSelectedEsriLayer(layer, addRoiGeometry);\n        setAgolLayerRequestedToImport(layer);\n        removeAgolLayerFromList(layer.url);\n      } else {\n        setToastMessage(`Invalid layer: ${layer?.name}`, \"error\");\n      }\n    }\n\n    // Clear selected rows so the table resets\n    setRowSelection({});\n    setIsLayerImportingPending(false);\n\n    onClose();\n  };\n\n  return (\n    <>\n      <Dialog open={isOpen} onOpenChange={handleCloseModal}>\n        <DialogContent className=\"max-w-4xl h-[95vh] flex flex-col\">\n          <DialogHeader>\n            <DialogTitle>Assets</DialogTitle>\n          </DialogHeader>\n\n          <div className=\"relative px-6 mt-2 mb-4\">\n            <div className=\"relative max-w-sm\">\n              {/* Absolutely positioned icon */}\n              <span className=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-muted-foreground\">\n                <Search className=\"h-4 w-4\" />\n              </span>\n\n              <Input\n                placeholder=\"Search layers...\"\n                value={globalFilter ?? \"\"}\n                onChange={(e) => setGlobalFilter(e.target.value)}\n                className=\"pl-10\"\n              />\n            </div>\n          </div>\n\n          {/* ScrollArea is the parent */}\n          <ScrollArea className=\"flex-1 relative\">\n            {/* 6. TanStack Table rendering */}\n            <Table>\n              <TableHeader>\n                {table.getHeaderGroups().map((headerGroup) => (\n                  <TableRow key={headerGroup.id}>\n                    {headerGroup.headers.map((header) => (\n                      <TableHead key={header.id}>\n                        {header.isPlaceholder\n                          ? null\n                          : flexRender(\n                              header.column.columnDef.header,\n                              header.getContext()\n                            )}\n                      </TableHead>\n                    ))}\n                  </TableRow>\n                ))}\n              </TableHeader>\n\n              <TableBody>\n                {table.getRowModel().rows.length ? (\n                  table.getRowModel().rows.map((row) => (\n                    <TableRow\n                      key={row.id}\n                      data-state={row.getIsSelected() && \"selected\"}\n                    >\n                      {row.getVisibleCells().map((cell) => (\n                        <TableCell key={cell.id}>\n                          {flexRender(\n                            cell.column.columnDef.cell,\n                            cell.getContext()\n                          )}\n                        </TableCell>\n                      ))}\n                    </TableRow>\n                  ))\n                ) : (\n                  <TableRow>\n                    <TableCell\n                      colSpan={columns.length}\n                      className=\"h-24 text-center\"\n                    >\n                      No results.\n                    </TableCell>\n                  </TableRow>\n                )}\n              </TableBody>\n            </Table>\n          </ScrollArea>\n\n          <div className=\"flex justify-between items-center mt-6 px-6 pb-6\">\n            <Button\n              onClick={handleImportLayerClick}\n              disabled={availableAgolLayers.length === 0}\n            >\n              {isLayerImportingPending && <Loader2 className=\"animate-spin\" />}\n              Import Layer\n            </Button>\n          </div>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n};\n\nexport default AssetsModal;\n"
  },
  {
    "path": "features/chat/components/input/chat-input-box.tsx",
    "content": "\"use client\";\nimport React, {\n  useRef,\n  memo,\n  useState,\n  useEffect,\n  useMemo,\n  RefObject,\n} from \"react\";\nimport { RichTextarea, RichTextareaHandle } from \"rich-textarea\";\nimport { createPortal } from \"react-dom\";\nimport SlashMenuForMapLayers from \"./slash-menu-for-map-layers\";\nimport { useAttachmentStore } from \"../../stores/use-attachments-store\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport {\n  getCommandRendererSlashMenuMapLayersRenderer,\n  COMMAND_REG,\n} from \"@/features/chat/utils/slash-menu-utils\";\n\n// Components\nimport ChatInputDropzone from \"./chat-input-dropzone\";\nimport { ChatInputButtons } from \"./chat-input-buttons/chat-input-buttons\";\n\n// Custom Hooks\nimport { useTextareaResize } from \"@/features/chat/utils/use-Textarea-resize\";\nimport { useSlashCommandMenu } from \"@/features/chat/utils/use-slash-command-menu\";\nimport { useDragAndDropFileImport } from \"@/features/chat/utils/use-drag-and-drop-file-import\";\n\ninterface ChatInputBoxProps {\n  onSendMessage: () => void;\n  inputValue: string;\n  handleInputChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;\n  handleKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;\n  isStreaming: boolean;\n}\n\nconst ChatInputBox = memo<ChatInputBoxProps>(\n  ({\n    onSendMessage,\n    inputValue,\n    handleInputChange,\n    handleKeyDown,\n    isStreaming,\n  }) => {\n    const textareaRef = useRef<RichTextareaHandle>(null);\n    const [isSubmitting, setIsSubmitting] = useState(false);\n\n    // Store hooks\n    const addAttachment = useAttachmentStore((state) => state.addAttachment);\n    const removeAttachment = useAttachmentStore(\n      (state) => state.removeAttachment\n    );\n\n    const sessionRoiGeometries = useROIStore((state) => state.roiGeometries);\n\n    // ROI geometries state\n    const [currentRoiGeometries, setCurrentRoiGeometries] = useState(\n      sessionRoiGeometries.length > 0\n        ? sessionRoiGeometries.map((roi) => roi.name)\n        : [\" \"]\n    );\n\n    // Update ROI geometries state\n    useEffect(() => {\n      if (sessionRoiGeometries.length > 0) {\n        setCurrentRoiGeometries(sessionRoiGeometries.map((roi) => roi.name));\n      }\n    }, [sessionRoiGeometries]);\n\n    // Custom hooks\n    const textareaHeight = useTextareaResize(\n      textareaRef as RefObject<RichTextareaHandle>,\n      inputValue\n    );\n\n    const {\n      menuPos,\n      setMenuPos,\n      selectedIndex,\n      setSelectedIndex,\n      menuHeight,\n      filteredCommands,\n      completeCommand,\n      handleKeyDown: handleSlashMenuKeyDown,\n    } = useSlashCommandMenu({\n      inputValue,\n      currentRoiGeometries,\n      textareaRef: textareaRef as RefObject<RichTextareaHandle>,\n    });\n\n    const { getRootProps, getInputProps, isDragActive, open } =\n      useDragAndDropFileImport(addAttachment);\n\n    // Command renderer memo\n    const commandRendererSlashMenuMapLayersRenderer = useMemo(\n      () =>\n        getCommandRendererSlashMenuMapLayersRenderer(\n          currentRoiGeometries.length > 0 ? currentRoiGeometries : [\" \"]\n        ),\n      [currentRoiGeometries]\n    );\n\n    // Submit handler\n    const handleSubmit = async () => {\n      if (isSubmitting || isStreaming || !inputValue.trim()) return;\n      try {\n        setIsSubmitting(true);\n        await onSendMessage();\n      } finally {\n        setIsSubmitting(false);\n      }\n    };\n\n    return (\n      <div\n        className=\"flex flex-col gap-4 bg-gray-100 [box-shadow:0_-20px_20px_rgba(240,250,250,0.6),0_6px_10px_rgba(0,0,0,0.2)] dark:bg-accent dark:[box-shadow:0_-20px_20px_rgba(31,31,33,0.9),0_6px_10px_rgba(0,0,0,0.2)] h-full rounded-3xl items-center z-[1000] border border-stone-300 dark:border-stone-600\"\n        style={{\n          minHeight: \"160px\",\n        }}\n      >\n        <div\n          {...getRootProps()}\n          className=\"relative w-[400px] md:w-[500px] lg:w-[600px] xl:w-[700px] max-w-[700px] h-full min-h-[160px]\"\n        >\n          <input {...getInputProps()} />\n          <RichTextarea\n            ref={textareaRef}\n            placeholder=\"Type a message...\"\n            value={inputValue}\n            onChange={handleInputChange}\n            autoHeight={true}\n            onKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) =>\n              handleSlashMenuKeyDown(e, handleKeyDown)\n            }\n            onSelectionChange={(r: any) => {\n              if (\n                r.focused &&\n                COMMAND_REG.test(inputValue.slice(0, r.selectionStart))\n              ) {\n                setMenuPos({\n                  top: r.top,\n                  left: r.left,\n                  caret: r.selectionStart,\n                });\n                setSelectedIndex(0);\n              } else {\n                setMenuPos(null);\n                setSelectedIndex(0);\n              }\n            }}\n            className=\"py-4 px-4 h-full w-[400px] md:w-[500px] lg:w-[600px] xl:w-[700px] max-w-[700px] pb-16 z-[1000] bg-transparent overflow-hidden focus:outline-none focus:border-[#44403C] resize-none\"\n            style={{\n              caretColor: \"hsl(var(--foreground))\",\n            }}\n          >\n            {currentRoiGeometries[0]?.trim() &&\n              commandRendererSlashMenuMapLayersRenderer}\n          </RichTextarea>\n\n          <ChatInputDropzone isDragActive={isDragActive} />\n          <ChatInputButtons\n            isStreaming={isStreaming}\n            handleSubmit={handleSubmit}\n            inputValue={inputValue}\n            openFileDialog={open}\n          />\n        </div>\n\n        {menuPos &&\n          currentRoiGeometries[0]?.trim() &&\n          createPortal(\n            <SlashMenuForMapLayers\n              commands={filteredCommands}\n              index={selectedIndex}\n              top={menuPos.top}\n              left={menuPos.left}\n              complete={completeCommand}\n              menuHeight={menuHeight}\n            />,\n            document.body\n          )}\n      </div>\n    );\n  }\n);\n\nChatInputBox.displayName = \"ChatInputBox\";\n\nexport default ChatInputBox;\n"
  },
  {
    "path": "features/chat/components/input/chat-input-buttons/assistants-list-dropup-in-chat-input.tsx",
    "content": "import React, { useState } from \"react\";\nimport { IconChevronDown } from \"@tabler/icons-react\";\nimport { useTheme } from \"next-themes\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\nimport { AI_ASSISTANTS } from \"@/custom-configs/ai-assistants\";\n\nconst AssistantsListInChatInputBoxBtn = () => {\n  const [selectedAssistant, setSelectedAssistant] = useState(\n    AI_ASSISTANTS.default[0]\n  );\n\n  return (\n    <div className=\"absolute left-36 bottom-[11px] -translate-y-2 z-[1000]\">\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <button className=\"bg-secondary/50 text-xs p-1 text-foreground px-2 rounded-lg border border-stone-300 dark:border-stone-600 hover:bg-secondary/80 flex items-center justify-between w-full outline-none\">\n            <span className=\"flex items-center justify-center\">\n              <span className=\"font-semibold text-xs mr-1\">Assistant:</span>\n              <img\n                src={AI_ASSISTANTS.icons[selectedAssistant]}\n                alt={selectedAssistant}\n                className=\"w-3 h-3 mr-1\"\n              />\n              <span className=\"font-medium\">{selectedAssistant}</span>\n            </span>\n            <IconChevronDown size={14} stroke={2} className=\"ml-1\" />\n          </button>\n        </DropdownMenuTrigger>\n\n        <DropdownMenuContent\n          className=\"w-[200px] bg-popover\"\n          align=\"start\"\n          sideOffset={5}\n          side=\"top\"\n        >\n          {Object.entries(AI_ASSISTANTS).map(\n            ([category, assistants], categoryIndex) =>\n              Array.isArray(assistants) && (\n                <React.Fragment key={category}>\n                  {categoryIndex > 0 && <DropdownMenuSeparator />}\n                  <DropdownMenuLabel className=\"capitalize font-semibold\">\n                    {category}\n                  </DropdownMenuLabel>\n                  <DropdownMenuGroup>\n                    {assistants.map((assistant) => (\n                      <DropdownMenuItem\n                        key={assistant}\n                        className={`flex items-center gap-2 cursor-pointer ${\n                          assistant === selectedAssistant ? \"bg-accent\" : \"\"\n                        }`}\n                        onClick={() => setSelectedAssistant(assistant)}\n                      >\n                        <img\n                          src={AI_ASSISTANTS.icons[assistant]}\n                          alt={assistant}\n                          className=\"w-4 h-4\"\n                        />\n                        <span className=\"text-sm\">{assistant}</span>\n                      </DropdownMenuItem>\n                    ))}\n                  </DropdownMenuGroup>\n                </React.Fragment>\n              )\n          )}\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </div>\n  );\n};\n\nexport default AssistantsListInChatInputBoxBtn;\n"
  },
  {
    "path": "features/chat/components/input/chat-input-buttons/attachments-list-dropup-in-chat-input.tsx",
    "content": "import React from \"react\";\nimport { IconTrash } from \"@tabler/icons-react\";\nimport { useAttachmentStore } from \"@/features/chat/stores/use-attachments-store\";\n\nconst formatFileSize = (bytes: number): string => {\n  if (bytes < 1024) return bytes + \" B\";\n  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + \" KB\";\n  return (bytes / (1024 * 1024)).toFixed(1) + \" MB\";\n};\n\nconst AttachmentsListDropUpInChatInput: React.FC = () => {\n  const { attachments, removeAttachment } = useAttachmentStore();\n\n  if (attachments.length === 0) return null;\n\n  return (\n    <div className=\"relative mb-2 bg-white rounded-xl p-2 min-w-[80px] border border-stone-300\">\n      {/* Header */}\n      <div className=\"font-medium text-sm bg-gray-100 rounded-t-lg p-2 text-gray-700\">\n        Attached Files ({attachments.length})\n      </div>\n\n      {/* Attachments List */}\n      <ul className=\"divide-y divide-gray-200\">\n        {attachments.map((file) => (\n          <li\n            key={file.id}\n            className=\"flex items-start justify-between px-2 py-2 gap-2\"\n          >\n            <div className=\"flex-1 min-w-0\">\n              <div className=\"truncate text-sm font-semibold text-gray-700\">\n                {file.name}\n              </div>\n              <div className=\"text-xs text-gray-500\">\n                {formatFileSize(file.size)}\n              </div>\n            </div>\n\n            {/* Remove Button */}\n            <button\n              onClick={() => removeAttachment(file.id)}\n              className=\"p-1 \"\n              aria-label=\"Remove attachment\"\n            >\n              <IconTrash\n                size={18}\n                className=\" hover:text-red-600 hover:scale-110\"\n              />\n            </button>\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n};\n\nexport default AttachmentsListDropUpInChatInput;\n"
  },
  {
    "path": "features/chat/components/input/chat-input-buttons/chat-input-buttons.tsx",
    "content": "import { useState } from \"react\";\n\n// Lucide Icons\nimport {\n  IconArrowUp,\n  IconPaperclip,\n  IconPlayerStopFilled,\n} from \"@tabler/icons-react\";\n\nimport { Loader2 } from \"lucide-react\";\n\n// Shadcn Tooltips\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n// ^ adjust import path based on where your Shadcn components live\n\nimport AssistantsListInChatInputBoxBtn from \"./assistants-list-dropup-in-chat-input\";\nimport MapToolsDropup from \"../map-tools-dropup\";\nimport CurrentSessionAssetsDropup from \"./current-session-assets-dropup\";\nimport AssetsModal from \"../../external-assets/assets-modal\";\n\ninterface ChatInputButtonsProps {\n  isStreaming: boolean;\n  handleSubmit: () => void;\n  inputValue: string;\n  openFileDialog: () => void;\n}\n\nexport const ChatInputButtons = ({\n  isStreaming,\n  handleSubmit,\n  inputValue,\n  openFileDialog,\n}: ChatInputButtonsProps) => {\n  const [isAssetsModalOpen, setIsAssetsModalOpen] = useState(false);\n\n  const onOpenAssetSrouces = () => {\n    setIsAssetsModalOpen(true);\n  };\n  const onAssetsModalClose = () => {\n    setIsAssetsModalOpen(false);\n  };\n\n  return (\n    <TooltipProvider>\n      <div className=\"z-[1000]\">\n        {/* Attach Files Button */}\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <button\n              onClick={openFileDialog}\n              className=\"absolute pb-1 left-3 bottom-2 transform -translate-y-2\"\n            >\n              <IconPaperclip className=\"h-6 w-6 rotate-45 scale-x-[-1] text-primary antialiased\" />\n            </button>\n          </TooltipTrigger>\n          <TooltipContent side=\"top\">Attach files</TooltipContent>\n        </Tooltip>\n\n        {/* Additional Dropup Buttons */}\n        {/* <AssistantsListInChatInputBoxBtn /> */}\n        <MapToolsDropup onOpenAssetSrouces={onOpenAssetSrouces} />\n        <CurrentSessionAssetsDropup />\n        <AssetsModal isOpen={isAssetsModalOpen} onClose={onAssetsModalClose} />\n\n        {/* Send or Stop Button */}\n        {isStreaming ? (\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <button className=\"absolute right-1 bottom-2 transform -translate-y-2 px-2 z-[1000]\">\n                <Loader2\n                  size={35}\n                  className=\"animate-spin text-white \n                             bg-gray-800 dark:bg-background \n                             dark:text-foreground p-[9px] \n                             rounded-full\"\n                  style={{ animation: \"spin 1.5s linear infinite\" }}\n                />\n              </button>\n            </TooltipTrigger>\n            <TooltipContent side=\"top\">Stop streaming</TooltipContent>\n          </Tooltip>\n        ) : (\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <button\n                onClick={handleSubmit}\n                className=\"absolute right-1 bottom-2 transform -translate-y-2 px-2 z-[1000]\"\n                disabled={!inputValue.trim() || isStreaming}\n              >\n                <IconArrowUp\n                  size={31}\n                  strokeWidth={3}\n                  className={`p-[6px] pb-2 rounded-full ${\n                    inputValue.trim()\n                      ? \"bg-gray-800 dark:bg-gray-100 text-white dark:text-background\"\n                      : \"bg-gray-400 dark:bg-background text-white dark:text-muted-foreground\"\n                  }`}\n                />\n              </button>\n            </TooltipTrigger>\n            <TooltipContent side=\"top\">Send message</TooltipContent>\n          </Tooltip>\n        )}\n      </div>\n    </TooltipProvider>\n  );\n};\n"
  },
  {
    "path": "features/chat/components/input/chat-input-buttons/current-session-assets-dropup.tsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { IconServer, IconX } from \"@tabler/icons-react\";\nimport AttachmentsListDropUpInChatInput from \"@/features/chat/components/input/chat-input-buttons/attachments-list-dropup-in-chat-input\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n\nimport { useAttachmentStore } from \"@/features/chat/stores/use-attachments-store\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { removeExtension } from \"@/utils/general/general-utils\";\n\ninterface Attachment {\n  id: string;\n  name: string;\n  type: string;\n  size: number;\n}\n\nconst CurrentSessionAssetsDropup = () => {\n  const [isDropupOpen, setIsDropupOpen] = useState(false);\n  const attachments = useAttachmentStore((state) => state.attachments);\n  const roiGeometries = useROIStore((state) => state.roiGeometries);\n  const [numberOfSessionAssets, setNumberOfSessionAssets] = useState(0);\n  const [uniqueROIs, setUniqueROIs] = useState<ROIGeometry[]>([]);\n  const [filteredAttachments, setFilteredAttachments] = useState<Attachment[]>(\n    []\n  );\n\n  // Handle duplicate detection and update counts\n  useEffect(() => {\n    // Get set of ROI names\n    const roiNames = new Set(roiGeometries.map((roi) => roi.name));\n\n    // Filter out attachments that have matching ROI names\n    const nonROIAttachments = attachments.filter(\n      (att) => !roiNames.has(removeExtension(att.name))\n    );\n\n    setFilteredAttachments(nonROIAttachments);\n\n    // Process ROIs - mark those with matching attachment names as imported\n    const processedROIs = roiGeometries.map((roi) => {\n      const matchingAttachment = attachments.find(\n        (att) => removeExtension(att.name) === roi.name\n      );\n      if (matchingAttachment) {\n        return { ...roi, source: \"attached\" as const };\n      }\n      return roi;\n    });\n\n    setUniqueROIs(processedROIs);\n\n    // Total count is unique ROIs plus non-ROI attachments\n    const totalAssets = processedROIs.length + nonROIAttachments.length;\n    setNumberOfSessionAssets(totalAssets);\n\n    // Close dropup if no assets\n    if (totalAssets === 0) {\n      setIsDropupOpen(false);\n    }\n  }, [attachments, roiGeometries]);\n\n  // Close dropup when pressing Escape\n  useEffect(() => {\n    const handleEscapeKey = (event: KeyboardEvent) => {\n      if (isDropupOpen && event.key === \"Escape\") {\n        setIsDropupOpen(false);\n      }\n    };\n    document.addEventListener(\"keydown\", handleEscapeKey);\n    return () => document.removeEventListener(\"keydown\", handleEscapeKey);\n  }, [isDropupOpen]);\n\n  const handleToggleDropup = () => {\n    if (numberOfSessionAssets > 0) {\n      setIsDropupOpen((prev) => !prev);\n    }\n  };\n\n  const handleCloseDropup = () => setIsDropupOpen(false);\n\n  const ROILayersList = () => {\n    if (uniqueROIs.length === 0) return null;\n\n    return (\n      <div className=\"mt-4 bg-accent/40 p-3 rounded-xl\">\n        <h3 className=\"text-sm font-bold mb-2 text-primary/80\">ROI Layers</h3>\n        <div className=\"space-y-2 w-fit\">\n          {uniqueROIs.map((roi: ROIGeometry) => (\n            <div\n              key={roi.id}\n              className=\"flex items-center justify-between p-2  rounded-md shadow-sm\"\n            >\n              <div className=\"flex items-center space-x-5\">\n                <span className=\"text-sm text-primary font-medium\">\n                  {roi.name}\n                </span>\n                <span className=\"text-xs text-primary/90 px-2 py-1 bg-accent/30 border border-stone-300 dark: dark:border-stone-600 rounded\">\n                  {roi.source}\n                </span>\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n    );\n  };\n\n  return (\n    <TooltipProvider>\n      <>\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <button\n              className={`absolute ml-2 left-20 bottom-5 z-[1000] ${\n                attachments.length === 0 && roiGeometries.length === 0\n                  ? \"text-muted-foreground\"\n                  : \"text-primary hover:text-accent\"\n              }`}\n              onClick={handleToggleDropup}\n              disabled={attachments.length === 0 && roiGeometries.length === 0}\n            >\n              <div>\n                <IconServer\n                  stroke={2}\n                  className={`h-6 w-6 ${\n                    numberOfSessionAssets === 0\n                      ? \"text-muted-foreground\"\n                      : isDropupOpen\n                      ? \"text-blue-600\"\n                      : \"text-primary\"\n                  }`}\n                />\n                {numberOfSessionAssets > 0 && (\n                  <span className=\"absolute -top-[6px] -right-[5px] h-4 min-w-4 flex items-center justify-center leading-none text-xs font-semibold text-white bg-blue-500 rounded-full\">\n                    {numberOfSessionAssets}\n                  </span>\n                )}\n              </div>\n            </button>\n          </TooltipTrigger>\n          <TooltipContent side=\"top\">\n            {attachments.length === 0 && roiGeometries.length === 0\n              ? \"No assets added\"\n              : \"View your session assets\"}\n          </TooltipContent>\n        </Tooltip>\n        {isDropupOpen && (\n          <>\n            <div\n              className=\"fixed inset-0 z-[1999]\"\n              onClick={handleCloseDropup}\n            />\n            <div\n              className=\"absolute w-full bottom-full left-0 mb-2 bg-secondary rounded-3xl border border-stone-300 dark:border-stone-600 shadow-lg p-6 z-[2000]\"\n              onClick={(e) => e.stopPropagation()}\n            >\n              <div className=\"flex justify-between items-center mb-4\">\n                <h2 className=\"text-md font-bold text-primary/80\">\n                  Session Assets\n                </h2>\n                <button\n                  onClick={handleCloseDropup}\n                  className=\"text-primary hover:text-primary/90\"\n                >\n                  <IconX size={17} />\n                </button>\n              </div>\n              <Separator />\n              {filteredAttachments.length > 0 && (\n                <div>\n                  <h3 className=\"text-sm font-semibold mb-2\">Attachments</h3>\n                  <div className=\"space-y-2\">\n                    {filteredAttachments.map((attachment: Attachment) => (\n                      <div\n                        key={attachment.id}\n                        className=\"flex items-center justify-between p-2 bg-primary rounded-md shadow-sm\"\n                      >\n                        <div className=\"flex items-center space-x-2\">\n                          <span className=\"text-sm text-primary/90\">\n                            {attachment.name}\n                          </span>\n                          <span className=\"text-xs text-primary/70 px-2 py-1 bg-primary/90 rounded\">\n                            {attachment.type}\n                          </span>\n                        </div>\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              )}\n              <ROILayersList />\n            </div>\n          </>\n        )}\n      </>\n    </TooltipProvider>\n  );\n};\n\nexport default CurrentSessionAssetsDropup;\n"
  },
  {
    "path": "features/chat/components/input/chat-input-buttons/open-database-in-chat-input-btn.tsx",
    "content": "import { IconDatabase } from \"@tabler/icons-react\";\nimport React from \"react\";\n\ninterface OpenDatabaseInChatInputBtnProps {\n  onOpenAssetSrouces: () => void;\n}\n\nconst OpenDatabaseInChatInputBtn = ({\n  onOpenAssetSrouces,\n}: OpenDatabaseInChatInputBtnProps) => {\n  return (\n    <button\n      className=\"flex p-2 hover:bg-accent rounded-lg w-full text-left cursor-pointer\"\n      onClick={onOpenAssetSrouces}\n    >\n      <span className=\"flex items-start gap-2 antialiased\">\n        <IconDatabase stroke={2} size={20} className=\"mt-1\" />\n        <div className=\"flex flex-col\">\n          <span className=\"text-sm font-semibold\">Database</span>\n          <span className=\"text-sm text-muted-foreground\">\n            Import data from database\n          </span>\n        </div>\n      </span>\n    </button>\n  );\n};\n\nexport default OpenDatabaseInChatInputBtn;\n"
  },
  {
    "path": "features/chat/components/input/chat-input-buttons/select-roi-on-map-btn.tsx",
    "content": "import { IconSquareRoundedPlus2 } from \"@tabler/icons-react\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport useBadgeStore from \"@/features/maps/stores/use-map-badge-store\";\nimport React from \"react\";\n\ninterface SelectRoiOnMapBtnProps {\n  setIsDropupOpen: (isOpen: boolean) => void;\n}\n\nconst SelectRoiOnMapBtn = ({ setIsDropupOpen }: SelectRoiOnMapBtnProps) => {\n  const isROIDrawingActive = useROIStore((state) => state.isROIDrawingActive);\n  const setIsROIDrawingActive = useROIStore(\n    (state) => state.setIsROIDrawingActive\n  );\n\n  function handleClick() {\n    setIsROIDrawingActive(!isROIDrawingActive);\n    useBadgeStore.getState().reset();\n    setIsDropupOpen(false);\n  }\n\n  return (\n    <>\n      <button\n        className=\"flex p-2 hover:bg-accent rounded-lg w-full text-left cursor-pointer\"\n        onClick={handleClick}\n      >\n        <span className=\"flex items-start gap-2 antialiased\">\n          <IconSquareRoundedPlus2 stroke={2} size={20} className=\"mt-1\" />\n          <div className=\"flex flex-col\">\n            <span className=\"text-sm font-semibold\">Select ROI on map</span>\n            <span className=\"text-sm text-muted-foreground\">\n              Select Region of Interest on Map\n            </span>\n          </div>\n        </span>\n      </button>\n    </>\n  );\n};\n\nexport default SelectRoiOnMapBtn;\n"
  },
  {
    "path": "features/chat/components/input/chat-input-dropzone.tsx",
    "content": "interface ChatInputDropzoneProps {\n  isDragActive: boolean;\n}\nconst ChatInputDropzone = ({ isDragActive }: ChatInputDropzoneProps) => {\n  if (!isDragActive) return null;\n\n  return (\n    <div className=\"absolute inset-0 flex flex-col items-center justify-center bg-gradient-to-b bg-secondary bg-opacity-90 rounded-3xl z-[2000]\">\n      <p className=\"text-foreground text-lg\">Drop files here...</p>\n      <p className=\"text-foreground text-sm mt-2\">\n        Supported formats:\n        <span className=\"font-semibold\">.shp (zipped shapefile)</span>,\n        <span className=\"font-semibold\">.geojson</span>,\n        {/* <span className=\"font-semibold\">.pdf</span>,\n        <span className=\"font-semibold\">.txt</span>,\n        <span className=\"font-semibold\">.docx</span> */}\n      </p>\n    </div>\n  );\n};\n\nexport default ChatInputDropzone;\n"
  },
  {
    "path": "features/chat/components/input/map-tools-dropup.tsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\nimport { IconBackpack, IconX } from \"@tabler/icons-react\";\nimport SelectRoiOnMapBtn from \"./chat-input-buttons/select-roi-on-map-btn\";\nimport OpenDatabaseInChatInputBtn from \"./chat-input-buttons/open-database-in-chat-input-btn\";\nimport { Separator } from \"@/components/ui/separator\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n\ninterface MapToolsDropupProps {\n  onOpenAssetSrouces: () => void;\n}\n\nconst MapToolsDropup = ({ onOpenAssetSrouces }: MapToolsDropupProps) => {\n  const [isDropupOpen, setIsDropupOpen] = useState(false);\n  const dropupRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        dropupRef.current &&\n        !dropupRef.current.contains(event.target as Node)\n      ) {\n        setIsDropupOpen(false);\n      }\n    };\n    const handleEscapeKey = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        setIsDropupOpen(false);\n      }\n    };\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    document.addEventListener(\"keydown\", handleEscapeKey);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n      document.removeEventListener(\"keydown\", handleEscapeKey);\n    };\n  }, []);\n\n  return (\n    <TooltipProvider>\n      <div className=\"z-[1000]\" ref={dropupRef}>\n        {isDropupOpen && (\n          <div className=\"absolute bottom-full left-0 mb-2 bg-[#f4f4f4] dark:bg-secondary rounded-3xl shadow-lg p-2 w-full border border-stone-300 dark:border-stone-600\">\n            <div className=\"flex justify-between items-center p-4\">\n              <h2 className=\"text-md font-bold text-primary/80 ml-2\">\n                Toolbox\n              </h2>\n              <button\n                onClick={() => setIsDropupOpen(false)}\n                className=\"text-primary hover:text-primary/90\"\n              >\n                <IconX size={17} />\n              </button>\n            </div>\n            <Separator className=\"mb-2\" />\n            <SelectRoiOnMapBtn setIsDropupOpen={setIsDropupOpen} />\n            <OpenDatabaseInChatInputBtn\n              onOpenAssetSrouces={onOpenAssetSrouces}\n            />\n          </div>\n        )}\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <button\n              className=\"absolute left-12 bottom-5 text-forground z-[1000] cursor-pointer\"\n              onClick={() => setIsDropupOpen(!isDropupOpen)}\n            >\n              <IconBackpack\n                stroke={2}\n                className={`h-6 w-6 ${\n                  isDropupOpen ? \"text-blue-600\" : \"text-primary\"\n                }`}\n              />\n            </button>\n          </TooltipTrigger>\n          <TooltipContent side=\"top\">View your toolbox</TooltipContent>\n        </Tooltip>\n      </div>\n    </TooltipProvider>\n  );\n};\n\nexport default MapToolsDropup;\n"
  },
  {
    "path": "features/chat/components/input/slash-menu-for-map-layers.tsx",
    "content": "\"use client\";\n\nimport { Layers } from \"lucide-react\";\n\ninterface CommandMenuProps {\n  commands: string[];\n  index: number;\n  top: number;\n  left: number;\n  complete: (index: number) => void;\n  menuHeight: number;\n}\n\nconst SlashMenuForMapLayers: React.FC<CommandMenuProps> = ({\n  commands,\n  index,\n  top,\n  left,\n  complete,\n  menuHeight,\n}) => {\n  // Calculate the adjusted top position considering the header height\n  const headerHeight = 55; // 12px (py-3) + 20px (text) + 12px (py-3)\n  const footerHeight = 32; // 8px (py-2) + 16px (text) + 8px (py-2)\n  const adjustedMenuHeight = menuHeight + headerHeight + footerHeight;\n\n  return (\n    <div\n      className=\"fixed z-[1000] w-fit rounded-lg overflow-hidden bg-secondary shadow-xl border border-stone-300 dark:border-stone-600\"\n      style={{ top: top - adjustedMenuHeight, left }}\n    >\n      {/* Header */}\n      {commands.length > 0 && (\n        <div className=\"px-4 py-3 bg-secondary border-b border-stone-300 dark:border-stone-600\">\n          <div className=\"flex items-center gap-4\">\n            <Layers size={20} className=\"text-foreground\" />\n            <h3 className=\"text-sm font-semibold text-foreground\">\n              ROI Layers\n            </h3>\n          </div>\n        </div>\n      )}\n\n      {/* Command List */}\n      <div className=\"max-h-64 overflow-y-auto\">\n        {commands.map((cmd, i) => (\n          <div\n            key={cmd}\n            className={`px-4 py-2.5 cursor-pointer transition-colors duration-150\n              ${\n                index === i\n                  ? \"bg-blue-600 text-white hover:bg-blue-700\"\n                  : \"text-foreground hover:bg-gray-100\"\n              } \n              text-sm font-medium flex items-center gap-2`}\n            onMouseDown={(e) => {\n              e.preventDefault();\n              complete(i);\n            }}\n          >\n            {cmd}\n          </div>\n        ))}\n      </div>\n\n      {/* Footer instruction */}\n      {commands.length > 0 && (\n        <div className=\"px-4 py-2 bg-seondary border-t border-gray-200\">\n          <p className=\"text-xs text-muted-foreground\">\n            Use ↑↓ to navigate, enter to select\n          </p>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default SlashMenuForMapLayers;\n"
  },
  {
    "path": "features/chat/components/text-analysis-suggestions/text-analysis-suggestions.tsx",
    "content": "\"use client\";\nimport React, { useState, useEffect } from \"react\";\nimport { Tooltip } from \"react-tooltip\";\n\n// Full list of analysis suggestions\nconst fullAnalysisSuggestions = [\n  \"Land cover mapping\",\n  \"Methane monitoring\",\n  \"Urban heat island analysis\",\n  \"Flood risk analysis\",\n  \"Vegetation health monitoring\",\n  \"Air quality prediction\",\n  \"Soil moisture estimation\",\n  \"Drought assessment\",\n  \"Wildfire risk mapping\",\n  \"Biodiversity tracking\",\n];\n\n// Array of gentle pastel colors\nconst pastelColors = [\n  \"rgba(255, 179, 186, .8)\", // Very Light Pink\n  \"rgba(255, 223, 186, .8)\", // Very Light Orange\n  \"rgba(255, 255, 186, .8)\", // Very Light Yellow\n  \"rgba(186, 255, 201, .8)\", // Very Light Green\n  \"rgba(186, 225, 255, .8)\", // Very Light Blue\n];\n\n// Helper function to get random elements from an array\nconst getRandomElements = <T,>(array: T[], count: number): T[] => {\n  const shuffled = [...array].sort(() => Math.random() - 0.5); // Shuffle array\n  return shuffled.slice(0, count); // Take the first `count` elements\n};\n\nconst TextAnalysisSuggestions = () => {\n  const [randomSuggestions, setRandomSuggestions] = useState<\n    { suggestion: string; color: string }[]\n  >([]);\n\n  // Select 3 random suggestions and colors on component mount\n  useEffect(() => {\n    const suggestions = getRandomElements(fullAnalysisSuggestions, 3);\n    const colors = getRandomElements(pastelColors, 3);\n    const randomizedData = suggestions.map((suggestion, index) => ({\n      suggestion,\n      color: colors[index % colors.length],\n    }));\n    setRandomSuggestions(randomizedData);\n  }, []);\n\n  return (\n    <>\n      <div className=\"flex items-center justify-center\">\n        {randomSuggestions.map(({ suggestion, color }, index) => (\n          <div\n            key={index}\n            className=\"py-2 px-3 m-2 text-xs rounded-2xl border border-stone-300 dark:border-stone-600 cursor-pointer hover:scale-105 transition-all\"\n            style={{\n              backgroundColor: color.replace(\".8)\", \"0.2)\"),\n            }}\n            data-tooltip-id={`tooltip-${index}`}\n            data-tooltip-content=\"Use this prompt\"\n          >\n            {suggestion}\n          </div>\n        ))}\n      </div>\n\n      {/* Render Tooltips */}\n      {randomSuggestions.map((_, index) => (\n        <Tooltip\n          key={index}\n          id={`tooltip-${index}`}\n          place=\"top\"\n          style={{\n            backgroundColor: \"white\",\n            color: \"black\",\n            position: \"fixed\",\n            zIndex: 10000,\n            padding: \"8px\",\n            borderRadius: \"4px\",\n            boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.2)\",\n          }}\n        />\n      ))}\n    </>\n  );\n};\n\nexport default TextAnalysisSuggestions;\n"
  },
  {
    "path": "features/chat/components/thumbnails-analysis-suggestions/thumbnails-analysis-suggestions.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport Image from \"next/image\";\nimport { Tooltip } from \"react-tooltip\";\n\nconst thumbnailImages = [\n  { src: \"/images/air_quality.webp\", tooltip: \"Air Quality Analysis\" },\n  {\n    src: \"/images/landcover_landuse.webp\",\n    tooltip: \"Land-Use/Land-Cover Mapping\",\n  },\n  { src: \"/images/risk_map.webp\", tooltip: \"Risk Assessment\" },\n];\n\nconst ThumbnailsAnalysisSuggestions = () => {\n  return (\n    <>\n      <div className=\"flex justify-center\">\n        {thumbnailImages.map((image, index) => (\n          <div\n            key={index}\n            className=\"m-2 text-xs shadow-lg hover:scale-125 rounded-2xl cursor-pointer transition-transform duration-500\"\n            style={{ width: \"100px\", height: \"100px\" }}\n            data-tooltip-id={`tooltip-${index}`}\n            data-tooltip-content={image.tooltip}\n          >\n            <Image\n              src={image.src}\n              alt=\"Thumbnail\"\n              width={100}\n              height={100}\n              className=\"rounded-2xl w-full h-full object-contain\"\n            />\n          </div>\n        ))}\n        <div\n          className=\"m-2 bg-base-200 text-xs border border-base-300 bg-opacity-50 rounded-2xl shadow-lg hover:scale-125 cursor-pointer transition-transform duration-500 flex justify-center items-center\"\n          style={{ width: \"100px\", height: \"100px\" }}\n          data-tooltip-id=\"explore-more\"\n          data-tooltip-content=\"Explore additional features\"\n        >\n          <p className=\"text-center\">Explore more...</p>\n        </div>\n      </div>\n\n      <Tooltip\n        place=\"top\"\n        style={{\n          backgroundColor: \"white\",\n          color: \"black\",\n          position: \"fixed\",\n          zIndex: 10000,\n          padding: \"8px\",\n          borderRadius: \"4px\",\n          boxShadow: \"0 2px 8px rgba(0, 0, 0, 0.2)\",\n          fontWeight: \"600\",\n        }}\n      />\n    </>\n  );\n};\n\nexport default ThumbnailsAnalysisSuggestions;\n"
  },
  {
    "path": "features/chat/hooks/use-slash-menu-map-layers-list.ts",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport {\n  filterSlashMenuMapLayers,\n  MAX_LIST_LENGTH,\n} from \"@/features/chat/utils/slash-menu-utils\";\n\ninterface MenuPosition {\n  top: number;\n  left: number;\n  caret: number;\n}\n\ninterface UseSlashMenuMapLayersListProps {\n  menuPos: MenuPosition | null;\n  inputValue: string;\n  commands: string[];\n  maxListLength?: number;\n}\n\nexport const useSlashMenuMapLayersList = ({\n  menuPos,\n  inputValue,\n  commands,\n  maxListLength = MAX_LIST_LENGTH,\n}: UseSlashMenuMapLayersListProps) => {\n  const filteredCommands = useMemo(\n    () =>\n      filterSlashMenuMapLayers(menuPos, inputValue, commands, maxListLength),\n    [menuPos, inputValue, commands, maxListLength]\n  );\n\n  return {\n    filteredCommands,\n  };\n};\n"
  },
  {
    "path": "features/chat/stores/use-attachments-store.ts",
    "content": "import { create } from \"zustand\";\nimport { checkLayerName } from \"@/features/maps/utils/general-checks\";\n\n// Define the type for an attachment\nexport interface AttachmentFile {\n  id: string; // Unique identifier for the attachment\n  name: string; // File name\n  size: number; // File size in bytes\n  type: string; // MIME type (e.g., \"application/pdf\")\n  uploadedAt: Date; // Timestamp for when the attachment was added\n}\n\n// Define the store's state and actions\ninterface AttachmentStore {\n  attachments: AttachmentFile[];\n  addAttachment: (file: File) => void;\n  removeAttachment: (id: string) => void;\n  clearAttachments: () => void;\n  reset: () => void;\n}\n\nconst initialState = {\n  attachments: [] as AttachmentFile[],\n};\n\nexport const useAttachmentStore = create<AttachmentStore>((set) => ({\n  ...initialState,\n\n  addAttachment: (file: File) =>\n    set((state) => {\n      // Step 1: Gather all existing attachment names for files of the same type\n      const existingNamesForThisType = state.attachments\n        .filter((att) => att.type === file.type)\n        .map((att) => att.name);\n\n      // Step 2: Generate a unique name based on the incoming file's name\n      const uniqueName = checkLayerName(file.name, existingNamesForThisType);\n\n      // Step 3: Create the new attachment object\n      const newAttachment: AttachmentFile = {\n        // Use the unique name as part of your ID, or just keep using file.name—your choice\n        id: `${uniqueName}-${Date.now()}`,\n        name: uniqueName,\n        size: file.size,\n        type: file.type,\n        uploadedAt: new Date(),\n      };\n\n      // Step 4: Return the updated attachments array\n      return { attachments: [...state.attachments, newAttachment] };\n    }),\n\n  removeAttachment: (id: string) =>\n    set((state) => ({\n      attachments: state.attachments.filter(\n        (attachment) => attachment.id !== id\n      ),\n    })),\n\n  clearAttachments: () => set({ attachments: [] }),\n\n  reset: () => set({ ...initialState }),\n}));\n"
  },
  {
    "path": "features/chat/stores/use-chat-response-sources-store.ts",
    "content": "import { create } from \"zustand\";\n\n// Define the page structure for sources\ninterface Page {\n  page: number;\n  score: number;\n}\n\n// Define the structure of each source\ninterface Source {\n  documentName: string;\n  pages: Page[];\n}\n\n// Define the store's state and actions\ninterface ChatSourcesState {\n  chatResponseId: string | null; // To track the current source ID\n  sources: Source[]; // Array to hold sources grouped by document\n  setSources: (id: string, newSources: Source[]) => void; // Set source ID and sources\n  clearSources: () => void; // Clear both source ID and sources\n  reset: () => void; // Reset to initial defaults\n}\n\nconst initialState = {\n  chatResponseId: null as string | null,\n  sources: [] as Source[],\n};\n\n// Create the Zustand store\nconst useChatSourcesStore = create<ChatSourcesState>((set) => ({\n  ...initialState,\n\n  setSources: (id, newSources) =>\n    set({\n      chatResponseId: id,\n      sources: newSources,\n    }),\n\n  clearSources: () => set({ chatResponseId: null, sources: [] }),\n\n  // Reset the store to initial state\n  reset: () => set({ ...initialState }),\n}));\n\nexport default useChatSourcesStore;\n"
  },
  {
    "path": "features/chat/stores/use-drafted-report-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface ReportState {\n  draftedReport: string | null; // Holds the report content, or null if no report is drafted\n  reportFileName: string; // Holds the name of the report file\n  setReportFileName: (fileName: string) => void; // Function to set the report file name\n  setDraftedReport: (report: string) => void; // Function to set a new report\n  clearDraftedReport: () => void; // Function to clear the report\n  reset: () => void; // Resets all store properties to their initial state\n}\n\nconst initialState = {\n  draftedReport: null,\n  reportFileName: \"\",\n};\n\nconst useDraftedReportStore = create<ReportState>((set) => ({\n  ...initialState,\n\n  setReportFileName: (fileName) => set({ reportFileName: fileName }),\n\n  setDraftedReport: (report) => set({ draftedReport: report }),\n\n  clearDraftedReport: () => set({ draftedReport: null }),\n\n  // Resets the store to the initial defaults\n  reset: () => set({ ...initialState }),\n}));\n\nexport default useDraftedReportStore;\n"
  },
  {
    "path": "features/chat/ui/fadeIn-with-delay.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\n\ninterface FadeInWithDelayProps {\n  delay: number;\n  opacityDuration?: number; // Duration for opacity animation\n  transformDuration?: number; // Duration for transform animation\n  easing?: string; // Easing for both animations\n  children: React.ReactNode;\n}\n\nconst FadeInWithDelay: React.FC<FadeInWithDelayProps> = ({\n  delay,\n  opacityDuration = 1, // Default to 1s\n  transformDuration = 1, // Default to 1s\n  easing = \"ease-in-out\", // Default easing\n  children,\n}) => {\n  const [isVisible, setIsVisible] = useState(false);\n\n  useEffect(() => {\n    const timeout = setTimeout(() => {\n      setIsVisible(true);\n    }, delay);\n\n    return () => clearTimeout(timeout); // Cleanup the timeout on unmount\n  }, [delay]);\n\n  return (\n    <div\n      style={{\n        opacity: isVisible ? 1 : 0,\n        transition: `\n          opacity ${opacityDuration}s ${easing},\n          transform ${transformDuration}s ${easing}\n        `, // Separate durations and shared easing\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n\nexport default FadeInWithDelay;\n"
  },
  {
    "path": "features/chat/utils/drag-and-drop-file-analyzer.ts",
    "content": "import proj4 from \"proj4\";\n//@ts-ignore\nimport { reproject } from \"reproject\";\n//@ts-ignore\nimport epsg from \"epsg\";\nimport shp from \"shpjs\";\nimport JSZip from \"jszip\";\nimport { booleanValid } from \"@turf/turf\";\n\nexport async function dragAndDropFileAnalyzer(file: File): Promise<any> {\n  const extension = file.name?.split(\".\").pop()?.toLowerCase();\n\n  if (!extension) {\n    throw new Error(\"Invalid file: Unable to determine file extension.\");\n  }\n\n  let geojson: any;\n\n  switch (extension) {\n    case \"geojson\":\n    case \"json\": {\n      try {\n        const parsed = JSON.parse(await file.text());\n\n        if (\n          !parsed.features ||\n          !Array.isArray(parsed.features) ||\n          !parsed.features.every((feature: any) =>\n            booleanValid(feature.geometry)\n          )\n        ) {\n          throw new Error(\"Invalid GeoJSON format or geometry.\");\n        }\n\n        geojson = parsed;\n        return { type: \"geojson\", content: geojson };\n      } catch (error) {\n        throw new Error(`Failed to parse GeoJSON file: ${error}`);\n      }\n    }\n\n    case \"zip\": {\n      try {\n        const zip = await JSZip.loadAsync(file);\n        const entries = Object.keys(zip.files);\n\n        // Check for required Shapefile components\n        const requiredFiles = [\".shp\", \".dbf\", \".shx\"];\n        const missingFiles = requiredFiles.filter(\n          (ext) => !entries.some((entry) => entry.endsWith(ext))\n        );\n\n        if (missingFiles.length > 0) {\n          throw new Error(\n            `Missing required Shapefile components: ${missingFiles.join(\", \")}`\n          );\n        }\n\n        // Check for optional PRJ file\n        const prjFile = entries.find((entry) => entry.endsWith(\".prj\"));\n        if (!prjFile) {\n          console.warn(\n            \"Projection file (.prj) is missing. Defaulting to WGS84.\"\n          );\n        }\n\n        // Parse the shapefile\n        geojson = await shp(await file.arrayBuffer());\n\n        // Reproject GeoJSON/Shapefile to WGS84\n        const fromProj = geojson?.crs?.properties?.name || \"EPSG:4326\"; // Default to WGS84\n        geojson = reproject(geojson, fromProj, \"EPSG:4326\", proj4, epsg);\n\n        return { type: \"shapefile\", content: geojson }; // Return as \"shapefile\"\n      } catch (error) {\n        throw new Error(\n          `Failed to parse zipped Shapefile: ${error}. Ensure it includes SHP, DBF, SHX, and optionally PRJ files.`\n        );\n      }\n    }\n\n    default: {\n      throw new Error(\n        `Unsupported file format: ${extension}. Supported formats are: GeoJSON and zipped Shapefiles.`\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "features/chat/utils/general-utils.ts",
    "content": "import type {\n  CoreAssistantMessage,\n  CoreMessage,\n  CoreToolMessage,\n  Message,\n  ToolInvocation,\n} from \"ai\";\nimport { openai } from \"@ai-sdk/openai\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { generateText, CoreUserMessage } from \"ai\";\nimport { checkGeometryAreaIsLessThanThreshold } from \"@/features/maps/utils/geometry-utils\";\n\nexport async function generateTitleFromUserMessage({\n  message,\n}: {\n  message: CoreUserMessage;\n}) {\n  const { text: title } = await generateText({\n    model: openai(\"gpt-4o-mini\"),\n    system: `\\n\n    - you will generate a short title based on the first message a user begins a conversation with\n    - ensure it is not more than 80 characters long\n    - the title should be a summary of the user's message\n    - do not use quotes or colons`,\n    prompt: JSON.stringify(message),\n  });\n\n  return title;\n}\n\nexport function getFormattedDate(): string {\n  const today = new Date();\n  const year = today.getFullYear();\n  const month = String(today.getMonth() + 1).padStart(2, \"0\");\n  const day = String(today.getDate()).padStart(2, \"0\");\n  return `${year}-${month}-${day}`;\n}\n\nexport function getLocalStorage(key: string) {\n  if (typeof window !== \"undefined\") {\n    return JSON.parse(localStorage.getItem(key) || \"[]\");\n  }\n  return [];\n}\n\nexport function generateUUID(): string {\n  return uuidv4();\n}\n\nexport function isQueryUuid(name: string) {\n  if (!name.startsWith(\"query_\")) return false;\n\n  // Remove \"query_\" and see if the rest matches a UUID (v4) pattern\n  const possibleUuid = name.slice(6); // length of \"query_\"\n  const uuidV4Regex =\n    /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n  return uuidV4Regex.test(possibleUuid);\n}\n\nexport function sanitizeResponseMessages(\n  messages: Array<CoreToolMessage | CoreAssistantMessage>\n): Array<CoreToolMessage | CoreAssistantMessage> {\n  const toolResultIds: Array<string> = [];\n\n  for (const message of messages) {\n    if (message.role === \"tool\") {\n      for (const content of message.content) {\n        if (content.type === \"tool-result\") {\n          toolResultIds.push(content.toolCallId);\n        }\n      }\n    }\n  }\n\n  const messagesBySanitizedContent = messages.map((message) => {\n    if (message.role !== \"assistant\") return message;\n\n    if (typeof message.content === \"string\") return message;\n\n    const sanitizedContent = message.content.filter((content) =>\n      content.type === \"tool-call\"\n        ? toolResultIds.includes(content.toolCallId)\n        : content.type === \"text\"\n        ? content.text.length > 0\n        : true\n    );\n\n    return {\n      ...message,\n      content: sanitizedContent,\n    };\n  });\n\n  return messagesBySanitizedContent.filter(\n    (message) => message.content.length > 0\n  );\n}\n\nexport function getMostRecentUserMessage(messages: Array<CoreMessage>) {\n  const userMessages = messages.filter((message) => message.role === \"user\");\n  return userMessages.at(-1);\n}\n\nfunction addToolMessageToChat({\n  toolMessage,\n  messages,\n}: {\n  toolMessage: CoreToolMessage;\n  messages: Array<Message>;\n}): Array<Message> {\n  return messages.map((message) => {\n    if (message.toolInvocations) {\n      return {\n        ...message,\n        toolInvocations: message.toolInvocations.map((toolInvocation) => {\n          const toolResult = toolMessage.content.find(\n            (tool) => tool.toolCallId === toolInvocation.toolCallId\n          );\n\n          if (toolResult) {\n            return {\n              ...toolInvocation,\n              state: \"result\",\n              result: toolResult.result,\n            };\n          }\n\n          return toolInvocation;\n        }),\n      };\n    }\n\n    return message;\n  });\n}\n\nexport function convertToUIMessages(messages: Array<any>): Array<Message> {\n  return messages.reduce((chatMessages: Array<Message>, message) => {\n    if (message.role === \"tool\") {\n      return addToolMessageToChat({\n        toolMessage: message as CoreToolMessage,\n        messages: chatMessages,\n      });\n    }\n\n    let textContent = \"\";\n    const toolInvocations: Array<ToolInvocation> = [];\n\n    if (typeof message.content === \"string\") {\n      textContent = message.content;\n    } else if (Array.isArray(message.content)) {\n      for (const content of message.content) {\n        if (content.type === \"text\") {\n          textContent += content.text;\n        } else if (content.type === \"tool-call\") {\n          toolInvocations.push({\n            state: \"call\",\n            toolCallId: content.toolCallId,\n            toolName: content.toolName,\n            args: content.args,\n          });\n        }\n      }\n    }\n\n    chatMessages.push({\n      id: message.id,\n      role: message.role as Message[\"role\"],\n      content: textContent,\n      toolInvocations,\n    });\n\n    return chatMessages;\n  }, []);\n}\n\nexport function checkUserUsageInChat(\n  maxRequests: number,\n  maxArea: number,\n  roiGeometry: any,\n  usageRequests: number\n) {\n  if (roiGeometry) {\n    const isGeometryAreaIsLessThanThreshold =\n      checkGeometryAreaIsLessThanThreshold(roiGeometry, maxArea);\n    if (!isGeometryAreaIsLessThanThreshold) {\n      return {\n        reason:\n          \"The area of the region of interest is too large. Please select a smaller area.\",\n      };\n    }\n  }\n  if (usageRequests >= maxRequests) {\n    return {\n      reason:\n        \"You have reached your maximum number of requests for this month. Please upgrade your subscription to continue using the tool.\",\n    };\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "features/chat/utils/slash-menu-utils.ts",
    "content": "\"use client\";\n\nimport { createRegexRenderer } from \"rich-textarea\";\n\nexport const MAX_LIST_LENGTH = 8;\n\n// Keep this the same for menu detection\nexport const COMMAND_REG = /\\B\\/([\\-+\\w]*)$/;\n\n// Modified to match commands without requiring the slash\nexport const createCommandHighlightRegex = (commands: string[]) => {\n  return new RegExp(`(${commands.join(\"|\")})(?=\\\\s|$)`, \"g\");\n};\n\nexport const getCommandRendererSlashMenuMapLayersRenderer = (\n  commands: string[]\n) => {\n  const highlightRegex = createCommandHighlightRegex(commands);\n  return createRegexRenderer([\n    [\n      highlightRegex,\n      {\n        background: \"#EAF5F9\",\n        color: \"#4276AA\",\n        borderRadius: \"3px\",\n        border: \"1px solid #B3D4E0\",\n      },\n    ],\n  ]);\n};\n\nexport const filterSlashMenuMapLayers = (\n  menuPos: { caret: number } | null,\n  inputValue: string,\n  commands: string[],\n  maxLength: number = MAX_LIST_LENGTH\n): string[] => {\n  if (!menuPos) return [];\n  const match = inputValue.slice(0, menuPos.caret).match(COMMAND_REG);\n  const command = match?.[1] ?? \"\";\n  return commands\n    .filter((cmd) => cmd.toLowerCase().startsWith(command.toLowerCase()))\n    .slice(0, maxLength);\n};\n"
  },
  {
    "path": "features/chat/utils/tool-calling-results-validation.ts",
    "content": "interface GeospatialAnalysisResult {\n  functionType?: string; // Optional properties to match ToolCallingMessageResults\n  layerName?: string;\n  legendConfig?: string;\n  urlFormat?: string;\n  mapStats?: any;\n  uhiMetrics?: any;\n}\n\nexport function validateToolCallingResults(\n  messageResults: ToolCallingMessageResults\n): Record<string, boolean> {\n  const validationResults: Record<string, boolean> = {};\n\n  // Validate geospatialAnalysis\n  if (messageResults.geospatialAnalysis) {\n    const { functionType, layerName, legendConfig, urlFormat } =\n      messageResults.geospatialAnalysis;\n    validationResults.geospatialAnalysis =\n      !!functionType && !!layerName && !!legendConfig && !!urlFormat;\n  } else {\n    validationResults.geospatialAnalysis = false;\n  }\n\n  if (messageResults.geospatialData) {\n    const { urlFormat, layerName, legendConfig } =\n      messageResults.geospatialData;\n    validationResults.geospatialData =\n      !!urlFormat && !!layerName && !!legendConfig;\n  } else {\n    validationResults.geospatialData = false;\n  }\n\n  if (messageResults.citationSources) {\n    // Validate citationSources\n    validationResults.citationSources = Array.isArray(\n      messageResults.citationSources\n    );\n  } else {\n    validationResults.citationSources = false;\n  }\n\n  // Validate draftedReport\n  if (messageResults.draftedReport) {\n    validationResults.draftedReport =\n      !!messageResults.draftedReport && !!messageResults.reportFileName; // Report file name must exist too\n  } else {\n    validationResults.draftedReport = false;\n  }\n\n  return validationResults;\n}\n"
  },
  {
    "path": "features/chat/utils/use-Textarea-resize.ts",
    "content": "import { useState, useLayoutEffect, RefObject } from \"react\";\nimport { RichTextareaHandle } from \"rich-textarea\";\n\nexport const useTextareaResize = (\n  textareaRef: RefObject<RichTextareaHandle>,\n  inputValue: string\n) => {\n  const [textareaHeight, setTextareaHeight] = useState(\"150px\");\n\n  useLayoutEffect(() => {\n    if (textareaRef.current) {\n      const scrollHeight = textareaRef.current.scrollHeight;\n      const maxHeight = 300;\n      const newHeight = Math.min(scrollHeight, maxHeight);\n      setTextareaHeight(`${newHeight}px`);\n    }\n  }, [inputValue]);\n\n  return textareaHeight;\n};\n"
  },
  {
    "path": "features/chat/utils/use-drag-and-drop-file-import.ts",
    "content": "import { useCallback } from \"react\";\nimport { useDropzone } from \"react-dropzone\";\nimport { dragAndDropFileAnalyzer } from \"@/features/chat/utils/drag-and-drop-file-analyzer\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport { removeExtension } from \"@/utils/general/general-utils\";\n\ntype AddAttachmentFunction = (file: File) => void;\n\nexport const useDragAndDropFileImport = (\n  addAttachment: AddAttachmentFunction\n) => {\n  const setToastMessage = useToastMessageStore(\n    (state) => state.setToastMessage\n  );\n\n  const setNewAttachedRoi = useROIStore((state) => state.setNewAttachedRoi);\n\n  const onDrop = useCallback(\n    async (acceptedFiles: File[]) => {\n      for (const file of acceptedFiles) {\n        try {\n          const { type: fileType, content: fileContent } =\n            await dragAndDropFileAnalyzer(file);\n\n          addAttachment(file);\n          if ([\"shapefile\", \"geojson\"].includes(fileType)) {\n            const fileNameWithoutExtension = removeExtension(file.name);\n            setNewAttachedRoi({\n              id: fileNameWithoutExtension,\n              geometry: fileContent,\n              name: fileNameWithoutExtension,\n              source: \"attached\",\n            });\n          }\n          setToastMessage(\n            `\"${file.name} (${fileType})\" added to session assets successfully.`,\n            \"success\"\n          );\n        } catch (error) {\n          setToastMessage(\n            `Error processing \"${file.name}\": ${\n              error || \"Your file is not supported/valid.\"\n            }`,\n            \"error\"\n          );\n        }\n      }\n    },\n    [addAttachment, setToastMessage]\n  );\n\n  return useDropzone({\n    onDrop,\n    noClick: true,\n    noKeyboard: true,\n  });\n};\n"
  },
  {
    "path": "features/chat/utils/use-slash-command-menu.ts",
    "content": "import { useState, useEffect, useRef } from \"react\";\nimport { RichTextareaHandle } from \"rich-textarea\";\nimport { COMMAND_REG } from \"@/features/chat/utils/slash-menu-utils\";\nimport { useSlashMenuMapLayersList } from \"@/features/chat/hooks/use-slash-menu-map-layers-list\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\n\ninterface MenuPosition {\n  top: number;\n  left: number;\n  caret: number;\n}\n\ninterface UseSlashCommandMenuProps {\n  inputValue: string;\n  currentRoiGeometries: string[];\n  textareaRef: React.RefObject<RichTextareaHandle>;\n}\n\nexport const useSlashCommandMenu = ({\n  inputValue,\n  currentRoiGeometries,\n  textareaRef,\n}: UseSlashCommandMenuProps) => {\n  const [menuPos, setMenuPos] = useState<MenuPosition | null>(null);\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [menuHeight, setMenuHeight] = useState(0);\n\n  const setSelectedRoiGeometryByName = useROIStore(\n    (state) => state.setSelectedGeometryByName\n  );\n\n  const { filteredCommands } = useSlashMenuMapLayersList({\n    menuPos,\n    inputValue,\n    commands: currentRoiGeometries,\n    maxListLength: 8,\n  });\n\n  useEffect(() => {\n    if (filteredCommands.length) {\n      // Approximate height calculation: 36px per item (24px height + 12px padding)\n      setMenuHeight(filteredCommands.length * 36);\n    }\n  }, [filteredCommands]);\n\n  const completeCommand = (index: number) => {\n    if (!textareaRef?.current || !menuPos) return;\n    const selected = filteredCommands[index];\n    const match = inputValue.slice(0, menuPos.caret).match(COMMAND_REG);\n    const command = match?.[1] ?? \"\";\n\n    setSelectedRoiGeometryByName(selected);\n    textareaRef.current.setRangeText(\n      `${selected} `,\n      menuPos.caret - command.length - 1,\n      menuPos.caret,\n      \"end\"\n    );\n\n    setMenuPos(null);\n    setSelectedIndex(0);\n  };\n\n  const handleKeyDown = (\n    e: React.KeyboardEvent<HTMLTextAreaElement>,\n    customKeyHandler?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void\n  ) => {\n    if (menuPos && filteredCommands.length) {\n      switch (e.code) {\n        case \"ArrowUp\":\n          e.preventDefault();\n          setSelectedIndex((prev) =>\n            prev <= 0 ? filteredCommands.length - 1 : prev - 1\n          );\n          break;\n        case \"ArrowDown\":\n          e.preventDefault();\n          setSelectedIndex((prev) =>\n            prev >= filteredCommands.length - 1 ? 0 : prev + 1\n          );\n          break;\n        case \"Enter\":\n          e.preventDefault();\n          completeCommand(selectedIndex);\n          break;\n        case \"Escape\":\n          e.preventDefault();\n          setMenuPos(null);\n          setSelectedIndex(0);\n          break;\n        default:\n          customKeyHandler?.(e);\n      }\n    } else {\n      customKeyHandler?.(e);\n    }\n  };\n\n  return {\n    menuPos,\n    setMenuPos,\n    selectedIndex,\n    setSelectedIndex,\n    menuHeight,\n    filteredCommands,\n    completeCommand,\n    handleKeyDown,\n  };\n};\n"
  },
  {
    "path": "features/chat-history/components/chat-history-row.tsx",
    "content": "\"use client\";\n\nimport { FC, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { TableCell, TableRow } from \"@/components/ui/table\";\nimport { Loader2 } from \"lucide-react\";\nimport { deleteChatById } from \"@/lib/database/chat/queries\";\nimport { formatDbDate } from \"@/utils/general/general-utils\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport ConfirmationModal from \"@/components/ui/confirmation-modal\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\nimport useSidebarButtonStores from \"@/stores/use-sidebar-button-stores\";\nimport { resetChatStores } from \"@/utils/reset-chat-stores\";\n\ninterface ChatHistory {\n  chatId: string;\n  title: string;\n  createdAt: string;\n}\n\ninterface ChatHistoryRowProps {\n  chat: ChatHistory;\n  onDeleteChat: () => void;\n  isSelected: boolean;\n  onSelectChat: (selected: boolean) => void;\n}\n\nconst ChatHistoryRow: FC<ChatHistoryRowProps> = ({\n  chat,\n  onDeleteChat,\n  isSelected,\n  onSelectChat,\n}) => {\n  const router = useRouter();\n  const setPageToOpen = useSidebarButtonStores((state) => state.setPageToOpen);\n  const setToastMessage = useToastMessageStore(\n    (state) => state.setToastMessage\n  );\n\n  const [loading, setLoading] = useState(false);\n  const [isDeleting, setIsDeleting] = useState(false);\n  const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] =\n    useState(false);\n\n  const onOpenChat = (id: string) => {\n    resetChatStores();\n    setPageToOpen(null);\n    setLoading(true);\n    router.push(`/chat/${id}`);\n  };\n\n  const handleDeleteClick = () => {\n    setIsConfirmDeleteModalOpen(true);\n  };\n\n  const confirmDeleteChat = async () => {\n    setIsDeleting(true);\n    try {\n      await deleteChatById(chat.chatId);\n      onDeleteChat(); // Parent callback\n      setToastMessage(\"Chat deleted successfully\", \"success\");\n    } catch (error) {\n      setToastMessage(\"Something went wrong during deletion!\", \"error\");\n    } finally {\n      setIsDeleting(false);\n    }\n  };\n\n  return (\n    <>\n      <TableRow\n        className={`cursor-pointer bg-background hover:bg-accent transition-colors ${\n          loading ? \"pointer-events-none cursor-default opacity-60\" : \"\"\n        }`}\n        onClick={() => {\n          // Only open chat if not already loading\n          if (!loading) onOpenChat(chat.chatId);\n        }}\n      >\n        <TableCell\n          className=\"w-10\"\n          // Stop propagation so clicking the checkbox doesn't open the chat\n          onClick={(e) => e.stopPropagation()}\n        >\n          <Checkbox\n            checked={isSelected}\n            onCheckedChange={(checked) => onSelectChat(Boolean(checked))}\n          />\n        </TableCell>\n\n        {/* Title */}\n        <TableCell>\n          <div className=\"text-sm font-medium truncate text-primary flex items-center\">\n            {chat.title}\n            {loading && <Loader2 className=\"ml-2 animate-spin h-4 w-4\" />}\n          </div>\n        </TableCell>\n\n        {/* Created At */}\n        <TableCell>\n          <div className=\"text-sm text-primary\">\n            {formatDbDate(chat.createdAt)}\n          </div>\n        </TableCell>\n      </TableRow>\n\n      {/* Confirmation modal for deletion */}\n      <ConfirmationModal\n        isOpen={isConfirmDeleteModalOpen}\n        title=\"Confirm Deletion\"\n        message=\"Are you sure you want to delete this chat? This action cannot be undone.\"\n        confirmText=\"Delete\"\n        confirmButtonClassName=\"bg-red-500 hover:bg-red-600\"\n        onCancel={() => setIsConfirmDeleteModalOpen(false)}\n        onConfirm={confirmDeleteChat}\n        isDeleting={isDeleting}\n      />\n    </>\n  );\n};\n\nexport default ChatHistoryRow;\n"
  },
  {
    "path": "features/chat-history/components/chat-history-table-skeleton.tsx",
    "content": "import { FC } from \"react\";\nimport {\n  Table,\n  TableBody,\n  TableHead,\n  TableHeader,\n  TableRow,\n  TableCell,\n} from \"@/components/ui/table\";\n\nconst ChatHistoryTableSkeleton: FC = () => {\n  // Create an array of 5 items to simulate multiple rows loading\n  const rows = Array(5).fill(null);\n\n  return (\n    <Table className=\"mb-10\">\n      <TableHeader className=\"sticky top-0 bg-accent\">\n        <TableRow>\n          {/* Matches the checkbox column */}\n          <TableHead className=\"w-10 text-sm font-semibold text-secondary-foreground\" />\n          <TableHead className=\"text-sm font-semibold text-secondary-foreground\">\n            Title\n          </TableHead>\n          <TableHead className=\"text-sm font-semibold text-secondary-foreground\">\n            Created At\n          </TableHead>\n        </TableRow>\n      </TableHeader>\n      <TableBody>\n        {rows.map((_, index) => (\n          <TableRow key={index} className=\"animate-pulse\">\n            {/* Checkbox skeleton */}\n            <TableCell className=\"w-10 p-4\">\n              <div className=\"h-4 bg-accent rounded w-4 mx-auto\" />\n            </TableCell>\n            {/* Title skeleton */}\n            <TableCell className=\"p-4\">\n              <div className=\"h-4 bg-accent rounded w-48\" />\n            </TableCell>\n            {/* Created At skeleton */}\n            <TableCell className=\"p-4\">\n              <div className=\"h-4 bg-accent rounded w-24\" />\n            </TableCell>\n          </TableRow>\n        ))}\n      </TableBody>\n    </Table>\n  );\n};\n\nexport default ChatHistoryTableSkeleton;\n"
  },
  {
    "path": "features/chat-history/components/chat-history-table.tsx",
    "content": "import { FC, useMemo } from \"react\";\nimport ChatHistoryRow from \"./chat-history-row\";\nimport {\n  Table,\n  TableBody,\n  TableHeader,\n  TableHead,\n  TableRow,\n  TableCell,\n} from \"@/components/ui/table\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { IndeterminateCheckbox } from \"./indeterminate-checkbox\";\nimport { Button } from \"@/components/ui/button\";\nimport { Trash2 } from \"lucide-react\";\n\ninterface ChatHistory {\n  chatId: string;\n  title: string;\n  createdAt: string;\n}\n\ninterface ChatHistoryTableProps {\n  chats: ChatHistory[];\n  onDeleteChat: (chatId: string) => void;\n  selectedChatIds: string[];\n  onToggleChatSelection: (chatId: string, selected: boolean) => void;\n  onToggleSelectAll: (selected: boolean) => void;\n  onDeleteSelectedClick: () => void; // triggers the multi-delete confirmation\n}\n\nconst ChatHistoryTable: FC<ChatHistoryTableProps> = ({\n  chats,\n  onDeleteChat,\n  selectedChatIds,\n  onToggleChatSelection,\n  onToggleSelectAll,\n  onDeleteSelectedClick,\n}) => {\n  // Check if all chats are selected\n  const allSelected = useMemo(\n    () =>\n      chats.length > 0 &&\n      chats.every((chat) => selectedChatIds.includes(chat.chatId)),\n    [chats, selectedChatIds]\n  );\n\n  // Indeterminate if some but not all are selected\n  const isIndeterminate = useMemo(\n    () => !allSelected && selectedChatIds.length > 0,\n    [allSelected, selectedChatIds]\n  );\n\n  return (\n    <ScrollArea\n      className=\"\n       h-[75vh]\n        pb-2\n        rounded-xl\n        border\n        border-stone-300\n        dark:border-stone-600\n      \"\n    >\n      <Table className=\"mb-10\">\n        <TableHeader className=\"sticky top-0 bg-accent\">\n          <TableRow>\n            <TableHead className=\"w-10 text-sm font-semibold text-secondary-foreground\">\n              <IndeterminateCheckbox\n                checked={allSelected}\n                indeterminate={isIndeterminate}\n                onChange={(checked) => onToggleSelectAll(checked)}\n              />\n            </TableHead>\n\n            <TableHead className=\"text-sm font-semibold text-secondary-foreground\">\n              <div className=\"inline-flex items-center gap-4\">\n                <span>Title</span>\n                {selectedChatIds.length > 0 && (\n                  <Button\n                    variant=\"destructive\"\n                    onClick={onDeleteSelectedClick}\n                    className=\"whitespace-nowrap\"\n                  >\n                    <Trash2 className=\"mr-2 h-4 w-4\" />\n                    Delete Selected ({selectedChatIds.length})\n                  </Button>\n                )}\n              </div>\n            </TableHead>\n\n            <TableHead className=\"text-sm font-semibold text-secondary-foreground\">\n              Created At\n            </TableHead>\n          </TableRow>\n        </TableHeader>\n\n        <TableBody>\n          {chats.map((chat) => (\n            <ChatHistoryRow\n              key={chat.chatId}\n              chat={chat}\n              onDeleteChat={() => onDeleteChat(chat.chatId)}\n              isSelected={selectedChatIds.includes(chat.chatId)}\n              onSelectChat={(selected) =>\n                onToggleChatSelection(chat.chatId, selected)\n              }\n            />\n          ))}\n        </TableBody>\n      </Table>\n    </ScrollArea>\n  );\n};\n\nexport default ChatHistoryTable;\n"
  },
  {
    "path": "features/chat-history/components/chat-history.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport ChatHistoryTable from \"./chat-history-table\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport { fetchChatHistory } from \"@/lib/fetchers/chat\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Button } from \"@/components/ui/button\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\nimport { deleteChatById } from \"@/lib/database/chat/queries\";\nimport ConfirmationModal from \"@/components/ui/confirmation-modal\";\nimport { Trash2 } from \"lucide-react\";\n\ninterface ChatHistory {\n  chatId: string;\n  title: string;\n  createdAt: string;\n}\n\nexport default function ChatHistory() {\n  const isSidebarCollapsed = useButtonsStore(\n    (state) => state.isSidebarCollapsed\n  );\n  const setToastMessage = useToastMessageStore(\n    (state) => state.setToastMessage\n  );\n\n  const [chats, setChats] = useState<ChatHistory[]>([]);\n  const [isDeleting, setIsDeleting] = useState(false);\n  const [selectedChatIds, setSelectedChatIds] = useState<string[]>([]);\n  const [isConfirmMultiDeleteOpen, setIsConfirmMultiDeleteOpen] =\n    useState(false);\n\n  useEffect(() => {\n    async function loadChats() {\n      try {\n        const fetchedChats = await fetchChatHistory();\n        setChats(\n          fetchedChats.map((chat: any) => ({\n            chatId: chat.id,\n            title: chat.chatTitle,\n            createdAt: chat.createdAt,\n          }))\n        );\n      } catch (err) {\n        console.error(\"Failed to fetch chat history\", err);\n      }\n    }\n\n    loadChats();\n  }, []);\n\n  // Single chat delete callback\n  const handleDeleteChat = (deletedChatId: string) => {\n    setChats((prevChats) =>\n      prevChats.filter((chat) => chat.chatId !== deletedChatId)\n    );\n    // Also remove it from selected if it exists\n    setSelectedChatIds((prev) => prev.filter((id) => id !== deletedChatId));\n  };\n\n  // Toggle single chat selection\n  const handleToggleChatSelection = (chatId: string, selected: boolean) => {\n    if (selected) {\n      setSelectedChatIds((prev) => [...prev, chatId]);\n    } else {\n      setSelectedChatIds((prev) => prev.filter((id) => id !== chatId));\n    }\n  };\n\n  // Toggle select/deselect all\n  const handleToggleSelectAll = (selected: boolean) => {\n    if (selected) {\n      setSelectedChatIds(chats.map((chat) => chat.chatId));\n    } else {\n      setSelectedChatIds([]);\n    }\n  };\n\n  // Multi-delete callback\n  const handleDeleteSelectedChats = async () => {\n    setIsDeleting(true);\n    try {\n      for (const chatId of selectedChatIds) {\n        await deleteChatById(chatId);\n      }\n      // Remove from local state\n      setChats((prev) =>\n        prev.filter((chat) => !selectedChatIds.includes(chat.chatId))\n      );\n      // Clear selection\n      setSelectedChatIds([]);\n      setToastMessage(\"Selected chats deleted successfully\", \"success\");\n    } catch (error) {\n      setToastMessage(\"Something went wrong while deleting chats\", \"error\");\n    } finally {\n      setIsConfirmMultiDeleteOpen(false);\n      setIsDeleting(false);\n    }\n  };\n\n  return (\n    <div\n      className={`\n        flex flex-col\n        h-screen\n        overflow-hidden\n        flex-grow\n        transition-all\n        duration-300\n        ${isSidebarCollapsed ? \"ml-20\" : \"ml-64\"}\n      `}\n    >\n      <div className=\"container mx-auto py-16 max-w-6xl\">\n        <h1 className=\"text-4xl font-bold leading-tight tracking-tight\">\n          Session History\n        </h1>\n        <p className=\"mt-2 font-semibold text-muted-foreground\">\n          Review and load past chat sessions\n        </p>\n\n        <Separator className=\"mt-3 mb-6\" />\n\n        {chats.length === 0 ? (\n          <p className=\"text-center text-muted-foreground\">No chats found</p>\n        ) : (\n          <ChatHistoryTable\n            chats={chats}\n            onDeleteChat={handleDeleteChat}\n            selectedChatIds={selectedChatIds}\n            onToggleChatSelection={handleToggleChatSelection}\n            onToggleSelectAll={handleToggleSelectAll}\n            // Pass a callback to open the multi-delete modal\n            onDeleteSelectedClick={() => setIsConfirmMultiDeleteOpen(true)}\n          />\n        )}\n\n        {/* Confirmation Modal for multi-delete */}\n        <ConfirmationModal\n          isOpen={isConfirmMultiDeleteOpen}\n          title=\"Confirm Deletion\"\n          message=\"Are you sure you want to delete all selected chats? This action cannot be undone.\"\n          confirmText=\"Delete\"\n          confirmButtonClassName=\"bg-red-500 hover:bg-red-600\"\n          onCancel={() => setIsConfirmMultiDeleteOpen(false)}\n          onConfirm={handleDeleteSelectedChats}\n          isDeleting={isDeleting}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "features/chat-history/components/indeterminate-checkbox.tsx",
    "content": "import React, { useEffect, useRef } from \"react\";\n\ninterface IndeterminateCheckboxProps {\n  checked: boolean;\n  indeterminate?: boolean;\n  onChange?: (checked: boolean) => void;\n  // ...any other props\n}\n\nexport function IndeterminateCheckbox(props: IndeterminateCheckboxProps) {\n  const { checked, indeterminate, onChange, ...rest } = props;\n  const ref = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    if (ref.current) {\n      // This is the native DOM property, not an HTML attribute\n      ref.current.indeterminate = !!indeterminate;\n    }\n  }, [indeterminate]);\n\n  return (\n    <input\n      ref={ref}\n      type=\"checkbox\"\n      checked={checked}\n      onChange={(e) => onChange?.(e.target.checked)}\n      {...rest}\n    />\n  );\n}\n"
  },
  {
    "path": "features/integrations/components/integration-actions.tsx",
    "content": "import React from \"react\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface IntegrationActionsProps {\n  serviceId: ServiceType;\n  isConnected: boolean;\n  onConnect: (id: ServiceType) => void;\n  onConfigure: (id: string) => void;\n}\n\nexport const IntegrationActions: React.FC<IntegrationActionsProps> = ({\n  serviceId,\n  isConnected,\n  onConnect,\n  onConfigure,\n}) => {\n  return (\n    <div className=\"flex items-center\">\n      {!isConnected && (\n        <Button\n          variant=\"default\"\n          size=\"sm\"\n          onClick={() => onConnect(serviceId)}\n        >\n          Connect\n        </Button>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "features/integrations/components/integration-header.tsx",
    "content": "import { Separator } from \"@/components/ui/separator\";\nimport React from \"react\";\n\ninterface IntegrationHeaderProps {\n  onAddNew: () => void;\n}\n\nexport const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({\n  onAddNew,\n}) => {\n  return (\n    <section>\n      <div className=\"flex justify-between items-center\">\n        <div>\n          <h1 className=\"text-4xl font-bold leading-tight tracking-tight\">\n            Integrations\n          </h1>\n          <p className=\"mt-2 font-semibold text-muted-foreground \">\n            Connect your preferred services and data sources\n          </p>\n        </div>\n        {/* <Button\n        variant=\"btn-ghost\"\n        size=\"md\"\n        onClick={onAddNew}\n        className=\"gap-2\"\n      >\n        <IconPlus size={18} />\n        Add New Integration\n      </Button> */}\n      </div>\n      <Separator className=\"mt-3 mb-10\" />\n    </section>\n  );\n};\n"
  },
  {
    "path": "features/integrations/components/integration-item.tsx",
    "content": "import React from \"react\";\nimport { IntegrationStatus } from \"./integration-status\";\nimport { IntegrationActions } from \"./integration-actions\";\n\ninterface IntegrationItemProps {\n  service: IntegrationService;\n  onConnect: (id: ServiceType) => void;\n  onConfigure: (id: string) => void;\n}\n\nexport const IntegrationItem: React.FC<IntegrationItemProps> = ({\n  service,\n  onConnect,\n  onConfigure,\n}) => {\n  const isConnected = service.status === \"connected\";\n\n  return (\n    <div className=\"flex items-center justify-between p-4 transition-colors\">\n      <div className=\"flex items-center gap-4 flex-1\">\n        <div className=\"w-12 h-12 flex items-center justify-center\">\n          <img\n            src={service.icon}\n            alt={service.name}\n            className=\"max-w-full max-h-full object-contain\"\n          />\n        </div>\n        <div className=\"flex-1\">\n          <h3 className=\"font-semibold\">{service.name}</h3>\n          <p className=\"text-sm text-muted-foreground\">{service.description}</p>\n        </div>\n      </div>\n\n      <div className=\"flex items-center\">\n        <IntegrationStatus isConnected={isConnected} />\n        <IntegrationActions\n          serviceId={service.id}\n          isConnected={isConnected}\n          onConnect={onConnect}\n          onConfigure={onConfigure}\n        />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "features/integrations/components/integration-list.tsx",
    "content": "import React from \"react\";\nimport { IntegrationItem } from \"./integration-item\";\n\ninterface IntegrationListProps {\n  services: IntegrationService[];\n  onConnect: (id: ServiceType) => void;\n  onConfigure: (id: string) => void;\n}\n\nexport const IntegrationList: React.FC<IntegrationListProps> = ({\n  services,\n  onConnect,\n  onConfigure,\n}) => {\n  return (\n    <div className=\"rounded-lg border border-stone-300 dark:border-stone-600\">\n      <div className=\"divide-y divide-stone-300 dark:divide-stone-600\">\n        {services.map((service) => (\n          <IntegrationItem\n            key={service.id}\n            service={service}\n            onConnect={onConnect}\n            onConfigure={onConfigure}\n          />\n        ))}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "features/integrations/components/integration-status.tsx",
    "content": "import React from \"react\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface IntegrationStatusProps {\n  isConnected: boolean;\n}\n\nexport const IntegrationStatus: React.FC<IntegrationStatusProps> = ({\n  isConnected,\n}) => {\n  if (!isConnected) return null;\n\n  return (\n    <span className=\"flex items-center text-sm\">\n      <Button variant=\"destructive\" size=\"sm\" onClick={() => {}}>\n        Disconnect\n      </Button>\n    </span>\n  );\n};\n"
  },
  {
    "path": "features/integrations/components/integrations-page.tsx",
    "content": "\"use client\";\n\nimport React, { use, useEffect } from \"react\";\n\nimport { IntegrationHeader } from \"./integration-header\";\nimport { IntegrationList } from \"./integration-list\";\nimport { handleArcGISAuth } from \"@/utils/service-handlers/esri\";\nimport { useIntegrationStore } from \"@/stores/use-integration-store\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\n\nconst IntegrationsPage = () => {\n  const services = useIntegrationStore((state) => state.services);\n  const updateServiceStatus = useIntegrationStore(\n    (state) => state.updateServiceStatus\n  );\n  const setToastMessage = useToastMessageStore(\n    (state) => state.setToastMessage\n  );\n  const isSidebarCollapsed = useButtonsStore(\n    (state) => state.isSidebarCollapsed\n  );\n\n  const handleAddNew = () => {\n    console.log(\"Add new integration\");\n  };\n\n  useEffect(() => {\n    const broadcastChannel = new BroadcastChannel(\"esriChannel\");\n\n    broadcastChannel.onmessage = (event) => {\n      const { connectionStatus } = event.data;\n      if (connectionStatus === \"connected\") {\n        updateServiceStatus(\"arcgis\", \"connected\");\n        setToastMessage(\n          \"Successfully connected to Esri Feature Services\",\n          \"success\"\n        );\n      }\n    };\n\n    return () => {\n      broadcastChannel.close();\n    };\n  }, [updateServiceStatus]);\n\n  const handleConnect = (serviceId: ServiceType) => {\n    if (serviceId === \"arcgis\") {\n      handleArcGISAuth();\n    } else {\n      console.log(serviceId);\n    }\n  };\n\n  const handleConfigure = (serviceId: string) => {\n    console.log(\"Configuring service:\", serviceId);\n  };\n\n  return (\n    <section\n      className={`flex h-screen overflow-hidden flex-grow transition-all duration-300 ${\n        isSidebarCollapsed ? \"ml-20\" : \"ml-64\"\n      }`}\n    >\n      <div className=\"container mx-auto py-16 max-w-6xl\">\n        <IntegrationHeader onAddNew={handleAddNew} />\n        <IntegrationList\n          services={services}\n          onConnect={handleConnect}\n          onConfigure={handleConfigure}\n        />\n      </div>\n    </section>\n  );\n};\n\nexport default IntegrationsPage;\n"
  },
  {
    "path": "features/knowledge-base/actions/document-actions.ts",
    "content": "\"use server\";\nimport {\n  handlePdfFile,\n  handleDocxFile,\n  handleTextFile,\n} from \"@/utils/general/document-utils\";\nimport { createClient } from \"@/utils/supabase/server\";\nimport { saveRagDocument } from \"@/app/actions/rag-actions\";\n\nexport async function fetchDocumentFiles(): Promise<DocumentFile[]> {\n  const supabase = await createClient();\n  const { data: authResult, error: userError } = await supabase.auth.getUser();\n  if (userError || !authResult?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n  const { data: documentFiles, error: documentFilesError } = await supabase\n    .from(\"document_files\")\n    .select()\n    .order(\"created_at\", { ascending: false })\n    .returns<DocumentFile[]>();\n\n  if (documentFilesError) {\n    throw new Error(\n      `Error fetching document files: ${documentFilesError.message}`\n    );\n  }\n\n  // Query the \"user_roles\" table to get the \"name\" for the authenticated user\n  const { data: userRoleData, error: userRoleError } = await supabase\n    .from(\"user_roles\")\n    .select(\"name\")\n    .eq(\"id\", authResult.user.id)\n    .single();\n\n  if (userRoleError) {\n    throw new Error(`Error fetching user role: ${userRoleError.message}`);\n  }\n\n  const owner = userRoleData?.name;\n\n  if (documentFiles && documentFiles?.length > 0) {\n    documentFiles.map((doc) => {\n      doc.owner = owner;\n    });\n  }\n\n  return documentFiles || [];\n}\n\nexport async function fetchByDocumentName(\n  documentName: string\n): Promise<string> {\n  const supabase = await createClient();\n  const { data: authResult, error: userError } = await supabase.auth.getUser();\n\n  if (userError || !authResult?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  const bucketName = \"documents_bucket\";\n\n  const { data: documentFile, error: fetchError } = await supabase\n    .from(\"document_files\")\n    .select(\"file_path\")\n    .ilike(\"name\", `%${documentName}%`)\n    .order(\"created_at\", { ascending: false })\n    .single();\n\n  if (fetchError) {\n    throw new Error(`Error fetching document: ${fetchError.message}`);\n  }\n\n  if (!documentFile) {\n    throw new Error(\"No document found with the given name.\");\n  }\n\n  const originalSignedUrl = documentFile.file_path;\n  const basePathSplit = originalSignedUrl.split(`${bucketName}/`);\n\n  if (basePathSplit.length < 2) {\n    throw new Error(\"Failed to extract storage path from file_path.\");\n  }\n\n  // Extract the storage path after bucketName/\n  const pathAndToken = basePathSplit[1].split(\"?\");\n  const storagePath = decodeURIComponent(pathAndToken[0]);\n\n  // Create a fresh signed URL\n  const { data: signedUrlData, error: signedUrlError } = await supabase.storage\n    .from(bucketName)\n    .createSignedUrl(storagePath, 60 * 60 * 1); // Valid for 1 hour\n\n  if (signedUrlError || !signedUrlData?.signedUrl) {\n    throw new Error(\n      `Failed to create signed URL: ${\n        signedUrlError?.message || \"Unknown error\"\n      }`\n    );\n  }\n\n  return signedUrlData.signedUrl;\n}\n\nexport async function deleteDocumentFile(\n  fileId: number\n): Promise<{ success: boolean; message: string }> {\n  const supabase = await createClient();\n  const { data: authResult, error: userError } = await supabase.auth.getUser();\n\n  if (userError || !authResult?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  const bucketName = \"documents_bucket\";\n\n  const { data: fileData, error: fetchError } = await supabase\n    .from(\"document_files\")\n    .select(\"file_path\")\n    .eq(\"id\", fileId)\n    .single();\n\n  if (fetchError) {\n    return {\n      success: false,\n      message: `Failed to retrieve file path: ${fetchError.message}`,\n    };\n  }\n\n  const filePath = fileData?.file_path;\n\n  if (!filePath) {\n    return {\n      success: false,\n      message: \"File path not found for the specified document ID.\",\n    };\n  }\n\n  // Extract the storage path (strip out the signed URL or base URL)\n  const storagePath = decodeURIComponent(\n    filePath\n      .split(`${bucketName}/`)[1] // Remove bucket prefix\n      .split(\"?\")[0] // Remove query string including token\n  );\n\n  //Delete the file from the Supabase bucket\n  const { error: deleteStorageError } = await supabase.storage\n    .from(bucketName)\n    .remove([storagePath]);\n\n  if (deleteStorageError) {\n    return {\n      success: false,\n      message: `Failed to delete file from storage: ${deleteStorageError.message}`,\n    };\n  }\n\n  // Delete the document file by its ID\n  const { error: deleteError } = await supabase\n    .from(\"document_files\")\n    .delete()\n    .eq(\"id\", fileId);\n\n  if (deleteError) {\n    return {\n      success: false,\n      message: `Failed to delete document file: ${deleteError.message}`,\n    };\n  }\n\n  return {\n    success: true,\n    message: `Document file with ID ${fileId} and its associated file in storage have been deleted.`,\n  };\n}\n\nexport async function processAndUploadDocumentFile({\n  file,\n  folderId,\n}: ProcessDocumentFileProps) {\n  const supabase = await createClient();\n  const { data: authResult, error: userError } = await supabase.auth.getUser();\n  if (userError || !authResult?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  try {\n    let pageCount = \"Unknown\";\n\n    switch (file.type) {\n      case \"application/pdf\":\n        pageCount = await handlePdfFile(file);\n        break;\n      case \"text/plain\":\n        pageCount = await handleTextFile(file);\n        break;\n      case \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\":\n        pageCount = await handleDocxFile(file);\n        break;\n      case \"application/msword\":\n        pageCount = `${(file.size / 1024).toFixed(2)} KB`;\n        break;\n    }\n\n    // Save the document & its embeddings to the database\n    const result = await saveRagDocument(file, parseInt(pageCount), folderId);\n    return { success: true, data: result };\n  } catch (error) {\n    console.error(\"Server error processing file:\", error);\n    return { success: false, error: `Error processing file: ${error}` };\n  }\n}\n"
  },
  {
    "path": "features/knowledge-base/components/add-group-modal.tsx",
    "content": "import React from \"react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface AddGroupModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  newGroupName: string;\n  setNewGroupName: (name: string) => void;\n  handleAddGroup: () => void;\n}\n\nconst AddGroupModal: React.FC<AddGroupModalProps> = ({\n  isOpen,\n  onClose,\n  newGroupName,\n  setNewGroupName,\n  handleAddGroup,\n}) => {\n  return (\n    <Dialog open={isOpen} onOpenChange={onClose}>\n      <DialogContent className=\"sm:max-w-[400px]\">\n        <DialogHeader>\n          <DialogTitle>Add New Group</DialogTitle>\n        </DialogHeader>\n\n        <Input\n          placeholder=\"Enter group name\"\n          value={newGroupName}\n          onChange={(e) => setNewGroupName(e.target.value)}\n        />\n\n        <DialogFooter className=\"gap-2\">\n          <Button variant=\"ghost\" size=\"sm\" onClick={onClose}>\n            Cancel\n          </Button>\n          <Button\n            onClick={handleAddGroup}\n            className=\"bg-green-500 hover:bg-green-600\"\n            size=\"sm\"\n          >\n            Add Group\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport default AddGroupModal;\n"
  },
  {
    "path": "features/knowledge-base/components/documents-table.tsx",
    "content": "import React from \"react\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { IconDotsVertical, IconPencil, IconTrash } from \"@tabler/icons-react\";\n\ninterface DocumentsTableProps {\n  documents: any[];\n  menuOpenId: any;\n  setMenuOpenId: (id: any) => void;\n  editDocumentId: any;\n  setEditDocumentId: (id: any) => void;\n  editedDocumentName: string;\n  setEditedDocumentName: (name: string) => void;\n  handleDeleteClick: (docId: any) => void;\n  formatDbDate: (dateStr: string) => string;\n}\n\nconst DocumentsTable: React.FC<DocumentsTableProps> = ({\n  documents,\n  menuOpenId,\n  setMenuOpenId,\n  editDocumentId,\n  setEditDocumentId,\n  editedDocumentName,\n  setEditedDocumentName,\n  handleDeleteClick,\n  formatDbDate,\n}) => {\n  return (\n    <Table>\n      <TableHeader className=\"sticky top-0\">\n        <TableRow>\n          <TableHead className=\"text-sm font-bold text-muted-foreground\">\n            Name\n          </TableHead>\n          <TableHead className=\"text-sm font-bold text-muted-foreground text-center\">\n            Owner\n          </TableHead>\n          <TableHead className=\"text-sm font-bold text-muted-foreground text-center\">\n            Pages\n          </TableHead>\n          <TableHead className=\"text-sm font-bold text-muted-foreground text-center\">\n            Last Edited\n          </TableHead>\n          <TableHead className=\"text-sm font-bold text-muted-foreground text-center\"></TableHead>\n        </TableRow>\n      </TableHeader>\n\n      <TableBody>\n        {documents.map((doc: any) => (\n          <TableRow\n            key={doc.id}\n            className=\"hover:bg-muted/50 dark:hover:bg-secondary\"\n          >\n            <TableCell className=\"text-primary/90\">\n              {editDocumentId === doc.id ? (\n                // If we’re editing this document’s name\n                <input\n                  type=\"text\"\n                  value={editedDocumentName}\n                  onChange={(e) => setEditedDocumentName(e.target.value)}\n                  className=\"w-full border p-1 text-sm\"\n                />\n              ) : (\n                doc.name\n              )}\n            </TableCell>\n\n            <TableCell className=\"text-primary/90 text-center\">\n              {doc.owner}\n            </TableCell>\n            <TableCell className=\"text-primary/90 text-center\">\n              {doc.number_of_pages}\n            </TableCell>\n            <TableCell className=\"text-primary/90 text-center\">\n              {formatDbDate(doc.created_at)}\n            </TableCell>\n\n            <TableCell className=\"text-left\">\n              <DropdownMenu\n                open={menuOpenId === doc.id}\n                onOpenChange={(open) => setMenuOpenId(open ? doc.id : null)}\n              >\n                <DropdownMenuTrigger asChild>\n                  <Button variant=\"ghost\" className=\"text-primary\" size=\"sm\">\n                    <IconDotsVertical size={16} />\n                  </Button>\n                </DropdownMenuTrigger>\n\n                <DropdownMenuContent className=\"w-28\" id={`doc-menu-${doc.id}`}>\n                  {/* <DropdownMenuItem\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      setEditDocumentId(doc.id);\n                      setEditedDocumentName(doc.name);\n                    }}\n                    className=\"flex items-center\"\n                  >\n                    <IconPencil className=\"mr-2 h-4 w-4\" />\n                    Update\n                  </DropdownMenuItem> */}\n\n                  <DropdownMenuItem\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      handleDeleteClick(doc.id);\n                    }}\n                    className=\"flex items-center text-red-600 focus:text-red-600\"\n                  >\n                    <IconTrash className=\"mr-2 h-4 w-4\" />\n                    Delete\n                  </DropdownMenuItem>\n                </DropdownMenuContent>\n              </DropdownMenu>\n            </TableCell>\n          </TableRow>\n        ))}\n\n        {documents.length === 0 && (\n          <TableRow>\n            <TableCell\n              colSpan={5}\n              className=\"text-center text-muted-foreground\"\n            >\n              No documents found.\n            </TableCell>\n          </TableRow>\n        )}\n      </TableBody>\n    </Table>\n  );\n};\n\nexport default DocumentsTable;\n"
  },
  {
    "path": "features/knowledge-base/components/edit-document-modal.tsx",
    "content": "import React from \"react\";\nimport { X } from \"lucide-react\";\n\ninterface EditDocumentModalProps {\n  editDocumentId: any;\n  setEditDocumentId: (id: any) => void;\n  editedDocumentName: string;\n  setEditedDocumentName: (name: string) => void;\n  documents: any[];\n  setDocuments: (docs: any[]) => void;\n}\n\nconst EditDocumentModal: React.FC<EditDocumentModalProps> = ({\n  editDocumentId,\n  setEditDocumentId,\n  editedDocumentName,\n  setEditedDocumentName,\n  documents,\n  setDocuments,\n}) => {\n  if (!editDocumentId) {\n    return null;\n  }\n\n  const handleSave = () => {\n    setDocuments(\n      documents.map((doc: any) =>\n        doc.id === editDocumentId\n          ? {\n              ...doc,\n              name: editedDocumentName,\n              lastEdited: new Date().toISOString(),\n            }\n          : doc\n      )\n    );\n    setEditDocumentId(null);\n  };\n\n  return (\n    <div className=\"fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50\">\n      <div className=\"bg-white rounded-lg p-6 w-[400px] relative\">\n        <button\n          className=\"absolute top-2 right-2 text-gray-500 hover:text-gray-700\"\n          onClick={() => setEditDocumentId(null)}\n        >\n          <X size={20} />\n        </button>\n        <h2 className=\"text-lg font-bold text-gray-800 mb-4\">\n          Edit Document Name\n        </h2>\n        <input\n          type=\"text\"\n          value={editedDocumentName}\n          onChange={(e) => setEditedDocumentName(e.target.value)}\n          className=\"w-full border rounded-md p-2 text-sm\"\n        />\n        <div className=\"mt-4 flex justify-end gap-2\">\n          <button\n            className=\"px-3 py-1 text-gray-700 rounded hover:underline transition\"\n            onClick={() => setEditDocumentId(null)}\n          >\n            Cancel\n          </button>\n          <button\n            className=\"px-2 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600 transition\"\n            onClick={handleSave}\n          >\n            Save\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default EditDocumentModal;\n"
  },
  {
    "path": "features/knowledge-base/components/knolwedge-base.tsx",
    "content": "\"use client\";\nimport React, { useState, useEffect } from \"react\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\nimport { useRouter } from \"next/navigation\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\n\nimport { deleteDocumentFile } from \"../actions/document-actions\";\n\nimport { useDocumentUpload } from \"@/hooks/docs-hooks/use-document-upload\";\nimport { formatDbDate } from \"@/utils/general/general-utils\";\n\nimport Sidebar from \"./knowledge-base-sidebar\";\nimport DocumentsTable from \"./documents-table\";\nimport AddGroupModal from \"./add-group-modal\";\nimport EditDocumentModal from \"./edit-document-modal\";\nimport ConfirmationModal from \"@/components/ui/confirmation-modal\";\nimport FileUploadModal from \"@/features/ui/modals/file-upload-modal\";\n\nimport { IconPlus, IconSearch } from \"@tabler/icons-react\";\nimport { useUserStore } from \"@/stores/use-user-profile-store\";\nimport MaxDocsAlertDialog from \"./max-docs-alert-dialog\";\n\ninterface KnowledgeBaseProps {\n  initialDocuments: DocumentFile[];\n}\n\nconst KnowledgeBase = ({ initialDocuments }: KnowledgeBaseProps) => {\n  const isSidebarCollapsed = useButtonsStore(\n    (state) => state.isSidebarCollapsed\n  );\n  const setToastMessage = useToastMessageStore(\n    (state) => state.setToastMessage\n  );\n\n  // --------------------\n  // States\n  // --------------------\n\n  const maxDocs = useUserStore((state) => state.maxDocs);\n\n  const [folders, setFolders] = useState<any>([\n    { id: \"all-documents\", name: \"All Documents\" },\n  ]);\n  const [currentFolder, setCurrentFolder] = useState<any>(null);\n  const [documents, setDocuments] = useState(initialDocuments);\n\n  const { handleFileUpload } = useDocumentUpload({ currentFolder });\n\n  const [isMaxDocsDialogOpen, setIsMaxDocsDialogOpen] = useState(false);\n\n  const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] =\n    useState(false);\n  const [docToDelete, setDocToDelete] = useState<any>(null);\n\n  const [isAddGroupModalOpen, setIsAddGroupModalOpen] = useState(false);\n  const [newGroupName, setNewGroupName] = useState(\"\");\n\n  const [isAddDocumentModalOpen, setIsAddDocumentModalOpen] = useState(false);\n  const [isUploadComplete, setUploadComplete] = useState(true);\n\n  const [isDeleteLoading, setIsDeleteLoading] = useState(false);\n\n  const [searchQuery, setSearchQuery] = useState(\"\");\n\n  const [editDocumentId, setEditDocumentId] = useState<any>(null);\n  const [editedDocumentName, setEditedDocumentName] = useState(\"\");\n\n  // For opening Dots menu (folder or document)\n  const [menuOpenId, setMenuOpenId] = useState<any>(null);\n  const [menuOpenFolderId, setMenuOpenFolderId] = useState<any>(null);\n\n  const router = useRouter();\n  // --------------------\n  // Hooks / Effects\n  // --------------------\n  useEffect(() => {\n    if (folders?.length > 0 && !currentFolder) {\n      setCurrentFolder(folders[0]);\n    }\n  }, [folders, currentFolder]);\n\n  // Close any open \"document action\" menu if user clicks outside\n  useEffect(() => {\n    const handleClickOutside = (event: any) => {\n      if (menuOpenId !== null) {\n        const menuElement = document.getElementById(`doc-menu-${menuOpenId}`);\n        if (menuElement && !menuElement.contains(event.target)) {\n          setMenuOpenId(null);\n        }\n      }\n    };\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, [menuOpenId]);\n\n  // Update documents when initialDocuments prop changes\n  useEffect(() => {\n    setDocuments(initialDocuments);\n  }, [initialDocuments]);\n\n  // Update folders when documents change\n  useEffect(() => {\n    // 1. Gather unique folder IDs from documents\n    const uniqueFolderIds = [\n      ...new Set(documents.map((doc) => doc.folder_id).filter(Boolean)),\n    ];\n\n    // 2. Build folder objects\n    const dynamicFolders = uniqueFolderIds.map((fid) => ({\n      id: fid,\n      name: fid,\n    }));\n\n    // 3. Combine them with the \"All Documents\" folder\n    setFolders([\n      { id: \"all-documents\", name: \"All Documents\" },\n      ...dynamicFolders,\n    ]);\n  }, [documents]);\n\n  // Close any open \"folder action\" menu if user clicks outside\n  useEffect(() => {\n    const handleClickOutsideFolder = (event: any) => {\n      if (menuOpenFolderId !== null) {\n        const menuElement = document.getElementById(\n          `folder-menu-${menuOpenFolderId}`\n        );\n        if (menuElement && !menuElement.contains(event.target)) {\n          setMenuOpenFolderId(null);\n        }\n      }\n    };\n    document.addEventListener(\"mousedown\", handleClickOutsideFolder);\n    return () =>\n      document.removeEventListener(\"mousedown\", handleClickOutsideFolder);\n  }, [menuOpenFolderId]);\n\n  // --------------------\n  // Actions / Handlers\n  // --------------------\n\n  const handleFileSelect = async (files: FileList) => {\n    if (documents.length >= maxDocs) {\n      setIsMaxDocsDialogOpen(true);\n      return;\n    }\n    setUploadComplete(false);\n    try {\n      await handleFileUpload({\n        target: { files },\n      } as React.ChangeEvent<HTMLInputElement>);\n\n      router.refresh();\n      setToastMessage(\"Document uploaded successfully\", \"success\");\n    } catch (error: any) {\n      setToastMessage(\"Error processing files\", \"error\");\n    }\n  };\n\n  const handleCloseAddDocumentModal = () => {\n    setIsAddDocumentModalOpen(false);\n    setUploadComplete(true);\n  };\n\n  const handleAddGroup = () => {\n    if (newGroupName.trim() !== \"\") {\n      setFolders([\n        ...folders,\n        { id: newGroupName.trim(), name: newGroupName.trim() },\n      ]);\n      setNewGroupName(\"\");\n      setIsAddGroupModalOpen(false);\n    }\n  };\n\n  const handleDeleteClick = (docId: any) => {\n    setIsDeleteLoading(false);\n    setDocToDelete(docId);\n    setIsConfirmDeleteModalOpen(true);\n  };\n\n  const confirmDeleteDocument = async () => {\n    if (!docToDelete) return;\n    setIsDeleteLoading(true);\n\n    const { success: deleteSuccess, message: deleteSuccessMessage } =\n      await deleteDocumentFile(docToDelete);\n\n    if (deleteSuccess) {\n      router.refresh();\n      setToastMessage(\"Document deleted successfully\", \"success\");\n      setDocuments(documents.filter((doc: any) => doc.id !== docToDelete));\n    } else {\n      setToastMessage(deleteSuccessMessage, \"error\");\n    }\n\n    setDocToDelete(null);\n    setIsConfirmDeleteModalOpen(false);\n  };\n\n  const handleDeleteGroup = (groupId: any) => {\n    setFolders((prev: any) =>\n      prev.filter((folder: any) => folder.id !== groupId)\n    );\n    setDocuments((prevDocs: any) =>\n      prevDocs.map((doc: any) =>\n        doc.folderId === groupId ? { ...doc, folderId: null } : doc\n      )\n    );\n    // If we deleted the folder currently selected, revert to \"All Documents\"\n    if (currentFolder && currentFolder.id === groupId) {\n      const allDocsFolder = folders.find(\n        (fld: any) => fld.id === \"all-documents\"\n      );\n      setCurrentFolder(allDocsFolder);\n    }\n  };\n\n  const filteredDocuments = documents.filter((doc) => {\n    const matchesFolder =\n      currentFolder && currentFolder.id !== \"all-documents\"\n        ? doc.folder_id === currentFolder.id\n        : true;\n    const matchesSearch = doc.name\n      .toLowerCase()\n      .includes(searchQuery.toLowerCase());\n    return matchesFolder && matchesSearch;\n  });\n\n  // --------------------\n  // Render\n  // --------------------\n  if (!folders || !documents) {\n    return <div>Loading...</div>;\n  }\n\n  return (\n    <>\n      <div\n        className={`flex h-screen text-gray-800 overflow-hidden flex-grow transition-all duration-300 ${\n          isSidebarCollapsed ? \"ml-20\" : \"ml-64\"\n        }`}\n      >\n        {/* SIDEBAR */}\n        <Sidebar\n          folders={folders}\n          currentFolder={currentFolder}\n          setCurrentFolder={setCurrentFolder}\n          setIsAddGroupModalOpen={setIsAddGroupModalOpen}\n          handleDeleteGroup={handleDeleteGroup}\n          menuOpenFolderId={menuOpenFolderId}\n          setMenuOpenFolderId={setMenuOpenFolderId}\n        />\n\n        {/* MAIN CONTENT */}\n        <div className=\"flex-grow p-6\">\n          <div className=\"mb-6\">\n            <h1 className=\"text-2xl text-primary font-bold mb-2\">\n              {currentFolder ? currentFolder.name : \"All Documents\"}\n            </h1>\n            <p className=\"text-muted-foreground text-sm mb-4\">\n              Here you can add documents that you want your AI Assistants to\n              access across the app.\n            </p>\n\n            <div className=\"flex items-center\">\n              <div className=\"relative w-full max-w-md\">\n                <Input\n                  type=\"text\"\n                  placeholder=\"Search\"\n                  value={searchQuery}\n                  onChange={(e) => setSearchQuery(e.target.value)}\n                  className=\"pl-10\"\n                />\n                <IconSearch\n                  className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 transform text-muted-foreground\"\n                  aria-hidden=\"true\"\n                />\n              </div>\n\n              <Button\n                onClick={() => setIsAddDocumentModalOpen(true)}\n                className=\"ml-4 flex items-center\"\n                variant={\"primary-blue\"}\n              >\n                <IconPlus size={16} className=\"inline-block mr-2\" />\n                Add Document\n              </Button>\n            </div>\n          </div>\n\n          {/* DOCUMENTS TABLE */}\n          <ScrollArea className=\"h-[75vh] rounded-xl border border-stone-300 dark:border-stone-600\">\n            <DocumentsTable\n              documents={filteredDocuments}\n              menuOpenId={menuOpenId}\n              setMenuOpenId={setMenuOpenId}\n              editDocumentId={editDocumentId}\n              setEditDocumentId={setEditDocumentId}\n              editedDocumentName={editedDocumentName}\n              setEditedDocumentName={setEditedDocumentName}\n              handleDeleteClick={handleDeleteClick}\n              formatDbDate={formatDbDate}\n            />\n          </ScrollArea>\n        </div>\n\n        {/* ADD GROUP MODAL */}\n        <AddGroupModal\n          isOpen={isAddGroupModalOpen}\n          onClose={() => setIsAddGroupModalOpen(false)}\n          newGroupName={newGroupName}\n          setNewGroupName={setNewGroupName}\n          handleAddGroup={handleAddGroup}\n        />\n\n        {/* ADD DOCUMENT MODAL */}\n        <FileUploadModal\n          isOpen={isAddDocumentModalOpen}\n          onClose={handleCloseAddDocumentModal}\n          onFileSelect={handleFileSelect}\n          isUploadComplete={isUploadComplete}\n          setUploadComplete={setUploadComplete}\n          currentFolder={{ name: \"Documents\" }}\n          // acceptedFileTypes=\".pdf, .doc, .docx, .txt\"\n          acceptedFileTypes=\".pdf\"\n        />\n\n        {/* EDIT DOCUMENT MODAL */}\n        <EditDocumentModal\n          editDocumentId={editDocumentId}\n          setEditDocumentId={setEditDocumentId}\n          editedDocumentName={editedDocumentName}\n          setEditedDocumentName={setEditedDocumentName}\n          documents={documents}\n          setDocuments={setDocuments}\n        />\n\n        {/* DELETE CONFIRMATION MODAL */}\n        <ConfirmationModal\n          isOpen={isConfirmDeleteModalOpen}\n          title=\"Confirm Deletion\"\n          message=\"Are you sure you want to delete this document? This action cannot be undone.\"\n          confirmText=\"Delete\"\n          onCancel={() => setIsConfirmDeleteModalOpen(false)}\n          onConfirm={confirmDeleteDocument}\n          isDeleting={isDeleteLoading}\n        />\n      </div>\n      <MaxDocsAlertDialog\n        isOpen={isMaxDocsDialogOpen}\n        onClose={() => setIsMaxDocsDialogOpen(false)}\n        maxDocs={maxDocs}\n      />\n    </>\n  );\n};\n\nexport default KnowledgeBase;\n"
  },
  {
    "path": "features/knowledge-base/components/knowledge-base-sidebar.tsx",
    "content": "import React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  IconDotsVertical,\n  IconFolder,\n  IconPencil,\n  IconPlus,\n  IconTrash,\n} from \"@tabler/icons-react\";\n\ninterface SidebarProps {\n  folders: any[];\n  currentFolder: any;\n  setCurrentFolder: (folder: any) => void;\n  setIsAddGroupModalOpen: (open: boolean) => void;\n  handleDeleteGroup: (folderId: any) => void;\n  menuOpenFolderId: any;\n  setMenuOpenFolderId: (id: any) => void;\n}\n\nconst Sidebar: React.FC<SidebarProps> = ({\n  folders,\n  currentFolder,\n  setCurrentFolder,\n  setIsAddGroupModalOpen,\n  handleDeleteGroup,\n  menuOpenFolderId,\n  setMenuOpenFolderId,\n}) => {\n  return (\n    <aside className=\"w-64 bg-secondary border-r border-stone-300 dark:border-stone-600 shadow-md pt-3\">\n      <div className=\"flex justify-center px-4 py-4 border-b border-gray-300 dark:border-stone-600\">\n        <Button onClick={() => setIsAddGroupModalOpen(true)}>\n          <IconPlus size={16} stroke={2} className=\"mr-2\" />\n          Add Group\n        </Button>\n      </div>\n\n      <nav className=\"overflow-y-auto h-full px-4 pt-4\">\n        <ul className=\"space-y-2\">\n          {folders.map((folder: any) => (\n            <li\n              key={folder.id}\n              className={`group flex items-center justify-between hover:bg-muted rounded-md px-3 py-2 cursor-pointer text-foreground ${\n                currentFolder && currentFolder.id === folder.id\n                  ? \"bg-accent\"\n                  : \"\"\n              }`}\n              onClick={() => setCurrentFolder(folder)}\n            >\n              <div className=\"flex items-center\">\n                <IconFolder size={20} className=\"text-foreground\" />\n                <span className=\"ml-3 text-sm font-medium\">{folder.name}</span>\n              </div>\n\n              {folder.id !== \"all-documents\" && (\n                <div className=\"relative\">\n                  <button\n                    className=\"text-foreground hover:bg-muted\"\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      setMenuOpenFolderId(\n                        menuOpenFolderId === folder.id ? null : folder.id\n                      );\n                    }}\n                  >\n                    <IconDotsVertical size={16} />\n                  </button>\n\n                  {menuOpenFolderId === folder.id && (\n                    <div\n                      id={`folder-menu-${folder.id}`}\n                      className=\"absolute right-0 mt-2 w-28 bg-accent border border-stone-300 dark:border-stone-600 rounded shadow-md z-50\"\n                      onClick={(e) => e.stopPropagation()}\n                    >\n                      <button\n                        className=\"w-full px-4 py-2 text-left text-sm text-foreground hover:bg-muted flex items-center\"\n                        onClick={(e) => {\n                          e.stopPropagation();\n                          // TODO: Add update folder functionality here\n                          setMenuOpenFolderId(null);\n                        }}\n                      >\n                        <IconPencil size={16} className=\"mr-2\" />\n                        Update\n                      </button>\n\n                      <button\n                        className=\"w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-muted flex items-center\"\n                        onClick={(e) => {\n                          e.stopPropagation();\n                          handleDeleteGroup(folder.id);\n                          setMenuOpenFolderId(null);\n                        }}\n                      >\n                        <IconTrash size={16} className=\"mr-2\" />\n                        Delete\n                      </button>\n                    </div>\n                  )}\n                </div>\n              )}\n            </li>\n          ))}\n        </ul>\n      </nav>\n    </aside>\n  );\n};\n\nexport default Sidebar;\n"
  },
  {
    "path": "features/knowledge-base/components/max-docs-alert-dialog.tsx",
    "content": "\"use client\";\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\";\n\nimport { Button } from \"@/components/ui/button\";\n\ninterface MaxDocsAlertDialogProps {\n  isOpen: boolean;\n  onClose: () => void;\n  maxDocs: number;\n}\n\n/**\n * A ShadCN AlertDialog that warns the user they have reached max docs.\n */\nexport default function MaxDocsAlertDialog({\n  isOpen,\n  onClose,\n  maxDocs,\n}: MaxDocsAlertDialogProps) {\n  return (\n    <AlertDialog open={isOpen} onOpenChange={onClose}>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>Maximum Documents Reached</AlertDialogTitle>\n          <AlertDialogDescription>\n            You have reached the maximum number of documents ({maxDocs}). Please\n            delete some documents to upload more.\n          </AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogAction onClick={onClose}>Ok</AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  );\n}\n"
  },
  {
    "path": "features/knowledge-base/lib/generate-embeddings.ts",
    "content": "import { OpenAIEmbeddings } from \"@langchain/openai\";\nimport { RecursiveCharacterTextSplitter } from \"langchain/text_splitter\";\n\nconst openAIApiKey: string = process.env.OPENAI_API_KEY || \"\";\n\nif (!openAIApiKey) throw new Error(\"OpenAI API key not found.\");\n\nexport const generateChunks = new RecursiveCharacterTextSplitter({\n  chunkSize: 800,\n  chunkOverlap: 80,\n  lengthFunction: (input) => input.length,\n});\n\nexport const generateEmbeddings = new OpenAIEmbeddings(\n  {\n    openAIApiKey,\n    modelName: \"text-embedding-3-large\",\n  },\n  { maxRetries: 3 }\n);\n"
  },
  {
    "path": "features/knowledge-base/utils/transform-metadata-to-citation.ts",
    "content": "// transformMetadataToCitations.ts\n\ninterface RawMatch {\n  pageContent: string;\n  metadata: {\n    loc: {\n      pageNumber: number;\n      lines: { to: number; from: number };\n    };\n    pdf: {\n      info: {\n        Title: string;\n        [key: string]: any;\n      };\n      [key: string]: any;\n    };\n    fileName: string;\n    fileId: number;\n    similarity?: number;\n  };\n}\n\ninterface Citation {\n  documentName: string;\n  pages: { page: number }[];\n  similarity?: number;\n}\n\nexport function transformMetadataToCitations(items: RawMatch[]): Citation[] {\n  const citationMap: Record<string, Citation> = {};\n\n  for (const item of items) {\n    const docName = item.metadata.fileName;\n    const pageNumber = item.metadata.loc.pageNumber;\n    const similarity = item.metadata.similarity;\n\n    if (!citationMap[docName]) {\n      citationMap[docName] = {\n        documentName: docName,\n        pages: [{ page: pageNumber }],\n        similarity: similarity, // store similarity from the first match\n      };\n    } else {\n      // Add another page for this document\n      citationMap[docName].pages.push({ page: pageNumber });\n\n      // If you want to update similarity for multiple matches from same doc,\n      // you could do something like:\n      // citationMap[docName].similarity = Math.max(\n      //   citationMap[docName].similarity ?? 0,\n      //   similarity ?? 0\n      // );\n    }\n  }\n\n  return Object.values(citationMap);\n}\n"
  },
  {
    "path": "features/maps/components/address-search.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Loader2, Search } from \"lucide-react\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport useZoomRequestStore from \"../stores/use-map-zoom-request-store\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\n\ninterface GeocodeResponse {\n  results: { geometry: any }[];\n  error?: string;\n}\n\nexport default function AddressSearch() {\n  const [address, setAddress] = useState(\"\");\n  const [suggestions, setSuggestions] = useState<\n    google.maps.places.AutocompletePrediction[]\n  >([]);\n  const [open, setOpen] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [loading, setLoading] = useState(false);\n\n  const autocompleteService =\n    useRef<google.maps.places.AutocompleteService | null>(null);\n  const { setToastMessage } = useToastMessageStore();\n\n  const setZoomToAddressRequest = useZoomRequestStore(\n    (state) => state.setZoomToAddressRequest\n  );\n\n  useEffect(() => {\n    const loadGoogleMapsScript = async () => {\n      try {\n        const res = await fetch(\"/api/services/google-maps/places\");\n        const data = await res.json();\n\n        if (res.ok && data.scriptUrl) {\n          if (typeof window !== \"undefined\" && !window.google) {\n            const script = document.createElement(\"script\");\n            script.src = data.scriptUrl;\n            script.async = true;\n            script.defer = true;\n            script.onload = () => {\n              if (window.google) {\n                autocompleteService.current =\n                  new window.google.maps.places.AutocompleteService();\n              } else {\n                console.error(\"Google Maps JavaScript API not loaded.\");\n              }\n            };\n            document.head.appendChild(script);\n          } else if (window.google) {\n            autocompleteService.current =\n              new window.google.maps.places.AutocompleteService();\n          }\n        } else {\n          console.error(\"Failed to load Google Maps script URL\");\n        }\n      } catch (err) {\n        console.error(\"Error fetching Google Maps script:\", err);\n      }\n    };\n\n    loadGoogleMapsScript();\n  }, []);\n\n  useEffect(() => {\n    if (error) {\n      setToastMessage(error, \"error\");\n    }\n  }, [error, setToastMessage]);\n\n  const handleAddressChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const input = e.target.value;\n    setAddress(input);\n\n    if (input && autocompleteService.current) {\n      autocompleteService.current.getPlacePredictions(\n        { input },\n        (predictions, status) => {\n          if (\n            status === window.google.maps.places.PlacesServiceStatus.OK &&\n            predictions\n          ) {\n            setSuggestions(predictions);\n          } else {\n            setSuggestions([]);\n          }\n        }\n      );\n    } else {\n      setSuggestions([]);\n    }\n  };\n\n  const handleSuggestionClick = (\n    suggestion: google.maps.places.AutocompletePrediction\n  ) => {\n    setAddress(suggestion.description);\n    setSuggestions([]);\n    handleGeocode(suggestion.description);\n  };\n\n  // Accept an optional address override parameter.\n  const handleGeocode = async (addressOverride?: string) => {\n    const queryAddress = addressOverride || address;\n    try {\n      setLoading(true);\n      const response = await fetch(\"/api/services/google-maps/geocode\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({ address: queryAddress }),\n      });\n\n      const data: GeocodeResponse = await response.json();\n\n      if (response.ok && data.results && data.results[0]) {\n        setError(null);\n        setZoomToAddressRequest(data.results[0].geometry.location);\n        setOpen(false);\n      } else {\n        setError(data.error || \"Failed to geocode address.\");\n      }\n    } catch (err) {\n      setError(\"An error occurred while fetching the geocode.\");\n    }\n    setLoading(false);\n    setAddress(\"\");\n  };\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <PopoverTrigger asChild>\n            <Button\n              size=\"icon\"\n              variant=\"ghost\"\n              className=\"bg-background rounded-xl [&_svg]:size-5\"\n            >\n              <Search className=\"text-foreground\" />\n            </Button>\n          </PopoverTrigger>\n        </TooltipTrigger>\n        <TooltipContent>Search Address</TooltipContent>\n      </Tooltip>\n      <PopoverContent className=\"w-80\">\n        <div className=\"relative\">\n          <Input\n            value={address}\n            onChange={handleAddressChange}\n            placeholder=\"Enter an address\"\n            className=\"w-full pl-10\"\n          />\n          <Button\n            onClick={() => handleGeocode()}\n            variant=\"ghost\"\n            className=\"absolute inset-y-0 left-0 flex items-center px-2\"\n            disabled={loading}\n          >\n            {loading ? (\n              <Loader2 className=\"w-4 h-4 animate-spin text-foreground\" />\n            ) : (\n              <Search className=\"w-4 h-4 text-foreground\" />\n            )}\n          </Button>\n        </div>\n        {suggestions.length > 0 && (\n          <ul className=\"mt-2 max-h-60 overflow-y-auto border border-stone-600 rounded-md bg-background\">\n            {suggestions.map((suggestion) => (\n              <li\n                key={suggestion.place_id}\n                className=\"cursor-pointer p-2 hover:bg-muted text-sm\"\n                onClick={() => handleSuggestionClick(suggestion)}\n              >\n                {suggestion.description}\n              </li>\n            ))}\n          </ul>\n        )}\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "features/maps/components/attribute-table/attribute-table-controls.tsx",
    "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { IconFocusCentered, IconTrash } from \"@tabler/icons-react\";\nimport useQueryRasterFromVectorLayerStore from \"../../stores/map-queries-stores/useQueryRasterFromVectorLayerStore\";\nimport useMapLayersStore from \"../../stores/use-map-layer-store\";\nimport useLayerSelectionStore from \"../../stores/use-layer-selection-store\";\nimport usePlotReadyDataFromVectorLayerStore from \"../../stores/plots-stores/usePlotReadyFromVectorLayerStore\";\nimport useDrawnFeatureOnMapStore from \"../../stores/use-drawn-feature-on-map-store\";\nimport useTableStore from \"../../stores/use-table-store\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { set } from \"lodash\";\n\ninterface AttributeTableControlsProps {\n  isDataAvailable: boolean;\n  handleZoomInQuery: () => void;\n  setSelectedRow: (row: number | null) => void;\n  onClose: () => void;\n}\n\ninterface RasterLayerProps {\n  name: string;\n}\n\nconst AttributeTableControls = ({\n  isDataAvailable,\n  handleZoomInQuery,\n  setSelectedRow,\n  onClose,\n}: AttributeTableControlsProps) => {\n  const { selectedDrawnFeature, removeDrawnFeature } =\n    useDrawnFeatureOnMapStore();\n  const { mapLayers } = useMapLayersStore();\n  const { setIsQueryActive } = useQueryRasterFromVectorLayerStore();\n  const deleteSelectedFeature = useTableStore(\n    (state) => state.deleteSelectedFeature\n  );\n  const setSelectRasterLayer = useLayerSelectionStore(\n    (state) => state.setSelectRasterLayer\n  );\n  const { setPlotReadyDataForSelectedFeatureFromTable } =\n    usePlotReadyDataFromVectorLayerStore();\n  const { drawnFeaturesOnMap } = useDrawnFeatureOnMapStore();\n  const [currentRasterLayer, setCurrentRasterLayer] =\n    useState<RasterLayerProps>({ name: \"\" });\n  const [availableRasterLayers, setAvailableRasterLayers] = useState<any[]>([]);\n\n  const handleDeleteFeatures = () => {\n    if (selectedDrawnFeature) {\n      setSelectedRow(null);\n      removeDrawnFeature(selectedDrawnFeature.UID);\n      deleteSelectedFeature(selectedDrawnFeature);\n    }\n  };\n\n  function handleQueryRaster() {\n    setIsQueryActive(true);\n  }\n\n  useEffect(() => {\n    const rasterLayers = mapLayers.filter(({ type }) => type === \"raster\");\n    setAvailableRasterLayers(rasterLayers);\n    const lastRasterLayer =\n      rasterLayers.length > 0 ? rasterLayers[rasterLayers.length - 1] : null;\n    if (lastRasterLayer) {\n      const tempCurrentLayer = { name: lastRasterLayer.name };\n      setCurrentRasterLayer(tempCurrentLayer);\n      setSelectRasterLayer(tempCurrentLayer.name);\n    } else {\n      setCurrentRasterLayer({ name: \"\" });\n      setSelectRasterLayer(\"\");\n    }\n  }, [mapLayers.length, mapLayers, setSelectRasterLayer]);\n\n  const handleRasterLayerChange = (\n    event: React.ChangeEvent<HTMLSelectElement>\n  ) => {\n    setPlotReadyDataForSelectedFeatureFromTable(null, \"\");\n    setCurrentRasterLayer({ name: event.target.value });\n    setSelectRasterLayer(event.target.value);\n  };\n\n  return (\n    <TooltipProvider>\n      <section className=\" flex w-full divide-x divide-stone-600 h-8 z-1000 bg-background\">\n        <section className=\"flex p-2 bg-background h-8 w-fit divide-x divide-stone-600\">\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <button className=\"ml-auto px-6\">\n                <IconTrash\n                  size={19}\n                  className=\"hover:text-red-500\"\n                  onClick={handleDeleteFeatures}\n                />\n              </button>\n            </TooltipTrigger>\n            <TooltipContent side=\"top\">Delete selected feature</TooltipContent>\n          </Tooltip>\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <button\n                className=\"px-6 text-foreground disabled:text-muted\"\n                onClick={handleZoomInQuery}\n                disabled={!selectedDrawnFeature}\n              >\n                <IconFocusCentered size={19} />\n              </button>\n            </TooltipTrigger>\n            <TooltipContent side=\"top\">Zoom in to query</TooltipContent>\n          </Tooltip>\n        </section>\n        <section className=\"flex h-fit w-full mb-5 text-sm\">\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <select\n                id=\"vector-layer-select\"\n                className=\" w-full h-8 max-w-md font-medium bg-background border border-r-stone-600 outline-none\"\n              >\n                {drawnFeaturesOnMap.length > 0 && (\n                  <option key=\"Drawn Features\" value=\"Drawn Features\">\n                    Drawn Features\n                  </option>\n                )}\n              </select>\n            </TooltipTrigger>\n            <TooltipContent side=\"top\">Select vector layer</TooltipContent>\n          </Tooltip>\n        </section>\n        <section className=\"flex h-fit w-full mb-5 text-sm \"></section>\n      </section>\n    </TooltipProvider>\n  );\n};\n\nexport default AttributeTableControls;\n"
  },
  {
    "path": "features/maps/components/attribute-table/attribute-table.tsx",
    "content": "import React, { useState, useEffect, useMemo, memo, useRef } from \"react\";\nimport { Rnd } from \"react-rnd\";\nimport {\n  useReactTable,\n  createColumnHelper,\n  ColumnDef,\n  getCoreRowModel,\n  getSortedRowModel,\n  flexRender,\n} from \"@tanstack/react-table\";\nimport { X } from \"lucide-react\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\n\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\n\nimport AttributeTableControls from \"./attribute-table-controls\";\nimport useGeojsonStore from \"../../stores/use-geojson-store\";\nimport useZoomRequestStore from \"../../stores/use-map-zoom-request-store\";\nimport useDrawnFeatureOnMapStore from \"../../stores/use-drawn-feature-on-map-store\";\nimport useLoadingStore from \"@/stores/use-loading-store\";\nimport useTableStore from \"../../stores/use-table-store\";\nimport { formatQueryForTable } from \"../../utils/other-utils\";\n\nconst columnHelper = createColumnHelper<any>();\nconst MIN_PANEL_HEIGHT = 150;\n\nconst AttributeTable: React.FC = () => {\n  const { setLoading } = useLoadingStore();\n  const { getGeojsonDataByName } = useGeojsonStore();\n  const [isDeleteFeaturesEnabled, setIsDeleteFeaturesEnabled] = useState(false);\n  const [isDataAvailable, setIsDataAvailable] = useState(false);\n  const [selectedRow, setSelectedRow] = useState<number | null>(null);\n\n  // NEW: Track expanded cell content in a dialog\n  const [expandedCellValue, setExpandedCellValue] = useState<string | null>(\n    null\n  );\n  const [isDialogOpen, setIsDialogOpen] = useState(false);\n\n  const setZoomRequestFromTable = useZoomRequestStore(\n    (state) => state.setZoomRequestFromTable\n  );\n\n  const isTableOpen = useTableStore((state) => state.isTableOpen);\n  const toggleTable = useTableStore((state) => state.toggleTable);\n  const setSelectedFeatureInTable = useTableStore(\n    (state) => state.setSelectedFeatureInTable\n  );\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [position, setPosition] = useState({ x: 100, y: 100 });\n  const [size, setSize] = useState({ width: 600, height: 200 });\n\n  const { drawnFeaturesOnMap, selectDrawnFeature, clearSelectedDrawnFeature } =\n    useDrawnFeatureOnMapStore();\n\n  // Transform drawnFeaturesOnMap to Feature[]\n  const features: Feature[] = useMemo(() => {\n    return drawnFeaturesOnMap\n      .map((feature) => {\n        if (feature.geometry === \"Point\") {\n          const { coordinates, query, ...otherProps } = feature;\n          return {\n            lat: feature.lat,\n            lon: feature.lon,\n            query: formatQueryForTable(query),\n            ...otherProps,\n          } as Feature;\n        } else if (feature.geometry === \"Polygon\") {\n          const {\n            coordinates = [],\n            query,\n            ...otherProps\n          } = feature.coordinates\n            ? { ...feature, coordinates: feature.coordinates[0] }\n            : feature;\n\n          const lats = coordinates.map((coord) => coord[1]);\n          const lons = coordinates.map((coord) => coord[0]);\n\n          return {\n            lat: lats,\n            lon: lons,\n            query: formatQueryForTable(query),\n            ...otherProps,\n          } as Feature;\n        }\n        return null;\n      })\n      .filter((item): item is Feature => item !== null);\n  }, [drawnFeaturesOnMap]);\n\n  // Zoom logic\n  const handleZoomInQuery = () => {\n    if (selectedRow === null) {\n      setZoomRequestFromTable(null);\n      return;\n    }\n    const selectedFeature = features[selectedRow];\n    setZoomRequestFromTable(selectedFeature);\n    setSelectedFeatureInTable(selectedFeature);\n  };\n\n  useEffect(() => {\n    setIsDeleteFeaturesEnabled(features.length > 0);\n    setIsDataAvailable(features.length > 0);\n  }, [features]);\n\n  // Prepare data for the table\n  const data = useMemo(() => {\n    if (features.length === 0) return [];\n    return features.map((feature: any, index: number) => {\n      const { featureLayerName, ...rest } = feature;\n      return {\n        UID: feature.UID || index + 1,\n        ...rest,\n      };\n    });\n  }, [features]);\n\n  // Build columns from data keys\n  const columns = useMemo<ColumnDef<any, any>[]>(() => {\n    if (features.length === 0) return [];\n    const keys = Object.keys(features[0]).filter(\n      (key) => key !== \"featureLayerName\"\n    );\n    const sortedKeys = keys.includes(\"UID\")\n      ? [\"UID\", ...keys.filter((key) => key !== \"UID\")]\n      : [\"UID\", ...keys];\n\n    return sortedKeys.map((key) =>\n      columnHelper.accessor(key, {\n        header: () => (key === \"rasterLayerName\" ? \"reference analysis\" : key),\n      })\n    );\n  }, [features]);\n\n  const [sorting, setSorting] = useState<any>([]);\n  const table = useReactTable({\n    data,\n    columns,\n    state: { sorting },\n    onSortingChange: setSorting,\n    getCoreRowModel: getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n  });\n\n  // onClick for each cell\n  const handleCellClick = (fullValue: string) => {\n    setExpandedCellValue(fullValue);\n    setIsDialogOpen(true);\n  };\n\n  // onClick for a row\n  const handleRowClick = (rowIndex: number) => {\n    if (selectedRow === rowIndex) {\n      setSelectedRow(null);\n      setZoomRequestFromTable(null);\n      clearSelectedDrawnFeature();\n    } else {\n      clearSelectedDrawnFeature();\n      selectDrawnFeature(data[rowIndex].UID);\n      setSelectedRow(rowIndex);\n    }\n  };\n\n  const columnCount = columns.length;\n  const gridTemplateColumns = `repeat(${columnCount}, 1fr)`;\n\n  // Virtual scrolling\n  const parentRef = useRef<HTMLDivElement>(null);\n  const rowVirtualizer = useVirtualizer({\n    count: table.getRowModel().rows.length,\n    getScrollElement: () => parentRef.current,\n    estimateSize: () => 40,\n  });\n  const allRows = table.getRowModel().rows;\n\n  // A memoized row\n  const Row = memo(({ index }: { index: number }) => {\n    const row = allRows[index];\n    const isSelected = selectedRow === index;\n\n    return (\n      <div\n        className={`border-b border-stone-300 dark:border-stone-600 ${\n          isSelected ? \"bg-green-300\" : \"bg-secondary\"\n        }`}\n        style={{\n          display: \"grid\",\n          gridTemplateColumns,\n        }}\n        onClick={() => handleRowClick(index)}\n      >\n        {row.getVisibleCells().map((cell) => {\n          const rawValue = cell.getValue<any>();\n          const cellValue = Array.isArray(rawValue)\n            ? rawValue.join(\", \")\n            : rawValue;\n\n          // Truncate in the table\n          const truncated =\n            cellValue && cellValue.length > 20\n              ? cellValue.slice(0, 20) + \"...\"\n              : cellValue;\n\n          return (\n            <div\n              key={cell.id}\n              className={`p-2 text-sm cursor-pointer custom-scrollbar scrollbar-right ${\n                isSelected ? \"dark:text-background\" : \"\"\n              }`}\n              style={{\n                whiteSpace: \"nowrap\",\n                overflow: \"hidden\",\n                textOverflow: \"ellipsis\",\n              }}\n              onClick={(e) => {\n                e.stopPropagation();\n                if (cell.column.id === \"UID\") {\n                  handleRowClick(index);\n                } else {\n                  // Show the full value in a dialog\n                  handleCellClick(cellValue);\n                }\n              }}\n            >\n              {truncated}\n            </div>\n          );\n        })}\n      </div>\n    );\n  });\n  Row.displayName = \"Row\";\n\n  // Center + bottom positioning logic\n  useEffect(() => {\n    if (isTableOpen && containerRef.current) {\n      const parentBounds = containerRef.current.getBoundingClientRect();\n      const parentWidth = parentBounds.width;\n      const parentHeight = parentBounds.height;\n\n      const rndWidth = size.width;\n      const rndHeight = size.height;\n\n      const centerX = parentWidth / 2 - rndWidth / 2;\n      const bottomY = parentHeight * 0.82 - rndHeight;\n\n      setPosition({\n        x: centerX,\n        y: bottomY,\n      });\n    }\n  }, [isTableOpen]);\n\n  return (\n    <div ref={containerRef} className=\"relative w-full h-screen\">\n      {isTableOpen && (\n        <Rnd\n          position={{ x: position.x, y: position.y }}\n          size={{ width: size.width, height: size.height }}\n          onDragStop={(e, data) => {\n            setPosition({ x: data.x, y: data.y });\n          }}\n          onResizeStop={(e, dir, ref, delta, pos) => {\n            setSize({\n              width: ref.offsetWidth,\n              height: ref.offsetHeight,\n            });\n            setPosition({ x: pos.x, y: pos.y });\n          }}\n          bounds=\"parent\"\n          minWidth={400}\n          minHeight={MIN_PANEL_HEIGHT}\n          dragHandleClassName=\"drag-handle\"\n          enableResizing={{\n            top: true,\n            right: true,\n            bottom: true,\n            left: true,\n            topRight: true,\n            bottomRight: true,\n            bottomLeft: true,\n            topLeft: true,\n          }}\n          className=\"z-[1000]\"\n        >\n          <section className=\"h-full w-full flex flex-col bg-muted rounded-lg overflow-hidden\">\n            {/* DRAG HANDLE */}\n            <div className=\"flex justify-end items-center bg-background border-b border-stone-300 dark:border-stone-600 rounded-t-lg drag-handle cursor-move\">\n              <Button\n                onClick={() => toggleTable()}\n                variant=\"ghost\"\n                size={\"icon\"}\n                className=\"inline-flex items-center\"\n              >\n                <X />\n              </Button>\n            </div>\n\n            {/* Controls */}\n            <div className=\"w-full\">\n              <AttributeTableControls\n                isDataAvailable={isDataAvailable}\n                setSelectedRow={setSelectedRow}\n                handleZoomInQuery={handleZoomInQuery}\n                onClose={() => toggleTable()}\n              />\n            </div>\n\n            {/* The table */}\n            <div className=\"flex flex-col flex-grow overflow-hidden\">\n              {/* Table header */}\n              <div className=\"table border-collapse w-full\">\n                <div className=\"flex-shrink-0 w-full\">\n                  <div\n                    className=\"border-b border-stone-300 dark:border-stone-600\"\n                    style={{\n                      display: \"grid\",\n                      gridTemplateColumns,\n                    }}\n                  >\n                    {table.getHeaderGroups().map((headerGroup) => (\n                      <div className=\"contents\" key={headerGroup.id}>\n                        {headerGroup.headers.map((header) => {\n                          const sorted = header.column.getIsSorted();\n                          return (\n                            <div\n                              key={header.id}\n                              onClick={header.column.getToggleSortingHandler()}\n                              className=\"flex justify-center items-center py-1 text-sm font-medium text-center bg-blue-500 cursor-pointer select-none\"\n                            >\n                              {flexRender(\n                                header.column.columnDef.header,\n                                header.getContext()\n                              )}\n                              <span>\n                                {sorted === \"desc\"\n                                  ? \" 🔽\"\n                                  : sorted === \"asc\"\n                                  ? \" 🔼\"\n                                  : \"\"}\n                              </span>\n                            </div>\n                          );\n                        })}\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              </div>\n\n              {/* Scrollable parent for rows */}\n              <div\n                ref={parentRef}\n                className=\"w-full flex-grow min-h-0 overflow-auto text-center\"\n              >\n                <div\n                  style={{\n                    height: rowVirtualizer.getTotalSize(),\n                    width: \"100%\",\n                    position: \"relative\",\n                  }}\n                >\n                  {rowVirtualizer.getVirtualItems().map((virtualRow) => {\n                    const index = virtualRow.index;\n                    return (\n                      <div\n                        key={virtualRow.key}\n                        style={{\n                          position: \"absolute\",\n                          top: 0,\n                          left: 0,\n                          width: \"100%\",\n                          height: 40,\n                          transform: `translateY(${virtualRow.start}px)`,\n                        }}\n                      >\n                        <Row index={index} />\n                      </div>\n                    );\n                  })}\n                </div>\n              </div>\n            </div>\n          </section>\n        </Rnd>\n      )}\n\n      <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>\n        <DialogContent className=\"max-w-x outline-none\">\n          <DialogHeader>\n            <DialogTitle>Full Cell Content</DialogTitle>\n            <DialogDescription>\n              Below is the entire cell text.\n            </DialogDescription>\n          </DialogHeader>\n\n          <div className=\"whitespace-pre-wrap overflow-auto max-h-[60vh] mt-2\">\n            {expandedCellValue}\n          </div>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n};\n\nexport default AttributeTable;\n"
  },
  {
    "path": "features/maps/components/map-badge.tsx",
    "content": "import { Separator } from \"@/components/ui/separator\";\nimport React from \"react\";\n\ninterface MapBadgeProps {\n  type?: \"drawing\" | \"query_layer\";\n  heading?: string;\n  secondaryText?: string;\n}\n\nconst MapBadge: React.FC<MapBadgeProps> = ({\n  type = \"drawing\",\n  heading = \"ROI Drawing Mode\",\n  secondaryText,\n}) => {\n  return (\n    <>\n      {heading && (\n        <div className=\"absolute top-2 left-1/2 -translate-x-1/2 z-[5000]\">\n          <div\n            className={`${\n              type === \"drawing\"\n                ? \"bg-warning text-gray-900\"\n                : \"bg-info text-white\"\n            } prose prose-sm text-center text-md font-bold w-full h-full p-2 rounded-lg text-nowrap`}\n            style={{\n              textShadow:\n                type === \"drawing\"\n                  ? undefined\n                  : \"0px 1px 2px rgba(0, 0, 0, 0.5)\",\n            }}\n          >\n            {/* Primary text */}\n            {heading}\n\n            {/* Secondary text (renders only if secondaryText is provided) */}\n            {secondaryText && (\n              <>\n                <Separator />\n                <div className=\"pt-1 text-sm font-medium text-center\">\n                  {secondaryText}\n                </div>\n              </>\n            )}\n          </div>\n        </div>\n      )}\n    </>\n  );\n};\n\nexport default MapBadge;\n"
  },
  {
    "path": "features/maps/components/map-container.tsx",
    "content": "\"use client\";\n\nimport React, { memo } from \"react\";\nimport { Maximize, Minimize } from \"lucide-react\";\nimport useMapDisplayStore from \"@/features/maps/stores/use-map-display-store\";\nimport MapCustomControls from \"./map-custom-controls/map-custom-controls\";\nimport useHandleClick from \"../hooks/use-handle-click/use-handle-click\";\nimport { useMapCursor } from \"@/features/maps/hooks/use-map-cursor\";\nimport useMap from \"../hooks/use-map/use-map\";\nimport MapChartPanel from \"./map-panels/map-chart-panel/map-chart-panel\";\nimport MapLayersPanel from \"./map-panels/map-layers-panel/map-layers-panel\";\nimport MapRoiControls from \"./map-custom-controls/map-roi-controls\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport MapBadge from \"./map-badge\";\nimport AttributeTable from \"./attribute-table/attribute-table\";\nimport LoadingWidget from \"@/components/loading-widgets/loading-primary\";\nimport useLayerSelectionStore from \"../stores/use-layer-selection-store\";\nimport useBadgeStore from \"../stores/use-map-badge-store\";\n\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n\nconst MAP_CONTAINER_ID = \"map\";\n\nconst MapContainer = () => {\n  const mapMaximizeRequested = useMapDisplayStore(\n    (state) => state.mapMaximizeRequested\n  );\n  const setMapMaximizeRequested = useMapDisplayStore(\n    (state) => state.setMapMaximizeRequested\n  );\n  const setDisplayMapRequestedFromChatResponse = useMapDisplayStore(\n    (state) => state.setDisplayMapRequestedFromChatResponse\n  );\n  const setDisplayMapRequestedFromInsightsViewerIcon = useMapDisplayStore(\n    (state) => state.setDisplayRawMapRequestedFromInsightsViewerIcon\n  );\n  const isROIDrawingActive = useROIStore((state) => state.isROIDrawingActive);\n  const selectedRasterLayer = useLayerSelectionStore(\n    (state) => state.selectedRasterLayer\n  );\n  const secondaryMapBadgeText = useBadgeStore((state) => state.secondaryText);\n\n  useMap(MAP_CONTAINER_ID);\n  useHandleClick();\n  useMapCursor();\n\n  const handleResize = () => {\n    setMapMaximizeRequested(!mapMaximizeRequested);\n  };\n\n  const isMapFullScreen = mapMaximizeRequested;\n\n  return (\n    <TooltipProvider>\n      <div className=\"flex w-full h-full justify-center items-center z-[1000]\">\n        <div\n          id={MAP_CONTAINER_ID}\n          className=\"relative w-full h-full rounded-lg overflow-hidden\"\n        >\n          <div className=\"absolute bottom-5 left-1/2 transform -translate-x-1/2 z-[10000] text-gray-800\">\n            {isROIDrawingActive ? <MapRoiControls /> : <MapCustomControls />}\n          </div>\n          {isROIDrawingActive ? (\n            <MapBadge secondaryText={secondaryMapBadgeText} />\n          ) : (\n            <MapBadge\n              type=\"query_layer\"\n              heading={selectedRasterLayer.layerName}\n            />\n          )}\n          <MapChartPanel />\n          <MapLayersPanel />\n          <AttributeTable />\n          <LoadingWidget />\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <button\n                className=\"absolute top-2 left-2 bg-background bg-opacity-80 hover:bg-muted p-1 rounded-lg z-[10] text-foreground\"\n                onClick={handleResize}\n              >\n                {isMapFullScreen ? (\n                  <Minimize size={21} />\n                ) : (\n                  <Maximize size={21} />\n                )}\n              </button>\n            </TooltipTrigger>\n            <TooltipContent side=\"right\">\n              {isMapFullScreen ? \"Minimize map\" : \"Maximize map\"}\n            </TooltipContent>\n          </Tooltip>\n        </div>\n      </div>\n    </TooltipProvider>\n  );\n};\n\nexport default memo(MapContainer);\n"
  },
  {
    "path": "features/maps/components/map-custom-controls/map-custom-controls.tsx",
    "content": "import React from \"react\";\nimport {\n  MousePointerClick,\n  SquareMousePointer,\n  Layers,\n  BarChart3,\n  Menu,\n  Table2 as TableIcon,\n} from \"lucide-react\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport useMapDisplayStore from \"@/features/maps/stores/use-map-display-store\";\nimport useTableStore from \"../../stores/use-table-store\";\nimport AddressSearch from \"../address-search\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\n\nconst MapCustomControls = () => {\n  const activeDrawingMode = useButtonsStore((state) => state.activeDrawingMode);\n  const setDrawingMode = useButtonsStore((state) => state.setDrawingMode);\n  const toggleMapLayersPanel = useMapDisplayStore(\n    (state) => state.toggleMapLayersPanel\n  );\n  const toggleMapChartPanel = useMapDisplayStore(\n    (state) => state.toggleMapChartPanel\n  );\n  const toggleTable = useTableStore((state) => state.toggleTable);\n  const toggleBasemap = useButtonsStore((state) => state.toggleBasemap);\n\n  const buttons = [\n    {\n      id: \"toggle-layers-panel\",\n      onClick: toggleMapLayersPanel,\n      tooltip: \"Toggle layers panel\",\n      icon: <Menu className=\"text-foreground\" />,\n      active: false,\n    },\n    {\n      id: \"draw-point\",\n      onClick: () => setDrawingMode(\"draw_point\"),\n      tooltip:\n        activeDrawingMode === \"draw_point\"\n          ? \"Click to cancel\"\n          : \"Select a location on the map\",\n      icon: <MousePointerClick className=\"text-foreground\" />,\n      active: activeDrawingMode === \"draw_point\",\n    },\n    {\n      id: \"draw-polygon\",\n      onClick: () => setDrawingMode(\"draw_polygon\"),\n      tooltip:\n        activeDrawingMode === \"draw_polygon\"\n          ? \"Click to cancel\"\n          : \"Draw a polygon on the map\",\n      icon: <SquareMousePointer className=\"text-foreground\" />,\n      active: activeDrawingMode === \"draw_polygon\",\n    },\n    {\n      id: \"toggle-basemaps\",\n      onClick: toggleBasemap,\n      tooltip: \"Toggle basemap\",\n      icon: <Layers className=\"text-foreground\" />,\n      active: false,\n    },\n    {\n      id: \"toggle-table\",\n      onClick: toggleTable,\n      tooltip: \"Attribute table\",\n      icon: <TableIcon className=\"text-foreground\" />,\n      active: false,\n    },\n    {\n      id: \"toggle-chart-panel\",\n      onClick: toggleMapChartPanel,\n      tooltip: \"Toggle chart panel\",\n      icon: <BarChart3 className=\"text-foreground\" />,\n      active: false,\n    },\n  ];\n\n  const firstGroup = buttons.slice(0, 3);\n  const secondGroup = buttons.slice(3);\n\n  return (\n    <div className=\"flex justify-center items-center gap-4 bg-foreground/30 border border-stone-600 w-fit p-2 h-fit rounded-2xl\">\n      {firstGroup.map((button) => (\n        <Tooltip key={button.id}>\n          <TooltipTrigger asChild>\n            <Button\n              size=\"icon\"\n              variant=\"ghost\"\n              className=\"bg-background rounded-xl [&_svg]:size-5\"\n              onClick={button.onClick}\n              disabled={button.active}\n            >\n              {button.icon}\n            </Button>\n          </TooltipTrigger>\n          <TooltipContent side=\"top\">{button.tooltip}</TooltipContent>\n        </Tooltip>\n      ))}\n\n      <AddressSearch />\n\n      {secondGroup.map((button) => (\n        <Tooltip key={button.id}>\n          <TooltipTrigger asChild>\n            <Button\n              size=\"icon\"\n              variant=\"ghost\"\n              className=\"bg-background rounded-xl [&_svg]:size-5\"\n              onClick={button.onClick}\n              disabled={button.active}\n            >\n              {button.icon}\n            </Button>\n          </TooltipTrigger>\n          <TooltipContent side=\"top\">{button.tooltip}</TooltipContent>\n        </Tooltip>\n      ))}\n    </div>\n  );\n};\n\nexport default MapCustomControls;\n"
  },
  {
    "path": "features/maps/components/map-custom-controls/map-roi-controls.tsx",
    "content": "import React, { useState } from \"react\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport AddressSearch from \"../address-search\";\nimport { Button } from \"@/components/ui/button\";\nimport InputTextConfirm from \"@/components/ui/input-text-confirm\";\nimport { Layers, SquareMousePointer } from \"lucide-react\";\n\n// 1) Import Shadcn tooltip components\nimport {\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n} from \"@/components/ui/tooltip\";\n\nconst MapRoiControls = () => {\n  const activeDrawingMode = useButtonsStore((state) => state.activeDrawingMode);\n  const setDrawingMode = useButtonsStore((state) => state.setDrawingMode);\n  const toggleBasemap = useButtonsStore((state) => state.toggleBasemap);\n\n  const isRoiCreated = useROIStore((state) => state.isRoiCreated);\n  const setIsRoiCreated = useROIStore((state) => state.setIsRoiCreated);\n  const setIsRoiFinalized = useROIStore((state) => state.setIsRoiFinalized);\n\n  const [isSelectRoiNameOpen, setIsSelectRoiNameOpen] = useState(false);\n\n  function handleRoiConfirm() {\n    setIsSelectRoiNameOpen(true);\n  }\n\n  function handleRoiFinalize(name: string) {\n    setIsRoiFinalized(true, name);\n    setIsRoiCreated(false);\n  }\n\n  // 2) Decide on tooltip text for the “polygon” button\n  const polygonButtonTooltip =\n    activeDrawingMode === \"draw_polygon\"\n      ? \"Click to cancel\"\n      : \"Select a location on the map\";\n\n  return (\n    <div className=\"flex justify-center items-center gap-4 bg-foreground/30 bg-opacity-60 w-fit p-2 h-fit rounded-2xl\">\n      {isSelectRoiNameOpen && (\n        <InputTextConfirm\n          isOpen={isSelectRoiNameOpen}\n          onClose={() => setIsSelectRoiNameOpen(false)}\n          onSubmit={handleRoiFinalize}\n          title=\"Enter ROI Name\"\n        />\n      )}\n\n      {isRoiCreated && (\n        <Button\n          size=\"sm\"\n          variant=\"warning\"\n          className=\"font-semibold text-gray-800\"\n          onClick={handleRoiConfirm}\n        >\n          Finalize ROI\n        </Button>\n      )}\n\n      {/* 4) “Draw Polygon” Button + Tooltip */}\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <Button\n            size=\"icon\"\n            variant=\"ghost\"\n            className=\"bg-background [&_svg]:size-5\"\n            onClick={() => setDrawingMode(\"draw_polygon\")}\n            disabled={activeDrawingMode === \"draw_polygon\"}\n          >\n            <SquareMousePointer className=\"text-foreground\" />\n          </Button>\n        </TooltipTrigger>\n        <TooltipContent>{polygonButtonTooltip}</TooltipContent>\n      </Tooltip>\n\n      <AddressSearch />\n\n      {/* 5) “Toggle Basemap” Button + Tooltip */}\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <Button\n            className=\"bg-background [&_svg]:size-5\"\n            variant=\"ghost\"\n            size=\"icon\"\n            onClick={toggleBasemap}\n          >\n            <Layers className=\"text-foreground\" />\n          </Button>\n        </TooltipTrigger>\n        <TooltipContent>Toggle basemap</TooltipContent>\n      </Tooltip>\n    </div>\n  );\n};\n\nexport default MapRoiControls;\n"
  },
  {
    "path": "features/maps/components/map-panels/map-chart-panel/map-chart-panel.tsx",
    "content": "import React, { useEffect } from \"react\";\nimport { ArrowRight } from \"lucide-react\";\nimport {\n  ChartQueryDisplay,\n  ChartTimeseriesQueryDisplay,\n} from \"@/features/charts/components/charts-display\";\nimport useMapDisplayStore from \"@/features/maps/stores/use-map-display-store\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport { Button } from \"@/components/ui/button\";\n\nconst MapChartPanel = () => {\n  const isChartPanelOpen = useMapDisplayStore(\n    (state) => state.isMapChartPanelOpen\n  );\n  const toggleChartPanel = useMapDisplayStore(\n    (state) => state.toggleMapChartPanel\n  );\n  const isROIDrawingActive = useROIStore((state) => state.isROIDrawingActive);\n\n  useEffect(() => {\n    if (isROIDrawingActive && isChartPanelOpen) {\n      toggleChartPanel();\n    }\n  }, [isROIDrawingActive]);\n\n  return (\n    <div\n      className={`absolute top-20 bottom-20 right-0 w-64 pl-2 z-[1000] bg-secondary\n                  transition-transform duration-300 rounded-l-xl\n                  ${isChartPanelOpen ? \"translate-x-0\" : \"translate-x-full\"}`}\n    >\n      <div className=\"flex flex-col gap-3 h-[70%]\">\n        {/* 1) Move the button slightly down (mt-2 or mt-3) and to the left (ml-2 or ml-3) */}\n        <Button\n          className=\"p-2 mt-3 ml-2\"\n          variant=\"ghost\"\n          size=\"icon\"\n          onClick={toggleChartPanel}\n        >\n          <ArrowRight className=\"w-4 h-4\" />\n        </Button>\n\n        <ChartQueryDisplay />\n        <ChartTimeseriesQueryDisplay />\n      </div>\n    </div>\n  );\n};\n\nexport default MapChartPanel;\n"
  },
  {
    "path": "features/maps/components/map-panels/map-layers-panel/color-picker-popover.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { HexColorPicker } from \"react-colorful\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport useColorPickerStore from \"@/features/maps/stores/use-color-picker-store\";\n\n// Combined in one file:\nexport default function ColorPickerPopover() {\n  const { isPickerOpen, pickedColor, setPickedColor, setPickerOpen } =\n    useColorPickerStore();\n\n  // The popover is controlled by `isPickerOpen`\n  const open = isPickerOpen;\n\n  const handleOpenChange = (openValue: boolean) => {\n    // If user clicks outside or presses Esc, close it\n    if (!openValue) {\n      setPickerOpen(false);\n    }\n  };\n\n  const handlePickColor = (newColor: string) => {\n    if (pickedColor.layerName) {\n      setPickedColor(newColor, pickedColor.layerName);\n    }\n  };\n\n  return (\n    <Popover open={open} onOpenChange={handleOpenChange}>\n      {/* \n        1) We'll dynamically place the trigger\n           near the \"Pick Color\" item. So here we can\n           render a 'dummy' or hidden trigger.\n      */}\n      <PopoverTrigger asChild>\n        <span style={{ display: \"none\" }} />\n      </PopoverTrigger>\n\n      {/* \n        2) The popover content shows the color picker\n        - Use side=\"right\" or sideOffset to position it\n          near the menu item. \n      */}\n      <PopoverContent side=\"right\" sideOffset={6} className=\"z-[9999] p-2\">\n        {pickedColor.layerName && (\n          <HexColorPicker\n            color={pickedColor.color}\n            onChange={handlePickColor}\n          />\n        )}\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "features/maps/components/map-panels/map-layers-panel/map-layers-panel.tsx",
    "content": "\"use client\";\n\nimport React, {\n  useState,\n  useCallback,\n  useEffect,\n  ReactNode,\n  CSSProperties,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { DragDropContext, Droppable, Draggable } from \"@hello-pangea/dnd\";\nimport {\n  GripVertical,\n  ZoomIn,\n  Settings,\n  Eye,\n  EyeOff,\n  Trash2,\n  Palette,\n  ChevronDown,\n  ChevronUp,\n  ArrowLeft,\n} from \"lucide-react\";\n\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n} from \"@/components/ui/popover\";\nimport { Button, buttonVariants } from \"@/components/ui/button\";\nimport { HexColorPicker } from \"react-colorful\";\nimport { cn } from \"@/lib/utils\";\n\n// ---- stores ----\nimport useMapDisplayStore from \"@/features/maps/stores/use-map-display-store\";\nimport useMapLayersStore from \"@/features/maps/stores/use-map-layer-store\";\nimport useZoomRequestStore from \"@/features/maps/stores/use-map-zoom-request-store\";\nimport useColorPickerStore from \"@/features/maps/stores/use-color-picker-store\";\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport useLayerSelectionStore from \"@/features/maps/stores/use-layer-selection-store\";\nimport { useAttachmentStore } from \"@/features/chat/stores/use-attachments-store\";\n\n// ---- components ----\nimport Legend from \"./map-legend\";\n\n/** 1) A small helper that conditionally portals the Draggable item. */\nfunction DraggablePortal({\n  provided,\n  snapshot,\n  children,\n}: {\n  provided: any;\n  snapshot: any;\n  children: ReactNode;\n}) {\n  // If not dragging, just render in place:\n  if (!snapshot.isDragging) {\n    return (\n      <div\n        ref={provided.innerRef}\n        {...provided.draggableProps}\n        style={provided.draggableProps.style as CSSProperties}\n      >\n        {children}\n      </div>\n    );\n  }\n\n  // If dragging, portal to document.body\n  return createPortal(\n    <div\n      ref={provided.innerRef}\n      {...provided.draggableProps}\n      style={provided.draggableProps.style as CSSProperties}\n    >\n      {children}\n    </div>,\n    document.body\n  );\n}\n\nexport default function MapLayersPanel() {\n  const isMapLayersPanelOpen = useMapDisplayStore(\n    (state) => state.isMapLayersPanelOpen\n  );\n  const toggleMapLayersPanel = useMapDisplayStore(\n    (state) => state.toggleMapLayersPanel\n  );\n\n  const mapLayers = useMapLayersStore((state) => state.mapLayers);\n  const toggleMapLayerVisibility = useMapLayersStore(\n    (state) => state.toggleMapLayerVisibility\n  );\n  const reorderLayers = useMapLayersStore((state) => state.reorderLayers);\n  const removeMapLayer = useMapLayersStore((state) => state.removeMapLayer);\n  const setZoomToLayerRequestWithGeometry = useZoomRequestStore(\n    (state) => state.setZoomToLayerRequestWithGeometry\n  );\n  const setSelectRasterLayer = useLayerSelectionStore(\n    (state) => state.setSelectRasterLayer\n  );\n  const isROIDrawingActive = useROIStore((state) => state.isROIDrawingActive);\n\n  const removeAttachedLayer = useAttachmentStore(\n    (state) => state.removeAttachment\n  );\n\n  // Color picker store\n  const { pickedColor, setPickedColor } = useColorPickerStore();\n\n  const [selectedLayer, setSelectedLayer] = useState(\"\");\n\n  // Track which legends are expanded\n  const [legendOpen, setLegendOpen] = useState<{\n    [layerName: string]: boolean;\n  }>({});\n\n  const toggleLegend = (layerName: string) => {\n    setLegendOpen((prev) => ({\n      ...prev,\n      [layerName]: !prev[layerName],\n    }));\n  };\n\n  // Toggle layer visibility\n  const handleToggleLayer = (layerName: string) => {\n    toggleMapLayerVisibility(layerName);\n  };\n\n  // DnD reorder\n  const handleDragEnd = (result: any) => {\n    if (!result.destination) return;\n    // This logic already accounts for the reversed UI display:\n    const sourceIndex = mapLayers.length - 1 - result.source.index;\n    const destinationIndex = mapLayers.length - 1 - result.destination.index;\n\n    const reordered = mapLayers.map((l) => l.name);\n    const [removed] = reordered.splice(sourceIndex, 1);\n    reordered.splice(destinationIndex, 0, removed);\n    reorderLayers(reordered);\n\n    const updatedMapLayers = useMapLayersStore.getState().mapLayers;\n    // Find the first (top-most) raster layer\n    const topRasterLayer = [...updatedMapLayers]\n      .reverse()\n      .find((layer) => layer.type === \"raster\");\n    // If we found a top raster layer, select it in the layer selection store\n    if (topRasterLayer) {\n      setSelectRasterLayer(topRasterLayer.name);\n    } else {\n      // optional: if you want to clear the selection if no raster layers remain\n      setSelectRasterLayer(\"\");\n    }\n  };\n\n  // Track selected row\n  const handleSelectLayer = useCallback((layerName: string) => {\n    setSelectedLayer(layerName);\n  }, []);\n\n  // Zoom\n  const handleZoomToLayer = (layerName: string) => {\n    setZoomToLayerRequestWithGeometry(layerName);\n  };\n\n  // Delete\n  const handleDeleteLayer = (layerName: string) => {\n    removeMapLayer(layerName);\n    const mapLayer = mapLayers.find((layer) => layer.name === layerName);\n    if (mapLayer?.type === \"roi\") {\n      removeAttachedLayer(layerName);\n    }\n  };\n\n  // Close the panel if the ROI drawing\n  useEffect(() => {\n    if (isROIDrawingActive && isMapLayersPanelOpen) {\n      toggleMapLayersPanel();\n    }\n  }, [isROIDrawingActive, isMapLayersPanelOpen]);\n\n  // Always select the top-most raster layer if it exists:\n  useEffect(() => {\n    const topRasterLayer = [...mapLayers]\n      .reverse()\n      .find((layer) => layer.type === \"raster\");\n\n    setSelectRasterLayer(topRasterLayer ? topRasterLayer.name : \"\");\n  }, [mapLayers, setSelectRasterLayer]);\n\n  return (\n    <div\n      className={cn(\n        \"absolute top-20 bottom-20 left-0 w-72 rounded-r-xl shadow-lg bg-secondary z-50 transition-transform duration-300\",\n        isMapLayersPanelOpen ? \"translate-x-0\" : \"-translate-x-full\"\n      )}\n    >\n      <div className=\"flex flex-col w-full h-full\">\n        {/* Panel Header */}\n        <div className=\"flex items-center justify-between p-4 border-b border-gray-200\">\n          <h2 className=\"text-sm font-semibold text-foreground\">Map Layers</h2>\n          <Button variant=\"ghost\" size=\"icon\" onClick={toggleMapLayersPanel}>\n            <ArrowLeft className=\"w-4 h-4\" />\n          </Button>\n        </div>\n\n        {/* Scrollable panel body */}\n        <div className=\"flex-1\">\n          <DragDropContext onDragEnd={handleDragEnd}>\n            <Droppable droppableId=\"layers\" direction=\"vertical\">\n              {(provided) => (\n                <div\n                  ref={provided.innerRef}\n                  {...provided.droppableProps}\n                  className=\"h-full\"\n                >\n                  {[...mapLayers].reverse().map((layer, index) => (\n                    <Draggable\n                      key={layer.id}\n                      draggableId={layer.name}\n                      index={index}\n                    >\n                      {(provided, snapshot) => {\n                        // The actual content you want to display:\n                        const content = (\n                          <div\n                            className={cn(\n                              \"relative group shadow-lg select-none transition-colors bg-secondary hover-bg-secondary/50\",\n                              snapshot.isDragging\n                                ? \"border-2 border-blue-500 shadow-lg\" // while dragging\n                                : \"border-b border-stone-300 dark:border-stone-600\"\n                            )}\n                          >\n                            <div className=\"px-3 py-3 flex flex-col gap-1\">\n                              {/* Row 1: Drag handle + Eye toggle + Name */}\n                              <div className=\"flex items-center gap-2 min-w-0\">\n                                <div\n                                  {...provided.dragHandleProps}\n                                  className=\"shrink-0 cursor-grab text-muted-foreground hover:text-foreground/80\"\n                                >\n                                  <GripVertical className=\"w-4 h-4\" />\n                                </div>\n\n                                <Button\n                                  onClick={() => handleToggleLayer(layer.name)}\n                                  variant=\"ghost\"\n                                  size=\"icon\"\n                                  className=\"shrink-0\"\n                                >\n                                  {layer.visible ? (\n                                    <Eye className=\"w-4 h-4 text-muted-foreground\" />\n                                  ) : (\n                                    <EyeOff className=\"w-4 h-4 text-muted-foreground\" />\n                                  )}\n                                </Button>\n\n                                <div\n                                  className=\"flex-1 cursor-pointer overflow-hidden break-words custom-scrollbar\"\n                                  onClick={() => handleSelectLayer(layer.name)}\n                                >\n                                  <span className=\"text-sm font-medium text-foreground\">\n                                    {layer.name}\n                                  </span>\n                                </div>\n                              </div>\n\n                              {/* Row 2: Zoom, Settings, Legend Toggle */}\n                              <div className=\"flex items-center gap-2 ml-11 transition-opacity relative\">\n                                {/* Zoom button */}\n                                <Button\n                                  variant=\"ghost\"\n                                  size=\"icon\"\n                                  onClick={() => handleZoomToLayer(layer.name)}\n                                >\n                                  <ZoomIn className=\"w-4 h-4 text-muted-foreground\" />\n                                </Button>\n\n                                {/* Settings Menu */}\n                                <DropdownMenu>\n                                  <DropdownMenuTrigger asChild>\n                                    <span\n                                      className={cn(\n                                        buttonVariants({\n                                          variant: \"ghost\",\n                                          size: \"icon\",\n                                        }),\n                                        \"cursor-pointer\"\n                                      )}\n                                    >\n                                      <Settings className=\"w-4 h-4 text-muted-foreground\" />\n                                    </span>\n                                  </DropdownMenuTrigger>\n\n                                  <DropdownMenuContent\n                                    align=\"end\"\n                                    side=\"bottom\"\n                                    sideOffset={8}\n                                    className=\"z-[9999] w-36\"\n                                  >\n                                    <DropdownMenuItem\n                                      onClick={() =>\n                                        handleDeleteLayer(layer.name)\n                                      }\n                                      className=\"text-red-600\"\n                                    >\n                                      <Trash2 className=\"h-4 w-4\" />\n                                      Delete\n                                    </DropdownMenuItem>\n                                    <DropdownMenuSeparator />\n\n                                    {layer.type !== \"raster\" && (\n                                      <DropdownMenuItem asChild>\n                                        <Popover>\n                                          <PopoverTrigger asChild>\n                                            <div\n                                              className={cn(\n                                                \"relative flex h-8 w-full select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none\",\n                                                \"hover:bg-accent/70 focus:bg-accent/70\",\n                                                \"focus:text-accent-foreground cursor-default\"\n                                              )}\n                                              onClick={() => {\n                                                const newColor =\n                                                  pickedColor.color ||\n                                                  \"#ffffff\";\n                                                setPickedColor(\n                                                  newColor,\n                                                  layer.name\n                                                );\n                                              }}\n                                            >\n                                              <Palette className=\"h-4 w-4 mr-2\" />\n                                              Pick Color\n                                            </div>\n                                          </PopoverTrigger>\n\n                                          <PopoverContent\n                                            side=\"right\"\n                                            align=\"start\"\n                                            sideOffset={8}\n                                            className=\"z-[9999] p-2 w-auto\"\n                                          >\n                                            <HexColorPicker\n                                              color={pickedColor.color}\n                                              onChange={(newColor) => {\n                                                if (pickedColor.layerName) {\n                                                  setPickedColor(\n                                                    newColor,\n                                                    pickedColor.layerName\n                                                  );\n                                                }\n                                              }}\n                                              className=\"min-w-[180px] max-w-[200px]\"\n                                            />\n                                          </PopoverContent>\n                                        </Popover>\n                                      </DropdownMenuItem>\n                                    )}\n                                  </DropdownMenuContent>\n                                </DropdownMenu>\n\n                                {/* Legend Toggle (Only for raster layers) */}\n                                {layer.type === \"raster\" && (\n                                  <button\n                                    onClick={() => toggleLegend(layer.name)}\n                                    className={cn(\n                                      buttonVariants({\n                                        variant: \"ghost\",\n                                        size: \"icon\",\n                                      }),\n                                      \"cursor-pointer flex items-center gap-1\"\n                                    )}\n                                  >\n                                    {legendOpen[layer.name] ? (\n                                      <ChevronUp className=\"w-4 h-4 text-muted-foreground\" />\n                                    ) : (\n                                      <ChevronDown className=\"w-4 h-4 text-muted-foreground\" />\n                                    )}\n                                  </button>\n                                )}\n                              </div>\n\n                              {/* Row 3: Legend if open (raster only) */}\n                              {layer.type === \"raster\" &&\n                                legendOpen[layer.name] && (\n                                  <div className=\"ml-11\">\n                                    <Legend\n                                      layerFunctionType={\n                                        layer.layerFunctionType || \"\"\n                                      }\n                                      layerName={layer.name}\n                                    />\n                                  </div>\n                                )}\n                            </div>\n                          </div>\n                        );\n\n                        // 2) Use DraggablePortal for your item\n                        return (\n                          <DraggablePortal\n                            provided={provided}\n                            snapshot={snapshot}\n                          >\n                            {content}\n                          </DraggablePortal>\n                        );\n                      }}\n                    </Draggable>\n                  ))}\n                  {provided.placeholder}\n                </div>\n              )}\n            </Droppable>\n          </DragDropContext>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "features/maps/components/map-panels/map-layers-panel/map-legend.tsx",
    "content": "import React from \"react\";\nimport chroma from \"chroma-js\";\nimport useMapLegendStore from \"../../../stores/use-map-legend-store\";\n\ninterface LegendProps {\n  layerFunctionType: string;\n  layerName: string;\n}\n\n// ---------------------------------\n// 1. Helper: Generate color gradient\n// ---------------------------------\nfunction generateColorGradient(min: number, max: number, palette: string[]) {\n  const scale = chroma.scale(palette).domain([min, max]);\n  const gradientColors = Array.from({ length: 100 }, (_, i) =>\n    scale(min + (i / 99) * (max - min)).hex()\n  );\n  return `linear-gradient(to right, ${gradientColors.join(\", \")})`;\n}\n\n// ---------------------------------\n// 2. Helper: Legend Container\n// ---------------------------------\ninterface LegendContainerProps {\n  title?: string;\n  children: React.ReactNode;\n}\n\nfunction LegendContainer({ title, children }: LegendContainerProps) {\n  return (\n    <div className=\"p-2 rounded-md\">\n      {title && <h4 className=\"mb-2 font-medium\">{title}</h4>}\n      {children}\n    </div>\n  );\n}\n\n// ---------------------------------\n// 3. Individual Legend Components\n// ---------------------------------\nconst LSTLegend: React.FC<{ min: number; max: number; palette: string[] }> = ({\n  min,\n  max,\n  palette,\n}) => {\n  const gradient = generateColorGradient(min, max, palette);\n\n  return (\n    <LegendContainer>\n      <div className=\"flex items-center justify-between\">\n        <span>{min.toFixed(2)}°C</span>\n        <div className=\"mx-2 flex-grow h-5\" style={{ background: gradient }} />\n        <span>{max.toFixed(2)}°C</span>\n      </div>\n    </LegendContainer>\n  );\n};\n\nconst LCZLegend: React.FC<{ labelNames: string[]; palette: string[] }> = ({\n  labelNames,\n  palette,\n}) => {\n  return (\n    <LegendContainer>\n      <ul>\n        {labelNames.map((className, index) => (\n          <li key={index} className=\"flex items-center mb-1\">\n            <div\n              className=\"w-4 h-4 rounded-full mr-2\"\n              style={{ backgroundColor: palette[index] }}\n            />\n            {className}\n          </li>\n        ))}\n      </ul>\n    </LegendContainer>\n  );\n};\n\nconst LCLULegend: React.FC<{ labelNames: string[]; palette: string[] }> = ({\n  labelNames,\n  palette,\n}) => {\n  return (\n    <LegendContainer>\n      <ul>\n        {labelNames.map((className, index) => (\n          <li key={index} className=\"flex items-center mb-1\">\n            <div\n              className=\"w-4 h-4 rounded-full mr-2\"\n              style={{ backgroundColor: palette[index] }}\n            />\n            {className}\n          </li>\n        ))}\n      </ul>\n    </LegendContainer>\n  );\n};\n\nconst CoastlineMapLegend: React.FC<{\n  labelNames: string[];\n  palette: string[];\n}> = ({ labelNames, palette }) => {\n  return (\n    <LegendContainer>\n      {labelNames.map((label, index) => (\n        <div key={index} className=\"flex items-center mb-1\">\n          <div\n            className=\"w-5 h-0 mr-2 border-t-4\"\n            style={{ borderColor: palette[index] }}\n          />\n          <span>{label}</span>\n        </div>\n      ))}\n    </LegendContainer>\n  );\n};\n\nconst AirPollutantsAnalysisMapLegend: React.FC<{\n  min: number;\n  max: number;\n  palette: string[];\n}> = ({ min, max, palette }) => {\n  const gradient = generateColorGradient(min, max, palette);\n\n  return (\n    <LegendContainer>\n      <div className=\"flex items-center justify-between\">\n        <span>Min</span>\n        <div className=\"mx-2 flex-grow h-5\" style={{ background: gradient }} />\n        <span>High</span>\n      </div>\n    </LegendContainer>\n  );\n};\n\nconst COLegend: React.FC<{ min: number; max: number; palette: string[] }> = ({\n  min,\n  max,\n  palette,\n}) => {\n  const gradient = generateColorGradient(min, max, palette);\n\n  return (\n    <LegendContainer title=\"mol/m²\">\n      <div className=\"flex items-center justify-between\">\n        <span>{min.toExponential(2)}</span>\n        <div className=\"mx-2 flex-grow h-5\" style={{ background: gradient }} />\n        <span>{max.toExponential(2)}</span>\n      </div>\n    </LegendContainer>\n  );\n};\n\nconst NO2Legend: React.FC<{ min: number; max: number; palette: string[] }> = ({\n  min,\n  max,\n  palette,\n}) => {\n  const gradient = generateColorGradient(min, max, palette);\n\n  return (\n    <LegendContainer title=\"mol/m²\">\n      <div className=\"flex items-center justify-between\">\n        <span>{min.toExponential(2)}</span>\n        <div className=\"mx-2 flex-grow h-5\" style={{ background: gradient }} />\n        <span>{max.toExponential(3)}</span>\n      </div>\n    </LegendContainer>\n  );\n};\n\nconst SusceptibilityMapLegend: React.FC<{\n  min: number;\n  max: number;\n  palette: string[];\n}> = ({ min, max, palette }) => {\n  const gradient = generateColorGradient(min, max, palette);\n\n  return (\n    <LegendContainer title=\"Vulnerability score (0-100)\">\n      <div className=\"flex items-center justify-between\">\n        <span>{min}</span>\n        <div className=\"mx-2 flex-grow h-5\" style={{ background: gradient }} />\n        <span>{max}</span>\n      </div>\n    </LegendContainer>\n  );\n};\n\nconst DefaultLegend: React.FC<{\n  min?: number;\n  max?: number;\n  palette?: string[];\n  labelNames?: string[];\n}> = ({ min, max, palette, labelNames }) => {\n  if (!min && !max && palette?.length === 0) {\n    return (\n      <LegendContainer title=\"Legend\">\n        <p className=\"text-sm text-gray-500\">\n          No visualization parameters available.\n        </p>\n      </LegendContainer>\n    );\n  }\n\n  const isValidColor = (color: string): boolean => {\n    const s = new Option().style;\n    s.color = color;\n    return s.color !== \"\";\n  };\n\n  // If labelNames exist and contain more than one element, render a discrete legend\n  if (labelNames && (labelNames.length > 1 || palette?.length === 0)) {\n    return (\n      <LegendContainer title=\"Legend\">\n        <ul>\n          {labelNames.map((label, index) => {\n            const color = isValidColor(label) ? label : \"#ccc\"; // Fallback to default if invalid\n\n            return (\n              <li key={index} className=\"flex items-center mb-1\">\n                <div\n                  className=\"w-4 h-4 rounded-full mr-2\"\n                  style={{ backgroundColor: color }}\n                />\n                {label}\n              </li>\n            );\n          })}\n        </ul>\n      </LegendContainer>\n    );\n  }\n\n  // Otherwise, show a continuous color gradient\n  const gradient =\n    min !== undefined && max !== undefined\n      ? generateColorGradient(min, max, palette || [])\n      : `linear-gradient(to right, ${palette?.join(\", \")})`;\n\n  return (\n    <LegendContainer>\n      <div className=\"flex items-center justify-between\">\n        {min !== undefined && <span>{min}</span>}\n        <div className=\"mx-2 flex-grow h-5\" style={{ background: gradient }} />\n        {max !== undefined && <span>{max}</span>}\n      </div>\n    </LegendContainer>\n  );\n};\n\n// ---------------------------------\n// 4. Main Legend Switch\n// ---------------------------------\nconst Legend: React.FC<LegendProps> = ({ layerFunctionType, layerName }) => {\n  const legend = useMapLegendStore((state) => state.getLegend(layerName));\n\n  // If no legend config is found, show nothing (or show DefaultLegend)\n  if (!legend) return null;\n\n  const { min, max, palette, labelNames } = legend.config;\n\n  if (\n    layerFunctionType === \"Urban Heat Island (UHI) Analysis\" &&\n    min !== undefined &&\n    max !== undefined &&\n    palette\n  ) {\n    return <LSTLegend min={min} max={max} palette={palette} />;\n  }\n\n  if (\n    layerFunctionType === \"Local Climate Zone (LCZ) Maps\" &&\n    labelNames &&\n    palette\n  ) {\n    return <LCZLegend labelNames={labelNames} palette={palette} />;\n  }\n\n  if (\n    layerFunctionType === \"Bi-Temporal Coastline Analysis\" &&\n    labelNames &&\n    palette\n  ) {\n    return <CoastlineMapLegend labelNames={labelNames} palette={palette} />;\n  }\n\n  if (\n    (layerFunctionType === \"Land Use/Land Cover Maps\" ||\n      layerFunctionType === \"Land Use/Land Cover Change Maps\") &&\n    labelNames &&\n    palette\n  ) {\n    return <LCLULegend labelNames={labelNames} palette={palette} />;\n  }\n\n  if (\n    layerFunctionType === \"CO Emissions Analysis\" &&\n    min !== undefined &&\n    max !== undefined &&\n    palette\n  ) {\n    return <COLegend min={min} max={max} palette={palette} />;\n  }\n\n  if (\n    layerFunctionType === \"NO2 Emissions Analysis\" &&\n    min !== undefined &&\n    max !== undefined &&\n    palette\n  ) {\n    return <NO2Legend min={min} max={max} palette={palette} />;\n  }\n\n  if (\n    layerFunctionType === \"Air Pollutants Analysis\" &&\n    min !== undefined &&\n    max !== undefined &&\n    palette\n  ) {\n    return (\n      <AirPollutantsAnalysisMapLegend min={min} max={max} palette={palette} />\n    );\n  }\n\n  if (\n    layerFunctionType === \"Vulnerability Map Builder\" &&\n    min !== undefined &&\n    max !== undefined &&\n    palette\n  ) {\n    return <SusceptibilityMapLegend min={min} max={max} palette={palette} />;\n  }\n\n  // Fallback if no match\n  return (\n    <DefaultLegend\n      min={legend?.config.min}\n      max={legend?.config.max}\n      palette={legend?.config.palette}\n      labelNames={legend?.config.labelNames}\n    />\n  );\n};\n\nexport default Legend;\n"
  },
  {
    "path": "features/maps/hooks/use-handle-click/use-handle-click.ts",
    "content": "\"use client\";\nimport { useState } from \"react\";\nimport useMapLayersStore from \"../../stores/use-map-layer-store\";\nimport useMapControls from \"./use-map-controls\";\nimport useRoiDrawing from \"./use-roi-drawing\";\nimport useQueryDrawing from \"./use-query-drawing\";\nimport useRemoveQueryFeatures from \"./use-remove-query-features\";\nimport useBadgeStore from \"../../stores/use-map-badge-store\";\n\n/**\n * The main hook that orchestrates all the smaller hooks.\n * It references the global map from your store, then\n * passes it to each specialized hook.\n */\nexport default function useHandleClick() {\n  const setSecondaryMapBadgeText = useBadgeStore(\n    (state) => state.setSecondaryText\n  );\n\n  // 1) Get the Map instance from store\n  const map = useMapLayersStore((state) => state.mapCurrent);\n\n  // 2) Set up map controls & return drawRef\n  const { drawRef } = useMapControls(map);\n\n  // 3) ROI drawing logic (all in one file)\n  useRoiDrawing({ map, drawRef, setSecondaryMapBadgeText });\n\n  // 4) Query logic for polygons + points\n  useQueryDrawing({ map, drawRef });\n\n  // 5) Removal of query features\n  useRemoveQueryFeatures(map);\n\n  // Optional: return anything if needed\n  return null;\n}\n"
  },
  {
    "path": "features/maps/hooks/use-handle-click/use-map-controls.ts",
    "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport maplibregl, { Map } from \"maplibre-gl\";\nimport MapboxDraw from \"@mapbox/mapbox-gl-draw\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport useCursorStore from \"@/features/maps/stores/use-cursor-store\";\n\n/**\n * Sets up the map controls (navigation, draw) and handles mode changes\n * for the draw tools (point, polygon, etc.).\n */\nexport default function useMapControls(map: Map | null) {\n  const drawRef = useRef<MapboxDraw | null>(null);\n\n  const activeDrawingMode = useButtonsStore((state) => state.activeDrawingMode);\n  const setActiveDrawingMode = useButtonsStore((state) => state.setDrawingMode);\n\n  const { setCursorState } = useCursorStore();\n\n  // Add navigation control & initialize MapboxDraw\n  useEffect(() => {\n    if (map && !drawRef.current) {\n      map.addControl(\n        new maplibregl.NavigationControl({\n          showCompass: true,\n          showZoom: true,\n          visualizePitch: true,\n        }),\n        \"bottom-right\"\n      );\n\n      // Initialize MapboxDraw\n      drawRef.current = new MapboxDraw({\n        displayControlsDefault: true,\n      });\n      // @ts-ignore\n      map.addControl(drawRef.current);\n\n      // If you want to handle \"draw.modechange\" globally:\n      map.on(\"draw.modechange\", (e: any) => {\n        if (e.mode === \"draw_point\") {\n          setActiveDrawingMode(e.mode);\n          setCursorState(\"pointer\");\n        } else if (e.mode === \"draw_polygon\") {\n          setActiveDrawingMode(e.mode);\n          setCursorState(\"crosshair\");\n        } else {\n          setCursorState(\"default\");\n        }\n      });\n    }\n  }, [map]);\n\n  // React to changes in the store’s activeDrawingMode\n  useEffect(() => {\n    if (!map || !drawRef.current) return;\n\n    if (activeDrawingMode === \"draw_point\") {\n      drawRef.current.changeMode(\"draw_point\");\n      setCursorState(\"pointer\");\n    } else if (activeDrawingMode === \"draw_polygon\") {\n      drawRef.current.changeMode(\"draw_polygon\");\n      setCursorState(\"crosshair\");\n    } else {\n      drawRef.current.changeMode(\"simple_select\");\n      setCursorState(\"default\");\n    }\n  }, [map, activeDrawingMode]);\n\n  return { drawRef };\n}\n"
  },
  {
    "path": "features/maps/hooks/use-handle-click/use-query-drawing.ts",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Map } from \"maplibre-gl\";\nimport maplibregl from \"maplibre-gl\";\nimport MapboxDraw from \"@mapbox/mapbox-gl-draw\";\nimport * as turf from \"@turf/turf\";\n\nimport useLoadingStore from \"@/stores/use-loading-store\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\n\nimport useQueryStore from \"@/features/maps/stores/map-queries-stores/useQueryReadyStore\";\nimport useDrawnFeatureOnMapStore from \"@/features/maps/stores/use-drawn-feature-on-map-store\";\nimport { addDrawnLayerToMap } from \"@/features/maps/utils/add-drawn-layer-to-map\";\nimport { checkGeometryIntersection } from \"@/features/maps/utils/geometry-utils\";\nimport projectConfigs from \"@/custom-configs/project-config\";\n\nimport { extractValuesFromGeeMap } from \"@/lib/geospatial/gee/extract-values-from-gee-layer/extract-values-from-gee-layer\";\nimport { dateToString } from \"@/utils/general/general-utils\";\nimport { calculateGeometryArea } from \"@/features/maps/utils/geometry-utils\";\n\nimport useFunctionStore from \"@/features/maps/stores/use-function-store\";\nimport useLayerSelectionStore from \"@/features/maps/stores/use-layer-selection-store\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport usePlotReadyDataFromVectorLayerStore from \"@/features/maps/stores/plots-stores/usePlotReadyFromVectorLayerStore\";\nimport { useGeeOutputStore } from \"../../stores/use-gee-ouput-store\";\nimport useROIStore from \"../../stores/use-roi-store\";\nimport { generateUUID } from \"@/features/chat/utils/general-utils\";\nimport { formatQueryForTable } from \"../../utils/other-utils\";\n\ninterface UseQueryDrawingProps {\n  map: Map | null;\n  drawRef: React.RefObject<MapboxDraw | null>;\n}\n\n/**\n * Consolidates the logic for BOTH query polygon drawing and query point drawing.\n */\nexport default function useQueryDrawing({\n  map,\n  drawRef,\n}: UseQueryDrawingProps) {\n  const { setLoading } = useLoadingStore();\n  const { setToastMessage } = useToastMessageStore();\n  const { queryReady, setQueryReady } = useQueryStore();\n  const { addDrawnFeatureOnMap } = useDrawnFeatureOnMapStore();\n\n  const { getFunctionConfig, getFunctionTypeByLayerName } = useFunctionStore();\n  const { selectedRasterLayer } = useLayerSelectionStore();\n\n  const setPlotReadyDataForSelectedAreaOnMap =\n    usePlotReadyDataFromVectorLayerStore(\n      (state) => state.setPlotReadyDataForSelectedAreaOnMap\n    );\n\n  const getTempCreatedMapInAssetsPath = useGeeOutputStore(\n    (state) => state.getTempCreatedMapInAssetsPath\n  );\n  const activeDrawingMode = useButtonsStore((state) => state.activeDrawingMode);\n  const setActiveDrawingMode = useButtonsStore((state) => state.setDrawingMode);\n\n  const isROIDrawingActive = useROIStore((state) => state.isROIDrawingActive);\n\n  /**\n   * (A) Handle query polygon drawing\n   */\n  useEffect(() => {\n    // If ROI drawing is active or not in polygon draw mode, skip\n    if (\n      !map ||\n      !drawRef.current ||\n      isROIDrawingActive ||\n      activeDrawingMode !== \"draw_polygon\"\n    )\n      return;\n\n    const handlePolygonDrawCreate = async (event: any) => {\n      setLoading(true);\n      setQueryReady(false);\n\n      const polygonFeature = event.features[0];\n      const geometry = polygonFeature.geometry;\n\n      try {\n        const config = getFunctionConfig(selectedRasterLayer.layerName);\n\n        // 1) Validate geometry intersection with ROI or default boundary\n        const useSelectedRoi = config?.selectedRoiGeometry; // object or null\n        if (!useSelectedRoi) {\n          drawRef.current?.deleteAll();\n          return;\n        }\n        const intersectionOK = checkGeometryIntersection(\n          geometry,\n          useSelectedRoi\n        );\n        if (!intersectionOK) {\n          setToastMessage(\"Selected area is outside the boundary.\", \"error\");\n          drawRef.current?.deleteAll();\n          setLoading(false);\n          return;\n        }\n\n        // 3) Call GEE to extract values\n        const outputsFromMap = await extractValuesFromGeeMap({\n          functionType: config?.functionType!,\n          tempCreatedMapInAsset: getTempCreatedMapInAssetsPath(\n            selectedRasterLayer.layerName\n          ),\n          analysisOptions: config?.multiAnalysisOptions!,\n          aggregationMethod: config?.aggregationMethod!,\n          geojsonFeature: geometry,\n          startDate1:\n            typeof config?.startDate === \"string\"\n              ? config.startDate\n              : dateToString(config?.startDate!),\n          endDate1:\n            typeof config?.endDate === \"string\"\n              ? config.endDate\n              : dateToString(config?.endDate!),\n          startDate2:\n            config?.startDate2 && typeof config?.startDate2 === \"string\"\n              ? config.startDate2\n              : dateToString(config?.startDate2!),\n          endDate2:\n            config?.endDate2 && typeof config?.endDate2 === \"string\"\n              ? config.endDate2\n              : dateToString(config?.endDate2!),\n        });\n\n        const description = `<strong>Layer</strong>: ${\n          selectedRasterLayer.layerName\n        }<br>\n        <strong>Query result</strong>: ${formatQueryForTable(outputsFromMap)}`;\n\n        // Build a valid GeoJSON FeatureCollection\n        const geojsonData = {\n          type: \"FeatureCollection\",\n          features: [\n            {\n              type: \"Feature\",\n              geometry: geometry,\n              properties: {\n                description,\n              },\n            },\n          ],\n        };\n        const featureId = `query_${generateUUID()}`; // or a random ID\n        // 2) Add geometry as a drawn layer on map\n        addDrawnLayerToMap(map, geojsonData, featureId);\n\n        // 5) Store result in plot store\n        setPlotReadyDataForSelectedAreaOnMap(\n          {\n            outputsFromMap,\n            vectorLayerName: \"\",\n            selectedFeature: \"\",\n          },\n          config?.functionType || \"\"\n        );\n\n        // 3) Store in \"drawnFeaturesOnMap\"\n        addDrawnFeatureOnMap({\n          featureLayerName: featureId,\n          geometry: \"Polygon\",\n          coordinates: geometry.coordinates,\n          rasterLayerName: selectedRasterLayer.layerName,\n          query: outputsFromMap,\n        });\n\n        // Clear from MapboxDraw’s memory\n        drawRef.current?.deleteAll();\n\n        setQueryReady(true);\n      } catch (error) {\n        console.error(error);\n      } finally {\n        // Return to simple_select mode\n        setActiveDrawingMode(\"simple_select\");\n        setLoading(false);\n      }\n    };\n\n    map.on(\"draw.create\", handlePolygonDrawCreate);\n\n    return () => {\n      map.off(\"draw.create\", handlePolygonDrawCreate);\n    };\n  }, [map, drawRef, activeDrawingMode]);\n\n  /**\n   * (B) Handle query point drawing\n   *    In your existing code, you’re using `map.on(\"click\", ...)` approach for points\n   */\n  useEffect(() => {\n    // If ROI drawing is active or not in point draw mode, skip\n    if (!map || activeDrawingMode !== \"draw_point\" || isROIDrawingActive)\n      return;\n\n    const handleClick = async (event: maplibregl.MapMouseEvent) => {\n      setLoading(true);\n      setQueryReady(false);\n\n      const { lng: lon, lat } = event.lngLat;\n      const geojsonCoords = turf.point([lon, lat]).geometry;\n\n      try {\n        const config = getFunctionConfig(selectedRasterLayer.layerName);\n        const useSelectedRoi = config?.selectedRoiGeometry;\n\n        if (!useSelectedRoi) {\n          drawRef.current?.deleteAll();\n          return;\n        }\n\n        // 1) Validate geometry intersection with ROI or default boundary\n        const intersectionOK = checkGeometryIntersection(\n          geojsonCoords,\n          useSelectedRoi\n        );\n        if (!intersectionOK) {\n          setToastMessage(\"Selected area is outside the boundary.\", \"error\");\n          drawRef.current?.deleteAll();\n          setQueryReady(true);\n          setLoading(false);\n          return;\n        }\n\n        // 2) Extract values from GEE\n        const outputsFromMap = await extractValuesFromGeeMap({\n          functionType: config?.functionType!,\n          aggregationMethod: config?.aggregationMethod!,\n          analysisOptions: config?.multiAnalysisOptions!,\n          tempCreatedMapInAsset: getTempCreatedMapInAssetsPath(\n            selectedRasterLayer.layerName\n          ),\n          geojsonFeature: geojsonCoords,\n          startDate1:\n            typeof config?.startDate === \"string\"\n              ? config.startDate\n              : dateToString(config?.startDate!),\n          endDate1:\n            typeof config?.endDate === \"string\"\n              ? config.endDate\n              : dateToString(config?.endDate!),\n          startDate2:\n            config?.startDate2 && typeof config?.startDate2 === \"string\"\n              ? config.startDate2\n              : dateToString(config?.startDate2!),\n          endDate2:\n            config?.endDate2 && typeof config?.endDate2 === \"string\"\n              ? config.endDate2\n              : dateToString(config?.endDate2!),\n        });\n\n        const description = `<strong>Layer</strong>: ${\n          selectedRasterLayer.layerName\n        }<br>\n        <strong>Query result</strong>: ${formatQueryForTable(outputsFromMap)}`;\n\n        // Build a valid GeoJSON FeatureCollection\n        const geojsonData = {\n          type: \"FeatureCollection\",\n          features: [\n            {\n              type: \"Feature\",\n              geometry: geojsonCoords,\n              properties: {\n                description,\n              },\n            },\n          ],\n        };\n\n        const pointFeatureId = `query_${generateUUID()}`; // or a random ID\n        addDrawnLayerToMap(map, geojsonData, pointFeatureId);\n\n        // 3) Store result\n        setPlotReadyDataForSelectedAreaOnMap(\n          {\n            outputsFromMap,\n            vectorLayerName: \"\",\n            selectedFeature: \"\",\n          },\n          config?.functionType || \"\"\n        );\n\n        // 4) Store as a drawn feature\n        addDrawnFeatureOnMap({\n          featureLayerName: pointFeatureId,\n          geometry: \"Point\",\n          lat,\n          lon,\n          rasterLayerName: selectedRasterLayer.layerName,\n          query: outputsFromMap,\n        });\n\n        drawRef.current?.deleteAll();\n\n        setQueryReady(true);\n      } catch (error) {\n        console.error(error);\n      } finally {\n        // Return to simple_select mode\n        setActiveDrawingMode(\"simple_select\");\n        setLoading(false);\n      }\n    };\n\n    map.on(\"click\", handleClick);\n\n    return () => {\n      map.off(\"click\", handleClick);\n    };\n  }, [map, activeDrawingMode]);\n\n  return null; // No return needed unless you want to expose state\n}\n"
  },
  {
    "path": "features/maps/hooks/use-handle-click/use-remove-query-features.ts",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Map } from \"maplibre-gl\";\nimport useTableStore from \"../../stores/use-table-store\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\n\n/**\n * Handles removing query features from the map if triggered by the store\n * (for example, user selects a row in a table to delete).\n */\nexport default function useRemoveQueryFeatures(map: Map | null) {\n  const selectedFeatureToDelete = useTableStore(\n    (state) => state.selectedFeatureToDelete\n  );\n  const { setToastMessage } = useToastMessageStore();\n\n  useEffect(() => {\n    if (!map || !selectedFeatureToDelete) return;\n\n    const featureLayerName = selectedFeatureToDelete.featureLayerName;\n\n    // Remove the main layer\n    if (map.getLayer(featureLayerName)) {\n      map.removeLayer(featureLayerName);\n    }\n\n    // Remove border layer if you have one\n    const borderLayerName = `${featureLayerName}-border`;\n    if (map.getLayer(borderLayerName)) {\n      map.removeLayer(borderLayerName);\n    }\n\n    // Remove the source\n    if (map.getSource(featureLayerName)) {\n      map.removeSource(featureLayerName);\n    }\n\n    setToastMessage(\"Selected feature removed.\", \"success\");\n  }, [selectedFeatureToDelete, map]);\n}\n"
  },
  {
    "path": "features/maps/hooks/use-handle-click/use-roi-drawing.ts",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Map } from \"maplibre-gl\";\nimport MapboxDraw from \"@mapbox/mapbox-gl-draw\";\n\nimport useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport useMapLayersStore from \"@/features/maps/stores/use-map-layer-store\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\nimport { generateUUID } from \"@/features/chat/utils/general-utils\";\nimport { calculateGeometryArea } from \"../../utils/geometry-utils\";\nimport { addRoiLayerToMap } from \"../../utils/add-roi-layer-to-map\";\n\ninterface UseRoiDrawingProps {\n  map: Map | null;\n  drawRef: React.RefObject<MapboxDraw | null>;\n  setSecondaryMapBadgeText: (size: string) => void;\n}\n\n/**\n * Handles all ROI-related logic:\n *  - Polygon creation\n *  - Finalizing the ROI\n *  - Resetting ROI on sidebar close\n */\nexport default function useRoiDrawing({\n  map,\n  drawRef,\n  setSecondaryMapBadgeText,\n}: UseRoiDrawingProps) {\n  const [finalizedRoiGeometry, setFinalizedRoiGeometry] = useState<any | null>(\n    null\n  );\n\n  const isROIDrawingActive = useROIStore((state) => state.isROIDrawingActive);\n  const setIsROIDrawingActive = useROIStore(\n    (state) => state.setIsROIDrawingActive\n  );\n  const isRoiCreated = useROIStore((state) => state.isRoiCreated);\n  const setIsRoiCreated = useROIStore((state) => state.setIsRoiCreated);\n\n  const isRoiFinalized = useROIStore((state) => state.isRoiFinalized);\n  const setIsRoiFinalized = useROIStore((state) => state.setIsRoiFinalized);\n\n  const addRoiFeatureId = useROIStore((state) => state.addRoiFeatureId);\n  const addRoiGeometry = useROIStore((state) => state.addRoiGeometry);\n\n  const { addMapLayer } = useMapLayersStore();\n  const { setToastMessage } = useToastMessageStore();\n  const isArtifactSidebarOpen = useButtonsStore(\n    (state) => state.isArtifactsSidebarOpen\n  );\n  const setActiveDrawingMode = useButtonsStore((state) => state.setDrawingMode);\n\n  /**\n   * (A) Handle creation of an ROI polygon\n   */\n  useEffect(() => {\n    const handlePolygonDrawCreate = (e: any) => {\n      const polygonFeature = e.features[0];\n      const geometry = polygonFeature.geometry;\n\n      if (!isROIDrawingActive) return;\n\n      // Mark ROI property\n      polygonFeature.properties.isROI = true;\n\n      // Store the feature ID\n      const featureId = polygonFeature.id;\n      addRoiFeatureId(featureId);\n\n      setIsRoiCreated(true);\n      // Remove from MapboxDraw\n      drawRef.current?.delete([featureId]);\n\n      // Temporarily show geometry on the map\n      addRoiLayerToMap(map!, geometry, \"drawnPolygon\");\n      setFinalizedRoiGeometry(geometry);\n\n      setSecondaryMapBadgeText(\n        `Size: ${calculateGeometryArea(geometry).toFixed(2)} km²`\n      );\n\n      // Switch mode to simple_select so user doesn’t keep drawing\n      setActiveDrawingMode(\"simple_select\");\n    };\n\n    // Handle \"Esc\" to abort ROI drawing\n    const handleEscapePress = (e: KeyboardEvent) => {\n      if (e.key !== \"Escape\") {\n        return;\n      }\n\n      if (map) {\n        if (map.getLayer(\"drawnPolygon\")) {\n          map.removeLayer(\"drawnPolygon\");\n        }\n        if (map.getLayer(\"drawnPolygon-border\")) {\n          map.removeLayer(\"drawnPolygon-border\");\n        }\n        if (map.getSource(\"drawnPolygon\")) {\n          map.removeSource(\"drawnPolygon\");\n        }\n      }\n\n      if (!isROIDrawingActive) return;\n\n      // Reset ROI states\n      setIsRoiCreated(false);\n      setIsRoiFinalized(false, \"\");\n      setFinalizedRoiGeometry(null);\n      setActiveDrawingMode(\"simple_select\");\n      setSecondaryMapBadgeText(\"\");\n\n      drawRef.current?.deleteAll();\n    };\n\n    if (map && drawRef.current) {\n      map.on(\"draw.create\", handlePolygonDrawCreate);\n      window.addEventListener(\"keydown\", handleEscapePress);\n    }\n\n    return () => {\n      if (map && drawRef.current) {\n        map.off(\"draw.create\", handlePolygonDrawCreate);\n      }\n      window.removeEventListener(\"keydown\", handleEscapePress);\n    };\n  }, [map, drawRef, isROIDrawingActive]);\n\n  /**\n   * (B) Finalize the ROI geometry\n   */\n  useEffect(() => {\n    if (!isRoiFinalized.finalized || !finalizedRoiGeometry || !map) return;\n\n    const roiName = isRoiFinalized.name;\n\n    // Add ROI geometry to the store\n    addRoiGeometry({\n      id: generateUUID(),\n      geometry: finalizedRoiGeometry,\n      name: roiName,\n      source: \"drawn\",\n    });\n\n    // Add ROI layer to the map\n    addMapLayer({\n      id: generateUUID(),\n      name: roiName,\n      visible: true,\n      type: \"roi\",\n      layerOpacity: 1,\n      roiName: null,\n    });\n\n    // Show final ROI layer (and remove the temporary drawnPolygon)\n    addRoiLayerToMap(map, finalizedRoiGeometry, `${roiName}_roi`);\n    if (map) {\n      if (map.getLayer(\"drawnPolygon\")) {\n        map.removeLayer(\"drawnPolygon\");\n      }\n      if (map.getLayer(\"drawnPolygon-border\")) {\n        map.removeLayer(\"drawnPolygon-border\");\n      }\n      if (map.getSource(\"drawnPolygon\")) {\n        map.removeSource(\"drawnPolygon\");\n      }\n    }\n\n    setToastMessage(`ROI \"${roiName}\" created successfully.`, \"success\");\n    setIsRoiFinalized(true, \"\");\n    setFinalizedRoiGeometry(null);\n    setIsROIDrawingActive(false);\n    setIsRoiCreated(false);\n  }, [isRoiFinalized, map]);\n\n  /**\n   * (C) Reset ROI if artifact sidebar closes while ROI is still incomplete\n   */\n  useEffect(() => {\n    if (isArtifactSidebarOpen || !isRoiCreated || isRoiFinalized.finalized)\n      return;\n\n    // If the sidebar closed, and ROI wasn't finalized, reset it\n    setIsRoiCreated(false);\n    setIsROIDrawingActive(false);\n    setFinalizedRoiGeometry(null);\n    setIsRoiFinalized(false, \"\");\n    setSecondaryMapBadgeText(\"\");\n\n    if (map?.getLayer(\"drawnPolygon\")) {\n      map.removeLayer(\"drawnPolygon\");\n      map.removeLayer(\"drawnPolygon-border\");\n    }\n  }, [isArtifactSidebarOpen, isRoiCreated, isRoiFinalized]);\n\n  return null; // No return needed, unless you want to expose state\n}\n"
  },
  {
    "path": "features/maps/hooks/use-map/use-add-arcgis-layers.ts",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Map } from \"maplibre-gl\";\nimport { useAgolLayersStore } from \"../../stores/use-agol-layers-store\";\nimport useMapLayersStore from \"../../stores/use-map-layer-store\";\nimport useROIStore from \"../../stores/use-roi-store\";\nimport { generateUUID } from \"@/features/chat/utils/general-utils\";\nimport { addRoiLayerToMap } from \"../../utils/add-roi-layer-to-map\";\n\nexport default function useAddArcGisLayers(mapInstance: Map | null) {\n  const agolLayerRequestedToImport = useAgolLayersStore(\n    (state) => state.agolLayerRequestedToImport\n  );\n  const mapLoaded = useMapLayersStore((state) => state.mapLoaded);\n  const addMapLayer = useMapLayersStore((state) => state.addMapLayer);\n  const roiGeometries = useROIStore((state) => state.roiGeometries);\n\n  useEffect(() => {\n    if (!mapInstance || !mapLoaded || !agolLayerRequestedToImport) return;\n\n    const layerName = agolLayerRequestedToImport.name;\n    const geojson = roiGeometries.find(\n      (roi) => roi.name === layerName && roi.source === \"arcgis\"\n    )?.geometry;\n\n    if (geojson) {\n      addRoiLayerToMap(mapInstance, geojson, `${layerName}_roi`);\n      addMapLayer({\n        id: generateUUID(),\n        name: layerName,\n        visible: true,\n        type: \"roi\",\n        roiName: null,\n      });\n      return;\n    }\n  }, [mapInstance, mapLoaded, agolLayerRequestedToImport]);\n}\n"
  },
  {
    "path": "features/maps/hooks/use-map/use-add-attached-layers.ts",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Map } from \"maplibre-gl\";\nimport useMapLayersStore from \"../../stores/use-map-layer-store\";\nimport useROIStore from \"../../stores/use-roi-store\";\nimport { generateUUID } from \"@/features/chat/utils/general-utils\";\nimport { addRoiLayerToMap } from \"../../utils/add-roi-layer-to-map\";\n\nexport default function useAddAttachedLayers(mapInstance: Map | null) {\n  const addRoiGeometry = useROIStore((state) => state.addRoiGeometry);\n  const newAttachedRoi = useROIStore((state) => state.newAttachedRoi);\n  const mapLoaded = useMapLayersStore((state) => state.mapLoaded);\n  const addMapLayer = useMapLayersStore((state) => state.addMapLayer);\n\n  useEffect(() => {\n    if (!mapInstance || !mapLoaded || !newAttachedRoi) return;\n    const { geometry: geojson, name: layerName } = newAttachedRoi;\n\n    addRoiLayerToMap(mapInstance, geojson, `${layerName}_roi`);\n\n    addRoiGeometry({\n      id: generateUUID(),\n      geometry: geojson,\n      name: layerName,\n      source: \"attached\",\n    });\n\n    addMapLayer({\n      id: generateUUID(),\n      name: layerName,\n      visible: true,\n      type: \"roi\",\n      roiName: null,\n    });\n  }, [mapInstance, mapLoaded, newAttachedRoi]);\n}\n"
  },
  {
    "path": "features/maps/hooks/use-map/use-add-gee-layers.ts",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Map } from \"maplibre-gl\";\nimport { useGeeOutputStore } from \"@/features/maps/stores/use-gee-ouput-store\";\nimport { addGeeLayerToMap } from \"@/features/maps/utils/add-gee-layer-to-map\";\nimport useMapLayersStore from \"../../stores/use-map-layer-store\";\n\nexport default function useAddGeeLayers(mapInstance: Map | null) {\n  const { geeLayersList } = useGeeOutputStore();\n  const mapLoaded = useMapLayersStore((state) => state.mapLoaded);\n\n  useEffect(() => {\n    if (!mapInstance || !mapLoaded || geeLayersList.length === 0) return;\n\n    const currentMapLayers = mapInstance.getStyle().layers || [];\n    const currentMapLayerIds = currentMapLayers.map((layer) => layer.id);\n\n    geeLayersList.forEach((geeLayer) => {\n      const { urlFormat, layerName } = geeLayer;\n\n      // Only add the layer if it has a urlFormat and isn't already on the map\n      if (urlFormat && !currentMapLayerIds.includes(layerName)) {\n        addGeeLayerToMap(mapInstance, urlFormat, layerName, layerName);\n      }\n    });\n  }, [mapInstance, mapLoaded, geeLayersList]);\n}\n"
  },
  {
    "path": "features/maps/hooks/use-map/use-add-roi-from-session.ts",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Map } from \"maplibre-gl\";\nimport useMapLayersStore from \"../../stores/use-map-layer-store\";\nimport useROIStore from \"../../stores/use-roi-store\";\nimport { addRoiLayerToMap } from \"../../utils/add-roi-layer-to-map\";\n\nexport default function useAddRoiFromSession(mapInstance: Map | null) {\n  const mapLoaded = useMapLayersStore((state) => state.mapLoaded);\n  const roiGeometryFromSessionHistory = useROIStore(\n    (state) => state.roiGeometryFromSessionHistory\n  );\n  const setRoiGeometryFromSessionHistory = useROIStore(\n    (state) => state.setRoiGeometryFromSessionHistory\n  );\n\n  useEffect(() => {\n    if (mapInstance && mapLoaded && roiGeometryFromSessionHistory) {\n      const { geometry, name } = roiGeometryFromSessionHistory;\n      const roiLayerName = `${name}_roi`;\n\n      addRoiLayerToMap(mapInstance, geometry, roiLayerName);\n      setRoiGeometryFromSessionHistory(null);\n    }\n  }, [mapInstance, mapLoaded, roiGeometryFromSessionHistory]);\n}\n"
  },
  {
    "path": "features/maps/hooks/use-map/use-basemap-toggle.ts",
    "content": "\"use client\";\n\nimport { useEffect, RefObject } from \"react\";\nimport { Map, AttributionControl } from \"maplibre-gl\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport useMapLayersStore from \"../../stores/use-map-layer-store\";\nimport useMapControls from \"../use-handle-click/use-map-controls\";\n\ninterface UseBasemapToggleParams {\n  mapInstance: Map | null;\n  googleSatelliteAttributionRef: RefObject<AttributionControl | null>;\n  osmAttributionRef: RefObject<AttributionControl | null>;\n  googleAttributionRef?: RefObject<AttributionControl | null>;\n}\n\nexport default function useBasemapToggle({\n  mapInstance,\n  googleSatelliteAttributionRef,\n  osmAttributionRef,\n  googleAttributionRef,\n}: UseBasemapToggleParams) {\n  // Subscribe to the store's current basemap\n  const activeBasemap = useButtonsStore((state) => state.activeBasemap);\n  const mapLoaded = useMapLayersStore((state) => state.mapLoaded);\n\n  useEffect(() => {\n    if (!mapInstance || !mapLoaded) return;\n\n    if (activeBasemap === \"satellite\") {\n      mapInstance.setLayoutProperty(\n        \"googleSatelliteImagery\",\n        \"visibility\",\n        \"visible\"\n      );\n      mapInstance.setLayoutProperty(\"googleRoadmap\", \"visibility\", \"none\");\n\n      if (googleSatelliteAttributionRef.current) {\n        googleSatelliteAttributionRef.current._container.style.display =\n          \"block\";\n      }\n      if (osmAttributionRef.current) {\n        osmAttributionRef.current._container.style.display = \"none\";\n      }\n      // googleAttributionRef?.current?._container.style.display = \"block\";\n    } else {\n      mapInstance.setLayoutProperty(\n        \"googleSatelliteImagery\",\n        \"visibility\",\n        \"none\"\n      );\n      mapInstance.setLayoutProperty(\"googleRoadmap\", \"visibility\", \"visible\");\n\n      if (googleSatelliteAttributionRef.current) {\n        googleSatelliteAttributionRef.current._container.style.display = \"none\";\n      }\n      if (osmAttributionRef.current) {\n        osmAttributionRef.current._container.style.display = \"block\";\n      }\n      // googleAttributionRef?.current?._container.style.display = \"none\";\n    }\n  }, [\n    mapInstance,\n    activeBasemap,\n    googleSatelliteAttributionRef,\n    osmAttributionRef,\n    googleAttributionRef,\n  ]);\n}\n"
  },
  {
    "path": "features/maps/hooks/use-map/use-map-initialization.ts",
    "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { Map, ScaleControl, AttributionControl } from \"maplibre-gl\";\nimport { initializeMap } from \"@/features/maps/utils/initialize-map\";\nimport { setupMapAttributions } from \"@/features/maps/utils/setup-map-attributions\";\nimport useMapLayersStore from \"../../stores/use-map-layer-store\";\n\ninterface UseMapInitializationProps {\n  containerId: string;\n  setMapInstance: (map: Map | null) => void;\n  googleSatelliteAttributionRef: React.RefObject<AttributionControl | null>;\n  osmAttributionRef: React.RefObject<AttributionControl | null>;\n  googleAttributionRef: React.RefObject<AttributionControl | null>;\n}\n\nexport default function useMapInitialization({\n  containerId,\n  setMapInstance,\n  googleSatelliteAttributionRef,\n  osmAttributionRef,\n  googleAttributionRef,\n}: UseMapInitializationProps) {\n  const mapInstanceRef = useRef<Map | null>(null);\n\n  const mapCurrent = useMapLayersStore((state) => state.mapCurrent);\n  const setMapCurrent = useMapLayersStore((state) => state.setMapCurrent);\n  const setMapLoaded = useMapLayersStore((state) => state.setMapLoaded);\n\n  useEffect(() => {\n    const container = document.getElementById(containerId);\n    if (!mapCurrent && container) {\n      const map = initializeMap(containerId);\n      mapInstanceRef.current = map;\n      setMapCurrent(map);\n\n      map.on(\"load\", () => {\n        setMapLoaded(true);\n      });\n\n      // Example: Add scale control\n      map.addControl(\n        new ScaleControl({ maxWidth: 80, unit: \"metric\" }),\n        \"bottom-left\"\n      );\n\n      // Setup attributions\n      setupMapAttributions(\n        map,\n        googleSatelliteAttributionRef,\n        osmAttributionRef,\n        googleAttributionRef\n      );\n\n      setMapInstance(map);\n    }\n\n    return () => {\n      if (mapInstanceRef.current) {\n        mapInstanceRef.current.remove();\n        mapInstanceRef.current = null;\n        setMapInstance(null);\n        setMapCurrent(null);\n        setMapLoaded(false);\n      }\n    };\n  }, []);\n}\n"
  },
  {
    "path": "features/maps/hooks/use-map/use-map.ts",
    "content": "\"use client\";\n\nimport { useState, useRef } from \"react\";\nimport { Map, AttributionControl } from \"maplibre-gl\";\nimport useMapInitialization from \"./use-map-initialization\";\nimport useAddGeeLayers from \"./use-add-gee-layers\";\nimport useAddRoiFromSession from \"./use-add-roi-from-session\";\nimport { useUpdateLayerColor } from \"./use-update-layer-style\";\nimport useZoomToGeometry from \"./use-zoom-to-geometry\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport useBasemapToggle from \"./use-basemap-toggle\";\nimport useAddArcGisLayers from \"./use-add-arcgis-layers\";\nimport useAddAttachedLayers from \"./use-add-attached-layers\";\n\nexport default function useMap(containerId: string) {\n  // We keep this in useState in case you need to return or further manipulate the map\n  const [mapInstance, setMapInstance] = useState<Map | null>(null);\n\n  const activeBasemap = useButtonsStore((state) => state.activeBasemap);\n  // References for attributions\n  const googleSatelliteAttributionRef = useRef<AttributionControl | null>(null);\n  const osmAttributionRef = useRef<AttributionControl | null>(null);\n  const googleAttributionRef = useRef<AttributionControl | null>(null);\n\n  // Initialize map\n  useMapInitialization({\n    containerId,\n    setMapInstance,\n    googleSatelliteAttributionRef,\n    osmAttributionRef,\n    googleAttributionRef,\n  });\n\n  // Add GEE layers\n  useAddGeeLayers(mapInstance);\n\n  useAddArcGisLayers(mapInstance);\n\n  useAddAttachedLayers(mapInstance);\n\n  // Add ROI from session\n  useAddRoiFromSession(mapInstance);\n\n  // Update layer color\n  useUpdateLayerColor(mapInstance);\n\n  // Zoom to geometry (table feature or drawn geometry)\n  useZoomToGeometry(mapInstance);\n\n  // Basemap toggle\n  useBasemapToggle({\n    mapInstance,\n    googleSatelliteAttributionRef,\n    osmAttributionRef,\n    googleAttributionRef,\n  });\n\n  return {\n    mapInstance,\n    googleSatelliteAttributionRef,\n    osmAttributionRef,\n    googleAttributionRef,\n  };\n}\n"
  },
  {
    "path": "features/maps/hooks/use-map/use-update-layer-style.ts",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Map } from \"maplibre-gl\";\nimport useMapLayersStore from \"../../stores/use-map-layer-store\";\nimport useColorPickerStore from \"../../stores/use-color-picker-store\";\n\nexport function useUpdateLayerColor(mapInstance: Map | null) {\n  const mapLoaded = useMapLayersStore((state) => state.mapLoaded);\n  const getLayerPropertiesByName = useMapLayersStore(\n    (state) => state.getLayerPropertiesByName\n  );\n  const pickedColor = useColorPickerStore((state) => state.pickedColor);\n\n  useEffect(() => {\n    if (\n      !mapInstance ||\n      !mapLoaded ||\n      !pickedColor ||\n      pickedColor.color === \"#NaNNaNNaN\"\n    )\n      return;\n\n    const mapLayer = getLayerPropertiesByName(pickedColor.layerName || \"\");\n    if (mapLayer?.layerType === \"roi\") {\n      const layerName = `${pickedColor.layerName}_roi`;\n      const styleLayer = mapInstance.getLayer(layerName);\n\n      if (styleLayer?.type === \"fill-extrusion\") {\n        mapInstance.setPaintProperty(\n          layerName,\n          \"fill-extrusion-color\",\n          pickedColor.color\n        );\n      } else if (styleLayer?.type === \"fill\") {\n        // Update the fill color\n        mapInstance.setPaintProperty(\n          layerName,\n          \"fill-color\",\n          pickedColor.color\n        );\n      }\n\n      // Also update the border color, if that layer exists\n      const borderLayerId = `${layerName}-border`;\n      const borderLayer = mapInstance.getLayer(borderLayerId);\n\n      if (borderLayer?.type === \"line\") {\n        mapInstance.setPaintProperty(\n          borderLayerId,\n          \"line-color\",\n          pickedColor.color\n        );\n      }\n    }\n  }, [mapInstance, mapLoaded, pickedColor]);\n}\n"
  },
  {
    "path": "features/maps/hooks/use-map/use-zoom-to-geometry.ts",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Map } from \"maplibre-gl\";\nimport useMapLayersStore from \"../../stores/use-map-layer-store\";\nimport useZoomRequestStore, {\n  AddressSearchProps,\n} from \"../../stores/use-map-zoom-request-store\";\nimport {\n  calculateGeometryCentroid,\n  calculateZoomLevel,\n  convertCoordinatesToGeoJson,\n  convertFeatureToGeometry,\n} from \"@/features/maps/utils/geometry-utils\";\nimport { addGeocodedPointToMap } from \"../../utils/add-geocoded-point-to-map\";\n\nexport default function useZoomToGeometry(mapInstance: Map | null) {\n  const mapLoaded = useMapLayersStore((state) => state.mapLoaded);\n\n  const zoomToLayerRequestWithGeometry = useZoomRequestStore(\n    (state) => state.zoomToLayerRequestWithGeometry\n  );\n  const setZoomToLayerRequestWithGeometry = useZoomRequestStore(\n    (state) => state.setZoomToLayerRequestWithGeometry\n  );\n\n  const zoomToAddressRequest = useZoomRequestStore(\n    (state) => state.zoomToAddressRequest\n  );\n  const setZoomToAddressRequest = useZoomRequestStore(\n    (state) => state.setZoomToAddressRequest\n  );\n\n  const zoomRequestFromTable = useZoomRequestStore(\n    (state) => state.zoomRequestFromTable\n  );\n  const setZoomRequestFromTable = useZoomRequestStore(\n    (state) => state.setZoomRequestFromTable\n  );\n\n  useEffect(() => {\n    if (\n      !mapInstance ||\n      !mapLoaded ||\n      (!zoomRequestFromTable &&\n        !zoomToLayerRequestWithGeometry &&\n        !zoomToAddressRequest)\n    )\n      return;\n\n    let targetPoint: [number, number] | AddressSearchProps = [\n      -75.7003, 45.4201,\n    ];\n    let targetZoom = 12;\n    let isHandled = false;\n\n    if (zoomToLayerRequestWithGeometry?.geometry) {\n      // Handle zoomToLayerRequestWithGeometry\n      targetPoint = calculateGeometryCentroid(\n        zoomToLayerRequestWithGeometry.geometry\n      );\n\n      targetZoom = calculateZoomLevel(zoomToLayerRequestWithGeometry.geometry);\n\n      isHandled = true;\n    } else if (zoomRequestFromTable?.geometry) {\n      // Handle zoomRequestFromTable\n      const geometry = convertFeatureToGeometry(zoomRequestFromTable);\n      targetPoint = calculateGeometryCentroid(geometry);\n      targetZoom = calculateZoomLevel(geometry);\n      isHandled = true;\n    } else if (zoomToAddressRequest) {\n      // Handle zoomToAddressRequest\n      targetPoint = zoomToAddressRequest;\n      targetZoom = 16;\n      isHandled = true;\n      const geocodedPoint = convertCoordinatesToGeoJson({\n        lat: zoomToAddressRequest.lat,\n        lon: zoomToAddressRequest.lng,\n      });\n      addGeocodedPointToMap(mapInstance, geocodedPoint, \"geocoded-point\");\n    }\n\n    if (isHandled) {\n      mapInstance.jumpTo({\n        center: targetPoint,\n        zoom: targetZoom,\n      });\n\n      // Delay clearing the state until after the animation completes\n      const timeout = setTimeout(() => {\n        if (zoomToLayerRequestWithGeometry?.geometry)\n          setZoomToLayerRequestWithGeometry(null);\n        if (zoomRequestFromTable?.geometry) setZoomRequestFromTable(null);\n        if (zoomToAddressRequest) setZoomToAddressRequest(null);\n      }, 1100);\n\n      return () => clearTimeout(timeout);\n    }\n  }, [\n    mapLoaded,\n    zoomToLayerRequestWithGeometry,\n    zoomToAddressRequest,\n    zoomRequestFromTable,\n  ]);\n}\n"
  },
  {
    "path": "features/maps/hooks/use-map-cursor.ts",
    "content": "import { useEffect } from \"react\";\nimport useCursorStore from \"@/features/maps/stores/use-cursor-store\";\nimport useMapLayersStore from \"../stores/use-map-layer-store\";\n\nexport const useMapCursor = () => {\n  const { cursorState } = useCursorStore();\n  const mapCurrent = useMapLayersStore((state) => state.mapCurrent);\n\n  useEffect(() => {\n    if (!mapCurrent) return;\n\n    const updateCursor = () => {\n      switch (cursorState) {\n        case \"default\":\n          mapCurrent.getCanvas().style.cursor = \"grab\";\n          break;\n        case \"pointer\":\n          mapCurrent.getCanvas().style.cursor = \"pointer\";\n          break;\n        case \"crosshair\":\n          mapCurrent.getCanvas().style.cursor = \"crosshair\";\n          break;\n        default:\n          mapCurrent.getCanvas().style.cursor = \"default\";\n          break;\n      }\n    };\n\n    updateCursor();\n\n    const handleMouseDown = () => {\n      if (mapCurrent.getCanvas().style.cursor === \"pointer\") return;\n\n      mapCurrent.getCanvas().style.cursor = \"grabbing\";\n    };\n    const handleMouseUp = updateCursor;\n    const handleMouseMove = updateCursor;\n    const handleMouseOut = updateCursor;\n\n    mapCurrent.on(\"mousedown\", handleMouseDown);\n    mapCurrent.on(\"mouseup\", handleMouseUp);\n    mapCurrent.on(\"mousemove\", handleMouseMove);\n    mapCurrent.on(\"mouseout\", handleMouseOut);\n\n    return () => {\n      mapCurrent.off(\"mousedown\", handleMouseDown);\n      mapCurrent.off(\"mouseup\", handleMouseUp);\n      mapCurrent.off(\"mousemove\", handleMouseMove);\n      mapCurrent.off(\"mouseout\", handleMouseOut);\n    };\n  }, [mapCurrent, cursorState]);\n};\n"
  },
  {
    "path": "features/maps/stores/map-queries-stores/useQueryOutputReadyFromVectorLayerStore.ts",
    "content": "import { create } from \"zustand\";\n\ninterface QueryState {\n  queryOutputReadyFromVectorLayer: boolean;\n  setQueryOutputReadyFromVectorLayer: (value: boolean) => void;\n  reset: () => void;\n}\n\nconst useQueryOutputReadyFromVectorLayer = create<QueryState>((set) => ({\n  queryOutputReadyFromVectorLayer: false,\n  setQueryOutputReadyFromVectorLayer: (value) =>\n    set({ queryOutputReadyFromVectorLayer: value }),\n  reset: () => set({ queryOutputReadyFromVectorLayer: false }),\n}));\n\nexport default useQueryOutputReadyFromVectorLayer;\n"
  },
  {
    "path": "features/maps/stores/map-queries-stores/useQueryRasterFromVectorLayerStore.ts",
    "content": "import { create } from \"zustand\";\n\ninterface QueryRasterFromVectorLayerState {\n  isQueryActive: boolean;\n  setIsQueryActive: (isActive: boolean) => void;\n  reset: () => void;\n}\n\nconst useQueryRasterFromVectorLayerStore =\n  create<QueryRasterFromVectorLayerState>((set) => ({\n    isQueryActive: false,\n    setIsQueryActive: (isActive) => set({ isQueryActive: isActive }),\n    reset: () => set({ isQueryActive: false }),\n  }));\n\nexport default useQueryRasterFromVectorLayerStore;\n"
  },
  {
    "path": "features/maps/stores/map-queries-stores/useQueryReadyStore.ts",
    "content": "import { create } from \"zustand\";\n\ninterface QueryState {\n  queryReady: boolean;\n  setQueryReady: (value: boolean) => void;\n  reset: () => void;\n}\n\nconst useQueryStore = create<QueryState>((set) => ({\n  queryReady: false,\n  setQueryReady: (value) => set({ queryReady: value }),\n  reset: () => set({ queryReady: false }),\n}));\n\nexport default useQueryStore;\n"
  },
  {
    "path": "features/maps/stores/plots-stores/useChartRequestedTypeStore.ts",
    "content": "import { create } from \"zustand\";\n\nenum ChartTypes {\n  BarChart,\n  LineChart,\n  BubbleChart,\n  ScatterPlot,\n  BoxPlot,\n  Histogram,\n  ViolinPlot,\n  Heatmap,\n  RadarChart,\n  StackedBarChart,\n  ParallelCoordinatesPlot,\n  PieChart,\n}\n\nconst functions: Record<string, ChartTypes[]> = {\n  \"CO Emissions Analysis\": [\n    ChartTypes.BarChart,\n    ChartTypes.LineChart,\n    ChartTypes.BubbleChart,\n  ],\n  \"NO2 Emissions Analysis\": [\n    ChartTypes.BarChart,\n    ChartTypes.LineChart,\n    ChartTypes.BubbleChart,\n  ],\n  \"PM2.5 Analysis\": [\n    ChartTypes.BarChart,\n    ChartTypes.ScatterPlot,\n    ChartTypes.BoxPlot,\n  ],\n  \"Crop Monitoring\": [\n    ChartTypes.LineChart,\n    ChartTypes.BarChart,\n    ChartTypes.BubbleChart,\n  ],\n  \"Soil Moisture Analysis\": [\n    ChartTypes.LineChart,\n    ChartTypes.BarChart,\n    ChartTypes.Histogram,\n  ],\n  \"Yield Prediction\": [\n    ChartTypes.LineChart,\n    ChartTypes.ScatterPlot,\n    ChartTypes.ViolinPlot,\n  ],\n  \"Habitat Suitability Analysis\": [\n    ChartTypes.BarChart,\n    ChartTypes.RadarChart,\n    ChartTypes.Heatmap,\n  ],\n  \"Protected Areas Monitoring\": [\n    ChartTypes.LineChart,\n    ChartTypes.RadarChart,\n    ChartTypes.StackedBarChart,\n  ],\n  \"Precipitation Trends Analysis\": [\n    ChartTypes.LineChart,\n    ChartTypes.BarChart,\n    ChartTypes.BoxPlot,\n  ],\n  \"Temperature Anomalies Detection\": [\n    ChartTypes.LineChart,\n    ChartTypes.ScatterPlot,\n    ChartTypes.Histogram,\n  ],\n  \"Bi-Temporal Coastline Analysis\": [\n    ChartTypes.LineChart,\n    ChartTypes.ScatterPlot,\n    ChartTypes.ParallelCoordinatesPlot,\n  ],\n  \"Time-Series Coastline Analysis and Prediction\": [\n    ChartTypes.LineChart,\n    ChartTypes.BarChart,\n    ChartTypes.StackedBarChart,\n  ],\n  \"Land Surface Temperature Analysis\": [\n    ChartTypes.LineChart,\n    ChartTypes.BarChart,\n  ],\n  \"Urban Heat Island (UHI) Analysis\": [\n    ChartTypes.LineChart,\n    ChartTypes.ScatterPlot,\n    ChartTypes.BoxPlot,\n  ],\n  \"Deforestation Monitoring\": [\n    ChartTypes.LineChart,\n    ChartTypes.BarChart,\n    ChartTypes.ScatterPlot,\n  ],\n  \"Forest Road Detection\": [\n    ChartTypes.ScatterPlot,\n    ChartTypes.LineChart,\n    ChartTypes.ParallelCoordinatesPlot,\n  ],\n  \"Wildfire Risk Assessment\": [\n    ChartTypes.LineChart,\n    ChartTypes.ScatterPlot,\n    ChartTypes.BoxPlot,\n  ],\n  \"Land Use/Land Cover Maps\": [\n    ChartTypes.PieChart,\n    ChartTypes.BarChart,\n    ChartTypes.StackedBarChart,\n  ],\n  \"Temporal Change Detection\": [\n    ChartTypes.LineChart,\n    ChartTypes.Heatmap,\n    ChartTypes.ScatterPlot,\n  ],\n  \"Urban Green Space Analysis\": [\n    ChartTypes.BarChart,\n    ChartTypes.PieChart,\n    ChartTypes.RadarChart,\n  ],\n  \"Drought Assessment\": [\n    ChartTypes.LineChart,\n    ChartTypes.Heatmap,\n    ChartTypes.ViolinPlot,\n  ],\n  \"Flood Risk Assessment\": [\n    ChartTypes.LineChart,\n    ChartTypes.ScatterPlot,\n    ChartTypes.Histogram,\n  ],\n  \"Surface Water Maps\": [\n    ChartTypes.PieChart,\n    ChartTypes.LineChart,\n    ChartTypes.ScatterPlot,\n  ],\n};\n\ninterface ChartRequestedTypeStore {\n  chartTypes: Record<string, ChartTypes[]>;\n  setChartTypes: (chartTypes: Record<string, ChartTypes[]>) => void;\n  reset: () => void;\n}\n\nconst useChartRequestedTypeStore = create<ChartRequestedTypeStore>((set) => ({\n  chartTypes: functions,\n  setChartTypes: (chartTypes) => set({ chartTypes }),\n  reset: () => set({ chartTypes: functions }),\n}));\n\nexport default useChartRequestedTypeStore;\n"
  },
  {
    "path": "features/maps/stores/plots-stores/usePlotReadyDataStore.ts",
    "content": "import { create } from \"zustand\";\n\ninterface PlotReadyState {\n  plotReadyData: {\n    data: any | null;\n    chartType: string | null;\n  };\n  setPlotReadyData: (data: any, chartType: string) => void;\n  reset: () => void;\n}\n\nconst usePlotReadyDataStore = create<PlotReadyState>((set) => ({\n  plotReadyData: {\n    data: null,\n    chartType: null,\n  },\n  setPlotReadyData: (data: any, chartType: string) =>\n    set({ plotReadyData: { data, chartType } }),\n  reset: () =>\n    set({\n      plotReadyData: {\n        data: null,\n        chartType: null,\n      },\n    }),\n}));\n\nexport default usePlotReadyDataStore;\n"
  },
  {
    "path": "features/maps/stores/plots-stores/usePlotReadyFromVectorLayerStore.ts",
    "content": "import { create } from \"zustand\";\n\ninterface PlotReadyState {\n  plotReadyDataFromVectorLayer: {\n    data: any | null;\n    functionTypeForVectorLayer: string | \"\";\n  };\n  setPlotReadyDataFromVectorLayer: (\n    data: any,\n    functionTypeForVectorLayer: string\n  ) => void;\n\n  plotReadyDataForSelectedFeatureFromTable: {\n    data: any | null;\n    functionTypeForSelectedFeature: string | \"\";\n  };\n  setPlotReadyDataForSelectedFeatureFromTable: (\n    data: any,\n    functionTypeForSelectedFeature: string\n  ) => void;\n\n  plotReadyDataForSelectedAreaOnMap: {\n    data: any | null;\n    functionTypeForSelectedArea: string | \"\";\n  };\n  setPlotReadyDataForSelectedAreaOnMap: (\n    data: any,\n    functionTypeForSelectedArea: string\n  ) => void;\n\n  reset: () => void;\n}\n\nconst usePlotReadyDataFromVectorLayerStore = create<PlotReadyState>((set) => ({\n  plotReadyDataFromVectorLayer: {\n    data: null,\n    functionTypeForVectorLayer: \"\",\n  },\n  setPlotReadyDataFromVectorLayer: (\n    data: any,\n    functionTypeForVectorLayer: string\n  ) =>\n    set({ plotReadyDataFromVectorLayer: { data, functionTypeForVectorLayer } }),\n\n  plotReadyDataForSelectedFeatureFromTable: {\n    data: null,\n    functionTypeForSelectedFeature: \"\",\n  },\n  setPlotReadyDataForSelectedFeatureFromTable: (\n    data: any,\n    functionTypeForSelectedFeature: string\n  ) =>\n    set({\n      plotReadyDataForSelectedFeatureFromTable: {\n        data,\n        functionTypeForSelectedFeature,\n      },\n    }),\n\n  plotReadyDataForSelectedAreaOnMap: {\n    data: null,\n    functionTypeForSelectedArea: \"\",\n  },\n  setPlotReadyDataForSelectedAreaOnMap: (\n    data: any,\n    functionTypeForSelectedArea: string\n  ) =>\n    set({\n      plotReadyDataForSelectedAreaOnMap: { data, functionTypeForSelectedArea },\n    }),\n  reset: () =>\n    set({\n      plotReadyDataFromVectorLayer: {\n        data: null,\n        functionTypeForVectorLayer: \"\",\n      },\n      plotReadyDataForSelectedFeatureFromTable: {\n        data: null,\n        functionTypeForSelectedFeature: \"\",\n      },\n      plotReadyDataForSelectedAreaOnMap: {\n        data: null,\n        functionTypeForSelectedArea: \"\",\n      },\n    }),\n}));\n\nexport default usePlotReadyDataFromVectorLayerStore;\n"
  },
  {
    "path": "features/maps/stores/use-agol-layers-store.ts",
    "content": "import { create } from \"zustand\";\nimport {\n  isValidUrl,\n  sanitizeUrl,\n  isValidLayerName,\n  sanitizeLayerName,\n} from \"@/utils/validation-utils/validation-utils\";\nimport { checkLayerName } from \"../utils/general-checks\";\n\ninterface Layer {\n  name: string;\n  type: string;\n  url: string;\n  data_url?: string;\n}\n\ninterface AgolLayers {\n  availableAgolLayers: Layer[];\n  agolLayerRequestedToImport: Layer | null;\n  removeAgolLayerFromList: (url: string) => void;\n  setAvailableAgolLayers: (layers: Layer[]) => void;\n  setAgolLayerRequestedToImport: (layer: Layer | null) => void;\n  reset: () => void;\n}\n\nexport const useAgolLayersStore = create<AgolLayers>((set) => {\n  const broadcastChannel = new BroadcastChannel(\"esriChannel\");\n\n  // Safely handle incoming messages from BroadcastChannel\n  broadcastChannel.onmessage = (event: MessageEvent) => {\n    try {\n      const { layers } = event.data;\n\n      if (Array.isArray(layers)) {\n        const sanitizedLayers = layers\n          .filter(\n            (layer) => isValidUrl(layer.url) && isValidLayerName(layer.name)\n            // (layer) => isValidLayerName(layer.name)\n          ) // Validate input\n          .map((layer) => ({\n            ...layer,\n            // name: sanitizeLayerName(layer.name),\n            name: layer.name,\n            // url: sanitizeUrl(layer.url),\n            url: layer.url,\n          }));\n\n        set({ availableAgolLayers: sanitizedLayers });\n      }\n    } catch (error) {\n      console.error(\"Error processing BroadcastChannel message:\", error);\n    }\n  };\n\n  const generateDataUrl = (layer: Layer): string => {\n    if (layer.type === \"Feature Service\") {\n      return `${layer.url}/0/query?f=pgeojson&where=1=1`;\n    } else if (layer.type === \"WFS Server\") {\n      return `${layer.url}?service=WFS&request=GetFeature&typeName=${layer.name}&outputFormat=application/json`;\n    }\n    return layer.url;\n  };\n\n  return {\n    availableAgolLayers: [],\n    agolLayerRequestedToImport: null,\n\n    removeAgolLayerFromList: (url: string) => {\n      set((state) => ({\n        availableAgolLayers: state.availableAgolLayers.filter(\n          (layer) => layer.url !== url\n        ),\n      }));\n    },\n\n    setAvailableAgolLayers: (layers: Layer[]) => {\n      // We'll build up our final array step by step\n      const finalLayers: Layer[] = [];\n\n      layers.forEach((incomingLayer) => {\n        // Validate\n        if (\n          isValidUrl(incomingLayer.url) &&\n          isValidLayerName(incomingLayer.name)\n        ) {\n          // Get the existing layer names so far\n          const existingNames = finalLayers.map((l) => l.name);\n\n          // Generate a unique name\n          const uniqueName = checkLayerName(incomingLayer.name, existingNames);\n\n          // Create the new layer object\n          const newLayer: Layer = {\n            ...incomingLayer,\n            name: uniqueName,\n            url: incomingLayer.url,\n            data_url: generateDataUrl(incomingLayer),\n          };\n\n          // Push it into the final array\n          finalLayers.push(newLayer);\n        }\n      });\n\n      // Now set finalLayers into zustand\n      set({ availableAgolLayers: finalLayers });\n\n      // And broadcast\n      try {\n        broadcastChannel.postMessage({ layers: finalLayers });\n      } catch (error) {\n        console.error(\"Error broadcasting layers:\", error);\n      }\n    },\n\n    setAgolLayerRequestedToImport: (layer: Layer | null) => {\n      if (layer && isValidUrl(layer.url) && isValidLayerName(layer.name)) {\n        set({ agolLayerRequestedToImport: layer });\n      } else {\n        console.warn(\"Attempted to set an invalid layer for import:\", layer);\n      }\n    },\n    reset: () => {\n      set({\n        availableAgolLayers: [],\n        agolLayerRequestedToImport: null,\n      });\n    },\n  };\n});\n"
  },
  {
    "path": "features/maps/stores/use-color-picker-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface ColorStore {\n  pickedColor: { color: string; layerName: string | null };\n  isPickerOpen: boolean;\n  setPickedColor: (color: string, layerName: string) => void;\n  setPickerOpen: (isOpen: boolean) => void;\n  reset: () => void;\n}\n\nconst useColorPickerStore = create<ColorStore>((set) => ({\n  pickedColor: { color: \"#FFFFFF\", layerName: null },\n  isPickerOpen: false,\n  setPickedColor: (color: string, layerName: string) =>\n    set({ pickedColor: { color, layerName } }),\n  setPickerOpen: (isOpen: boolean) => set({ isPickerOpen: isOpen }),\n  reset: () =>\n    set({\n      pickedColor: { color: \"#FFFFFF\", layerName: null },\n      isPickerOpen: false,\n    }),\n}));\n\nexport default useColorPickerStore;\n"
  },
  {
    "path": "features/maps/stores/use-cursor-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface CursorState {\n  cursorState: string;\n  setCursorState: (cursor: string) => void;\n  reset: () => void;\n}\n\nconst useCursorStore = create<CursorState>((set) => ({\n  cursorState: \"default\",\n  setCursorState: (cursor) => set({ cursorState: cursor }),\n  reset: () => set({ cursorState: \"default\" }),\n}));\n\nexport default useCursorStore;\n"
  },
  {
    "path": "features/maps/stores/use-drawn-feature-on-map-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface FeatureStore {\n  drawnFeaturesOnMap: Feature[];\n  selectedDrawnFeature: Feature | null;\n  lastRemovedDrawnFeature: string | null;\n  addDrawnFeatureOnMap: (drawnFeatureOnMap: Omit<Feature, \"UID\">) => void;\n  removeDrawnFeature: (uid: string) => void;\n  removeAllDrawnFeatures: () => void;\n  selectDrawnFeature: (uid: string) => void;\n  clearSelectedDrawnFeature: () => void;\n  uidCounter: number;\n  reset: () => void;\n}\n\nconst useDrawnFeatureOnMapStore = create<FeatureStore>((set) => ({\n  drawnFeaturesOnMap: [],\n  selectedDrawnFeature: null,\n  lastRemovedDrawnFeature: null,\n  uidCounter: 1,\n\n  // Add a drawn feature\n  addDrawnFeatureOnMap: (drawnFeatureOnMap) =>\n    set((state) => {\n      let featureExists = false;\n\n      if (drawnFeatureOnMap.Geometry === \"Point\") {\n        // Check for existing Point feature with the same Lat/Lon\n        featureExists = state.drawnFeaturesOnMap.some(\n          (existingFeature) =>\n            existingFeature.Geometry === \"Point\" &&\n            existingFeature.Lat === drawnFeatureOnMap.Lat &&\n            existingFeature.Lon === drawnFeatureOnMap.Lon\n        );\n      } else if (drawnFeatureOnMap.Geometry === \"Polygon\") {\n        // Check for existing Polygon feature with the same Coordinates\n        featureExists = state.drawnFeaturesOnMap.some(\n          (existingFeature) =>\n            existingFeature.Geometry === \"Polygon\" &&\n            JSON.stringify(existingFeature.Coordinates) ===\n              JSON.stringify(drawnFeatureOnMap.Coordinates)\n        );\n      }\n\n      if (featureExists) {\n        console.warn(\"Feature with the same coordinates already exists\");\n        return state;\n      }\n\n      const newFeature: Feature = {\n        ...drawnFeatureOnMap,\n        UID: state.uidCounter.toString(),\n      } as Feature;\n\n      return {\n        drawnFeaturesOnMap: [...state.drawnFeaturesOnMap, newFeature],\n        uidCounter: state.uidCounter + 1,\n      };\n    }),\n\n  // Remove a drawn feature\n  removeDrawnFeature: (uid) =>\n    set((state) => ({\n      drawnFeaturesOnMap: state.drawnFeaturesOnMap.filter(\n        (drawnFeatureOnMap) => drawnFeatureOnMap.UID !== uid\n      ),\n      lastRemovedDrawnFeature: uid,\n      // If the removed feature was selected, clear the selection\n      selectedDrawnFeature:\n        state.selectedDrawnFeature?.UID === uid\n          ? null\n          : state.selectedDrawnFeature,\n    })),\n\n  // Remove all drawn features\n  removeAllDrawnFeatures: () =>\n    set({ drawnFeaturesOnMap: [], uidCounter: 1, selectedDrawnFeature: null }),\n\n  // Select a drawn feature by UID\n  selectDrawnFeature: (uid) =>\n    set((state) => ({\n      selectedDrawnFeature:\n        state.drawnFeaturesOnMap.find(\n          (drawnFeatureOnMap) => drawnFeatureOnMap.UID === uid\n        ) || null,\n    })),\n\n  // Clear the selected drawn feature\n  clearSelectedDrawnFeature: () => set({ selectedDrawnFeature: null }),\n  reset: () =>\n    set({\n      drawnFeaturesOnMap: [],\n      selectedDrawnFeature: null,\n      lastRemovedDrawnFeature: null,\n      uidCounter: 1,\n    }),\n}));\n\nexport default useDrawnFeatureOnMapStore;\n"
  },
  {
    "path": "features/maps/stores/use-function-store.ts",
    "content": "import { create } from \"zustand\";\n\ntype FunctionConfig = {\n  functionType: string | null;\n  startDate: Date | null;\n  endDate: Date | null;\n  startDate2?: Date | null;\n  endDate2?: Date | null;\n  layerName: string | null;\n  selectedRoiGeometry: any | null;\n  multiAnalysisOptions?: MultiAnalysisOptionsType[];\n  aggregationMethod?: AggregationMethodType;\n  legendConfig: any;\n};\n\ntype FunctionStore = {\n  functionConfigs: FunctionConfig[];\n  lastFunctionType: string | null;\n  addFunctionConfig: (config: FunctionConfig) => void;\n  getFunctionConfig: (layerName: string) => FunctionConfig | undefined;\n  getFunctionTypeByLayerName: (layerName: string) => string | null;\n  setLastFunctionType: (funcType: string | null) => void;\n  removeFunctionConfig: (layerName: string) => void;\n  reset: () => void;\n};\n\nconst useFunctionStore = create<FunctionStore>((set, get) => ({\n  functionConfigs: [],\n  lastFunctionType: null,\n\n  // Function to add a new function configuration\n  addFunctionConfig: (config) => {\n    set((state) => ({\n      functionConfigs: [\n        ...state.functionConfigs,\n        {\n          ...config,\n          selectedRoiGeometry: config.selectedRoiGeometry || null, // Ensure selectedRoiGeometry is always populated, default to null if missing\n        },\n      ],\n      lastFunctionType: config.functionType, // Set the last function type to the newly added config\n    }));\n  },\n\n  // Function to get a function configuration based on layerName and functionType\n  getFunctionConfig: (layerName) => {\n    return get().functionConfigs.find(\n      (config) => config.layerName === layerName\n    );\n  },\n\n  // Function to get the functionType based on layerName\n  getFunctionTypeByLayerName: (layerName) => {\n    const config = get().functionConfigs.find(\n      (config) => config.layerName === layerName\n    );\n    return config ? config.functionType : null;\n  },\n\n  // Setter for lastFunctionType\n  setLastFunctionType: (funcType) => set({ lastFunctionType: funcType }),\n\n  // Function to remove a function configuration by layerName\n  removeFunctionConfig: (layerName) => {\n    set((state) => ({\n      functionConfigs: state.functionConfigs.filter(\n        (config) => config.layerName !== layerName\n      ),\n    }));\n  },\n  reset: () =>\n    set({\n      functionConfigs: [],\n      lastFunctionType: null,\n    }),\n}));\n\nexport default useFunctionStore;\n"
  },
  {
    "path": "features/maps/stores/use-gee-ouput-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface GeeOutputItem {\n  layerName: string;\n  urlFormat?: string;\n  mapStats?: any;\n  uhiMetrics?: any;\n  legendConfig?: any;\n}\n\ninterface GeeOutputState {\n  geeLayersList: GeeOutputItem[];\n  tempCreatedMapInAssetsPath: Record<string, any>;\n  addNewGeeOutput: (output: GeeOutputItem) => void;\n  getGeeOutputByLayerName: (layerName: string) => GeeOutputItem | undefined;\n  getLastAddedGeeOutput: () => GeeOutputItem | undefined;\n  addTempCreatedMapInAssetsPath: (\n    layerName: string,\n    tempMapInAssets: any\n  ) => void;\n  removeTempCreatedMapInAssetsPath: (layerName: string) => void;\n  getTempCreatedMapInAssetsPath: (layerName: string) => any | undefined;\n  removeGeeLayer: (layerName: string) => void;\n  reset: () => void;\n}\n\nexport const useGeeOutputStore = create<GeeOutputState>((set, get) => ({\n  geeLayersList: [],\n  tempCreatedMapInAssetsPath: {},\n\n  addNewGeeOutput: (output) =>\n    set((state) => {\n      // Check if a layer with the same name already exists\n      const exists = state.geeLayersList.some(\n        (item) => item.layerName === output.layerName\n      );\n\n      // Only add if the layer name doesn't exist\n      if (!exists) {\n        return {\n          geeLayersList: [...state.geeLayersList, output],\n        };\n      }\n\n      // If layer name exists, return state unchanged\n      return state;\n    }),\n\n  getGeeOutputByLayerName: (layerName) =>\n    get().geeLayersList.find((item) => item.layerName === layerName),\n\n  getLastAddedGeeOutput: () =>\n    get().geeLayersList[get().geeLayersList.length - 1],\n\n  addTempCreatedMapInAssetsPath: (layerName, tempMapInAssetsPath) => {\n    const currentTempMapsInAssetsPath = get().tempCreatedMapInAssetsPath;\n    set({\n      tempCreatedMapInAssetsPath: {\n        ...currentTempMapsInAssetsPath,\n        [layerName]: tempMapInAssetsPath,\n      },\n    });\n  },\n\n  removeTempCreatedMapInAssetsPath: (layerName: string) => {\n    const { [layerName]: _, ...rest } = get().tempCreatedMapInAssetsPath;\n    set({ tempCreatedMapInAssetsPath: rest });\n  },\n\n  getTempCreatedMapInAssetsPath: (layerName: string) => {\n    const currentTempMapsInAssetsPath = get().tempCreatedMapInAssetsPath;\n    return currentTempMapsInAssetsPath[layerName];\n  },\n\n  // New method: remove items from geeLayersList with the given layerName\n  removeGeeLayer: (layerName: string) => {\n    set((state) => ({\n      geeLayersList: state.geeLayersList.filter(\n        (item) => item.layerName !== layerName\n      ),\n    }));\n  },\n  reset: () =>\n    set({\n      geeLayersList: [],\n      tempCreatedMapInAssetsPath: {},\n    }),\n}));\n"
  },
  {
    "path": "features/maps/stores/use-geojson-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface GeojsonItem {\n  name: string;\n  data: any;\n}\n\ninterface GeojsonState {\n  geojsonData: GeojsonItem[];\n  addGeojsonData: (name: string, data: any) => void;\n  clearGeojsonData: () => void;\n  getGeojsonDataByName: (name: string) => any | null;\n  getLastGeojsonData: () => GeojsonItem | null;\n  removeGeojsonDataByName: (name: string) => void;\n  reset: () => void;\n}\n\nconst useGeojsonStore = create<GeojsonState>((set, get) => ({\n  geojsonData: [],\n  addGeojsonData: (name, data) =>\n    set((state) => ({ geojsonData: [...state.geojsonData, { name, data }] })),\n  clearGeojsonData: () => set({ geojsonData: [] }),\n  getGeojsonDataByName: (name) => {\n    const item = get().geojsonData.find((item) => item.name === name);\n    return item ? item.data : null;\n  },\n  getLastGeojsonData: () => {\n    const geojsonData = get().geojsonData;\n    return geojsonData.length > 0 ? geojsonData[geojsonData.length - 1] : null;\n  },\n  removeGeojsonDataByName: (name) =>\n    set((state) => ({\n      geojsonData: state.geojsonData.filter((item) => item.name !== name),\n    })),\n  reset: () => set({ geojsonData: [] }),\n}));\n\nexport default useGeojsonStore;\n"
  },
  {
    "path": "features/maps/stores/use-layer-selection-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface LayerState {\n  selectedRasterLayer: {\n    layerName: string;\n  };\n\n  setSelectRasterLayer: (layerName: string) => void;\n  reset: () => void;\n}\n\nconst useLayerSelectionStore = create<LayerState>((set) => ({\n  selectedRasterLayer: {\n    layerName: \"\",\n  },\n\n  setSelectRasterLayer: (layerName) =>\n    set({ selectedRasterLayer: { layerName } }),\n  reset: () => set({ selectedRasterLayer: { layerName: \"\" } }),\n}));\n\nexport default useLayerSelectionStore;\n"
  },
  {
    "path": "features/maps/stores/use-map-badge-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface BadgeState {\n  text: string;\n  secondaryText: string;\n  setText: (text: string) => void;\n  setSecondaryText: (secondaryText: string) => void;\n  reset: () => void;\n}\n\nconst useBadgeStore = create<BadgeState>((set) => ({\n  text: \"ROI Drawing Mode\", // Default text\n  secondaryText: \"\", // Default secondaryText\n\n  // Action to set the primary text\n  setText: (text) => set({ text }),\n\n  // Action to set the secondary text\n  setSecondaryText: (secondaryText) => set({ secondaryText }),\n\n  // Reset both text and secondaryText to defaults\n  reset: () => set({ text: \"ROI Drawing Mode\", secondaryText: \"\" }),\n}));\n\nexport default useBadgeStore;\n"
  },
  {
    "path": "features/maps/stores/use-map-display-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface MapDisplayState {\n  isMapReady: boolean;\n  mapMaximizeRequested: boolean;\n  isMapChartPanelOpen: boolean;\n  isMapLayersPanelOpen: boolean;\n  mapContainerIds: string[];\n  displayMapRequestedFromChatResponse: boolean;\n  displayRawMapRequestedFromInsightsViewerIcon: boolean;\n\n  setMapReady: (ready: boolean) => void;\n  toggleMapReady: () => void;\n  toggleMapChartPanel: () => void;\n  toggleMapLayersPanel: () => void;\n  setMapMaximizeRequested: (maximize: boolean) => void;\n  setDisplayMapRequestedFromChatResponse: (open: boolean) => void;\n  setDisplayRawMapRequestedFromInsightsViewerIcon: (open: boolean) => void;\n  toggleMapMaximizeRequested: () => void;\n  addMapContainerId: (id: string) => void;\n  removeMapContainerId: (id: string) => void;\n  hasMapContainerId: (id: string) => boolean;\n\n  // ← Add the reset method\n  reset: () => void;\n}\n\nconst initialState = {\n  isMapReady: false,\n  mapMaximizeRequested: false,\n  isMapChartPanelOpen: false,\n  isMapLayersPanelOpen: false,\n  mapContainerIds: [] as string[],\n  displayMapRequestedFromChatResponse: false,\n  displayRawMapRequestedFromInsightsViewerIcon: false,\n};\n\nconst useMapDisplayStore = create<MapDisplayState>((set) => ({\n  ...initialState,\n\n  setMapReady: (ready: boolean) => set({ isMapReady: ready }),\n  toggleMapReady: () => set((state) => ({ isMapReady: !state.isMapReady })),\n\n  toggleMapChartPanel: () =>\n    set((state) => ({ isMapChartPanelOpen: !state.isMapChartPanelOpen })),\n\n  toggleMapLayersPanel: () =>\n    set((state) => ({ isMapLayersPanelOpen: !state.isMapLayersPanelOpen })),\n\n  setMapMaximizeRequested: (maximize: boolean) =>\n    set({ mapMaximizeRequested: maximize }),\n\n  toggleMapMaximizeRequested: () =>\n    set((state) => ({ mapMaximizeRequested: !state.mapMaximizeRequested })),\n\n  setDisplayMapRequestedFromChatResponse: (open: boolean) =>\n    set({ displayMapRequestedFromChatResponse: open }),\n\n  setDisplayRawMapRequestedFromInsightsViewerIcon: (open: boolean) =>\n    set({ displayRawMapRequestedFromInsightsViewerIcon: open }),\n\n  addMapContainerId: (id: string) =>\n    set((state) => ({\n      mapContainerIds: [...state.mapContainerIds, id],\n    })),\n\n  removeMapContainerId: (id: string) =>\n    set((state) => ({\n      mapContainerIds: state.mapContainerIds.filter(\n        (containerId) => containerId !== id\n      ),\n    })),\n\n  hasMapContainerId: (id: string): boolean =>\n    !!useMapDisplayStore.getState().mapContainerIds.includes(id),\n\n  // Reset the store to its initial state\n  reset: () => set({ ...initialState }),\n}));\n\nexport default useMapDisplayStore;\n"
  },
  {
    "path": "features/maps/stores/use-map-layer-store.ts",
    "content": "import { create } from \"zustand\";\nimport { useGeeOutputStore } from \"./use-gee-ouput-store\";\nimport useROIStore from \"./use-roi-store\";\nimport maplibregl from \"maplibre-gl\";\nimport { checkLayerName } from \"../utils/general-checks\";\nimport { isQueryUuid } from \"@/features/chat/utils/general-utils\";\n\ninterface MapLayersState {\n  mapCurrent: maplibregl.Map | null;\n  setMapCurrent: (mapInstance: maplibregl.Map | null) => void;\n  mapLoaded: boolean;\n  setMapLoaded: (loaded: boolean) => void;\n  mapLayers: MapLayer[];\n  addMapLayer: (layer: MapLayer) => void;\n  removeMapLayer: (id: string) => void;\n  removeLayerSignal: string | null;\n\n  getMapLayersLength: () => number;\n  getMapLayer: (index: number) => MapLayer | undefined;\n  toggleMapLayerVisibility: (id: string) => void;\n  setLayerOpacity: (id: string, opacity: number) => void;\n  getLayerPropertiesByName: (id: string) => {\n    layerOpacity: number;\n    layerType: string;\n    roiName: string | null;\n    uhiMetrics: UHIMetrics | null;\n    layerFunctionType?: string;\n  } | null;\n  getMapStats: (layerName: string) => Record<string, any> | undefined;\n  reorderLayers: (newOrder: string[]) => void;\n  getMapLayerNames: () => string[];\n  reset: () => void;\n}\n\n// Define the initial state explicitly, so it's easy to reuse in `reset`\nconst initialState = {\n  mapCurrent: null as maplibregl.Map | null,\n  mapLoaded: false,\n  mapLayers: [] as MapLayer[],\n  removeLayerSignal: null as string | null,\n};\n\nconst useMapLayersStore = create<MapLayersState>((set, get) => ({\n  // Spread the initial state properties\n  ...initialState,\n\n  setMapCurrent: (mapInstance) => set({ mapCurrent: mapInstance }),\n\n  setMapLoaded: (loaded) => set({ mapLoaded: loaded }),\n\n  addMapLayer: (layer) =>\n    set((state) => {\n      // 1) Check if a layer with the same `id` + `type` already exists\n      const existingLayer = state.mapLayers.find(\n        (l) => l.id === layer.id && l.type === layer.type\n      );\n      if (existingLayer) {\n        // If found, skip adding\n        return state;\n      }\n\n      // 2) Otherwise, continue with the rest of your logic (unique naming, etc.)\n      const allLayerNames = state.mapLayers.map((l) => l.name);\n      const uniqueName = checkLayerName(layer.name, allLayerNames);\n\n      // Create a new layer object with the updated (or unchanged) name\n      const newLayer = { ...layer, name: uniqueName };\n\n      return {\n        mapLayers: [...state.mapLayers, newLayer],\n      };\n    }),\n\n  removeMapLayer: (name: string) => {\n    const removeGeeLayer = useGeeOutputStore.getState().removeGeeLayer;\n    const { mapCurrent, mapLayers, removeLayerSignal } = get();\n    const map = mapCurrent;\n\n    if (!map) return;\n\n    const layerToRemove = mapLayers.find((layer) => layer.name === name);\n\n    if (!layerToRemove) return;\n\n    // Determine if this is an ROI layer (uses \"_roi\") or not\n    const isRoiLayer = layerToRemove.type === \"roi\";\n    // The actual name for the Mapbox layer\n    const mapLayerName = isRoiLayer ? `${name}_roi` : name;\n\n    // --- Remove main layer ---\n    if (map.getLayer(mapLayerName)) {\n      map.removeLayer(mapLayerName);\n    }\n\n    // --- If it's an ROI layer, remove the border layer as well ---\n    if (isRoiLayer) {\n      const borderLayerName = `${name}_roi-border`;\n      if (map.getLayer(borderLayerName)) {\n        map.removeLayer(borderLayerName);\n      }\n    }\n\n    // --- Remove the underlying source ---\n    // • For ROI: source is `${name}_roi`\n    // • For raster: source is `name`\n    const mapSourceName = isRoiLayer ? `${name}_roi` : name;\n    if (map.getSource(mapSourceName)) {\n      map.removeSource(mapSourceName);\n    }\n\n    // --- Update Zustand state to remove it from mapLayers ---\n    set((state) => ({\n      mapLayers: state.mapLayers.filter((layer) => layer.name !== name),\n      removeLayerSignal: name,\n    }));\n\n    if (isRoiLayer) {\n      useROIStore.getState().removeRoiGeometry(name);\n    } else {\n      removeGeeLayer(name);\n      if (removeLayerSignal === name) {\n        setTimeout(() => {\n          set({ removeLayerSignal: name });\n        }, 1000);\n      }\n    }\n  },\n\n  getMapLayersLength: () => get().mapLayers.length,\n\n  getMapLayer: (index: number) => get().mapLayers[index],\n\n  toggleMapLayerVisibility: (name: string) =>\n    set((state) => {\n      const map = get().mapCurrent;\n      const updatedLayers = state.mapLayers.map((layer) => {\n        if (layer.name === name) {\n          let layerName = layer.name;\n          if (layer.type === \"roi\") {\n            layerName = `${layer.name}_roi`;\n          }\n          const newVisibility = !layer.visible;\n          if (map && map.getLayer(layerName)) {\n            map.setLayoutProperty(\n              layerName,\n              \"visibility\",\n              newVisibility ? \"visible\" : \"none\"\n            );\n          }\n\n          const borderLayerId = `${layerName}-border`;\n          if (map && map.getLayer(borderLayerId)) {\n            map.setLayoutProperty(\n              borderLayerId,\n              \"visibility\",\n              newVisibility ? \"visible\" : \"none\"\n            );\n          }\n\n          return { ...layer, visible: newVisibility };\n        }\n        return layer;\n      });\n\n      return { mapLayers: updatedLayers };\n    }),\n\n  setLayerOpacity: (name: string, opacity: number) =>\n    set((state) => ({\n      mapLayers: state.mapLayers.map((layer) =>\n        layer.name === name ? { ...layer, layerOpacity: opacity } : layer\n      ),\n    })),\n\n  getLayerPropertiesByName: (name: string) => {\n    const layer = get().mapLayers.find((layer) => layer.name === name);\n    return layer\n      ? {\n          layerOpacity: layer.layerOpacity ?? 0.8,\n          layerType: layer.type,\n          roiName: layer.roiName,\n          uhiMetrics: layer.uhiMetrics ?? null,\n          layerFunctionType: layer.layerFunctionType,\n        }\n      : null;\n  },\n\n  getMapStats: (layerName: string) => {\n    const layer = get().mapLayers.find((layer) => layer.name === layerName);\n    return layer ? layer.mapStats : undefined;\n  },\n\n  reorderLayers: (newOrder: string[]) => {\n    const map = get().mapCurrent;\n    if (!map) return;\n\n    // 2a) Update Zustand mapLayers order\n    const currentLayers = get().mapLayers;\n    const reorderedLayers = newOrder\n      .map((layerName) =>\n        currentLayers.find((layer) => layer.name === layerName)\n      )\n      .filter(Boolean) as MapLayer[];\n\n    set({ mapLayers: reorderedLayers });\n\n    // 2b) Move those store-layers in the map\n    //     from top -> bottom so the last is visually on top\n    for (let i = reorderedLayers.length - 1; i >= 0; i--) {\n      const layer = reorderedLayers[i];\n      const nextLayer = reorderedLayers[i + 1];\n\n      const mapLayerName =\n        layer.type === \"roi\" ? `${layer.name}_roi` : layer.name;\n\n      let beforeLayerName: string | undefined;\n      if (nextLayer) {\n        beforeLayerName =\n          nextLayer.type === \"roi\"\n            ? `${nextLayer.name}_roi-border`\n            : nextLayer.name;\n      }\n\n      try {\n        if (map.getLayer(mapLayerName)) {\n          map.moveLayer(mapLayerName, beforeLayerName);\n\n          // Move a possible border layer above its main layer\n          const borderLayerName = `${mapLayerName}-border`;\n          if (map.getLayer(borderLayerName)) {\n            map.moveLayer(borderLayerName);\n          }\n        }\n      } catch (error) {\n        console.error(`Failed to move layer ${mapLayerName}:`, error);\n      }\n    }\n\n    // 3) Now handle \"query_UUID\" layers\n    //    (those that do NOT live in your Zustand store)\n    const styleLayers = map.getStyle().layers;\n    if (styleLayers) {\n      // a) Move only the layers matching \"query_ + valid UUID\" above the store-layers,\n      //    but below gl-draw.\n      const queryLayers = styleLayers.filter((l) => isQueryUuid(l.id));\n      queryLayers.forEach((qlayer) => {\n        try {\n          // Calling moveLayer with NO second argument places it on top of all layers\n          // that have already been positioned so far. We'll handle gl-draw next\n          // so those remain truly at the top.\n          map.moveLayer(qlayer.id);\n        } catch (error) {\n          console.error(`Failed to move query layer ${qlayer.id}:`, error);\n        }\n      });\n\n      // b) Finally, ensure all gl-draw layers remain on top\n      const drawLayers = styleLayers.filter((l) => l.id.startsWith(\"gl-draw\"));\n      drawLayers.forEach((drawLayer) => {\n        try {\n          map.moveLayer(drawLayer.id);\n        } catch (error) {\n          console.error(`Failed to move gl-draw layer ${drawLayer.id}:`, error);\n        }\n      });\n    }\n  },\n\n  getMapLayerNames: () => get().mapLayers.map((layer) => layer.name),\n\n  // Reset method to revert all relevant state to initial defaults\n  reset: () => {\n    set({ ...initialState });\n  },\n}));\n\nexport default useMapLayersStore;\n"
  },
  {
    "path": "features/maps/stores/use-map-legend-store.ts",
    "content": "import { create } from \"zustand\";\n\n// Define the type for the configuration object\ntype LegendConfig = {\n  min?: number;\n  max?: number;\n  palette?: string[];\n  labelNames?: string[];\n};\n\n// Define the type for each legend entry\ninterface LegendEntry {\n  layerName: string;\n  config: LegendConfig;\n}\n\n// Define the type for the store's state\ninterface MapLegendState {\n  legends: LegendEntry[];\n  addLegend: (layerName: string, config: LegendConfig) => void;\n  updateLegend: (layerName: string, config: LegendConfig) => void;\n  removeLegend: (layerName: string) => void;\n  getLegend: (layerName: string) => LegendEntry | undefined;\n  reset: () => void; // ← Add the reset method\n}\n\nconst useMapLegendStore = create<MapLegendState>((set, get) => ({\n  legends: [],\n\n  addLegend: (layerName, config) =>\n    set((state) => ({\n      legends: [...state.legends, { layerName, config }],\n    })),\n\n  updateLegend: (layerName, config) =>\n    set((state) => ({\n      legends: state.legends.map((legend) =>\n        legend.layerName === layerName ? { layerName, config } : legend\n      ),\n    })),\n\n  removeLegend: (layerName) =>\n    set((state) => ({\n      legends: state.legends.filter((legend) => legend.layerName !== layerName),\n    })),\n\n  getLegend: (layerName) =>\n    get().legends.find((legend) => legend.layerName === layerName),\n\n  // Reset the store to its initial state\n  reset: () => set({ legends: [] }),\n}));\n\nexport default useMapLegendStore;\n"
  },
  {
    "path": "features/maps/stores/use-map-zoom-request-store.ts",
    "content": "import { create } from \"zustand\";\nimport useROIStore from \"./use-roi-store\";\nimport useMapLayersStore from \"./use-map-layer-store\";\n\nexport interface AddressSearchProps {\n  lat: number;\n  lng: number;\n}\n\ninterface ZoomInLayerState {\n  zoomToLayerRequestWithGeometry: ROIGeometry | null;\n  setZoomToLayerRequestWithGeometry: (layerName: string | null) => void;\n\n  zoomToAddressRequest: AddressSearchProps | null;\n  setZoomToAddressRequest: (coordinates: AddressSearchProps | null) => void;\n\n  zoomRequestFromTable: Feature | null;\n  setZoomRequestFromTable: (feature: Feature | null) => void;\n\n  // Reset method to revert the store to initial state\n  reset: () => void;\n}\n\n// Define the initial defaults for easy reference\nconst initialState = {\n  zoomToLayerRequestWithGeometry: null as ROIGeometry | null,\n  zoomToAddressRequest: null as AddressSearchProps | null,\n  zoomRequestFromTable: null as Feature | null,\n};\n\nconst useZoomRequestStore = create<ZoomInLayerState>((set) => ({\n  ...initialState,\n\n  setZoomRequestFromTable: (feature) => {\n    set({ zoomRequestFromTable: feature });\n  },\n\n  setZoomToLayerRequestWithGeometry: (layerName) => {\n    // Clear any existing geometry\n    set({ zoomToLayerRequestWithGeometry: null });\n    if (!layerName) {\n      return;\n    }\n    const getLayerPropertiesByName =\n      useMapLayersStore.getState().getLayerPropertiesByName;\n    const mapLayer = getLayerPropertiesByName(layerName);\n\n    const isLayerTypeROI = mapLayer?.layerType === \"roi\";\n\n    let roiName = \"\";\n    if (isLayerTypeROI) {\n      roiName = layerName;\n    } else {\n      roiName = mapLayer?.roiName || \"\";\n    }\n\n    const geometry = useROIStore.getState().getRoiGeometryByName(roiName);\n\n    set({ zoomToLayerRequestWithGeometry: geometry });\n  },\n\n  // Setter for zoomToAddressRequest using AddressSearchProps\n  setZoomToAddressRequest: (coordinates) => {\n    set({ zoomToAddressRequest: coordinates });\n  },\n\n  // Reset method to revert the store to initial state\n  reset: () => {\n    set({ ...initialState });\n  },\n}));\n\nexport default useZoomRequestStore;\n"
  },
  {
    "path": "features/maps/stores/use-roi-store.ts",
    "content": "import { create } from \"zustand\";\nimport { checkLayerName } from \"../utils/general-checks\";\n\ninterface ROIStoreState {\n  // ROI drawing state\n  isROIDrawingActive: boolean;\n  setIsROIDrawingActive: (isActive: boolean) => void;\n\n  isRoiCreated: boolean;\n  setIsRoiCreated: (isCreated: boolean) => void;\n\n  isRoiFinalized: { finalized: boolean; name: string };\n  setIsRoiFinalized: (finalized: boolean, name: string) => void;\n\n  selectedGeometryInChat: ROIGeometry | null;\n  setSelectedGeometryByName: (name: string) => void;\n\n  newAttachedRoi: ROIGeometry | null;\n  setNewAttachedRoi: (roi: ROIGeometry | null) => void;\n\n  roiGeometries: ROIGeometry[];\n  addRoiGeometry: ({ id, geometry, name, source }: ROIGeometry) => void;\n  removeRoiGeometry: (id: string) => void;\n  getRoiGeometryByName: (name: string) => ROIGeometry | null;\n\n  roiGeometryFromSessionHistory: ROIGeometry | null;\n  setRoiGeometryFromSessionHistory: (roi: ROIGeometry | null) => void;\n  // Method to get the last ROI geometry\n  getLastRoiGeometry: () => ROIGeometry | null;\n  // ROI Feature IDs\n  roiFeatureIds: string[];\n  setRoiFeatureIds: (ids: string[]) => void;\n  addRoiFeatureId: (id: string) => void;\n  removeRoiFeatureIds: (ids: string[]) => void;\n  reset: () => void;\n}\n\nconst useROIStore = create<ROIStoreState>((set, get) => ({\n  // ROI drawing state\n  isROIDrawingActive: false,\n  setIsROIDrawingActive: (isActive) => set({ isROIDrawingActive: isActive }),\n\n  selectedGeometryInChat: null,\n  setSelectedGeometryByName: (name) => {\n    const roiGeometries = get().roiGeometries;\n    const selectedGeometryInChat = roiGeometries.find(\n      (roi) => roi.name === name\n    );\n    set({ selectedGeometryInChat });\n  },\n\n  roiGeometryFromSessionHistory: null,\n  setRoiGeometryFromSessionHistory: (roi) =>\n    set({ roiGeometryFromSessionHistory: roi }),\n\n  newAttachedRoi: null,\n  setNewAttachedRoi: (roi) => set({ newAttachedRoi: roi }),\n\n  isRoiCreated: false,\n  setIsRoiCreated: (isCreated) => set({ isRoiCreated: isCreated }),\n\n  isRoiFinalized: { finalized: false, name: \"\" },\n  setIsRoiFinalized: (finalized, name) =>\n    set({ isRoiFinalized: { finalized, name } }),\n\n  // ROIs with geometry and name\n  roiGeometries: [],\n  addRoiGeometry: ({ id, geometry, name, source }: ROIGeometry) => {\n    const currentGeometries = get().roiGeometries;\n\n    // If an ROI with the same id already exists, skip adding a new one\n    const existingRoiWithSameId = currentGeometries.find(\n      (roi) => roi.id === id\n    );\n    if (existingRoiWithSameId) {\n      return;\n    }\n\n    // Gather all existing names into one string for checkLayerName\n    const existingNames = currentGeometries.map((roi) => roi.name);\n\n    // If name not provided, create a default \"ROI X\"\n    if (!name) {\n      const count = currentGeometries.filter((roi) =>\n        roi.name.startsWith(\"ROI\")\n      ).length;\n      name = `ROI ${count + 1}`;\n    }\n\n    // Ensure uniqueness via checkLayerName\n    const uniqueName = checkLayerName(name, existingNames);\n\n    // Save the geometry with the final unique name\n    set({\n      roiGeometries: [\n        ...currentGeometries,\n        { id, name: uniqueName, geometry, source },\n      ],\n    });\n  },\n\n  removeRoiGeometry: (id) => {\n    const currentGeometries = get().roiGeometries;\n    set({\n      roiGeometries: currentGeometries.filter((roi) => roi.id !== id),\n    });\n  },\n\n  getRoiGeometryByName: (name) => {\n    const roiGeometries = get().roiGeometries;\n    return roiGeometries.find((roi) => roi.name === name) ?? null;\n  },\n\n  // Method to get the last ROI geometry\n  getLastRoiGeometry: () => {\n    const roiGeometries = get().roiGeometries;\n    return roiGeometries.length > 0\n      ? roiGeometries[roiGeometries.length - 1]\n      : null;\n  },\n\n  // ROI Feature IDs\n  roiFeatureIds: [],\n  setRoiFeatureIds: (ids) => set({ roiFeatureIds: ids }),\n  addRoiFeatureId: (id) => {\n    const currentIds = get().roiFeatureIds;\n    set({ roiFeatureIds: [...currentIds, id] });\n  },\n\n  removeRoiFeatureIds: (idsToRemove) => {\n    const currentIds = get().roiFeatureIds;\n    set({\n      roiFeatureIds: currentIds.filter((id) => !idsToRemove.includes(id)),\n    });\n  },\n  reset: () =>\n    set({\n      isROIDrawingActive: false,\n      isRoiCreated: false,\n      newAttachedRoi: null,\n      isRoiFinalized: { finalized: false, name: \"\" },\n      selectedGeometryInChat: null,\n      roiGeometries: [],\n      roiGeometryFromSessionHistory: null,\n      roiFeatureIds: [],\n    }),\n}));\n\nexport default useROIStore;\n"
  },
  {
    "path": "features/maps/stores/use-table-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface TableState {\n  vectorLayersInTable: string[];\n  selectedVectorLayerInTable: string;\n  selectedFeatureInTable: Feature | null;\n  isTableOpen: boolean;\n  toggleTable: () => void;\n  setTableOpen: (value: boolean) => void;\n  setSelectedVectorLayerInTable: (layer: string) => void;\n  setSelectedFeatureInTable: (feature: Feature) => void;\n  selectedFeatureToDelete: Feature | null;\n  deleteSelectedFeature: (feature: Feature) => void;\n  addVectorLayer: (layer: string) => void;\n  removeVectorLayer: (layer: string) => void;\n\n  // ← Add the reset method\n  reset: () => void;\n}\n\n// Reusable initial state\nconst initialState = {\n  vectorLayersInTable: [] as string[],\n  selectedVectorLayerInTable: \"\",\n  selectedFeatureInTable: null as Feature | null,\n  isTableOpen: false,\n  selectedFeatureToDelete: null as Feature | null,\n};\n\nconst useTableStore = create<TableState>((set) => ({\n  ...initialState,\n\n  toggleTable: () => set((state) => ({ isTableOpen: !state.isTableOpen })),\n  setTableOpen: (value) => set({ isTableOpen: value }),\n\n  setSelectedVectorLayerInTable: (layer) =>\n    set({ selectedVectorLayerInTable: layer }),\n\n  setSelectedFeatureInTable: (feature) =>\n    set({ selectedFeatureInTable: feature }),\n\n  deleteSelectedFeature: (feature) => set({ selectedFeatureToDelete: feature }),\n\n  addVectorLayer: (layer) =>\n    set((state) => ({\n      vectorLayersInTable: [...state.vectorLayersInTable, layer],\n    })),\n\n  removeVectorLayer: (layer) =>\n    set((state) => ({\n      vectorLayersInTable: state.vectorLayersInTable.filter(\n        (existingLayer) => existingLayer !== layer\n      ),\n    })),\n\n  // Reset everything to the initial defaults\n  reset: () => set({ ...initialState }),\n}));\n\nexport default useTableStore;\n"
  },
  {
    "path": "features/maps/utils/add-drawn-layer-to-map.ts",
    "content": "import { Map, Popup } from \"maplibre-gl\";\nimport useColorPickerStore from \"@/features/maps/stores/use-color-picker-store\";\nimport { Feature as GeoJSONFeature, Geometry } from \"geojson\";\n\nfunction getRandomBrightColor(): string {\n  let r, g, b, brightness;\n  do {\n    r = Math.floor(Math.random() * 256);\n    g = Math.floor(Math.random() * 256);\n    b = Math.floor(Math.random() * 256);\n    brightness = 0.299 * r + 0.587 * g + 0.114 * b;\n  } while (brightness < 130);\n  return `rgb(${r}, ${g}, ${b})`;\n}\n\nexport const addDrawnLayerToMap = async (\n  map: Map,\n  geojsonData: any,\n  layerName: string\n) => {\n  const { setPickedColor } = useColorPickerStore.getState();\n  const randomColor = getRandomBrightColor();\n\n  // Remove existing layers/sources with the same name\n  if (map.getSource(layerName)) {\n    const layers = map.getStyle().layers || [];\n    layers.forEach((layer) => {\n      if (layer.id === layerName || layer.id.startsWith(`${layerName}-`)) {\n        if (map.getLayer(layer.id)) {\n          map.removeLayer(layer.id);\n        }\n      }\n    });\n    map.removeSource(layerName);\n  }\n\n  // Add the new GeoJSON source\n  map.addSource(layerName, {\n    type: \"geojson\",\n    data: geojsonData,\n  });\n\n  // Determine geometry type\n  const geometryType =\n    geojsonData.features?.[0]?.geometry?.type || geojsonData?.type;\n\n  let isPoint = false;\n  let isPolygon = false;\n  let isLine = false;\n\n  if (geometryType === \"Point\" || geometryType === \"MultiPoint\") {\n    isPoint = true;\n  } else if (geometryType === \"Polygon\" || geometryType === \"MultiPolygon\") {\n    isPolygon = true;\n  } else if (\n    geometryType === \"LineString\" ||\n    geometryType === \"MultiLineString\"\n  ) {\n    isLine = true;\n  }\n\n  // Add layers based on geometry type\n  if (isPoint) {\n    map.addLayer({\n      id: layerName,\n      type: \"circle\",\n      source: layerName,\n      paint: {\n        \"circle-radius\": 6,\n        \"circle-color\": randomColor,\n        \"circle-opacity\": 0.8,\n        \"circle-stroke-color\": \"#4d4d4d\",\n        \"circle-stroke-width\": 2,\n      },\n    });\n  } else if (isPolygon) {\n    // If you also want to show the polygon fill (optional):\n    map.addLayer({\n      id: layerName,\n      type: \"fill\",\n      source: layerName,\n      paint: {\n        \"fill-color\": randomColor,\n        \"fill-opacity\": 0.3,\n      },\n    });\n\n    map.addLayer({\n      id: `${layerName}-border`,\n      type: \"line\",\n      source: layerName,\n      paint: {\n        \"line-color\": randomColor,\n        \"line-width\": 3.5,\n        \"line-opacity\": 1,\n      },\n    });\n  } else if (isLine) {\n    map.addLayer({\n      id: layerName,\n      type: \"line\",\n      source: layerName,\n      paint: {\n        \"line-color\": randomColor,\n        \"line-width\": 4,\n        \"line-opacity\": 1,\n      },\n    });\n  } else {\n    // Fallback: treat unknown geometry as a line\n    map.addLayer({\n      id: layerName,\n      type: \"line\",\n      source: layerName,\n      paint: {\n        \"line-color\": randomColor,\n        \"line-width\": 4,\n        \"line-opacity\": 1,\n      },\n    });\n  }\n\n  // Store the picked color if it's not a special case\n  if (layerName !== \"geocodedPoint\") {\n    setPickedColor(randomColor, layerName);\n  }\n\n  /**\n   * Add popup on hover\n   */\n  const popup = new Popup({\n    closeButton: false,\n    closeOnClick: false,\n  });\n\n  // Change cursor to pointer when hovering on the layer\n  map.on(\"mouseenter\", layerName, () => {\n    map.getCanvas().style.cursor = \"pointer\";\n  });\n\n  // Show popup on mousemove\n  map.on(\"mousemove\", layerName, (e) => {\n    if (!e.features || !e.features[0]) return;\n    const feature = e.features[0];\n    const description = feature.properties?.description || \"No info\";\n\n    popup.setLngLat(e.lngLat).setHTML(description).addTo(map);\n  });\n\n  // Reset cursor and remove popup on mouse leave\n  map.on(\"mouseleave\", layerName, () => {\n    map.getCanvas().style.cursor = \"\";\n    popup.remove();\n  });\n};\n"
  },
  {
    "path": "features/maps/utils/add-gee-layer-to-map.ts",
    "content": "import { Map } from \"maplibre-gl\";\n\nexport const addGeeLayerToMap = async (\n  map: Map,\n  urlFormat: string,\n  layerName: string,\n  sourceId: string\n) => {\n  if (map.getSource(sourceId)) {\n    map.removeLayer(layerName);\n    map.removeSource(sourceId);\n  }\n\n  map.addSource(sourceId, {\n    type: \"raster\",\n    tiles: [urlFormat],\n    tileSize: 256,\n  });\n\n  const layers = map.getStyle().layers;\n  let firstDrawLayerId;\n  if (layers) {\n    const drawLayer = layers.find((layer) => layer.id.startsWith(\"gl-draw\"));\n    if (drawLayer) {\n      firstDrawLayerId = drawLayer.id;\n    }\n  }\n\n  map.addLayer(\n    {\n      id: layerName,\n      type: \"raster\",\n      source: sourceId,\n      paint: {\n        \"raster-opacity\": 0.8,\n      },\n    },\n    firstDrawLayerId\n  );\n};\n"
  },
  {
    "path": "features/maps/utils/add-geocoded-point-to-map.ts",
    "content": "import { Map } from \"maplibre-gl\";\n\nexport const addGeocodedPointToMap = async (\n  map: Map,\n  geojsonData: any,\n  layerName: string\n) => {\n  // Remove any existing source and its associated layers with the same layerName.\n  if (map.getSource(layerName)) {\n    const existingLayers = map.getStyle().layers || [];\n    existingLayers.forEach((layer) => {\n      if (layer.id === layerName || layer.id.startsWith(`${layerName}-`)) {\n        if (map.getLayer(layer.id)) {\n          map.removeLayer(layer.id);\n        }\n      }\n    });\n    map.removeSource(layerName);\n  }\n\n  // Add the new GeoJSON source.\n  map.addSource(layerName, {\n    type: \"geojson\",\n    data: geojsonData,\n  });\n\n  // Determine the geometry type from the GeoJSON.\n  const geometryType = geojsonData?.geometry?.type;\n\n  let isPoint = false;\n  if (geometryType === \"Point\") {\n    isPoint = true;\n  }\n\n  // Add a circle layer for a point geometry.\n  if (isPoint) {\n    map.addLayer({\n      id: layerName,\n      type: \"circle\",\n      source: layerName,\n      paint: {\n        \"circle-radius\": 7,\n        \"circle-color\": \"yellow\",\n        \"circle-opacity\": 0.8,\n        // Smooth transition for the opacity change.\n        \"circle-opacity-transition\": { duration: 800, delay: 0 } as any,\n        // Set the stroke color to blue.\n        \"circle-stroke-color\": \"blue\",\n        \"circle-stroke-width\": 2,\n        \"circle-stroke-opacity\": 1,\n        \"circle-stroke-opacity-transition\": { duration: 800, delay: 0 } as any,\n      },\n    } as any);\n  }\n\n  // If the geometry is a point, create a flashing effect for both the inner circle and its border.\n  if (isPoint) {\n    let visible = true;\n    const flashInterval = setInterval(() => {\n      visible = !visible;\n      // Update both the inner circle and the stroke opacities.\n      map.setPaintProperty(layerName, \"circle-opacity\", visible ? 1 : 0.2);\n      map.setPaintProperty(\n        layerName,\n        \"circle-stroke-opacity\",\n        visible ? 1 : 0.2\n      );\n    }, 1000);\n\n    // After 10 seconds, clear the flashing and remove the layer and source.\n    setTimeout(() => {\n      clearInterval(flashInterval);\n      if (map.getLayer(layerName)) {\n        map.removeLayer(layerName);\n      }\n      if (map.getSource(layerName)) {\n        map.removeSource(layerName);\n      }\n    }, 10000);\n  }\n};\n"
  },
  {
    "path": "features/maps/utils/add-layers-to-map/addGeojsonLayer.ts",
    "content": "import { Map } from \"maplibre-gl\";\nimport useColorPickerStore from \"@/features/maps/stores/use-color-picker-store\";\n\nconst getRandomColor = () => {\n  const letters = \"0123456789ABCDEF\";\n  let color = \"#\";\n  for (let i = 0; i < 6; i++) {\n    color += letters[Math.floor(Math.random() * 16)];\n  }\n  return color;\n};\n\nexport const addGeojsonLayer = async (\n  map: Map,\n  geojsonData: any,\n  layerName: string\n) => {\n  const { setPickedColor } = useColorPickerStore.getState();\n  const fillColor = getRandomColor();\n  const outlineColor = \"#ffffff\";\n\n  const sourceId = geojsonData.fileName || layerName;\n\n  if (map.getSource(sourceId)) {\n    map.removeLayer(layerName);\n    map.removeSource(sourceId);\n  }\n\n  map.addSource(sourceId, {\n    type: \"geojson\",\n    data: geojsonData,\n  });\n\n  const geometryType = geojsonData.features[0].geometry.type;\n\n  if (geometryType === \"LineString\") {\n    map.addLayer({\n      id: layerName,\n      type: \"line\",\n      source: sourceId,\n      paint: {\n        \"line-color\": fillColor,\n        \"line-width\": 2,\n      },\n    });\n  } else if (geometryType === \"Polygon\" || geometryType === \"MultiPolygon\") {\n    map.addLayer({\n      id: layerName,\n      type: \"fill-extrusion\",\n      source: sourceId,\n      paint: {\n        \"fill-extrusion-color\": fillColor,\n        \"fill-extrusion-height\": [\"get\", \"Height\"],\n        \"fill-extrusion-base\": 0,\n        \"fill-extrusion-opacity\": 0.85,\n      },\n    });\n\n    map.addLayer({\n      id: `${layerName}-border`,\n      type: \"line\",\n      source: sourceId,\n      paint: {\n        \"line-color\": outlineColor,\n        \"line-width\": 0,\n        \"line-opacity\": 1,\n      },\n    });\n  } else if (geometryType === \"Point\") {\n    map.addLayer({\n      id: layerName,\n      type: \"circle\",\n      source: sourceId,\n      paint: {\n        \"circle-color\": fillColor,\n        \"circle-radius\": 6,\n      },\n    });\n  }\n  setPickedColor(fillColor, layerName);\n};\n"
  },
  {
    "path": "features/maps/utils/add-roi-layer-to-map.ts",
    "content": "import { Map, Popup } from \"maplibre-gl\";\nimport useColorPickerStore from \"@/features/maps/stores/use-color-picker-store\";\n\nfunction getRandomBrightColor(): string {\n  let hue;\n  do {\n    hue = Math.floor(Math.random() * 360);\n  } while (hue >= 60 && hue <= 90);\n\n  const saturation = 85 + Math.random() * 15;\n  const lightness = 45 + Math.random() * 15;\n\n  return `hsl(${hue}, ${saturation}%, ${lightness}%)`;\n}\n\nexport const addRoiLayerToMap = async (\n  map: Map,\n  geojsonData: any,\n  layerName: string\n) => {\n  const { setPickedColor } = useColorPickerStore.getState();\n  let randomColor = getRandomBrightColor();\n  if (layerName === \"drawnPolygon\") {\n    randomColor = \"yellow\";\n  }\n\n  // Remove existing layers/sources with the same name\n  if (map.getSource(layerName)) {\n    const layers = map.getStyle().layers || [];\n    layers.forEach((layer) => {\n      if (layer.id === layerName || layer.id.startsWith(`${layerName}-`)) {\n        if (map.getLayer(layer.id)) {\n          map.removeLayer(layer.id);\n        }\n      }\n    });\n    map.removeSource(layerName);\n  }\n\n  // Add the new GeoJSON source\n  map.addSource(layerName, {\n    type: \"geojson\",\n    data: geojsonData,\n  });\n\n  // Determine geometry type\n  const geometryType =\n    geojsonData.features?.[0]?.geometry?.type || geojsonData?.type;\n\n  let isPoint = false;\n  let isPolygon = false;\n  let isLine = false;\n\n  if (geometryType === \"Point\" || geometryType === \"MultiPoint\") {\n    isPoint = true;\n  } else if (geometryType === \"Polygon\" || geometryType === \"MultiPolygon\") {\n    isPolygon = true;\n  } else if (\n    geometryType === \"LineString\" ||\n    geometryType === \"MultiLineString\"\n  ) {\n    isLine = true;\n  }\n\n  // Add layers based on geometry type\n  if (isPoint) {\n    map.addLayer({\n      id: layerName,\n      type: \"circle\",\n      source: layerName,\n      paint: {\n        \"circle-radius\": 6,\n        \"circle-color\": randomColor,\n        \"circle-opacity\": 0.8,\n        \"circle-stroke-color\": \"#4d4d4d\",\n        \"circle-stroke-width\": 2,\n      },\n    });\n  } else if (isPolygon) {\n    map.addLayer({\n      id: `${layerName}-border`,\n      type: \"line\",\n      source: layerName,\n      paint: {\n        \"line-color\": randomColor,\n        \"line-width\": 3.5,\n        \"line-opacity\": 1,\n      },\n    });\n  } else if (isLine) {\n    map.addLayer({\n      id: layerName,\n      type: \"line\",\n      source: layerName,\n      paint: {\n        \"line-color\": randomColor,\n        \"line-width\": 4,\n        \"line-opacity\": 1,\n      },\n    });\n  } else {\n    // Fallback: treat unknown geometry as a line\n    map.addLayer({\n      id: layerName,\n      type: \"line\",\n      source: layerName,\n      paint: {\n        \"line-color\": randomColor,\n        \"line-width\": 4,\n        \"line-opacity\": 1,\n      },\n    });\n  }\n\n  // Store the picked color if it's not a special case\n  if (layerName !== \"geocodedPoint\") {\n    setPickedColor(randomColor, layerName);\n  }\n};\n"
  },
  {
    "path": "features/maps/utils/authentication-utils/gee-auth.ts",
    "content": "\"use server\";\nimport ee from \"@google/earthengine\";\nimport { GoogleAuth } from \"google-auth-library\";\nimport { createClient } from \"@/utils/supabase/server\";\n\nexport async function geeAuthenticate(): Promise<void> {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  const key = process.env.GCP_SERVICE_ACCOUNT_KEY;\n\n  return new Promise((resolve, reject) => {\n    ee.data.authenticateViaPrivateKey(\n      JSON.parse(key || \"\"),\n      () =>\n        ee.initialize(\n          null,\n          null,\n          () => resolve(),\n          (error: any) => reject(new Error(error))\n        ),\n      (error: any) => reject(new Error(error))\n    );\n  });\n}\n\nexport const getIdentityTokenGoogle = async (targetAudience: any) => {\n  const auth = new GoogleAuth({\n    credentials: JSON.parse(process.env.GCP_SERVICE_ACCOUNT_KEY || \"\"),\n  });\n\n  const client = await auth.getIdTokenClient(targetAudience);\n\n  const headers = await client.getRequestHeaders();\n\n  return headers.Authorization;\n};\n"
  },
  {
    "path": "features/maps/utils/gee-eval-utils.ts",
    "content": "export function getMapId(image: any, vis: any) {\n  return new Promise((resolve, reject) => {\n    image.getMapId(vis, (obj: any, error: any) =>\n      error ? reject(new Error(error)) : resolve(obj)\n    );\n  });\n}\n\n// Utility function to evaluate objects\nexport function evaluate(obj: any) {\n  return new Promise((resolve, reject) =>\n    obj.evaluate((result: any, error: any) =>\n      error ? reject(new Error(error)) : resolve(result)\n    )\n  );\n}\n"
  },
  {
    "path": "features/maps/utils/general-checks.ts",
    "content": "export const checkLayerName = (layerName: string, existingNames: string[]) => {\n  let uniqueLayerName = layerName;\n  let counter = 1;\n\n  while (existingNames.includes(uniqueLayerName)) {\n    uniqueLayerName = `${layerName} (${counter})`;\n    counter++;\n  }\n\n  return uniqueLayerName;\n};\n"
  },
  {
    "path": "features/maps/utils/geometry-utils.ts",
    "content": "import * as turf from \"@turf/turf\";\nimport ee from \"@google/earthengine\";\n\nexport const checkGeometryIntersection = (geojson1: any, geojson2: any) => {\n  const intersection = turf.booleanIntersects(geojson1, geojson2);\n  return intersection;\n};\n\nexport const calculateGeometryCentroid = (geometry: any) => {\n  if (!geometry || !geometry.type) {\n    throw new Error(\"Invalid or missing geometry object.\");\n  }\n\n  switch (geometry.type) {\n    case \"Polygon\":\n    case \"MultiPolygon\":\n    case \"FeatureCollection\":\n      // Use turf.centroid directly\n      return turf.centroid(geometry).geometry.coordinates as [number, number];\n\n    case \"LineString\":\n    case \"MultiLineString\":\n      // Calculate midpoint from first to last coordinate\n      const line = turf.lineString(geometry.coordinates);\n      return turf.midpoint(\n        turf.point(line.geometry.coordinates[0]),\n        turf.point(\n          line.geometry.coordinates[line.geometry.coordinates.length - 1]\n        )\n      ).geometry.coordinates as [number, number];\n\n    case \"Point\":\n      // Return Point’s coordinates\n      return geometry.coordinates as [number, number];\n\n    case \"MultiPoint\":\n      // For MultiPoint, let turf.centroid figure out a representative point\n      return turf.centroid(geometry).geometry.coordinates as [number, number];\n\n    default:\n      throw new Error(\n        \"Unsupported geometry type. Supported types: Point, MultiPoint, Polygon, \" +\n          \"MultiPolygon, LineString, MultiLineString, FeatureCollection.\"\n      );\n  }\n};\n\nexport const calculateZoomLevel = (layerGeometry: any) => {\n  let boundaryArea;\n\n  if (\n    layerGeometry.type === \"Polygon\" ||\n    layerGeometry.type === \"MultiPolygon\"\n  ) {\n    boundaryArea = turf.area(layerGeometry);\n  } else if (\n    layerGeometry.type === \"LineString\" ||\n    layerGeometry.type === \"MultiLineString\"\n  ) {\n    // Approximate \"area\" via length in meters\n    boundaryArea = turf.length(layerGeometry, { units: \"meters\" });\n  } else if (\n    layerGeometry.type === \"Point\" ||\n    layerGeometry.type === \"MultiPoint\"\n  ) {\n    boundaryArea = 1; // minimal area\n  } else if (layerGeometry.type === \"FeatureCollection\") {\n    // Create a bounding box polygon for the entire FeatureCollection\n    const envelope = turf.envelope(layerGeometry);\n    boundaryArea = turf.area(envelope);\n\n    // (If you prefer a different approach, you could sum areas of each feature,\n    // or handle lines differently, etc. The envelope approach is simplest.)\n  } else {\n    throw new Error(\n      \"Unsupported geometry type. Only Polygon, MultiPolygon, LineString, MultiLineString, Point, MultiPoint, and FeatureCollection are supported.\"\n    );\n  }\n\n  // Now apply your “log area -> zoom scale” logic\n  const maxZoom = 18;\n  const minZoom = 10;\n  const areaMax = 1000000000;\n  const areaMin = 1000;\n\n  const zoomLevel =\n    maxZoom -\n    ((Math.log(boundaryArea) - Math.log(areaMin)) /\n      (Math.log(areaMax) - Math.log(areaMin))) *\n      (maxZoom - minZoom);\n\n  return Math.max(minZoom, Math.min(maxZoom, zoomLevel));\n};\n\nexport const calculateBoundingBoxCenter = (featureCollection: any) => {\n  const bbox = turf.bbox(featureCollection);\n  const center = turf.center(turf.bboxPolygon(bbox));\n  return center.geometry.coordinates as [number, number];\n};\n\nexport const calculateEnvelopeCenter = (featureCollection: any) => {\n  const envelope = turf.envelope(featureCollection);\n  const center = turf.center(envelope);\n  return center.geometry.coordinates as [number, number];\n};\n\nexport const convertFeatureToGeometry = (feature: Feature) => {\n  let geometry: string | undefined;\n  let coordinates: any;\n\n  // If feature.coordinates already exists\n  if (feature?.coordinates) {\n    coordinates = feature.coordinates;\n\n    // For a single ring closed feature => Polygon\n    if (\n      Array.isArray(coordinates) &&\n      coordinates.length > 0 &&\n      JSON.stringify(coordinates[0]) ===\n        JSON.stringify(coordinates[coordinates.length - 1])\n    ) {\n      geometry = \"Polygon\";\n      coordinates = [coordinates]; // <-- wrap in an array for Polygon\n    } else {\n      geometry = \"LineString\";\n    }\n  } else if (feature?.lat && feature?.lon) {\n    const { lat, lon } = feature;\n\n    // lat/lon are arrays => can be LineString or Polygon\n    if (Array.isArray(lat) && Array.isArray(lon)) {\n      const coords = lat.map((latVal, i) => [lon[i], latVal]);\n\n      if (\n        coords.length > 0 &&\n        JSON.stringify(coords[0]) === JSON.stringify(coords[coords.length - 1])\n      ) {\n        geometry = \"Polygon\";\n        coordinates = [coords]; // <-- wrap in an array\n      } else {\n        geometry = \"LineString\";\n        coordinates = coords;\n      }\n\n      // Single lat/lon => Point\n    } else {\n      geometry = \"Point\";\n      coordinates = [lon, lat];\n    }\n  }\n\n  return {\n    type: geometry,\n    coordinates,\n  };\n};\n\nexport const calculateGeometryArea = (geometry: any) => {\n  const area = turf.area(geometry);\n  const areaInKm2 = turf.convertArea(area, \"meters\", \"kilometers\");\n  return areaInKm2;\n};\n\nexport const checkGeometryAreaIsLessThanThreshold = (\n  geometry: any,\n  threshold: number // in square kilometers\n) => {\n  const area = calculateGeometryArea(geometry);\n  return area < threshold;\n};\n\nexport function convertToEeGeometry(geometry: any) {\n  if (\n    geometry.type === \"FeatureCollection\" &&\n    Array.isArray(geometry.features)\n  ) {\n    if (geometry.features.length === 1) {\n      return ee.Geometry(geometry.features[0].geometry);\n    }\n\n    const multiPolygonCoordinates = geometry.features.map((feature: any) => {\n      if (!feature.geometry || !feature.geometry.coordinates) {\n        throw new Error(\"Invalid feature geometry provided.\");\n      }\n      return feature.geometry.coordinates;\n    });\n    return ee.Geometry.MultiPolygon(multiPolygonCoordinates);\n  }\n\n  if (geometry.type === \"Feature\" && geometry.geometry) {\n    return ee.Geometry(geometry.geometry);\n  }\n\n  return ee.Geometry(geometry);\n}\n\nexport function convertEeGeometryToGeoJSON(eeGeom: any) {\n  // If eeGeom.type_ === 'Polygon', transform it to GeoJSON\n  if (eeGeom.type_ === \"Polygon\" && eeGeom.coordinates_) {\n    return {\n      type: \"Polygon\",\n      coordinates: eeGeom.coordinates_,\n    };\n  }\n  // Extend this if you handle other geometry types (Point, MultiPolygon, etc.)\n  throw new Error(\"Unsupported geometry type or invalid EE geometry.\");\n}\n\nexport function convertCoordinatesToGeoJson(coordinates: {\n  lat: number;\n  lon: number;\n}) {\n  return turf.point([coordinates.lon, coordinates.lat]);\n}\n\nexport function convertToNDecimals(value: number, decimals: number): number {\n  return Number(value.toFixed(decimals));\n}\n\nfunction flattenPolygonCoordinates(coords?: number[][][]): number[][] {\n  if (!coords) return [];\n  return coords.flatMap((ring) => ring);\n}\n"
  },
  {
    "path": "features/maps/utils/initialize-map.ts",
    "content": "import maplibregl from \"maplibre-gl\";\n\nexport const initializeMap = (containerId: string): maplibregl.Map => {\n  return new maplibregl.Map({\n    container: containerId,\n    attributionControl: false,\n\n    style: {\n      version: 8,\n      sources: {\n        \"google-satellite-imagery\": {\n          type: \"raster\",\n          tiles: [\n            \"/api/services/google-maps/basemaps/satellite?z={z}&x={x}&y={y}\",\n          ],\n          tileSize: 256,\n        },\n        \"osm-tiles\": {\n          type: \"raster\",\n          tiles: [\n            \"/api/services/google-maps/basemaps/roadmap?z={z}&x={x}&y={y}\",\n          ],\n          tileSize: 256,\n        },\n      },\n      layers: [\n        {\n          id: \"googleSatelliteImagery\",\n          type: \"raster\",\n          source: \"google-satellite-imagery\",\n          minzoom: 0,\n          maxzoom: 24,\n          layout: {\n            visibility: \"visible\",\n          },\n        },\n        {\n          id: \"googleRoadmap\",\n          type: \"raster\",\n          source: \"osm-tiles\",\n          minzoom: 0,\n          maxzoom: 24,\n          layout: {\n            visibility: \"none\",\n          },\n        },\n      ],\n    },\n\n    center: [-76.493, 44.2334],\n    zoom: 2,\n    pitch: 0,\n    bearing: 0,\n    antialias: true,\n  });\n};\n"
  },
  {
    "path": "features/maps/utils/other-utils.ts",
    "content": "function stringifyNested(value: any): string {\n  if (value == null) {\n    return \"No data available\";\n  }\n\n  if (typeof value !== \"object\") {\n    return String(value);\n  }\n\n  const copy = { ...value };\n\n  if (\n    Array.isArray(copy.monoTemporalQueryValues) &&\n    copy.monoTemporalQueryValues.length === 0\n  ) {\n    delete copy.monoTemporalQueryValues;\n  }\n\n  if (\n    Array.isArray(copy.timeSeriesQueryValues) &&\n    copy.timeSeriesQueryValues.length === 0\n  ) {\n    delete copy.timeSeriesQueryValues;\n  }\n\n  if (Object.keys(copy).length === 0) {\n    return \"No data available\";\n  }\n\n  return JSON.stringify(copy, null, 2);\n}\n\nexport function formatQueryForTable(query: any): string {\n  return stringifyNested(query);\n}\n"
  },
  {
    "path": "features/maps/utils/setup-map-attributions.ts",
    "content": "import maplibregl from \"maplibre-gl\";\n\nexport const setupMapAttributions = (\n  map: maplibregl.Map,\n  googleSatelliteAttributionRef: any,\n  osmAttributionRef: any,\n  googleAttributionRef: any\n) => {\n  googleSatelliteAttributionRef.current = new maplibregl.AttributionControl({\n    compact: false,\n    customAttribution:\n      '<span style=\"font-weight: 500;\">Map data © 2024 Google</span>',\n  });\n\n  osmAttributionRef.current = new maplibregl.AttributionControl({\n    compact: false,\n    customAttribution:\n      '<span style=\"font-weight: 500;\">Map data © 2024 Google</span>',\n  });\n\n  googleAttributionRef.current = new maplibregl.AttributionControl({\n    compact: false,\n    customAttribution: \"Google\",\n  });\n\n  map.addControl(googleSatelliteAttributionRef.current, \"bottom-right\");\n  map.addControl(osmAttributionRef.current, \"bottom-right\");\n  map.addControl(googleAttributionRef.current, \"bottom-right\");\n\n  osmAttributionRef.current._container.style.display = \"none\";\n  googleAttributionRef.current._container.style.display = \"none\";\n};\n"
  },
  {
    "path": "features/maps/utils/type-guards.ts",
    "content": "// Type guard for Feature\nexport function isFeature(obj: unknown): obj is Feature {\n  return (\n    typeof obj === \"object\" &&\n    obj !== null &&\n    \"UID\" in obj &&\n    \"geometry\" in obj &&\n    \"drawnFeatureName\" in obj &&\n    \"rasterLayerName\" in obj\n  );\n}\n\n// Type guard for ROIGeometry\nexport function isROIGeometry(obj: unknown): obj is ROIGeometry {\n  return (\n    typeof obj === \"object\" &&\n    obj !== null &&\n    \"id\" in obj &&\n    \"name\" in obj &&\n    \"geometry\" in obj &&\n    \"source\" in obj\n  );\n}\n"
  },
  {
    "path": "features/text-editor/components/dynamic-text-editor.tsx",
    "content": "\"use client\";\n\nimport dynamic from \"next/dynamic\";\n\nexport const TextEditor = dynamic(() => import(\"./text-editor\"), {\n  ssr: false,\n});\n"
  },
  {
    "path": "features/text-editor/components/text-editor.tsx",
    "content": "\"use client\";\nimport React, { useEffect } from \"react\";\nimport { locales } from \"@blocknote/core\";\n\nimport \"@blocknote/core/fonts/inter.css\";\nimport { useCreateBlockNote } from \"@blocknote/react\";\nimport { useTheme } from \"next-themes\";\nimport { BlockNoteView } from \"@blocknote/mantine\";\nimport \"@blocknote/mantine/style.css\";\nimport {\n  BlockNoteEditor,\n  BlockNoteSchema,\n  defaultInlineContentSpecs,\n} from \"@blocknote/core\";\nimport {\n  DefaultReactSuggestionItem,\n  SuggestionMenuController,\n  SuggestionMenuProps,\n} from \"@blocknote/react\";\nimport { IconSparkles } from \"@tabler/icons-react\";\nimport { Mention, Command } from \"../schema/text-editor-schema\";\n\nconst schema = BlockNoteSchema.create({\n  inlineContentSpecs: {\n    ...defaultInlineContentSpecs,\n    mention: Mention,\n    command: Command,\n  },\n});\n\n// Custom Slash Menu Component\nfunction CustomSlashMenu(\n  props: SuggestionMenuProps<DefaultReactSuggestionItem>\n) {\n  return (\n    <div className=\"absolute z-50 bg-white rounded-xl shadow-lg border border-gray-200 p-2 min-w-[300px]\">\n      {props.items.map((item, index) => (\n        <div\n          key={index}\n          className={`p-2 cursor-pointer hover:bg-gray-100 rounded-xl flex items-center gap-2 ${\n            props.selectedIndex === index ? \"bg-gray-100\" : \"\"\n          }`}\n          onClick={() => {\n            props.onItemClick?.(item);\n          }}\n        >\n          {item.icon && <span className=\"w-5 h-5\">{item.icon}</span>}\n          <div>\n            <div className=\"text-sm font-semibold\">{item.title}</div>\n            {item.subtext && (\n              <div className=\"text-sm text-gray-500\">{item.subtext}</div>\n            )}\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n\n// Define a custom item to insert \"Hello World\"\nconst insertAssistantItem = (editor: BlockNoteEditor) => ({\n  title: \"Assistant\",\n  onItemClick: () => {\n    editor.addStyles({\n      textColor: \"black\",\n      bold: true,\n      backgroundColor: \"blue\",\n    });\n    editor.insertInlineContent(\"Assistant: \");\n  },\n  aliases: [\"assistant\"],\n  group: \"Custom\",\n  icon: <IconSparkles stroke={1} size={25} />,\n  subtext: \"Ask AI for analysis\",\n});\n\ninterface TextEditorProps {\n  inputText?: string;\n}\nconst TextEditor = ({ inputText }: TextEditorProps) => {\n  const locale = locales[\"en\"];\n  const { theme: appTheme } = useTheme();\n  const blocknoteTheme = appTheme === \"dark\" ? \"dark\" : \"light\";\n  // Creates a new editor instance.\n  const editor = useCreateBlockNote({\n    schema,\n    dictionary: {\n      ...locale,\n      placeholders: {\n        ...locale.placeholders,\n        default: \"Press '/' for commands\",\n        heading: \"This is a custom heading\",\n      },\n    },\n    initialContent: inputText\n      ? [\n          {\n            type: \"paragraph\",\n            content: inputText,\n          },\n        ]\n      : undefined,\n  });\n\n  useEffect(() => {\n    const initializeMarkdown = async () => {\n      if (editor && inputText) {\n        const blocks = await editor.tryParseMarkdownToBlocks(inputText);\n        editor.replaceBlocks(editor.document, blocks);\n      }\n    };\n    initializeMarkdown();\n  }, [editor, inputText]);\n\n  return (\n    <div className=\"h-full\">\n      <main>\n        <div\n          className={`bg-white dark:bg-background h-fit pb-20 rounded-b-xl pt-10 border-t-0 ${\n            inputText ? \"border-none\" : \"border border-gray-300 shadow-md\"\n          }`}\n        >\n          {editor && (\n            <BlockNoteView\n              editor={editor}\n              slashMenu={false}\n              theme={blocknoteTheme}\n            >\n              <SuggestionMenuController\n                triggerCharacter=\"/\"\n                suggestionMenuComponent={CustomSlashMenu}\n              />\n            </BlockNoteView>\n          )}\n        </div>\n      </main>\n    </div>\n  );\n};\n\nexport default TextEditor;\n"
  },
  {
    "path": "features/text-editor/schema/text-editor-schema.tsx",
    "content": "import { createReactInlineContentSpec } from \"@blocknote/react\";\nimport {\n  IconSparkles,\n  IconFolderSearch,\n  IconFileExport,\n  IconDeviceAnalytics,\n  IconWorldSearch,\n  IconDatabaseImport,\n} from \"@tabler/icons-react\";\nimport { JSX } from \"react\";\nexport const Mention = createReactInlineContentSpec(\n  {\n    type: \"mention\",\n    propSchema: {\n      mention: {\n        default: \"Unknown\",\n      },\n    },\n    content: \"none\",\n  },\n  {\n    render: (props) => (\n      <span\n        style={{ backgroundColor: \"rgba(255, 165, 0, 0.2)\" }}\n        className=\"p-1 rounded-md font-bold text-sm\"\n      >\n        @{props.inlineContent.props.mention}\n      </span>\n    ),\n  }\n);\n\nexport const Command = createReactInlineContentSpec(\n  {\n    type: \"command\",\n    propSchema: {\n      command: {\n        default: \"Unknown\",\n      },\n    },\n    content: \"none\",\n  },\n  {\n    render: (props) => {\n      return (\n        <span\n          className={`font-bold text-sm rounded-md gap-1 inline-flex items-center px-2 py-1`}\n        >\n          {props.inlineContent.props.command}\n        </span>\n      );\n    },\n  }\n);\n"
  },
  {
    "path": "features/ui/modals/file-upload-modal.tsx",
    "content": "import React, { useState, useCallback } from \"react\";\nimport { IconX, IconUpload, IconFile } from \"@tabler/icons-react\";\nimport { useDropzone } from \"react-dropzone\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, X } from \"lucide-react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogFooter,\n} from \"@/components/ui/dialog\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { Badge } from \"@/components/ui/badge\";\n\ninterface Folder {\n  name: string;\n  id?: string | number;\n}\n\ninterface FileUploadModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n  onFileSelect: (files: FileList) => void;\n  isUploadComplete: boolean;\n  setUploadComplete: (value: boolean) => void;\n  currentFolder?: Folder | null;\n  acceptedFileTypes?: string;\n  title?: string;\n  multiple?: boolean;\n}\n\nconst FileUploadModal: React.FC<FileUploadModalProps> = ({\n  isOpen,\n  onClose,\n  onFileSelect,\n  currentFolder = null,\n  isUploadComplete,\n  setUploadComplete,\n  acceptedFileTypes = \".pdf,.doc,.docx,.txt\",\n  title = \"Add New Document\",\n  multiple = true,\n}) => {\n  const [selectedFiles, setSelectedFiles] = useState<File[]>([]);\n\n  const createFileList = (files: File[]): FileList => {\n    const dataTransfer = new DataTransfer();\n    files.forEach((file) => dataTransfer.items.add(file));\n    return dataTransfer.files;\n  };\n\n  const onDrop = useCallback((acceptedFiles: File[]) => {\n    setSelectedFiles(acceptedFiles);\n  }, []);\n\n  const { getRootProps, getInputProps, isDragActive } = useDropzone({\n    onDrop,\n    multiple,\n    accept: {\n      \"application/pdf\": [\".pdf\"],\n    },\n  });\n\n  const handleConfirmUpload = async () => {\n    if (selectedFiles.length > 0) {\n      setUploadComplete(false);\n      try {\n        const fileList = createFileList(selectedFiles);\n        await onFileSelect(fileList);\n        setSelectedFiles([]);\n        onClose();\n      } catch (error) {\n        console.error(\"Error during file upload:\", error);\n      } finally {\n        setUploadComplete(true);\n      }\n    }\n  };\n\n  const handleCancel = () => {\n    setSelectedFiles([]);\n    onClose();\n  };\n\n  const formatFileSize = (bytes: number): string => {\n    if (bytes === 0) return \"0 Bytes\";\n    const k = 1024;\n    const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\"];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + \" \" + sizes[i];\n  };\n\n  return (\n    <Dialog open={isOpen} onOpenChange={handleCancel}>\n      <DialogContent className=\"sm:max-w-[500px]\">\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n        </DialogHeader>\n\n        {selectedFiles.length === 0 ? (\n          <div\n            {...getRootProps()}\n            className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors\n              ${\n                isDragActive\n                  ? \"border-primary bg-primary/5\"\n                  : \"border-border hover:border-primary/50\"\n              }`}\n          >\n            <input {...getInputProps()} />\n            <IconUpload\n              size={32}\n              className=\"mx-auto text-muted-foreground mb-4\"\n            />\n            <p className=\"text-sm text-muted-foreground mb-2\">\n              {isDragActive\n                ? \"Drop the files here...\"\n                : \"Drop your files here or click to upload\"}\n            </p>\n            <p className=\"text-xs text-muted-foreground mb-4\">\n              Supported format: PDF\n            </p>\n            <Button variant=\"outline\">Select Files</Button>\n          </div>\n        ) : (\n          <ScrollArea className=\"h-[250px] w-full rounded-md border p-4\">\n            {selectedFiles.map((file, index) => (\n              <div\n                key={index}\n                className=\"flex items-center p-3 rounded-md mb-2 bg-muted\"\n              >\n                <IconFile size={20} className=\"text-muted-foreground mr-3\" />\n                <div className=\"flex-grow\">\n                  <p className=\"text-sm font-medium truncate\">{file.name}</p>\n                  <p className=\"text-xs text-muted-foreground\">\n                    {formatFileSize(file.size)}\n                  </p>\n                </div>\n              </div>\n            ))}\n          </ScrollArea>\n        )}\n\n        <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n          <span>Selected folder:</span>\n          <Badge variant=\"outline\" className=\"font-normal\">\n            {currentFolder ? currentFolder.name : \"All Documents\"}\n          </Badge>\n        </div>\n\n        <DialogFooter>\n          <Button variant=\"ghost\" size=\"sm\" onClick={handleCancel}>\n            Cancel\n          </Button>\n          {selectedFiles.length > 0 && (\n            <Button\n              onClick={handleConfirmUpload}\n              disabled={!isUploadComplete}\n              size=\"sm\"\n            >\n              {!isUploadComplete && <Loader2 className=\"mr-2 animate-spin\" />}\n              Upload\n              {multiple && selectedFiles.length > 1\n                ? ` (${selectedFiles.length} files)`\n                : \"\"}\n            </Button>\n          )}\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport default FileUploadModal;\n"
  },
  {
    "path": "features/ui/toast-message.tsx",
    "content": "\"use client\";\nimport { useEffect } from \"react\";\nimport { toast, ToastPosition } from \"react-hot-toast\";\nimport useToastMessageStore from \"@/stores/use-toast-message-store\";\nimport { useTheme } from \"next-themes\";\nimport {\n  IconCircleCheckFilled,\n  IconCircleXFilled,\n  IconAlertCircleFilled,\n  IconInfoCircleFilled,\n  IconX,\n} from \"@tabler/icons-react\";\n\nexport default function ToastMessage() {\n  const { toastMessage, toastType, toastId, setToastMessage } =\n    useToastMessageStore();\n  const { theme } = useTheme();\n\n  useEffect(() => {\n    // If toastId is 0, or no message, skip\n    if (!toastId || !toastMessage) return;\n\n    // Otherwise, show the toast\n    // (same code you had before)\n    const baseToastOptions = {\n      duration: toastType === \"error\" ? Infinity : 5000,\n      position: \"custom\" as ToastPosition,\n      className: \"mr-3\",\n    };\n\n    const darkBgText = \"bg-muted text-foreground\";\n    const lightBgText = \"bg-[#fffdf7] text-gray-700\";\n\n    const toastStyles = {\n      success: {\n        background: theme === \"dark\" ? darkBgText : lightBgText,\n        ring: \"ring-green-600/10\",\n        shadowColor: \"shadow-green-600/10\",\n        icon: <IconCircleCheckFilled size={20} className=\"text-green-600\" />,\n      },\n      error: {\n        background: theme === \"dark\" ? darkBgText : lightBgText,\n        ring: \"ring-red-600/10\",\n        shadowColor: \"shadow-red-600/10\",\n        icon: <IconCircleXFilled size={20} className=\"text-red-600\" />,\n      },\n      warning: {\n        background: theme === \"dark\" ? darkBgText : lightBgText,\n        ring: \"ring-amber-500/10\",\n        shadowColor: \"shadow-amber-500/10\",\n        icon: <IconAlertCircleFilled size={20} className=\"text-amber-500\" />,\n      },\n      default: {\n        background: theme === \"dark\" ? darkBgText : lightBgText,\n        ring: \"ring-blue-600/10\",\n        shadowColor: \"shadow-blue-600/10\",\n        icon: <IconInfoCircleFilled size={20} className=\"text-blue-600\" />,\n      },\n    };\n\n    const selectedStyle = toastStyles[toastType || \"default\"];\n\n    const CustomToast = ({ message, t, visible }: any) => (\n      <div className=\"fixed bottom-8 right-3\">\n        <div\n          className={`\n            ${selectedStyle.background}\n            ${selectedStyle.ring}\n            ${selectedStyle.shadowColor}\n            shadow-lg ring-1 rounded-xl w-[400px]\n            border border-stone-300 dark:border-stone-600\n            transition-all duration-300 ease-in-out transform\n            ${\n              visible\n                ? \"translate-x-0 opacity-100\"\n                : \"translate-x-full opacity-0\"\n            }\n            relative\n          `}\n        >\n          <div className=\"flex gap-3 py-8 px-5\">\n            <div className=\"shrink-0 mt-1\">{selectedStyle.icon}</div>\n            <div className=\"flex-1 min-w-0\">\n              <div className=\"text-[15px] leading-relaxed font-medium break-words whitespace-pre-wrap line-clamp-4\">\n                {message}\n              </div>\n            </div>\n            <button\n              onClick={() => toast.dismiss(t.id)}\n              className=\"shrink-0 -mt-2 -me-2 p-2 rounded-lg text-gray-400/80 hover:text-gray-400 hover:bg-gray-50 transition-all absolute top-3 right-3\"\n              aria-label=\"Close\"\n            >\n              <IconX size={16} />\n            </button>\n          </div>\n        </div>\n      </div>\n    );\n\n    const toastOptions = {\n      ...baseToastOptions,\n      style: {\n        background: \"transparent\",\n        boxShadow: \"none\",\n        padding: 0,\n      },\n    };\n\n    const toastIdVal = toast.custom(\n      (t: { id: string; visible: boolean }) => (\n        <CustomToast message={toastMessage} t={t} visible={t.visible} />\n      ),\n      toastOptions\n    );\n\n    // Optionally: auto-dismiss if user clicks anywhere\n    const handleClick = () => {\n      const toastElement = document.querySelector(\n        `[data-toast-id=\"${toastIdVal}\"]`\n      );\n      if (toastElement) {\n        toastElement.classList.add(\"translate-x-full\", \"opacity-0\");\n        setTimeout(() => {\n          toast.dismiss(toastIdVal);\n        }, 300);\n      } else {\n        toast.dismiss(toastIdVal);\n      }\n    };\n\n    document.addEventListener(\"click\", handleClick);\n\n    // Cleanup\n    return () => {\n      document.removeEventListener(\"click\", handleClick);\n      // Optionally clear message:\n      // setToastMessage(\"\"); // If you want the same message to show again easily\n    };\n  }, [toastId, toastMessage, toastType, theme, setToastMessage]);\n\n  return null;\n}\n"
  },
  {
    "path": "features/user-profile/components/user-profile-modal.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect, useTransition } from \"react\";\nimport { IconUserSquareRounded } from \"@tabler/icons-react\";\nimport { useUserStore } from \"@/stores/use-user-profile-store\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\nimport { logout } from \"@/app/(auth)/login/actions\";\nimport { Label } from \"@/components/ui/label\";\nimport { Button } from \"@/components/ui/button\";\nimport { Loader2, User } from \"lucide-react\";\n\nimport {\n  Dialog,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n  DialogDescription,\n  DialogFooter,\n} from \"@/components/ui/dialog\";\n\nexport default function UserProfile() {\n  const {\n    userName,\n    userEmail,\n    userRole,\n    userOrganization,\n    licenseStartDate,\n    licenseEndDate,\n    usageRequests,\n    usageDocs,\n    maxRequests,\n    maxDocs,\n    maxArea,\n    fetchAndSetUsage,\n  } = useUserStore();\n\n  const isSidebarCollapsed = useButtonsStore(\n    (state) => state.isSidebarCollapsed\n  );\n\n  const [isPending, startTransition] = useTransition();\n\n  useEffect(() => {\n    fetchAndSetUsage();\n  }, [fetchAndSetUsage]);\n\n  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {\n    e.preventDefault();\n    startTransition(() => {\n      logout();\n    });\n  }\n\n  return (\n    <section className=\"z-[5000]\">\n      <Dialog>\n        {/* Trigger: clickable icon (and optional label) */}\n        <DialogTrigger asChild>\n          <div className=\"px-4 py-2 text-accent-foreground cursor-pointer text-sm font-normal\">\n            <div\n              className={`flex items-center px-3 py-2 gap-4 w-full rounded-xl text-gray-100 hover:bg-muted dark:hover:bg-muted-foreground/20 hover:text-foreground ${\n                isSidebarCollapsed ? \"justify-center\" : \"justify-start\"\n              }`}\n            >\n              <button>\n                <IconUserSquareRounded\n                  stroke={1.5}\n                  className=\"h-7 w-7 flex-shrink-0\"\n                />\n              </button>\n              {!isSidebarCollapsed && <span>Profile</span>}\n            </div>\n          </div>\n        </DialogTrigger>\n\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Your Profile</DialogTitle>\n            <DialogDescription>\n              View your account details and license information.\n            </DialogDescription>\n          </DialogHeader>\n\n          {/* Modal Body */}\n          <div className=\"flex flex-col items-center px-6 py-4 space-y-4\">\n            {/* Avatar */}\n            <div className=\"flex h-24 w-24 items-center justify-center rounded-full bg-muted\">\n              <User size={48} className=\"text-muted-foreground\" />\n            </div>\n\n            {/* Name & Email */}\n            <div className=\"text-center space-y-1\">\n              <h3 className=\"text-lg font-semibold\">{userName || \"—\"}</h3>\n              <p className=\"text-sm text-muted-foreground\">\n                {userEmail || \"—\"}\n              </p>\n            </div>\n\n            {/* Info Grid */}\n            <div className=\"grid w-full max-w-md grid-cols-2 gap-4 mt-3 text-center\">\n              {/* Role */}\n              <div className=\"flex flex-col\">\n                <Label className=\"mb-0.5 text-xs uppercase text-muted-foreground\">\n                  Role\n                </Label>\n                <p className=\"text-sm font-medium\">{userRole || \"—\"}</p>\n              </div>\n\n              {/* Organization */}\n              <div className=\"flex flex-col\">\n                <Label className=\"mb-0.5 text-xs uppercase text-muted-foreground\">\n                  Organization\n                </Label>\n                <p className=\"text-sm font-medium\">{userOrganization || \"—\"}</p>\n              </div>\n\n              {/* License Start */}\n              <div className=\"flex flex-col\">\n                <Label className=\"mb-0.5 text-xs uppercase text-muted-foreground\">\n                  License Start\n                </Label>\n                <p className=\"text-sm font-medium\">\n                  {licenseStartDate\n                    ? licenseStartDate.split(\"T\")[0]\n                    : \"Not Available\"}\n                </p>\n              </div>\n\n              {/* License End */}\n              <div className=\"flex flex-col\">\n                <Label className=\"mb-0.5 text-xs uppercase text-muted-foreground\">\n                  License End\n                </Label>\n                <p className=\"text-sm font-medium\">\n                  {licenseEndDate\n                    ? licenseEndDate.split(\"T\")[0]\n                    : \"Not Available\"}\n                </p>\n              </div>\n\n              {/* Requests Usage */}\n              <div className=\"flex flex-col\">\n                <Label className=\"mb-0.5 text-xs uppercase text-muted-foreground\">\n                  Requests Used\n                </Label>\n                <p className=\"text-sm font-medium\">\n                  {maxRequests > 0 ? `${usageRequests} / ${maxRequests}` : \"—\"}\n                </p>\n              </div>\n\n              {/* Docs Usage */}\n              <div className=\"flex flex-col\">\n                <Label className=\"mb-0.5 text-xs uppercase text-muted-foreground\">\n                  Knowledge Base Docs\n                </Label>\n                <p className=\"text-sm font-medium\">\n                  {maxDocs > 0 ? `${usageDocs} / ${maxDocs}` : \"—\"}\n                </p>\n              </div>\n            </div>\n          </div>\n\n          {/* Modal Footer with the Logout button */}\n          <DialogFooter>\n            <form onSubmit={handleSubmit}>\n              <Button type=\"submit\" variant=\"destructive\" disabled={isPending}>\n                {isPending ? (\n                  <>\n                    <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                    Logging out...\n                  </>\n                ) : (\n                  \"Logout\"\n                )}\n              </Button>\n            </form>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    </section>\n  );\n}\n"
  },
  {
    "path": "hooks/docs-hooks/use-document-upload.ts",
    "content": "\"use client\";\n\nimport { useCallback } from \"react\";\nimport { processAndUploadDocumentFile } from \"@/features/knowledge-base/actions/document-actions\";\n\nexport type FileUploadHookProps = {\n  currentFolder: { id: string } | null;\n};\n\nexport const useDocumentUpload = ({ currentFolder }: FileUploadHookProps) => {\n  const handleFileUpload = useCallback(\n    async (event: React.ChangeEvent<HTMLInputElement>) => {\n      const files = event.target.files;\n      if (!files) return;\n\n      try {\n        await Promise.all(\n          Array.from(files).map(async (file) => {\n            const result = await processAndUploadDocumentFile({\n              file,\n              // folderId: currentFolder?.id || null,\n              folderId:\n                currentFolder?.id === \"all-documents\"\n                  ? null\n                  : currentFolder?.id || null,\n            });\n\n            if (!result.success) {\n              throw new Error(result.error);\n            }\n          })\n        );\n      } catch (error) {\n        throw new Error(`Error processing files: ${error}`);\n      }\n    },\n    [currentFolder]\n  );\n\n  return { handleFileUpload };\n};\n"
  },
  {
    "path": "hooks/docs-hooks/use-document-viewer.ts",
    "content": "\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { fetchByDocumentName } from \"@/features/knowledge-base/actions/document-actions\";\n\nexport const useDocumentViewer = (documentName: string) => {\n  const [pdfUrl, setPdfUrl] = useState<string | null>(null);\n  const [error, setError] = useState<string | null>(null);\n  const [isLoading, setIsLoading] = useState(true);\n\n  useEffect(() => {\n    const loadDocument = async () => {\n      try {\n        setIsLoading(true);\n        const signedUrl = await fetchByDocumentName(documentName);\n        if (signedUrl) {\n          setPdfUrl(signedUrl);\n        } else {\n          setError(\"Document not found\");\n        }\n      } catch (err: any) {\n        setError(err.message || \"Failed to load document\");\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    loadDocument();\n  }, [documentName]);\n\n  return { pdfUrl, error, isLoading };\n};\n"
  },
  {
    "path": "lib/auth.ts",
    "content": "// In production, this is handled in the database. You can use the Supabase UI to add a new row to the `user_roles` table.\n\"use server\";\n\nexport type UserRole = \"ADMIN\" | \"USER\" | \"TRIAL\";\nexport type SubscriptionTier = \"Essentials\" | \"Pro\" | \"Enterprise\";\n\nexport interface PermissionSet {\n  maxRequests: number;\n  maxDocs: number;\n  maxArea: number;\n  experimental: boolean;\n}\n\nconst PERMISSION_MATRIX: Record<\n  UserRole,\n  Record<SubscriptionTier, PermissionSet>\n> = {\n  ADMIN: {\n    Essentials: {\n      maxRequests: 100,\n      maxDocs: 100,\n      maxArea: 100,\n      experimental: false,\n    },\n    Pro: { maxRequests: 100, maxDocs: 100, maxArea: 100, experimental: false },\n    Enterprise: {\n      maxRequests: 9999,\n      maxDocs: 9999,\n      maxArea: 9999,\n      experimental: true,\n    },\n  },\n  USER: {\n    Essentials: {\n      maxRequests: 100,\n      maxDocs: 50,\n      maxArea: 100,\n      experimental: false,\n    },\n    Pro: {\n      maxRequests: 2000,\n      maxDocs: 100,\n      maxArea: 2000,\n      experimental: false,\n    },\n    Enterprise: {\n      maxRequests: 100,\n      maxDocs: 200,\n      maxArea: 100,\n      experimental: false,\n    },\n  },\n  TRIAL: {\n    Essentials: {\n      maxRequests: 10,\n      maxDocs: 10,\n      maxArea: 10,\n      experimental: false,\n    },\n    Pro: { maxRequests: 50, maxDocs: 20, maxArea: 50, experimental: false },\n    Enterprise: {\n      maxRequests: 30,\n      maxDocs: 50,\n      maxArea: 1,\n      experimental: false,\n    },\n  },\n};\n\nexport async function getPermissionSet(\n  role: UserRole,\n  subscriptionTier: SubscriptionTier\n): Promise<PermissionSet> {\n  return Promise.resolve(PERMISSION_MATRIX[role][subscriptionTier]);\n}\n"
  },
  {
    "path": "lib/changelog.ts",
    "content": "export interface ChangelogEntry {\n  version: string;\n  date: string;\n  content: string;\n}\nconst appVersion = process.env.NEXT_PUBLIC_APP_VERSION;\nexport const changelog: ChangelogEntry[] = [\n  {\n    version: `${appVersion}`,\n    date: \"2025-02-19\",\n    content: `\n\n  #### New Features\n  - Added a capability to load any dataset on Google Earth Engine (GEE).\n  - Add new database query function to find GEE datasets based on user's search query (given by the LLM).\n  - Added a web scraper function to retrieve information on the dataset selected for the user.\n  \n  #### Improvements\n  - Updated the Vercel AI SDK to have better streaming experience.\n  - Cleaned up the codebase to improve clarity.\n  \n  #### Bug Fixes\n   - Fixed some minor bugs.\n  \n      `.trim(),\n  },\n];\n"
  },
  {
    "path": "lib/database/chat/queries.ts",
    "content": "\"use server\";\nimport { createClient } from \"@/utils/supabase/server\";\nimport { NextResponse } from \"next/server\";\n\n// Get all chats by user\nexport async function getChatsByUser(userId: string) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error: userError,\n  } = await supabase.auth.getUser();\n\n  if (userError || !user) {\n    return NextResponse.json({ message: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const { data, error } = await supabase\n    .from(\"chats\")\n    .select(\"*\")\n    .eq(\"userId\", userId)\n    .order(\"createdAt\", { ascending: false });\n\n  if (error) {\n    console.error(\"Failed to get chats\", error);\n    throw new Error(\"Failed to get chats\");\n  }\n\n  return data;\n}\n\n// Save chat by id\nexport async function saveChat({ id, title }: { id: string; title: string }) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error: userError,\n  } = await supabase.auth.getUser();\n\n  if (userError || !user) {\n    return NextResponse.json({ message: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const userId = user.id;\n\n  const { error } = await supabase.from(\"chats\").insert({\n    id, // Chat ID\n    userId: userId, // Associate the chat with the authenticated user's ID\n    chatTitle: title, // Chat title\n    createdAt: new Date(), // Timestamp\n  });\n\n  if (error) {\n    console.error(\"Failed to save chat\", error);\n    throw new Error(\"Failed to save chat\");\n  }\n\n  return NextResponse.json(\n    { message: \"Chat saved successfully\" },\n    { status: 200 }\n  );\n}\n\n// Get chat by id\nexport async function getChatById(chatId: string) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error: userError,\n  } = await supabase.auth.getUser();\n\n  if (userError || !user) {\n    return NextResponse.json({ message: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const userId = user.id;\n\n  // Fetch the chat and ensure it belongs to the authenticated user\n  const { data: chat, error: chatError } = await supabase\n    .from(\"chats\")\n    .select(\"*\")\n    .eq(\"id\", chatId)\n    .eq(\"userId\", userId);\n\n  return chat && chat.length > 0 ? chat : null;\n}\n\n// Save messages\nexport async function saveMessages({\n  messages,\n}: {\n  messages: Array<\n    | {\n        chatId?: string;\n        id: string;\n        role: \"assistant\" | \"tool\";\n        content: any;\n        createdAt?: Date;\n      }\n    | {\n        id: string;\n        createdAt: Date;\n        chatId: string;\n        role?: \"user\" | undefined;\n        content?: any;\n        experimental_providerMetadata?: any;\n      }\n  >;\n}) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error: userError,\n  } = await supabase.auth.getUser();\n\n  if (userError || !user) {\n    return NextResponse.json({ message: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const { error } = await supabase.from(\"messages\").insert(messages);\n\n  if (error) {\n    console.error(\"Failed to save messages\", error);\n    throw new Error(\"Failed to save messages\");\n  }\n\n  return { message: \"Messages saved successfully\" };\n}\n\n// Get messages by chat id\nexport async function getMessagesByChatId(chatId: string) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error: userError,\n  } = await supabase.auth.getUser();\n\n  if (userError || !user) {\n    return NextResponse.json({ message: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const { data, error } = await supabase\n    .from(\"messages\")\n    .select(\"*\")\n    .eq(\"chatId\", chatId)\n    .order(\"createdAt\", { ascending: true });\n\n  if (error) {\n    console.error(\"Failed to get messages\", error);\n    throw new Error(\"Failed to get messages\");\n  }\n\n  return data;\n}\n\nexport async function getDraftedReportById(draftedReportId: string) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error: userError,\n  } = await supabase.auth.getUser();\n\n  if (userError || !user) {\n    return NextResponse.json({ message: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const userId = user.id;\n\n  // Fetch the drafted report and ensure it belongs to the authenticated user\n  const { data: draftedReport, error: reportError } = await supabase\n    .from(\"drafted_reports\")\n    .select(\"*\")\n    .eq(\"id\", draftedReportId) // Match by the drafted report ID\n    .eq(\"userId\", userId) // Ensure the report belongs to the authenticated user\n    .single();\n\n  if (reportError || !draftedReport) {\n    return NextResponse.json(\n      { message: \"Drafted report not found or not authorized\" },\n      { status: 404 }\n    );\n  }\n\n  return NextResponse.json(draftedReport, { status: 200 });\n}\n\n// Delete chat by id\nexport async function deleteChatById(chatId: string) {\n  const supabase = await createClient();\n  const {\n    data: { user },\n    error: userError,\n  } = await supabase.auth.getUser();\n\n  if (userError || !user) {\n    return NextResponse.json({ message: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  const userId = user.id;\n\n  // First, check if the chat belongs to the user\n  const { data: chat, error: chatError } = await supabase\n    .from(\"chats\")\n    .select(\"id, userId\")\n    .eq(\"id\", chatId)\n    .single();\n\n  if (chatError || !chat || chat.userId !== userId) {\n    return NextResponse.json(\n      { message: \"Chat not found or not authorized\" },\n      { status: 404 }\n    );\n  }\n\n  // Delete messages associated with the chat\n  const { error: messageError } = await supabase\n    .from(\"messages\")\n    .delete()\n    .eq(\"chatId\", chatId);\n\n  if (messageError) {\n    console.error(\"Failed to delete messages\", messageError);\n    throw new Error(\"Failed to delete messages\");\n  }\n\n  // Delete the chat\n  const { error: chatDeleteError } = await supabase\n    .from(\"chats\")\n    .delete()\n    .eq(\"id\", chatId);\n\n  if (chatDeleteError) {\n    console.error(\"Failed to delete chat\", chatDeleteError);\n    throw new Error(\"Failed to delete chat\");\n  }\n\n  return { message: \"Chat and associated messages deleted successfully\" };\n}\n\n// Search Google Earth Engine (GEE) datasets\nexport async function searchGeeDatasets(query: string) {\n  const supabase = await createClient();\n  const { data: authData, error: authError } = await supabase.auth.getUser();\n  if (authError || !authData?.user) {\n    return NextResponse.json({ message: \"Unauthenticated!\" }, { status: 401 });\n  }\n\n  if (!query || query.trim() === \"\") {\n    return [];\n  }\n\n  const { data: matches, error } = await supabase.rpc(\n    \"search_gee_datasets_ft\",\n    {\n      query,\n    }\n  );\n  if (error) {\n    console.error(\"Error performing full text search:\", error);\n    return [];\n  }\n\n  return matches || [];\n}\n"
  },
  {
    "path": "lib/database/chat/tools.ts",
    "content": "\"use server\";\nimport { answerQuery } from \"@/app/actions/rag-actions\";\nimport {\n  calculateGeometryArea,\n  checkGeometryAreaIsLessThanThreshold,\n} from \"@/features/maps/utils/geometry-utils\";\nimport { azure } from \"@ai-sdk/azure\";\nimport { convertToCoreMessages, generateText } from \"ai\";\nimport { NextResponse } from \"next/server\";\n\nexport async function requestGeospatialAnalysis(args: any) {\n  const {\n    functionType,\n    startDate1String,\n    endDate1String,\n    startDate2String,\n    endDate2String,\n    aggregationMethod,\n    layerName,\n    title,\n    cookieStore,\n    selectedRoiGeometryInChat,\n    maxArea,\n    experimental,\n  } = args;\n\n  const selectedRoiGeometry = selectedRoiGeometryInChat?.geometry;\n  if (!selectedRoiGeometry) {\n    return {\n      error:\n        \"It seems you didn't provide a valid region of interest (ROI) for the analysis. you need to provide an ROI through importing a shapefile/geojson file or drawing a shape on the map.\",\n    };\n  }\n\n  if (\n    selectedRoiGeometry.type !== \"Polygon\" &&\n    selectedRoiGeometry.type !== \"MultiPolygon\" &&\n    selectedRoiGeometry.type !== \"FeatureCollection\"\n  ) {\n    return {\n      error:\n        \"Selected ROI geometry must be a Polygon, MultiPolygon, or a FeatureCollection of polygons.\",\n    };\n  }\n\n  // If it's a FeatureCollection, ensure every feature's geometry is a Polygon or MultiPolygon.\n  if (selectedRoiGeometry.type === \"FeatureCollection\") {\n    for (const feature of selectedRoiGeometry.features) {\n      if (\n        !feature.geometry ||\n        (feature.geometry.type !== \"Polygon\" &&\n          feature.geometry.type !== \"MultiPolygon\")\n      ) {\n        return {\n          error: \"All features in the ROI must be polygons.\",\n        };\n      }\n    }\n  }\n\n  const geometryAreaCheckResult = checkGeometryAreaIsLessThanThreshold(\n    selectedRoiGeometryInChat?.geometry,\n    maxArea\n  );\n  const areaSqKm = calculateGeometryArea(selectedRoiGeometryInChat?.geometry);\n  if (!geometryAreaCheckResult) {\n    return {\n      error: `The area of the selected region of interest (ROI) is ${areaSqKm} sq km, which exceeds the maximum area limit of ${maxArea} sq km. Please select a smaller ROI and try again.`,\n    };\n  }\n\n  const url = new URL(\n    \"/api/gee/request-geospatial-analysis\",\n    process.env.BASE_URL\n  );\n\n  const payload = {\n    functionType,\n    startDate1: startDate1String,\n    endDate1: endDate1String,\n    startDate2: startDate2String,\n    endDate2: endDate2String,\n    aggregationMethod,\n    selectedRoiGeometry,\n    experimental,\n  };\n\n  try {\n    const response = await fetch(url.toString(), {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        cookie: cookieStore || \"\",\n      },\n      body: JSON.stringify(payload),\n    });\n\n    if (!response.ok) {\n      const errorData = await response.json();\n      console.error(\n        \"Error during fetch:\",\n        errorData.error || response.statusText\n      );\n      return NextResponse.json(\n        { error: \"Failed to run the analysis\" },\n        { status: 500 }\n      );\n    }\n\n    const data = await response.json();\n\n    // This is not suitable for production, but it's a good way to check if the response is correct\n    if (Object.keys(data.mapStats).length === 0) {\n      return {\n        error: \"Something went wrong! Failed to run the analysis.\",\n      };\n    }\n    return {\n      ...data,\n      title,\n      layerName,\n      functionType,\n      startDate1: startDate1String,\n      endDate1: endDate1String,\n      startDate2: startDate2String,\n      endDate2: endDate2String,\n      aggregationMethod,\n      selectedRoiGeometry: selectedRoiGeometryInChat,\n    };\n  } catch (error) {\n    console.error(\"Error during fetch:\", error);\n    return NextResponse.json(\n      { error: \"Failed to run the analysis\" },\n      { status: 500 }\n    );\n  }\n}\n\nexport async function requestLoadingGeospatialData(args: any) {\n  const {\n    geospatialDataType,\n    dataType,\n    selectedRoiGeometryInChat,\n    datasetId,\n    layerName,\n    title,\n    startDate,\n    endDate,\n    divideValue,\n    visParams,\n    labelNames,\n    cookieStore,\n  } = args;\n\n  const selectedRoiGeometry = selectedRoiGeometryInChat?.geometry;\n\n  if (!selectedRoiGeometry) {\n    return {\n      error:\n        \"It seems you didn't provide a valid region of interest (ROI). You need to provide an ROI through importing a shapefile/geojson file or drawing a shape on the map.\",\n    };\n  }\n\n  if (\n    selectedRoiGeometry.type !== \"Polygon\" &&\n    selectedRoiGeometry.type !== \"MultiPolygon\" &&\n    selectedRoiGeometry.type !== \"FeatureCollection\"\n  ) {\n    return {\n      error:\n        \"Selected ROI geometry must be a Polygon, MultiPolygon, or a FeatureCollection of polygons.\",\n    };\n  }\n\n  if (selectedRoiGeometry.type === \"FeatureCollection\") {\n    for (const feature of selectedRoiGeometry.features) {\n      if (\n        !feature.geometry ||\n        (feature.geometry.type !== \"Polygon\" &&\n          feature.geometry.type !== \"MultiPolygon\")\n      ) {\n        return {\n          error: \"All features in the ROI must be polygons.\",\n        };\n      }\n    }\n  }\n\n  const url = new URL(\n    \"/api/gee/request-loading-geospatial-data\",\n    process.env.BASE_URL\n  );\n\n  const payload = {\n    geospatialDataType,\n    dataType,\n    datasetId,\n    startDate,\n    endDate,\n    divideValue,\n    visParams,\n    labelNames,\n    selectedRoiGeometry,\n  };\n\n  try {\n    const response = await fetch(url.toString(), {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        cookie: cookieStore || \"\",\n      },\n      body: JSON.stringify(payload),\n    });\n\n    const data = await response.json();\n\n    if (!response.ok) {\n      console.error(\"Error during fetch:\", data.error || response.statusText);\n      return {\n        error:\n          typeof data.error === \"string\"\n            ? data.error\n            : \"Failed to load geospatial data\",\n      };\n    }\n\n    if (!data.urlFormat || Object.keys(data.mapStats).length === 0) {\n      return {\n        error: \"Something went wrong! Failed to load geospatial data.\",\n      };\n    }\n\n    return {\n      ...data,\n      layerName,\n      title,\n      datasetId,\n      startDate,\n      endDate,\n      geospatialDataType,\n      selectedRoiGeometry: selectedRoiGeometryInChat,\n    };\n  } catch (error) {\n    console.error(\"Error during fetch:\", error);\n    return {\n      error: \"Failed to load geospatial data.\",\n    };\n  }\n}\n\n// Tool to request a RAG query\nexport async function requestRagQuery(args: any) {\n  const { query, title } = args; // Extract query parameter from arguments\n\n  try {\n    const data = await answerQuery(query);\n\n    return { data, title };\n  } catch (error) {\n    console.error(\"Error during RAG fetch:\", error);\n    return NextResponse.json({ error: \"Failed to fetch RAG\" }, { status: 500 });\n  }\n}\n\n// Tool to generate a report based on the conversation history\nexport async function draftReport(args: any) {\n  try {\n    const { messages, title, reportFileName } = args;\n\n    const relevantMessages = messages.filter(\n      (msg: any) =>\n        msg.role === \"user\" ||\n        (msg.role === \"assistant\" &&\n          !msg.content.startsWith(\"You are an AI Assistant\"))\n    );\n\n    // Create a prompt that focuses on synthesizing the existing conversation\n    const reportPrompt = {\n      role: \"user\",\n      content: `Please draft a comprehensive report based on our previous conversation and analyses. The report should NOT inlcude your own comments. \n            Format it with the following structure:\n            - Introduction: Brief context and purpose\n            - Analyses Performed: Summary of conducted analyses\n            - Key Findings: Important results, patterns, and trends\n            - Limitations and Caveats: Important constraints\n            - Recommendations & Next Steps: Future suggestions.\"`,\n    };\n\n    // Use all relevant conversation history plus the report request\n    const conversationContext = [...relevantMessages, reportPrompt];\n\n    const reportResponse = await generateText({\n      model: azure(\"gpt-4o\"),\n      messages: convertToCoreMessages(conversationContext),\n      tools: {}, // Empty tools object since we don't need tools for report generation\n    });\n\n    // For simplicity here, assume it's resolved into a single string once complete.\n    const report = await reportResponse.text;\n\n    return {\n      report,\n      title,\n      reportFileName,\n    };\n  } catch (error) {\n    console.error(\"Error generating report:\", error);\n    return NextResponse.json(\n      { error: \"Failed to draft report\" },\n      { status: 500 }\n    );\n  }\n}\n\nexport async function requestWebScraping(args: any) {\n  const { url, title } = args;\n\n  try {\n    const baseUrl = process.env.BASE_URL;\n\n    const response = await fetch(\n      new URL(\n        `/api/web-scraper?url=${encodeURIComponent(url)}`,\n        baseUrl\n      ).toString()\n    );\n    const result = await response.json();\n\n    if (result.success) {\n      return result.data;\n    } else {\n      return { error: \"Failed to scrape the webpage\" };\n    }\n  } catch (error) {\n    console.error(\"Error during fetch:\", error);\n    return { error: \"Failed to scrape the webpage\" };\n  }\n}\n"
  },
  {
    "path": "lib/database/usage.ts",
    "content": "\"use server\";\nimport { createClient } from \"@/utils/supabase/server\";\n\nexport async function incrementRequestCount(userId: string) {\n  const supabase = await createClient();\n\n  const { data: usageData, error: usageError } = await supabase\n    .from(\"user_usage\")\n    .select(\"*\")\n    .eq(\"user_id\", userId)\n    .single();\n\n  if (usageError || !usageData) {\n    await supabase.from(\"user_usage\").insert({\n      user_id: userId,\n      requests_count: 1,\n      docs_uploaded_count: 0,\n    });\n    return 1;\n  }\n\n  const newCount = usageData.requests_count + 1;\n\n  const { error: updateError } = await supabase\n    .from(\"user_usage\")\n    .update({ requests_count: newCount })\n    .eq(\"user_id\", userId);\n\n  if (updateError) {\n    console.error(\"Failed to update request count\", updateError);\n    return usageData.requests_count;\n  }\n\n  return newCount;\n}\n\nexport async function getUsageForUser(userId: string) {\n  const supabase = await createClient();\n\n  const { data, error } = await supabase\n    .from(\"user_usage\")\n    .select(\"*\")\n    .eq(\"user_id\", userId)\n    .single();\n\n  if (error || !data) {\n    return { requests_count: 0, knowledge_base_docs_count: 0 };\n  }\n  return data;\n}\n\nexport async function getUserRoleAndTier(userId: string) {\n  const supabase = await createClient();\n  const { data, error } = await supabase\n    .from(\"user_roles\")\n    .select(\"*\")\n    .eq(\"id\", userId)\n    .single();\n\n  if (error || !data) {\n    return;\n  }\n  return data;\n}\n"
  },
  {
    "path": "lib/fetchers/chat.ts",
    "content": "export async function fetchChatHistory() {\n  const response = await fetch(\"api/chat/chat-history\");\n  const chats = await response.json();\n  return chats;\n}\n"
  },
  {
    "path": "lib/fetchers/services/esri/fetch-layers-list.ts",
    "content": "export const fetchAgolLayersList = async () => {\n  try {\n    const response = await fetch(\"/api/services/esri/fetch-layers-list\");\n    if (!response.ok) {\n      throw new Error(\"Failed to fetch services\");\n    }\n\n    const serviceData = await response.json();\n\n    const serviceDataResults = serviceData.results;\n\n    if (!serviceDataResults || serviceDataResults.length === 0) {\n      console.warn(\"No services found.\");\n      return [];\n    }\n\n    const filteredServiceDataResults = serviceDataResults.map(\n      (service: any) => {\n        return {\n          name: service.title,\n          type: service.type,\n          url: service.url,\n        };\n      }\n    );\n\n    return filteredServiceDataResults;\n  } catch (error) {\n    console.error(\"Error fetching services:\", error);\n    return [];\n  }\n};\n"
  },
  {
    "path": "lib/fetchers/services/esri/fetch-selected-layer.ts",
    "content": "import { generateUUID } from \"@/features/chat/utils/general-utils\";\n\nexport const fetchSelectedEsriLayer = async (\n  selectedAgolLayerToAdd: any,\n  addRoiGeometry: ({ id, name, geometry, source }: ROIGeometry) => void\n) => {\n  if (selectedAgolLayerToAdd) {\n    try {\n      const response = await fetch(\n        `/api/services/esri/fetch-selected-layer?layerUrl=${encodeURIComponent(\n          selectedAgolLayerToAdd?.url || \"\"\n        )}`\n      );\n\n      if (!response.ok) {\n        throw new Error(\"Failed to fetch AGOL layer\");\n      }\n\n      const data = await response.json();\n\n      addRoiGeometry({\n        id: generateUUID(),\n        geometry: data,\n        name: selectedAgolLayerToAdd.name,\n        source: \"arcgis\",\n      });\n    } catch (error) {\n      console.error(\"Error fetching and adding AGOL layer:\", error);\n    }\n  }\n};\n"
  },
  {
    "path": "lib/geospatial/gee/analysis-functions/heat-analysis/urban-heat-island-analysis.ts",
    "content": "\"use server\";\n\nimport ee from \"@google/earthengine\";\nimport { extractYear } from \"@/utils/general/general-utils\";\nimport { evaluate, getMapId } from \"@/features/maps/utils/gee-eval-utils\";\nimport { createClient } from \"@/utils/supabase/server\";\ntype AggregationMethodType = \"Median\" | \"Mean\" | \"Max\" | \"Min\";\n\n// Function to apply the correct aggregation method\nconst applyAggregationMethod = (\n  collection: any,\n  method: AggregationMethodType\n): any => {\n  switch (method) {\n    case \"Mean\":\n      return collection.mean();\n    case \"Median\":\n      return collection.median();\n    case \"Max\":\n      return collection.max();\n    case \"Min\":\n      return collection.min();\n    default:\n      throw new Error(\"Unsupported aggregation method\");\n  }\n};\n\n// Function to get the urban areas from the Dynamic World dataset\nconst getUrbanAreas = (geometry: any, startDate: any, endDate: any) => {\n  const landCover = ee\n    .ImageCollection(\"GOOGLE/DYNAMICWORLD/V1\")\n    .filterBounds(geometry)\n    .filterDate(startDate, endDate)\n    .select(\"label\")\n    .mode()\n    .clip(geometry);\n\n  // Select built areas (label 6 in the Dynamic World dataset represents urban/built)\n  const urbanAreas = landCover.eq(6);\n  const nonUrbanAreas = landCover.neq(6);\n\n  return { urbanAreas, nonUrbanAreas };\n};\n\n// Function to calculate the Surface Urban Heat Island Intensity (SUHII) index\nconst calculateSUHIIIndex = async (\n  lst: any,\n  geometry: any,\n  startDate: any,\n  endDate: any\n) => {\n  const { urbanAreas, nonUrbanAreas } = getUrbanAreas(\n    geometry,\n    startDate,\n    endDate\n  );\n  const urbanLST = lst.updateMask(urbanAreas);\n  const nonUrbanLST = lst.updateMask(nonUrbanAreas);\n  const urbanLSTMean = urbanLST\n    .reduceRegion({\n      reducer: ee.Reducer.mean(),\n      geometry: geometry,\n      scale: 30,\n      maxPixels: 1e15,\n    })\n    .get(\"LST_Celsius\");\n\n  const nonUrbanLSTMean = nonUrbanLST\n    .reduceRegion({\n      reducer: ee.Reducer.mean(),\n      geometry: geometry,\n      scale: 30,\n      maxPixels: 1e13,\n    })\n    .get(\"LST_Celsius\");\n\n  const urbanMean: any = await evaluate(urbanLSTMean);\n  const nonUrbanMean: any = await evaluate(nonUrbanLSTMean);\n\n  const suhiiIndex = (urbanMean - nonUrbanMean).toFixed(2);\n\n  return suhiiIndex;\n};\n\n// Function to calculate the Impervious Surface Area (ISA) index\nconst calculateISAIndex = async (\n  geometry: any,\n  startDate: any,\n  endDate: any\n) => {\n  const { urbanAreas, nonUrbanAreas } = getUrbanAreas(\n    geometry,\n    startDate,\n    endDate\n  );\n\n  const isaUrbanArea = urbanAreas\n    .reduceRegion({\n      reducer: ee.Reducer.sum(),\n      geometry: geometry,\n      scale: 30,\n      maxPixels: 1e15,\n    })\n    .get(\"label\");\n\n  const nonUrbanArea = nonUrbanAreas\n    .reduceRegion({\n      reducer: ee.Reducer.sum(),\n      geometry: geometry,\n      scale: 30,\n      maxPixels: 1e13,\n    })\n    .get(\"label\");\n\n  const urbanArea: any = await evaluate(isaUrbanArea);\n  const nonUrbanAreaValue: any = await evaluate(nonUrbanArea);\n\n  const isaRatio = (urbanArea / (urbanArea + nonUrbanAreaValue)).toFixed(2);\n  return isaRatio;\n};\n\n// Function to calculate the Urban Heat Hazard Index (UHHI)\nconst calculateUHHIInex = async (\n  geometry: any,\n  lst: any,\n  populationLayer: any,\n  startDate: any,\n  endDate: any\n) => {\n  // Load population density from Earth Engine (replace with actual population layer when you get it later from the city)\n  const populationDensity = ee\n    .ImageCollection(populationLayer)\n    .select(\"population_density\")\n    .filterBounds(geometry)\n    .filterDate(\"2020-01-01\", \"2020-02-01\")\n    .median()\n    .clip(geometry);\n\n  const populationMean = populationDensity\n    .reduceRegion({\n      reducer: ee.Reducer.mean(),\n      geometry: geometry,\n      scale: 30,\n      maxPixels: 1e13,\n    })\n    .get(\"population_density\");\n\n  const lstMean = lst\n    .reduceRegion({\n      reducer: ee.Reducer.mean(),\n      geometry: geometry,\n      scale: 30,\n      maxPixels: 1e13,\n    })\n    .get(\"LST_Celsius\");\n\n  const populationMeanValue: any = await evaluate(populationMean);\n  const lstMeanValue: any = await evaluate(lstMean);\n\n  const uhhIndex = (lstMeanValue * populationMeanValue).toFixed(2);\n  return uhhIndex;\n};\n\n// Entry point for the Urban Heat Island Analysis\nexport const urbanHeatIslandAnalysis = async (\n  geometry: any,\n  startDate: any,\n  endDate: any,\n  aggregationMethod: AggregationMethodType\n) => {\n  // Authenticate the user\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  const startYear = extractYear(startDate);\n  const endYear = extractYear(endDate);\n\n  const applyScaleFactors = (image: any): any => {\n    const thermalBand = image.select(\"ST_B10\").multiply(0.00341802).add(149.0);\n    const lst = thermalBand.subtract(273.15).rename(\"LST_Celsius\");\n    const QABand = image.select(\"QA_PIXEL\");\n    // Create a cloud mask\n    const cloudMask = QABand.bitwiseAnd(1 << 3)\n      .or(QABand.bitwiseAnd(1 << 1))\n      .or(QABand.bitwiseAnd(1 << 4))\n      .eq(0);\n\n    const maskedLST = lst.updateMask(cloudMask);\n\n    return maskedLST.set(\"system:time_start\", image.get(\"system:time_start\"));\n  };\n\n  // Function to remove outliers from the collection\n  const removeOutliers = (collection: any): any => {\n    // Compute statistics\n    const mean = collection.mean();\n    const stdDev = collection.reduce(ee.Reducer.stdDev());\n    const threshold = stdDev.multiply(2); // 2 standard deviations threshold\n\n    // Filter images within the threshold\n    const filteredCollection = collection.map((image: any) => {\n      const lst = image.select(\"LST_Celsius\");\n      const diff = lst.subtract(mean).abs();\n      return image.updateMask(diff.lte(threshold));\n    });\n\n    return filteredCollection;\n  };\n\n  // Function to get LST for a given year\n  const getLSTForYear = (year: any): any => {\n    const startDate = ee.Date.fromYMD(year, 6, 1);\n    const endDate = ee.Date.fromYMD(year, 8, 31);\n\n    let combinedCollection = ee\n      .ImageCollection(\"LANDSAT/LC08/C02/T1_L2\")\n      .merge(ee.ImageCollection(\"LANDSAT/LC09/C02/T1_L2\"))\n      .filterBounds(geometry)\n      .filterDate(startDate, endDate)\n      .map(applyScaleFactors);\n\n    // Remove outliers if necessary\n    if (aggregationMethod === \"Max\" || aggregationMethod === \"Min\") {\n      combinedCollection = removeOutliers(combinedCollection);\n    }\n\n    // Apply aggregation\n    const aggregatedResult = applyAggregationMethod(\n      combinedCollection,\n      aggregationMethod\n    );\n\n    return aggregatedResult.set(\"year\", year);\n    // return combinedCollection.median().set(\"year\", year);\n  };\n\n  const years = ee.List.sequence(startYear, endYear);\n\n  // Calculate statistics for each year\n  const lstStatsByYear = years.map((year: any) => {\n    const lstForYear = getLSTForYear(year);\n    const stats = lstForYear.reduceRegion({\n      reducer: ee.Reducer.mean()\n        .combine(ee.Reducer.median(), \"\", true)\n        .combine(ee.Reducer.min(), \"\", true)\n        .combine(ee.Reducer.max(), \"\", true)\n        .combine(ee.Reducer.percentile([25]), \"\", true)\n        .combine(ee.Reducer.percentile([75]), \"\", true),\n      geometry: geometry,\n      scale: 30,\n      maxPixels: 1e9,\n    });\n\n    return ee.Dictionary({\n      year: year,\n      mean: stats.get(\"LST_Celsius_mean\"),\n      median: stats.get(\"LST_Celsius_median\"),\n      min: stats.get(\"LST_Celsius_min\"),\n      max: stats.get(\"LST_Celsius_max\"),\n      q1: stats.get(\"LST_Celsius_p25\"),\n      q3: stats.get(\"LST_Celsius_p75\"),\n    });\n  });\n\n  // Convert lstStatsByYear to client-side data\n  const lstStatsByYearInfo = (await evaluate(lstStatsByYear)) as number[];\n\n  // Structure the output as required\n  const mapStats: any = {};\n  lstStatsByYearInfo.forEach((stat: any) => {\n    const year = stat.year.toString();\n    mapStats[year] = {\n      Mean: Number(stat.mean.toFixed(0)),\n      Median: Number(stat.median.toFixed(0)),\n      Min: Number(stat.min.toFixed(0)),\n      Max: Number(stat.max.toFixed(0)),\n      Q1: Number(stat.q1.toFixed(0)), // 25th percentile (Q1)\n      Q3: Number(stat.q3.toFixed(0)), // 75th percentile (Q3)\n    };\n  });\n\n  // Visualization part\n  const lstImages = ee.ImageCollection.fromImages(\n    years.map((year: any) => getLSTForYear(year))\n  );\n\n  const lastMapForVisualization = applyAggregationMethod(\n    lstImages,\n    aggregationMethod\n  ).clip(geometry);\n\n  const suhiiIndex = await calculateSUHIIIndex(\n    lastMapForVisualization,\n    geometry,\n    startDate,\n    endDate\n  );\n  const isaIndex = await calculateISAIndex(geometry, startDate, endDate);\n  const uhhiIndex = await calculateUHHIInex(\n    geometry,\n    lastMapForVisualization,\n    \"CIESIN/GPWv411/GPW_Population_Density\",\n    startDate,\n    endDate\n  );\n\n  const lstVizDict = lastMapForVisualization.reduceRegion({\n    reducer: ee.Reducer.min().combine({\n      reducer2: ee.Reducer.percentile([99]),\n      sharedInputs: true,\n    }),\n    geometry: geometry,\n    scale: 30,\n    maxPixels: 1e9,\n  });\n\n  const minLSTValue = await evaluate(lstVizDict.get(\"LST_Celsius_min\"));\n  const maxLSTValue = await evaluate(lstVizDict.get(\"LST_Celsius_p99\"));\n\n  const legendPalette = [\n    \"#0000FF\", // Blue\n    \"#00FFFF\", // Cyan\n    \"#00FF00\", // Green\n    \"#FFFF00\", // Yellow\n    \"#FFA500\", // Orange\n    \"#FF0000\", // Red\n  ];\n\n  const vis = { min: minLSTValue, max: maxLSTValue, palette: legendPalette };\n\n  // Get the URL format for the visualization\n  const { urlFormat } = (await getMapId(lastMapForVisualization, vis)) as any;\n  const imageGeom = lastMapForVisualization.geometry();\n  const imageGeometryGeojson = await evaluate(imageGeom);\n\n  const uhiMetrics = [\n    {\n      Metric: \"SUHII\",\n      Value: suhiiIndex,\n      Unit: \"°C\",\n      Description:\n        \"Surface Urban Heat Island Intensity (SUHII) measures the temperature difference between urban and rural areas.\",\n    },\n    {\n      Metric: \"ISA\",\n      Value: isaIndex,\n      Unit: \"%\",\n      Description:\n        \"Impervious Surface Area (ISA) to LST ratio represents the proportion of impervious surfaces (like buildings and roads) relative to the land surface temperature.\",\n    },\n    {\n      Metric: \"UHHI\",\n      Value: uhhiIndex,\n      Unit: \"°C × people/km²\",\n      Description:\n        \"Urban Heat Hazard Index (UHHI) combines population density and land surface temperature (LST) to quantify the heat exposure risk for the urban population.\",\n    },\n  ];\n\n  return {\n    urlFormat,\n    uhiMetrics,\n    geojson: imageGeometryGeojson,\n    legendConfig: {\n      min: minLSTValue,\n      max: maxLSTValue,\n      palette: legendPalette,\n    },\n    mapStats: mapStats,\n  };\n};\n"
  },
  {
    "path": "lib/geospatial/gee/analysis-functions/lancover-landuse-mapping/google-dynamic-world-landcover-mapping.ts",
    "content": "\"use server\";\nimport ee from \"@google/earthengine\";\nimport { evaluate, getMapId } from \"@/features/maps/utils/gee-eval-utils\";\nimport { createClient } from \"@/utils/supabase/server\";\n\ninterface LegendConfig {\n  labelNames: string[];\n  palette: string[];\n}\n\ninterface DynamicWorldResult {\n  urlFormat: string;\n  geojson: any;\n  legendConfig: LegendConfig;\n  mapStats: Record<string, string>;\n  extraDescription: string;\n}\n\nexport default async function googleDynamicWorldMapping(\n  geometry: any,\n  startDate: string,\n  endDate: string\n): Promise<DynamicWorldResult> {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  const dynamicWorld = ee\n    .ImageCollection(\"GOOGLE/DYNAMICWORLD/V1\")\n    .filterBounds(geometry)\n    .filterDate(startDate, endDate)\n    .select(\"label\")\n    .mode()\n    .clip(geometry);\n\n  const labelNames: string[] = [\n    \"Water\",\n    \"Trees\",\n    \"Grass\",\n    \"Flooded Vegetation\",\n    \"Crops\",\n    \"Shrub & Scrub\",\n    \"Built Area\",\n    \"Bare Ground\",\n    \"Snow & Ice\",\n  ];\n\n  const palette: string[] = [\n    \"#419BDF\", // Water\n    \"#397D49\", // Trees\n    \"#88B053\", // Grass\n    \"#7A87C6\", // Flooded Vegetation\n    \"#E49635\", // Crops\n    \"#DFC35A\", // Shrub & Scrub\n    \"#C4281B\", // Built Area\n    \"#A59B8F\", // Bare Ground\n    \"#D1DDF9\", // Snow & Ice\n  ];\n\n  const classOccurrences = dynamicWorld\n    .reduceRegion({\n      reducer: ee.Reducer.frequencyHistogram(),\n      geometry,\n      scale: 30,\n      tileScale: 16,\n      maxPixels: 1e100,\n      bestEffort: true,\n    })\n    .get(\"label\");\n\n  let mapStats: Record<string, string> = {};\n\n  await classOccurrences.evaluate((histogram: { [key: string]: number }) => {\n    if (!histogram) {\n      mapStats = {};\n      return;\n    }\n\n    const total = Object.values(histogram).reduce(\n      (sum, count) => sum + count,\n      0\n    );\n\n    // Convert raw counts to percentages\n    mapStats = Object.fromEntries(\n      Object.entries(histogram).map(([key, value]) => {\n        const labelIndex = parseInt(key, 10);\n        const className = labelNames[labelIndex] ?? `Class_${key}`;\n        const percentage = ((value / total) * 100).toFixed(2);\n        return [className, percentage];\n      })\n    );\n  });\n\n  const visualization = {\n    min: 0,\n    max: 8,\n    palette,\n  };\n\n  const { urlFormat } = (await getMapId(dynamicWorld, visualization)) as any;\n  const imageGeom = dynamicWorld.geometry();\n  const imageGeometryGeojson = await evaluate(imageGeom);\n\n  return {\n    urlFormat,\n    geojson: imageGeometryGeojson,\n    legendConfig: { labelNames, palette },\n    mapStats,\n    extraDescription: ``,\n  };\n}\n"
  },
  {
    "path": "lib/geospatial/gee/analysis-functions/lancover-landuse-mapping/landcover-change-mapping.ts",
    "content": "\"use server\";\nimport ee from \"@google/earthengine\";\nimport { evaluate, getMapId } from \"@/features/maps/utils/gee-eval-utils\";\nimport { createClient } from \"@/utils/supabase/server\";\n\ninterface LandCoverChangeResult {\n  urlFormat: string;\n  geojson: any;\n  legendConfig: any;\n  mapStats: Record<string, any>;\n  extraDescription: string;\n}\n\n// The 9 DW classes for \"label\" interpretation\nconst DW_LABELS = [\n  \"Water\",\n  \"Trees\",\n  \"Grass\",\n  \"Flooded Vegetation\",\n  \"Crops\",\n  \"Shrub & Scrub\",\n  \"Built Area\",\n  \"Bare Ground\",\n  \"Snow & Ice\",\n];\n\n// The 9 DW palette colors\nconst DW_PALETTE = [\n  \"#419BDF\", // Water\n  \"#397D49\", // Trees\n  \"#88B053\", // Grass\n  \"#7A87C6\", // Flooded Vegetation\n  \"#E49635\", // Crops\n  \"#DFC35A\", // Shrub & Scrub\n  \"#C4281B\", // Built Area\n  \"#A59B8F\", // Bare Ground\n  \"#D1DDF9\", // Snow & Ice\n];\n\nconst PROB_THRESHOLD = 0.5;\n\n/**\n * Compute class distribution (0..8 => class names)\n */\nasync function computeClassDistribution(\n  image: any,\n  geometry: any\n): Promise<Record<string, string>> {\n  const histogram = image\n    .reduceRegion({\n      reducer: ee.Reducer.frequencyHistogram(),\n      geometry,\n      scale: 10,\n      maxPixels: 1e13,\n    })\n    .get(\"label\");\n\n  return new Promise((resolve) => {\n    histogram.evaluate((result: { [key: string]: number }) => {\n      if (!result) return resolve({});\n      const totalPixels = Object.values(result).reduce(\n        (sum, val) => sum + val,\n        0\n      );\n      if (totalPixels === 0) return resolve({});\n\n      const distribution: Record<string, string> = {};\n      Object.entries(result).forEach(([classIndexStr, count]) => {\n        const classIndex = parseInt(classIndexStr, 10);\n        const className =\n          DW_LABELS[classIndex] ?? `UnknownClass_${classIndexStr}`;\n        const pct = ((count / totalPixels) * 100).toFixed(2);\n        distribution[className] = pct;\n      });\n\n      resolve(distribution);\n    });\n  });\n}\n\n/**\n * Returns:\n * 1) A tile for changed (1) vs. unchanged (0).\n * 2) mapStats with changed/unchanged percentages + class distributions.\n */\nexport default async function landcoverChangeMapping(\n  geometry: any,\n  startDate1: string,\n  endDate1: string,\n  startDate2: string,\n  endDate2: string\n): Promise<LandCoverChangeResult> {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  // 2) Filter the Dynamic World collection with a probability mask.\n  const fromCollection = ee\n    .ImageCollection(\"GOOGLE/DYNAMICWORLD/V1\")\n    .filterBounds(geometry)\n    .filterDate(startDate1, endDate1)\n    .map((img: any) => {\n      const probBands = img.select([\n        \"water\",\n        \"trees\",\n        \"grass\",\n        \"flooded_vegetation\",\n        \"crops\",\n        \"shrub_and_scrub\",\n        \"built\",\n        \"bare\",\n        \"snow_and_ice\",\n      ]);\n      const maxProb = probBands.reduce(ee.Reducer.max());\n      return img.updateMask(maxProb.gte(PROB_THRESHOLD));\n    });\n\n  const toCollection = ee\n    .ImageCollection(\"GOOGLE/DYNAMICWORLD/V1\")\n    .filterBounds(geometry)\n    .filterDate(startDate2, endDate2)\n    .map((img: any) => {\n      const probBands = img.select([\n        \"water\",\n        \"trees\",\n        \"grass\",\n        \"flooded_vegetation\",\n        \"crops\",\n        \"shrub_and_scrub\",\n        \"built\",\n        \"bare\",\n        \"snow_and_ice\",\n      ]);\n      const maxProb = probBands.reduce(ee.Reducer.max());\n      return img.updateMask(maxProb.gte(PROB_THRESHOLD));\n    });\n\n  // 3) Create mode images for each period\n  const fromImage = fromCollection.select(\"label\").mode().clip(geometry);\n  const toImage = toCollection.select(\"label\").mode().clip(geometry);\n\n  // 4) Build changed vs. unchanged image (0 => same, 1 => different)\n  const changeImage = fromImage.neq(toImage).rename(\"label\");\n\n  // 5) Frequency histogram (changed vs. unchanged)\n  const changeHistogram = changeImage\n    .reduceRegion({\n      reducer: ee.Reducer.frequencyHistogram(),\n      geometry,\n      scale: 10,\n      maxPixels: 1e13,\n    })\n    .get(\"label\");\n\n  let changedPct = \"0.0\";\n  let unchangedPct = \"0.0\";\n\n  await changeHistogram.evaluate((histogram: { [key: string]: number }) => {\n    if (!histogram) return;\n    const changedCount = histogram[\"1\"] || 0;\n    const unchangedCount = histogram[\"0\"] || 0;\n    const total = changedCount + unchangedCount;\n    if (total > 0) {\n      changedPct = ((changedCount / total) * 100).toFixed(2);\n      unchangedPct = ((unchangedCount / total) * 100).toFixed(2);\n    }\n  });\n\n  // 6) Compute distributions\n  const [year1Distribution, year2Distribution] = await Promise.all([\n    computeClassDistribution(fromImage, geometry),\n    computeClassDistribution(toImage, geometry),\n  ]);\n\n  // 7) Visualization (0 => gray, 1 => red)\n  const palette = [\"#aaaaaa\", \"#ff0000\"];\n  const visualization = {\n    min: 0,\n    max: 1,\n    palette,\n  };\n\n  // 8) Build tile & geometry\n  const { urlFormat } = (await getMapId(changeImage, visualization)) as any;\n  const imageGeom = changeImage.geometry();\n  const imageGeometryGeojson = await evaluate(imageGeom);\n\n  // 9) Minimal legend for changed vs. unchanged\n  const legendConfig: any = {\n    labelNames: [\"Unchanged\", \"Changed\"],\n    labelNamesStats: DW_LABELS,\n    palette: palette,\n    statsPalette: DW_PALETTE,\n  };\n\n  // 10) Combine stats\n  const mapStats = {\n    changedPercentage: changedPct,\n    unchangedPercentage: unchangedPct,\n    year1Distribution,\n    year2Distribution,\n  };\n\n  return {\n    urlFormat,\n    geojson: imageGeometryGeojson,\n    legendConfig,\n    mapStats,\n    extraDescription: `Extra info the user needs to know: detected class values with a probability threshold less than ${PROB_THRESHOLD} were masked out.`,\n  };\n}\n"
  },
  {
    "path": "lib/geospatial/gee/analysis-functions/lancover-landuse-mapping/sentinel-landcover-landuse-mapping.ts",
    "content": "// This is an example of how to invoke a Google Cloud Run function.\n// The Cloud Run function contains the JavaScript code that invokes a Vertex AI deep-leanring model to perform landcover landuse mapping using Sentinel-2 data in Google Eearth Egnine.\n\n\"use server\";\nimport { getIdentityTokenGoogle } from \"@/features/maps/utils/authentication-utils/gee-auth\";\nimport { convertEeGeometryToGeoJSON } from \"@/features/maps/utils/geometry-utils\";\nimport { createClient } from \"@/utils/supabase/server\";\nexport const sentinelLandcoverLanduseMapping = async (\n  geometry: any,\n  startDate: any,\n  endDate: any\n) => {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  try {\n    const url = `${process.env.GEE_CLOUD_RUN_URL}/lulc_mapping`;\n    const token = await getIdentityTokenGoogle(url);\n\n    const jsonGeometry = convertEeGeometryToGeoJSON(geometry);\n    const response = await fetch(url, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: token,\n      },\n      body: JSON.stringify({\n        geometry: jsonGeometry,\n        startDate,\n        endDate,\n        aggregationMethod: \"Median\", // Median here means the aggregation method for the image collection not the resulting land-cover map\n      }),\n    });\n\n    if (!response.ok) {\n      const errorText = await response.text();\n      console.error(\"Error response body:\", errorText);\n      throw new Error(\n        `Cloud Run request failed with status ${response.status}: ${response.statusText}`\n      );\n    }\n\n    const data = await response.json();\n\n    return data;\n  } catch (error) {\n    console.error(\"Error calling Cloud Run function:\", error);\n    throw error;\n  }\n};\n"
  },
  {
    "path": "lib/geospatial/gee/analysis-functions/pollution-analysis/air-pollution-analysis.ts",
    "content": "// An example of a bi-temporal analysis function that compares air pollution data between two time periods.\n// In this version, no outlier detection and filtering is done. So, the extreme values may be erroneous.\n\nimport ee from \"@google/earthengine\";\nimport { getMapId, evaluate } from \"@/features/maps/utils/gee-eval-utils\";\n\ninterface MonitoringResult {\n  urlFormat: string;\n  geojson: any;\n  legendConfig: {\n    min: number;\n    max: number;\n    palette: string[];\n  };\n  mapStats: {\n    [gas: string]: {\n      Mean: {\n        actual: string;\n        percentage: string | null;\n      };\n      Median: {\n        actual: string;\n        percentage: string | null;\n      };\n      Min: {\n        actual: string;\n        percentage: string | null;\n      };\n      Max: {\n        actual: string;\n        percentage: string | null;\n      };\n      unit: AirPollutantsUnits;\n    };\n  };\n}\n\n// Function to apply the correct aggregation method\nconst applyAggregationMethod = (\n  collection: any,\n  method: AggregationMethodTypeNumerical\n): any => {\n  switch (method) {\n    case \"Mean\":\n      return collection.mean();\n    case \"Median\":\n      return collection.median();\n    case \"Max\":\n      return collection\n        .reduce(ee.Reducer.max())\n        .rename([collection.first().bandNames().get(0)]);\n    case \"Min\":\n      return collection.min();\n    default:\n      throw new Error(\"Unsupported aggregation method\");\n  }\n};\n\nexport const airPollutionAnalysis = async (\n  geometry: any,\n  analysisOptions: MultiAnalysisOptionsTypeForAirPollutantsAnalysisType[],\n  startDate1: string,\n  endDate1: string,\n  aggregationMethod: AggregationMethodTypeNumerical,\n  startDate2 = \"undefined--\",\n  endDate2 = \"undefined--\"\n): Promise<MonitoringResult> => {\n  if (endDate1 === \"undefined--\") {\n    throw new Error(\"endDate1 is required\");\n  }\n\n  const gasInfo: {\n    [key: string]: {\n      collectionId: string;\n      bandName: string;\n      unit: AirPollutantsUnits;\n    };\n  } = {\n    CO: {\n      collectionId: \"COPERNICUS/S5P/NRTI/L3_CO\",\n      bandName: \"CO_column_number_density\",\n      unit: \"mol/m²\",\n    },\n    NO2: {\n      collectionId: \"COPERNICUS/S5P/NRTI/L3_NO2\",\n      bandName: \"tropospheric_NO2_column_number_density\",\n      unit: \"mol/m²\",\n    },\n    CH4: {\n      collectionId: \"COPERNICUS/S5P/OFFL/L3_CH4\",\n      bandName: \"CH4_column_volume_mixing_ratio_dry_air\",\n      unit: \"ppb\",\n    },\n    Aerosols: {\n      collectionId: \"COPERNICUS/S5P/NRTI/L3_AER_AI\",\n      bandName: \"absorbing_aerosol_index\",\n      unit: \"index\",\n    },\n  };\n\n  const period1Images: { [gas: string]: any } = {};\n  const period2Images: { [gas: string]: any } = {};\n  const analysisImages: { [gas: string]: any } = {};\n  const minValues: { [gas: string]: number } = {};\n  const maxValues: { [gas: string]: number } = {};\n  const meanValues: { [gas: string]: number } = {};\n  const medianValues: { [gas: string]: number } = {};\n\n  // New variables to store period1 and period2 stats\n  const meanPeriod1Values: { [gas: string]: number } = {};\n  const medianPeriod1Values: { [gas: string]: number } = {};\n\n  const meanPeriod2Values: { [gas: string]: number } = {};\n  const medianPeriod2Values: { [gas: string]: number } = {};\n\n  const percentageMode =\n    startDate2 !== \"undefined--\" && endDate2 !== \"undefined--\";\n\n  for (const gas of analysisOptions) {\n    const gasData = gasInfo[gas];\n\n    // Period 1 collection and image\n    const collection1 = ee\n      .ImageCollection(gasData.collectionId)\n      .select([gasData.bandName], [gas])\n      .filterDate(startDate1, endDate1)\n      .filterBounds(geometry);\n\n    const img_period1 = applyAggregationMethod(\n      collection1,\n      aggregationMethod\n    ).clip(geometry);\n\n    period1Images[gas] = img_period1;\n\n    // Compute stats for period 1\n    const statsPeriod1 = img_period1.reduceRegion({\n      reducer: ee.Reducer.min()\n        .combine({ reducer2: ee.Reducer.max(), sharedInputs: true })\n        .combine({ reducer2: ee.Reducer.mean(), sharedInputs: true })\n        .combine({ reducer2: ee.Reducer.median(), sharedInputs: true }),\n      scale: 1113.2,\n      geometry: geometry,\n      maxPixels: 1e9,\n    });\n\n    const [minValue1, maxValue1, meanValue1, medianValue1] = (await Promise.all(\n      [\n        evaluate(statsPeriod1.get(`${gas}_min`)),\n        // evaluate(statsPeriod1.get(`${gas}_p90`)),\n        evaluate(statsPeriod1.get(`${gas}_max`)),\n        evaluate(statsPeriod1.get(`${gas}_mean`)),\n        evaluate(statsPeriod1.get(`${gas}_median`)),\n      ]\n    )) as number[];\n\n    if (!percentageMode) {\n      // If not in percentage mode, use period1 stats\n      minValues[gas] = minValue1;\n      maxValues[gas] = maxValue1;\n      meanValues[gas] = meanValue1;\n      medianValues[gas] = medianValue1;\n    }\n\n    meanPeriod1Values[gas] = meanValue1;\n    medianPeriod1Values[gas] = medianValue1;\n\n    if (percentageMode) {\n      // Period 2 collection and image\n      const collection2 = ee\n        .ImageCollection(gasData.collectionId)\n        .select([gasData.bandName], [gas])\n        .filterDate(startDate2, endDate2)\n        .filterBounds(geometry);\n\n      const img_period2 = applyAggregationMethod(\n        collection2,\n        aggregationMethod\n      ).clip(geometry);\n\n      period2Images[gas] = img_period2;\n\n      // Compute stats for period 2\n      const statsPeriod2 = img_period2.reduceRegion({\n        reducer: ee.Reducer.min()\n          .combine({\n            // reducer2: ee.Reducer.percentile([90]),\n            reducer2: ee.Reducer.max(),\n            sharedInputs: true,\n          })\n          .combine({ reducer2: ee.Reducer.mean(), sharedInputs: true })\n          .combine({ reducer2: ee.Reducer.median(), sharedInputs: true }),\n        scale: 1113.2,\n        geometry: geometry,\n        maxPixels: 1e9,\n      });\n\n      const [minValue2, maxValue2, meanValue2, medianValue2] =\n        (await Promise.all([\n          evaluate(statsPeriod2.get(`${gas}_min`)),\n          // evaluate(statsPeriod2.get(`${gas}_p90`)),\n          evaluate(statsPeriod2.get(`${gas}_max`)),\n          evaluate(statsPeriod2.get(`${gas}_mean`)),\n          evaluate(statsPeriod2.get(`${gas}_median`)),\n        ])) as number[];\n\n      // Use period 2 stats for actual values\n      minValues[gas] = minValue2;\n      maxValues[gas] = maxValue2;\n      meanValues[gas] = meanValue2;\n      medianValues[gas] = medianValue2;\n\n      meanPeriod2Values[gas] = meanValue2;\n      medianPeriod2Values[gas] = medianValue2;\n\n      // Compute percentage change images\n      analysisImages[gas] = img_period2\n        .subtract(img_period1)\n        .divide(img_period1)\n        .multiply(100);\n    } else {\n      // If not in percentage mode, analysis image is period1 image\n      analysisImages[gas] = img_period1;\n    }\n  }\n\n  const mapStats: { [gas: string]: any } = {};\n  for (const gas of analysisOptions) {\n    let percentageMeanChange = null;\n    let percentageMedianChange = null;\n\n    if (percentageMode) {\n      // Handle mean percentage change\n      if (meanPeriod1Values[gas] === 0) {\n        // Period 1 is zero\n        percentageMeanChange = meanPeriod2Values[gas] !== 0 ? 100 : 0;\n      } else if (meanPeriod1Values[gas] < 0 && meanPeriod2Values[gas] < 0) {\n        // Both periods are negative\n        percentageMeanChange =\n          ((meanPeriod2Values[gas] - meanPeriod1Values[gas]) /\n            Math.abs(meanPeriod1Values[gas])) *\n          100;\n      } else if (meanPeriod1Values[gas] < 0 && meanPeriod2Values[gas] > 0) {\n        // Negative to positive\n        percentageMeanChange =\n          ((meanPeriod2Values[gas] - meanPeriod1Values[gas]) /\n            Math.abs(meanPeriod1Values[gas])) *\n          100;\n      } else if (meanPeriod1Values[gas] > 0 && meanPeriod2Values[gas] < 0) {\n        // Positive to negative\n        percentageMeanChange =\n          ((meanPeriod2Values[gas] - meanPeriod1Values[gas]) /\n            Math.abs(meanPeriod1Values[gas])) *\n          100;\n      } else {\n        // Default case (both positive or one is zero)\n        percentageMeanChange =\n          ((meanPeriod2Values[gas] - meanPeriod1Values[gas]) /\n            Math.abs(meanPeriod1Values[gas])) *\n          100;\n      }\n\n      // Handle median percentage change\n      if (medianPeriod1Values[gas] === 0) {\n        // Period 1 is zero\n        percentageMedianChange = medianPeriod2Values[gas] !== 0 ? 100 : 0;\n      } else if (medianPeriod1Values[gas] < 0 && medianPeriod2Values[gas] < 0) {\n        // Both periods negative\n        percentageMedianChange =\n          ((medianPeriod2Values[gas] - medianPeriod1Values[gas]) /\n            Math.abs(medianPeriod1Values[gas])) *\n          100;\n      } else if (medianPeriod1Values[gas] < 0 && medianPeriod2Values[gas] > 0) {\n        // Negative to positive\n        percentageMedianChange =\n          ((medianPeriod2Values[gas] - medianPeriod1Values[gas]) /\n            Math.abs(medianPeriod1Values[gas])) *\n          100;\n      } else if (medianPeriod1Values[gas] > 0 && medianPeriod2Values[gas] < 0) {\n        // Positive to negative\n        percentageMedianChange =\n          ((medianPeriod2Values[gas] - medianPeriod1Values[gas]) /\n            Math.abs(medianPeriod1Values[gas])) *\n          100;\n      } else {\n        // Default case\n        percentageMedianChange =\n          ((medianPeriod2Values[gas] - medianPeriod1Values[gas]) /\n            Math.abs(medianPeriod1Values[gas])) *\n          100;\n      }\n    }\n\n    mapStats[gas] = {\n      Mean: {\n        actual: meanValues[gas].toExponential(2),\n        percentage: percentageMode ? percentageMeanChange?.toFixed(0) : null,\n      },\n      Median: {\n        actual: medianValues[gas].toExponential(2),\n        percentage: percentageMode ? percentageMedianChange?.toFixed(0) : null,\n      },\n      Min: {\n        actual: minValues[gas].toExponential(2),\n        percentage: null,\n      },\n      Max: {\n        actual: maxValues[gas].toExponential(2),\n        percentage: null,\n      },\n      unit: gasInfo[gas].unit,\n    };\n  }\n\n  // Combine the analysis images\n  const combinedHotspot = analysisOptions.reduce((acc, gas) => {\n    return acc ? acc.max(analysisImages[gas]) : analysisImages[gas];\n  }, null as any);\n\n  // Get the band name of combinedHotspot\n  const bandNames: any = await evaluate(combinedHotspot.bandNames());\n  const bandName = bandNames[0]; // Assuming single band\n\n  // Compute max value from combinedHotspot\n  const combinedHotspotStats = combinedHotspot.reduceRegion({\n    reducer: ee.Reducer.min().combine({\n      reducer2: ee.Reducer.max(),\n      sharedInputs: true,\n    }),\n    geometry: geometry,\n    scale: 1113.2,\n    maxPixels: 1e9,\n  });\n\n  // Retrieve both values\n  const [minValue, maxValue] = (await Promise.all([\n    evaluate(combinedHotspotStats.get(`${bandName}_min`)),\n    evaluate(combinedHotspotStats.get(`${bandName}_max`)),\n  ])) as number[];\n\n  const hotspotVisParams = percentageMode\n    ? { min: minValue, max: maxValue, palette: [\"blue\", \"white\", \"red\"] }\n    : { min: 0, max: 1, palette: [\"blue\", \"yellow\", \"red\"] };\n\n  const { urlFormat } = (await getMapId(\n    combinedHotspot.clip(geometry),\n    hotspotVisParams\n  )) as any;\n  const imageGeom = combinedHotspot.geometry();\n  const imageGeometryGeojson = await evaluate(imageGeom);\n\n  return {\n    urlFormat,\n    geojson: imageGeometryGeojson,\n    legendConfig: percentageMode\n      ? { min: minValue, max: maxValue, palette: [\"blue\", \"white\", \"red\"] }\n      : { min: 0, max: 1, palette: [\"blue\", \"yellow\", \"red\"] },\n    mapStats,\n  };\n};\n"
  },
  {
    "path": "lib/geospatial/gee/extract-values-from-gee-layer/extract-values-from-gee-layer.ts",
    "content": "\"use server\";\nimport { geeAuthenticate } from \"@/features/maps/utils/authentication-utils/gee-auth\";\nimport extractValuesFromUrbanHeatIslandMap from \"./geospatial-analyses/extract-values-from-urban-heat-island-map\";\nimport { extractValuesFromAirPollutionMap } from \"./geospatial-analyses/extract-values-from-air-pollution-map\";\nimport { extractValuesFromDynamicWorldMap } from \"./geospatial-analyses/extract-values-from-google-dynamic-world-map\";\nimport { extractValuesFromLandcoverChangeMap } from \"./geospatial-analyses/extract-values-from-landcover-change-map\";\n\ntype inputArgsType = {\n  functionType: string;\n  aggregationMethod?:\n    | AggregationMethodTypeCategorical\n    | AggregationMethodTypeNumerical;\n  analysisOptions?: MultiAnalysisOptionsType[];\n  tempCreatedMapInAsset?: any;\n  geojsonFeature: any;\n  startDate1: string;\n  endDate1: string;\n  startDate2?: string;\n  endDate2?: string;\n};\n\nconst validNumericalMethods: AggregationMethodTypeNumerical[] = [\n  \"Mean\",\n  \"Median\",\n  \"Max\",\n  \"Min\",\n];\nconst validCategoricalMethods: AggregationMethodTypeCategorical[] = [\n  \"Mode\",\n  \"90th Percentile\",\n];\n\nexport async function extractValuesFromGeeMap({\n  functionType,\n  analysisOptions,\n  aggregationMethod,\n  tempCreatedMapInAsset,\n  geojsonFeature,\n  startDate1,\n  endDate1,\n  startDate2,\n  endDate2,\n}: inputArgsType) {\n  if (!functionType) return;\n  await initializeGee();\n  switch (functionType) {\n    case \"Air Pollution Analysis\":\n      if (\n        validNumericalMethods.includes(\n          aggregationMethod as AggregationMethodTypeNumerical\n        )\n      ) {\n        return await extractValuesFromAirPollutionMap({\n          geojsonFeature,\n          gases:\n            analysisOptions as MultiAnalysisOptionsTypeForAirPollutantsAnalysisType[],\n          aggregationMethod:\n            aggregationMethod as AggregationMethodTypeNumerical,\n          startDate1,\n          endDate1,\n          startDate2,\n          endDate2,\n        });\n      } else {\n        throw new Error(\n          \"Invalid aggregation method for Air Pollutants Analysis. Expected 'Mean', 'Median', 'Max', or 'Min'.\"\n        );\n      }\n\n    case \"Urban Heat Island (UHI) Analysis\":\n      if (\n        validNumericalMethods.includes(\n          aggregationMethod as AggregationMethodTypeNumerical\n        )\n      ) {\n        return await extractValuesFromUrbanHeatIslandMap({\n          geojsonFeature,\n          aggregationMethod:\n            aggregationMethod as AggregationMethodTypeNumerical,\n          startDate: startDate1,\n          endDate: endDate1,\n        });\n      } else {\n        throw new Error(\n          \"Invalid aggregation method for Urban Heat Island (UHI) Analysis. Expected 'Mean', 'Median', 'Max', or 'Min'.\"\n        );\n      }\n\n    case \"Land Use/Land Cover Maps\":\n      return await extractValuesFromDynamicWorldMap({\n        geojsonFeature,\n        startDate: startDate1,\n        endDate: endDate1,\n      });\n\n    case \"Land Use/Land Cover Change Maps\":\n      return await extractValuesFromLandcoverChangeMap({\n        geojsonFeature,\n        startDate1,\n        endDate1,\n        startDate2,\n        endDate2,\n      });\n\n    default:\n      return;\n  }\n}\n\nconst initializeGee = async () => {\n  await geeAuthenticate();\n};\n"
  },
  {
    "path": "lib/geospatial/gee/extract-values-from-gee-layer/geospatial-analyses/extract-values-from-air-pollution-map.ts",
    "content": "import ee from \"@google/earthengine\";\nimport { evaluate } from \"@/features/maps/utils/gee-eval-utils\";\n\ntype InputArgsType = {\n  geojsonFeature: any;\n  aggregationMethod: AggregationMethodType;\n  startDate1: string;\n  endDate1: string;\n  startDate2?: string;\n  endDate2?: string;\n  gases: MultiAnalysisOptionsTypeForAirPollutantsAnalysisType[];\n};\n\ntype AggregationMethodType = \"Mean\" | \"Median\" | \"Max\" | \"Min\";\n\nexport async function extractValuesFromAirPollutionMap({\n  geojsonFeature,\n  aggregationMethod,\n  startDate1,\n  endDate1,\n  startDate2,\n  endDate2,\n  gases,\n}: InputArgsType) {\n  const features =\n    geojsonFeature.type === \"FeatureCollection\"\n      ? geojsonFeature.features\n      : [geojsonFeature];\n\n  // Adjusted the structure to store objects instead of arrays of strings\n  const results: {\n    monoTemporalQueryValues: {\n      [gas: string]: {\n        gasValue1?: string;\n        gasValue2?: string;\n        differenceValue?: string;\n        percentageChange?: string;\n        unit?: AirPollutantsUnits; // \"mol/m²\" | \"ppm\" | \"mol fraction\" | \"unitless\";\n      };\n    };\n    timeSeriesQueryValues: { [gas: string]: any[] };\n  } = {\n    monoTemporalQueryValues: {},\n    timeSeriesQueryValues: {},\n  };\n\n  // Initialize result object for each gas\n  gases.forEach((gas) => {\n    results.monoTemporalQueryValues[gas] = {}; // Initialize as an object\n    results.timeSeriesQueryValues[gas] = [];\n  });\n\n  // Map the aggregationMethod to the corresponding EE reducer\n  const reducerMapping: { [key in AggregationMethodType]: any } = {\n    Mean: ee.Reducer.mean(),\n    Median: ee.Reducer.median(),\n    Max: ee.Reducer.max(),\n    Min: ee.Reducer.min(),\n  };\n\n  // Get the appropriate reducer based on the aggregationMethod\n  const selectedReducer = reducerMapping[aggregationMethod];\n\n  const percentageMode =\n    startDate2 !== \"undefined--\" && endDate2 !== \"undefined--\";\n\n  for (const feature of features) {\n    let geometry: any;\n    try {\n      if (feature) {\n        if (feature.type === \"Point\") {\n          geometry = ee.Geometry.Point(feature.coordinates);\n        } else if (\n          feature.type === \"Polygon\" ||\n          feature.type === \"MultiPolygon\"\n        ) {\n          let allCoordinates = [];\n          if (feature.type === \"Polygon\") {\n            allCoordinates = feature.coordinates.reduce(\n              (acc: any, ring: any) => acc.concat(ring),\n              []\n            );\n          }\n          if (feature.type === \"MultiPolygon\") {\n            allCoordinates = feature.coordinates.reduce(\n              (acc: any, polygon: any) =>\n                acc.concat(\n                  polygon.reduce(\n                    (accRing: any, ring: any) => accRing.concat(ring),\n                    []\n                  )\n                ),\n              []\n            );\n          }\n          if (\n            allCoordinates.length >= 3 &&\n            allCoordinates[0] !== allCoordinates[allCoordinates.length - 1]\n          ) {\n            allCoordinates.push(allCoordinates[0]);\n          }\n          geometry = ee.Geometry.Polygon(allCoordinates);\n        } else {\n          throw new Error(\"Unsupported geometry type\");\n        }\n      }\n    } catch (error) {\n      console.error(\"Error creating geometry:\", error);\n      continue;\n    }\n\n    let originalBandName: string;\n    let unit: AirPollutantsUnits;\n    for (const gas of gases) {\n      switch (gas) {\n        case \"CO\":\n          originalBandName = `CO_column_number_density`;\n          unit = \"mol/m²\";\n          break;\n        case \"NO2\":\n          originalBandName = `tropospheric_NO2_column_number_density`;\n          unit = \"mol/m²\";\n          break;\n        case \"CH4\":\n          originalBandName = `CH4_column_volume_mixing_ratio_dry_air`;\n          unit = \"ppb\";\n          break;\n        case \"Aerosols\":\n          originalBandName = `absorbing_aerosol_index`;\n          unit = \"index\";\n          break;\n        default:\n          throw new Error(\"Unsupported gas type\");\n      }\n\n      let imageBaseUrl;\n      switch (gas) {\n        case \"CO\":\n        case \"NO2\":\n          imageBaseUrl = `COPERNICUS/S5P/NRTI/L3_${gas}`;\n          break;\n        case \"CH4\":\n          imageBaseUrl = `COPERNICUS/S5P/OFFL/L3_${gas}`;\n          break;\n        case \"Aerosols\":\n          imageBaseUrl = `COPERNICUS/S5P/NRTI/L3_AER_AI`;\n          break;\n      }\n\n      // Load gas data for the first time period\n      const gas_period1 = ee\n        .ImageCollection(imageBaseUrl)\n        .select([originalBandName])\n        .filterDate(startDate1, endDate1)\n        .filterBounds(geometry);\n\n      let combined_gas_period = gas_period1;\n\n      if (\n        startDate2 &&\n        endDate2 &&\n        startDate2 !== \"undefined--\" &&\n        endDate2 !== \"undefined--\"\n      ) {\n        // Load gas data for the second time period\n        const gas_period2 = ee\n          .ImageCollection(imageBaseUrl)\n          .select([originalBandName])\n          .filterDate(startDate2, endDate2)\n          .filterBounds(geometry);\n\n        // Merge the two periods\n        combined_gas_period = gas_period1.merge(gas_period2);\n      }\n      ///////////////////////////// Time series analysis ///////////////////////////////\n      // Create time series features by date\n      const timeSeriesFeatures = combined_gas_period.map(function (image: any) {\n        const gasValuesDict = image.reduceRegion({\n          reducer: selectedReducer, // Use the chosen aggregation method (mean, median, etc.)\n          geometry: geometry,\n          scale: 10,\n          maxPixels: 1e9,\n        });\n\n        return ee.Feature(null, {\n          date: image.date().format(\"YYYY-MM-dd\"),\n          [gas]: gasValuesDict.get(originalBandName),\n        });\n      });\n\n      // Aggregate by date to handle multiple data points for the same day\n      const uniqueDates = timeSeriesFeatures.aggregate_array(\"date\").distinct(); // Get unique dates\n\n      const groupedByDate = uniqueDates.map(function (date: any) {\n        const filteredForDate = timeSeriesFeatures.filter(\n          ee.Filter.eq(\"date\", date)\n        );\n\n        // Apply the selected reducer to aggregate data for each day\n        const reducedForDate = filteredForDate\n          .aggregate_array(gas) // Collect all values for the same day\n          .reduce(selectedReducer); // Reduce them using the selected method (mean, median, etc.)\n\n        return ee.Feature(null, {\n          date: date,\n          [gas]: reducedForDate,\n        });\n      });\n\n      // Convert the ee.List to an actual list\n      const timeSeriesValues: any = await evaluate(groupedByDate);\n\n      // Extract date and aggregated gas values for daily intervals\n      const timeSeriesData = timeSeriesValues.map((feature: any) => {\n        return {\n          date: feature.properties.date,\n          [gas]: feature.properties[gas]\n            ? parseFloat(feature.properties[gas]).toExponential(2)\n            : \"N/A\",\n        };\n      });\n\n      // Add to timeSeriesQueryValues\n      results.timeSeriesQueryValues[gas].push(...timeSeriesData);\n\n      ///////////////////////////// Mono-temporal analysis ///////////////////////////////\n      const gasValueDict1 = gas_period1.reduce(selectedReducer).reduceRegion({\n        reducer: ee.Reducer.median(),\n        scale: 10,\n        geometry: geometry,\n        maxPixels: 1e9,\n      });\n\n      let gasValue1: number | null;\n      try {\n        gasValue1 = (await evaluate(\n          gasValueDict1.get(\n            `${originalBandName}_${aggregationMethod.toLowerCase()}`\n          )\n        )) as number;\n      } catch (e) {\n        console.error(`Error retrieving ${gas} value for Period 1:`, e);\n        gasValue1 = null;\n      }\n\n      if (gasValue1 !== null) {\n        results.monoTemporalQueryValues[gas].gasValue1 =\n          gasValue1.toExponential(2);\n      } else {\n        results.monoTemporalQueryValues[gas].gasValue1 = \"N/A\";\n      }\n\n      results.monoTemporalQueryValues[gas].unit = unit;\n\n      if (percentageMode) {\n        // Load gas data for the second time period\n        const gas_period2 = ee\n          .ImageCollection(imageBaseUrl)\n          .select([originalBandName])\n          .filterDate(startDate2, endDate2)\n          .filterBounds(geometry);\n\n        // Compute the median (or selected) value for period 2\n        const gasValueDict2 = gas_period2.reduce(selectedReducer).reduceRegion({\n          reducer: ee.Reducer.median(),\n          scale: 10,\n          geometry: geometry,\n          maxPixels: 1e9,\n        });\n\n        let gasValue2: number | null;\n        try {\n          gasValue2 = (await evaluate(\n            gasValueDict2.get(\n              `${originalBandName}_${aggregationMethod.toLowerCase()}`\n            )\n          )) as number;\n        } catch (e) {\n          console.error(`Error retrieving ${gas} value for Period 2:`, e);\n          gasValue2 = null;\n        }\n\n        if (gasValue2 !== null) {\n          results.monoTemporalQueryValues[gas].gasValue2 =\n            gasValue2.toExponential(2);\n        } else {\n          results.monoTemporalQueryValues[gas].gasValue2 = \"N/A\";\n        }\n\n        if (gasValue1 !== null && gasValue2 !== null) {\n          const differenceValue = gasValue2 - gasValue1;\n          let percentageChange: number | string;\n\n          // Handle edge cases for percentage change\n          if (gasValue1 === 0) {\n            // If the initial value is zero, avoid division by zero\n            percentageChange =\n              gasValue2 === 0 ? 0 : gasValue2 > 0 ? \"+∞\" : \"-∞\"; // Infinite percentage change\n          } else {\n            percentageChange = (differenceValue / Math.abs(gasValue1)) * 100;\n          }\n          results.monoTemporalQueryValues[gas].differenceValue =\n            differenceValue.toExponential(2);\n          results.monoTemporalQueryValues[gas].percentageChange =\n            typeof percentageChange === \"string\"\n              ? percentageChange\n              : percentageChange.toFixed(2) + \"%\";\n        } else {\n          results.monoTemporalQueryValues[gas].differenceValue = \"N/A\";\n          results.monoTemporalQueryValues[gas].percentageChange = \"N/A\";\n        }\n      }\n    }\n  }\n\n  // Sort the time series data for each gas from oldest to newest date\n  for (const gas of gases) {\n    results.timeSeriesQueryValues[gas].sort((a, b) => {\n      const dateA = new Date(a.date) as any;\n      const dateB = new Date(b.date) as any;\n      return dateA - dateB; // Sorts in ascending order (oldest first)\n    });\n  }\n\n  return {\n    type: \"function\",\n    name: \"Air Pollutants Analysis \",\n    values: results,\n  };\n}\n"
  },
  {
    "path": "lib/geospatial/gee/extract-values-from-gee-layer/geospatial-analyses/extract-values-from-google-dynamic-world-map.ts",
    "content": "import ee from \"@google/earthengine\";\nimport * as turf from \"@turf/turf\";\n\ntype InputArgsType = {\n  geojsonFeature: any;\n  startDate: string;\n  endDate: string;\n};\n\nexport async function extractValuesFromDynamicWorldMap({\n  geojsonFeature,\n  startDate,\n  endDate,\n}: InputArgsType) {\n  const features =\n    geojsonFeature.type === \"FeatureCollection\"\n      ? geojsonFeature.features\n      : [geojsonFeature];\n\n  const results: {\n    monoTemporalQueryValues: any[];\n    timeSeriesQueryValues: any[];\n  } = { monoTemporalQueryValues: [], timeSeriesQueryValues: [] };\n\n  const labelNames: string[] = [\n    \"Water\",\n    \"Trees\",\n    \"Grass\",\n    \"Flooded Vegetation\",\n    \"Crops\",\n    \"Shrub & Scrub\",\n    \"Built Area\",\n    \"Bare Ground\",\n    \"Snow & Ice\",\n  ];\n\n  for (const feature of features) {\n    let geometry;\n    try {\n      if (feature) {\n        if (feature.type === \"Point\") {\n          geometry = ee.Geometry.Point(feature.coordinates);\n        } else if (\n          feature.type === \"Polygon\" ||\n          feature.type === \"MultiPolygon\"\n        ) {\n          let allCoordinates = [];\n          if (feature.type === \"Polygon\") {\n            allCoordinates = feature.coordinates.reduce(\n              (acc: any, ring: any) => acc.concat(ring),\n              []\n            );\n          }\n          if (feature.type === \"MultiPolygon\") {\n            allCoordinates = feature.coordinates.reduce(\n              (acc: any, polygon: any) =>\n                acc.concat(\n                  polygon.reduce(\n                    (accRing: any, ring: any) => accRing.concat(ring),\n                    []\n                  )\n                ),\n              []\n            );\n          }\n          if (\n            allCoordinates.length >= 3 &&\n            allCoordinates[0] !== allCoordinates[allCoordinates.length - 1]\n          ) {\n            allCoordinates.push(allCoordinates[0]);\n          }\n          geometry = ee.Geometry.Polygon(allCoordinates);\n        } else {\n          throw new Error(\"Unsupported geometry type\");\n        }\n      }\n    } catch (error) {\n      console.error(\"Error creating geometry:\", error);\n      continue;\n    }\n\n    const dwMap = ee\n      .ImageCollection(\"GOOGLE/DYNAMICWORLD/V1\")\n      .filterBounds(geometry)\n      .filterDate(startDate, endDate)\n      .select(\"label\")\n      .mode()\n      .clip(geometry);\n\n    try {\n      const reduce = dwMap.reduceRegion({\n        reducer: ee.Reducer.frequencyHistogram(),\n        scale: 10,\n        geometry,\n        maxPixels: 1e13,\n      });\n\n      const result = await new Promise<{ label: { [key: string]: number } }>(\n        (resolve, reject) => {\n          reduce.getInfo((info: { label: { [key: string]: number } }) => {\n            if (info && info.label) {\n              resolve(info);\n            } else {\n              reject(new Error(\"Dynamic World data is undefined\"));\n            }\n          });\n        }\n      );\n\n      const totalPixels = Object.values(result.label).reduce(\n        (sum, count) => sum + count,\n        0\n      );\n      const classPercentages = Object.keys(result.label).map((classId) => {\n        const count = result.label[classId];\n        const percentage = ((count / totalPixels) * 100).toFixed(0);\n        return {\n          name: labelNames[parseInt(classId)],\n          percentage,\n        };\n      });\n\n      results.monoTemporalQueryValues.push(...classPercentages);\n    } catch (error) {\n      console.error(\n        \"Error obtaining DW histogram, attempting mode reducer:\",\n        error\n      );\n      try {\n        const reduceMode = dwMap.reduceRegion({\n          reducer: ee.Reducer.mode(),\n          scale: 10,\n          geometry,\n          maxPixels: 1e13,\n        });\n\n        const modeResult = await new Promise<{ label: number }>(\n          (resolve, reject) => {\n            reduceMode.getInfo((info: { label: number }) => {\n              if (info) {\n                resolve(info);\n              } else {\n                reject(new Error(\"Failed to get mode value\"));\n              }\n            });\n          }\n        );\n\n        if (modeResult.label >= 0) {\n          results.monoTemporalQueryValues.push({\n            name: labelNames[modeResult.label],\n            percentage: 100,\n          });\n        }\n      } catch (modeError) {\n        console.error(\"Failed to obtain mode value:\", modeError);\n      }\n    }\n  }\n\n  return results;\n}\n"
  },
  {
    "path": "lib/geospatial/gee/extract-values-from-gee-layer/geospatial-analyses/extract-values-from-landcover-change-map.ts",
    "content": "import ee from \"@google/earthengine\";\n\nconst PROB_THRESHOLD = 0.5;\n\ntype InputArgsType = {\n  geojsonFeature: any;\n  startDate1: string;\n  endDate1: string;\n  startDate2: string | undefined;\n  endDate2: string | undefined;\n};\n\nexport async function extractValuesFromLandcoverChangeMap({\n  geojsonFeature,\n  startDate1,\n  endDate1,\n  startDate2,\n  endDate2,\n}: InputArgsType) {\n  const results: {\n    monoTemporalQueryValues: any[];\n    timeSeriesQueryValues: any[];\n    biTemporalQueryValues: Array<{\n      year1Distribution: Record<string, string>;\n      year2Distribution: Record<string, string>;\n    }>;\n  } = {\n    monoTemporalQueryValues: [],\n    timeSeriesQueryValues: [],\n    biTemporalQueryValues: [],\n  };\n\n  const labelNames: string[] = [\n    \"Water\",\n    \"Trees\",\n    \"Grass\",\n    \"Flooded Vegetation\",\n    \"Crops\",\n    \"Shrub & Scrub\",\n    \"Built Area\",\n    \"Bare Ground\",\n    \"Snow & Ice\",\n  ];\n\n  const features =\n    geojsonFeature.type === \"FeatureCollection\"\n      ? geojsonFeature.features\n      : [geojsonFeature];\n\n  for (const feature of features) {\n    let geometry;\n    try {\n      geometry = parseGeometry(feature);\n    } catch (error) {\n      console.error(\"Error creating geometry:\", error);\n      continue;\n    }\n\n    try {\n      const fromCollection = ee\n        .ImageCollection(\"GOOGLE/DYNAMICWORLD/V1\")\n        .filterBounds(geometry)\n        .filterDate(startDate1, endDate1)\n        .map((img: any) => {\n          const probBands = img.select([\n            \"water\",\n            \"trees\",\n            \"grass\",\n            \"flooded_vegetation\",\n            \"crops\",\n            \"shrub_and_scrub\",\n            \"built\",\n            \"bare\",\n            \"snow_and_ice\",\n          ]);\n          const maxProb = probBands.reduce(ee.Reducer.max());\n          // Mask out pixels where the max probability < threshold\n          return img.updateMask(maxProb.gte(PROB_THRESHOLD));\n        });\n\n      const fromImage = fromCollection.select(\"label\").mode().clip(geometry);\n\n      const toCollection = ee\n        .ImageCollection(\"GOOGLE/DYNAMICWORLD/V1\")\n        .filterBounds(geometry)\n        .filterDate(startDate2, endDate2)\n        .map((img: any) => {\n          const probBands = img.select([\n            \"water\",\n            \"trees\",\n            \"grass\",\n            \"flooded_vegetation\",\n            \"crops\",\n            \"shrub_and_scrub\",\n            \"built\",\n            \"bare\",\n            \"snow_and_ice\",\n          ]);\n          const maxProb = probBands.reduce(ee.Reducer.max());\n          return img.updateMask(maxProb.gte(PROB_THRESHOLD));\n        });\n\n      const toImage = toCollection.select(\"label\").mode().clip(geometry);\n\n      const fromHistogram = fromImage\n        .reduceRegion({\n          reducer: ee.Reducer.frequencyHistogram(),\n          geometry,\n          scale: 10,\n          maxPixels: 1e13,\n        })\n        .get(\"label\");\n\n      const toHistogram = toImage\n        .reduceRegion({\n          reducer: ee.Reducer.frequencyHistogram(),\n          geometry,\n          scale: 10,\n          maxPixels: 1e13,\n        })\n        .get(\"label\");\n\n      const [fromResult, toResult] = await Promise.all([\n        new Promise<any>((resolve, reject) => {\n          fromHistogram.evaluate((info: any) =>\n            info ? resolve(info) : reject(new Error(\"No year1 histogram\"))\n          );\n        }),\n        new Promise<any>((resolve, reject) => {\n          toHistogram.evaluate((info: any) =>\n            info ? resolve(info) : reject(new Error(\"No year2 histogram\"))\n          );\n        }),\n      ]);\n\n      const year1Distribution: Record<string, string> = {};\n      if (fromResult) {\n        const totalYear1Pixels = Object.values(fromResult).reduce(\n          (sum: number, val: unknown) => sum + (val as number),\n          0\n        );\n        for (const key in fromResult) {\n          const classIndex = parseInt(key, 10);\n          const count = fromResult[key];\n          const pct = ((count / totalYear1Pixels) * 100).toFixed(1);\n          const className = labelNames[classIndex] || `Unknown_${classIndex}`;\n          year1Distribution[className] = pct;\n        }\n      }\n\n      const year2Distribution: Record<string, string> = {};\n      if (toResult) {\n        const totalYear2Pixels = Object.values(toResult).reduce(\n          (sum: number, val: unknown) => sum + (val as number),\n          0\n        );\n        for (const key in toResult) {\n          const classIndex = parseInt(key, 10);\n          const count = toResult[key];\n          const pct = ((count / totalYear2Pixels) * 100).toFixed(1);\n          const className = labelNames[classIndex] || `Unknown_${classIndex}`;\n          year2Distribution[className] = pct;\n        }\n      }\n\n      results.biTemporalQueryValues.push({\n        year1Distribution,\n        year2Distribution,\n      });\n    } catch (err) {\n      console.error(\"Error computing year1/year2 distributions:\", err);\n    }\n  }\n\n  return results;\n}\n\nfunction parseGeometry(featureGeom: any) {\n  if (!featureGeom) throw new Error(\"Missing geometry\");\n\n  if (featureGeom.type === \"Feature\") {\n    return parseGeometry(featureGeom.geometry);\n  }\n\n  switch (featureGeom.type) {\n    case \"Point\":\n      return ee.Geometry.Point(featureGeom.coordinates);\n\n    case \"Polygon\":\n    case \"MultiPolygon\": {\n      let allCoords: number[][] = [];\n      if (featureGeom.type === \"Polygon\") {\n        allCoords = featureGeom.coordinates.reduce(\n          (acc: any, ring: any) => acc.concat(ring),\n          []\n        );\n      } else {\n        allCoords = featureGeom.coordinates.reduce(\n          (acc: any, polygon: any) =>\n            acc.concat(\n              polygon.reduce(\n                (accRing: any, ring: any) => accRing.concat(ring),\n                []\n              )\n            ),\n          []\n        );\n      }\n\n      if (\n        allCoords.length >= 3 &&\n        JSON.stringify(allCoords[0]) !==\n          JSON.stringify(allCoords[allCoords.length - 1])\n      ) {\n        allCoords.push(allCoords[0]);\n      }\n      return ee.Geometry.Polygon(allCoords);\n    }\n\n    default:\n      throw new Error(`Unsupported geometry type: ${featureGeom.type}`);\n  }\n}\n"
  },
  {
    "path": "lib/geospatial/gee/extract-values-from-gee-layer/geospatial-analyses/extract-values-from-sentinel-landcover-landuse-map.ts",
    "content": "// This is an example of\n\nimport ee from \"@google/earthengine\";\n\ntype InputArgsType = {\n  geojsonFeature: any;\n  tempCreatedMapInAsset: any;\n  startDate: string;\n  endDate: string;\n};\n\nexport async function extractValuesFromSentinelLandcoverLanduseMap({\n  geojsonFeature,\n  tempCreatedMapInAsset,\n}: InputArgsType) {\n  const features =\n    geojsonFeature.type === \"FeatureCollection\"\n      ? geojsonFeature.features\n      : [geojsonFeature];\n  const results: {\n    monoTemporalQueryValues: any[];\n    timeSeriesQueryValues: any[];\n  } = { monoTemporalQueryValues: [], timeSeriesQueryValues: [] };\n\n  const labelNames = [\n    \"Built\",\n    \"Tree\",\n    \"Rangeland\",\n    \"Flooded Vegetation\",\n    \"Water\",\n    \"Bare\",\n  ];\n\n  for (const feature of features) {\n    let geometry;\n    try {\n      if (feature) {\n        if (feature.type === \"Point\") {\n          geometry = ee.Geometry.Point(feature.coordinates);\n        } else if (\n          feature.type === \"Polygon\" ||\n          feature.type === \"MultiPolygon\"\n        ) {\n          geometry = ee.Geometry(feature);\n        } else {\n          throw new Error(\"Unsupported geometry type\");\n        }\n      }\n    } catch (error) {\n      console.error(\"Error creating geometry:\", error);\n      continue;\n    }\n\n    try {\n      // Extract the task ID from tempCreatedMapInAsset\n      const [gcpPath, taskId] = tempCreatedMapInAsset.split(\"|\");\n\n      // Wait until the prediction map is ready in the asset\n      await waitForTaskCompletion(taskId);\n\n      // const eePredictionMap = ee.Image(geeAssetsId + \"/\" + assetName);\n      const eePredictionMap = ee.Image.loadGeoTIFF(`${gcpPath}.tif`);\n\n      // Reduce region and calculate class occurrences\n      const reduce = eePredictionMap\n        .reduceRegion({\n          reducer: ee.Reducer.frequencyHistogram(),\n          scale: 10,\n          geometry: geometry,\n          maxPixels: 1e20,\n        })\n        .get(\"band1\");\n\n      const result = await new Promise<{ [key: string]: number }>(\n        (resolve, reject) => {\n          reduce.evaluate((info: any, error: any) => {\n            if (error) {\n              reject(error);\n            } else if (info) {\n              resolve(info);\n            } else {\n              reject(new Error(\"Failed to get class histogram\"));\n            }\n          });\n        }\n      );\n\n      if (result) {\n        const totalPixels = Object.values(result).reduce(\n          (sum, count) => sum + count,\n          0\n        );\n        const classPercentages = Object.keys(result).map((classId) => {\n          const count = result[classId];\n          const percentage = ((count / totalPixels) * 100).toFixed(0);\n          return {\n            name: labelNames[parseInt(classId) - 1],\n            percentage,\n          };\n        });\n\n        results.monoTemporalQueryValues.push(...classPercentages);\n      }\n    } catch (error) {\n      console.error(\"Error obtaining class histogram:\", error);\n    }\n  }\n\n  return results;\n}\n\n// Function to track task completion on GEE\nasync function waitForTaskCompletion(taskId: string) {\n  return new Promise<void>((resolve, reject) => {\n    const checkStatus = () => {\n      ee.data.getTaskStatus([taskId], (status: any) => {\n        if (status[0].state === \"COMPLETED\") {\n          resolve();\n        } else if (\n          status[0].state === \"FAILED\" ||\n          status[0].state === \"CANCELLED\"\n        ) {\n          reject(new Error(`Task failed or cancelled: ${status[0].state}`));\n        } else {\n          // Continue checking every 5 seconds\n          setTimeout(checkStatus, 5000);\n        }\n      });\n    };\n    checkStatus();\n  });\n}\n"
  },
  {
    "path": "lib/geospatial/gee/extract-values-from-gee-layer/geospatial-analyses/extract-values-from-urban-heat-island-map.ts",
    "content": "\"use server\";\nimport ee from \"@google/earthengine\";\nimport { evaluate } from \"@/features/maps/utils/gee-eval-utils\";\nimport { extractYear } from \"@/utils/general/general-utils\";\n\ntype inputArgsType = {\n  geojsonFeature: any;\n  aggregationMethod: AggregationMethodType;\n  startDate: string;\n  endDate: string;\n};\n\ntype AggregationMethodType = \"Mean\" | \"Median\" | \"Max\" | \"Min\";\n\nexport default async function extractValuesFromUrbanHeatIslandMap({\n  geojsonFeature,\n  aggregationMethod,\n  startDate,\n  endDate,\n}: inputArgsType): Promise<any> {\n  const startYearNumber = extractYear(startDate);\n  const endYearNumber = extractYear(endDate);\n\n  // Map the aggregationMethod to the corresponding EE reducer\n  const reducerMapping: { [key in AggregationMethodType]: any } = {\n    Mean: ee.Reducer.mean(),\n    Median: ee.Reducer.median(),\n    Max: ee.Reducer.max(),\n    Min: ee.Reducer.min(),\n  };\n\n  // Get the appropriate reducer based on the aggregationMethod\n  const selectedReducer = reducerMapping[aggregationMethod];\n\n  const results: {\n    monoTemporalQueryValues: string[];\n    timeSeriesQueryValues: any[];\n  } = {\n    monoTemporalQueryValues: [],\n    timeSeriesQueryValues: [],\n  };\n\n  let geometry: any;\n\n  try {\n    const features =\n      geojsonFeature.type === \"FeatureCollection\"\n        ? geojsonFeature.features\n        : [geojsonFeature];\n\n    for (const feature of features) {\n      if (feature) {\n        if (feature.type === \"Point\") {\n          geometry = ee.Geometry.Point(feature.coordinates);\n        } else if (\n          feature.type === \"Polygon\" ||\n          feature.type === \"MultiPolygon\"\n        ) {\n          let allCoordinates = [];\n          if (feature.type === \"Polygon\") {\n            allCoordinates = feature.coordinates.reduce(\n              (acc: any, ring: any) => acc.concat(ring),\n              []\n            );\n          }\n          if (feature.type === \"MultiPolygon\") {\n            allCoordinates = feature.coordinates.reduce(\n              (acc: any, polygon: any) =>\n                acc.concat(\n                  polygon.reduce(\n                    (accRing: any, ring: any) => accRing.concat(ring),\n                    []\n                  )\n                ),\n              []\n            );\n          }\n          if (\n            allCoordinates.length >= 3 &&\n            allCoordinates[0] !== allCoordinates[allCoordinates.length - 1]\n          ) {\n            allCoordinates.push(allCoordinates[0]);\n          }\n          geometry = ee.Geometry.Polygon(allCoordinates);\n        } else {\n          throw new Error(\"Unsupported geometry type\");\n        }\n      }\n    }\n  } catch (error) {\n    console.error(\"Error creating geometry:\", error);\n    return null;\n  }\n\n  const applyScaleFactors = (image: any): any => {\n    const thermalBand = image.select(\"ST_B10\").multiply(0.00341802).add(149.0);\n    const lst = thermalBand.subtract(273.15).rename(\"LST_Celsius\");\n    const QABand = image.select(\"QA_PIXEL\");\n    // Create a cloud mask\n    const cloudMask = QABand.bitwiseAnd(1 << 3)\n      .or(QABand.bitwiseAnd(1 << 1))\n      .or(QABand.bitwiseAnd(1 << 4))\n      .eq(0);\n\n    const maskedLST = lst.updateMask(cloudMask);\n\n    return maskedLST.set(\"system:time_start\", image.get(\"system:time_start\"));\n  };\n\n  const getLSTForYear = (year: any): any => {\n    const startDate = ee.Date.fromYMD(year, 6, 1);\n    const endDate = ee.Date.fromYMD(year, 8, 31);\n\n    const combinedCollection = ee\n      .ImageCollection(\"LANDSAT/LC08/C02/T1_L2\")\n      .merge(ee.ImageCollection(\"LANDSAT/LC09/C02/T1_L2\"))\n      .filterBounds(geometry)\n      .filterDate(startDate, endDate)\n      .map((image: any) => applyScaleFactors(image.clip(geometry)));\n\n    // const maxLST = combinedCollection.max();\n    const aggregatedLST = combinedCollection.reduce(selectedReducer);\n    const yearStr = ee.String(\"LST_\").cat(ee.Number(year).format(\"%d\"));\n\n    return ee.Algorithms.If(\n      aggregatedLST.bandNames().size().gt(0),\n      aggregatedLST.set(\"year\", year).rename([yearStr]),\n      null\n    );\n  };\n\n  const years = ee.List.sequence(startYearNumber, endYearNumber);\n\n  const lstImages = ee.ImageCollection.fromImages(\n    years.map((year: any) => ee.Image(getLSTForYear(year)))\n  ).toBands();\n\n  const reduce = lstImages.reduceRegion({\n    reducer: selectedReducer,\n    scale: 10,\n    geometry: geometry,\n    maxPixels: 1e13,\n  });\n\n  const values = reduce.values().map((value: any) => ee.Number(value).toInt());\n\n  const evaluateData: number[] = (await evaluate(values)) as number[];\n\n  const yearsList: any = [];\n  for (let year = startYearNumber; year <= endYearNumber; year++) {\n    yearsList.push(year);\n  }\n\n  const timeSeriesData = evaluateData.map((value, index) => {\n    return {\n      year: yearsList[index],\n      value: value,\n    };\n  });\n\n  // Add time series data to results\n  results.timeSeriesQueryValues.push(...timeSeriesData);\n\n  /////////////////////////////// Mono-temporal analysis ///////////////////////////////\n\n  const getAggregatedLSTForPeriod = (\n    startYearNum: number,\n    endYearNum: number\n  ) => {\n    const startDate = ee.Date.fromYMD(startYearNum, 6, 1);\n    const endDate = ee.Date.fromYMD(endYearNum, 8, 31);\n\n    const combinedCollection = ee\n      .ImageCollection(\"LANDSAT/LC08/C02/T1_L2\")\n      .merge(ee.ImageCollection(\"LANDSAT/LC09/C02/T1_L2\"))\n      .filterBounds(geometry)\n      .filterDate(startDate, endDate)\n      .map((image: any) => applyScaleFactors(image.clip(geometry)));\n\n    // Reduce the collection using selectedReducer\n    const aggregatedLST = combinedCollection.reduce(selectedReducer);\n\n    return aggregatedLST;\n  };\n\n  // Compute mono-temporal analysis\n  let monoTemporalValue;\n\n  // Compute aggregated LST over the period\n  const aggregatedLST = getAggregatedLSTForPeriod(\n    startYearNumber,\n    endYearNumber\n  );\n\n  // Reduce over geometry\n  const reducedAggregatedLST = aggregatedLST.reduceRegion({\n    reducer: selectedReducer,\n    scale: 1,\n    geometry: geometry,\n    maxPixels: 1e13,\n  });\n\n  // Get the value\n  monoTemporalValue = reducedAggregatedLST.values().get(0);\n\n  // Evaluate the value\n  let evaluatedMonoTemporalValue;\n  try {\n    evaluatedMonoTemporalValue = await evaluate(monoTemporalValue);\n  } catch (e) {\n    console.error(\"Error retrieving mono-temporal value:\", e);\n    evaluatedMonoTemporalValue = null;\n  }\n\n  if (\n    evaluatedMonoTemporalValue !== null &&\n    evaluatedMonoTemporalValue !== undefined\n  ) {\n    results.monoTemporalQueryValues.push(\n      parseInt(evaluatedMonoTemporalValue.toString()).toFixed(0)\n    );\n  }\n\n  return results;\n}\n"
  },
  {
    "path": "lib/geospatial/gee/load-data/load-raster-data.ts",
    "content": "\"use server\";\nimport ee from \"@google/earthengine\";\nimport { evaluate, getMapId } from \"@/features/maps/utils/gee-eval-utils\";\nimport { createClient } from \"@/utils/supabase/server\";\n\ninterface RasterDataResult {\n  urlFormat: string;\n  geojson: any;\n  legendConfig: {\n    labelNames: string[];\n    palette: string[];\n  };\n  mapStats: Record<string, any>;\n  extraDescription?: string;\n}\n\ninterface visParams {\n  palette?: string[];\n  min?: number;\n  max?: number;\n}\n\nexport async function loadRasterData(\n  datasetId: string,\n  dataType: \"Image\" | \"ImageCollection\",\n  geometry: any,\n  startDate: string,\n  endDate: string,\n  divideValue: number,\n  visParams: visParams,\n  labelNames: string[]\n): Promise<RasterDataResult> {\n  const supabase = await createClient();\n  const { data, error } = await supabase.auth.getUser();\n  if (error || !data?.user) {\n    throw new Error(\"Unauthenticated!\");\n  }\n\n  let image;\n\n  if (dataType === \"ImageCollection\") {\n    const collection = ee\n      .ImageCollection(datasetId)\n      .filterBounds(geometry)\n      .filterDate(startDate, endDate);\n\n    // Check if collection is empty\n    const count = await evaluate(collection.size());\n    if (count === 0) {\n      return {\n        urlFormat: \"\",\n        geojson: null,\n        legendConfig: { labelNames: [], palette: [] },\n        mapStats: {},\n        extraDescription: \"No data available for this area and date range.\",\n      };\n    }\n\n    image = collection.median();\n  } else {\n    image = ee.Image(datasetId);\n  }\n\n  // Check if the image contains bands\n  const bandNames = (await evaluate(image.bandNames())) as any;\n  if (!bandNames || bandNames.length === 0) {\n    return {\n      urlFormat: \"\",\n      geojson: null,\n      legendConfig: { labelNames: [], palette: [] },\n      mapStats: {},\n      extraDescription:\n        \"No bands available in this dataset for the selected area.\",\n    };\n  }\n\n  if (divideValue) {\n    image = image.divide(divideValue);\n  }\n\n  const composite = image.clip(geometry);\n  const { urlFormat } = (await getMapId(composite, visParams)) as any;\n  const geojson = await evaluate(composite.geometry());\n  let meanValue: number | null = null;\n  await image.evaluate((res: any) => {\n    const bands = Object.keys(res || {});\n    if (bands.length && res[bands[0]] != null) {\n      meanValue = res[bands[0]];\n    }\n  });\n  const legendConfig = {\n    labelNames: labelNames.length ? labelNames : [\"Value\"],\n    palette: visParams?.palette || [],\n    min: visParams?.min,\n    max: visParams?.max,\n  };\n\n  const mapStats = { meanValue };\n  return {\n    urlFormat,\n    geojson,\n    legendConfig,\n    mapStats,\n    extraDescription: `Data loaded from ${datasetId} between ${startDate} and ${endDate}.`,\n  };\n}\n"
  },
  {
    "path": "lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "middleware.ts",
    "content": "import { type NextRequest } from \"next/server\";\nimport { updateSession } from \"@/utils/supabase/middleware\";\n\nexport async function middleware(request: NextRequest) {\n  // update user's auth session\n  return await updateSession(request);\n}\n\nexport const config = {\n  matcher: [\n    \"/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)\",\n  ],\n};\n"
  },
  {
    "path": "next.config.ts",
    "content": "import type { NextConfig } from \"next\";\nimport { version } from \"./package.json\";\n\nconst nextConfig: NextConfig = {\n  reactStrictMode: true,\n  experimental: {\n    serverActions: {\n      bodySizeLimit: \"20mb\",\n    },\n  },\n\n  // 2) Put it in NEXT_PUBLIC_APP_VERSION\n  env: {\n    NEXT_PUBLIC_APP_VERSION: version,\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"chat2geo\",\n  \"version\": \"0.6.4-alpha\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev --turbo\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"@ai-sdk/azure\": \"^1.0.20\",\n    \"@ai-sdk/google\": \"^1.0.6\",\n    \"@ai-sdk/openai\": \"^1.0.7\",\n    \"@blocknote/core\": \"^0.21.0\",\n    \"@blocknote/mantine\": \"^0.21.0\",\n    \"@blocknote/react\": \"^0.21.0\",\n    \"@google/earthengine\": \"^1.4.0\",\n    \"@googlemaps/google-maps-services-js\": \"^3.4.0\",\n    \"@hello-pangea/dnd\": \"^17.0.0\",\n    \"@hookform/resolvers\": \"^3.9.1\",\n    \"@langchain/community\": \"^0.3.19\",\n    \"@langchain/core\": \"^0.3.23\",\n    \"@langchain/openai\": \"^0.3.14\",\n    \"@lexical/react\": \"^0.21.0\",\n    \"@mapbox/mapbox-gl-draw\": \"^1.4.3\",\n    \"@radix-ui/react-alert-dialog\": \"^1.1.4\",\n    \"@radix-ui/react-checkbox\": \"^1.1.3\",\n    \"@radix-ui/react-dialog\": \"^1.1.4\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.4\",\n    \"@radix-ui/react-label\": \"^2.1.1\",\n    \"@radix-ui/react-popover\": \"^1.1.4\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.2\",\n    \"@radix-ui/react-select\": \"^2.1.4\",\n    \"@radix-ui/react-separator\": \"^1.1.1\",\n    \"@radix-ui/react-slot\": \"^1.1.1\",\n    \"@radix-ui/react-tooltip\": \"^1.1.6\",\n    \"@react-pdf/renderer\": \"^4.1.5\",\n    \"@supabase/ssr\": \"^0.5.2\",\n    \"@tabler/icons-react\": \"^3.22.0\",\n    \"@tailwindcss/typography\": \"^0.5.15\",\n    \"@tanstack/react-table\": \"^8.20.6\",\n    \"@tanstack/react-virtual\": \"^3.11.2\",\n    \"@turf/turf\": \"^7.1.0\",\n    \"@types/mapbox-gl\": \"^3.4.0\",\n    \"@types/proj4\": \"^2.5.5\",\n    \"@types/shpjs\": \"^3.4.7\",\n    \"@vercel/analytics\": \"^1.4.1\",\n    \"@xyflow/react\": \"^12.3.5\",\n    \"ai\": \"^4.1.17\",\n    \"cheerio\": \"^1.0.0\",\n    \"chroma-js\": \"^3.1.2\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"dayjs\": \"^1.11.13\",\n    \"deck.gl\": \"^9.0.24\",\n    \"echarts\": \"^5.5.1\",\n    \"echarts-for-react\": \"^3.0.2\",\n    \"epsg\": \"^0.5.0\",\n    \"google-auth-library\": \"^9.14.2\",\n    \"langchain\": \"^0.3.7\",\n    \"lexical\": \"^0.21.0\",\n    \"lodash\": \"^4.17.21\",\n    \"lucide-react\": \"^0.469.0\",\n    \"mailgun.js\": \"^11.1.0\",\n    \"mammoth\": \"^1.8.0\",\n    \"maplibre-gl\": \"^4.4.1\",\n    \"next\": \"^15.1.6\",\n    \"next-themes\": \"^0.4.4\",\n    \"pdf-lib\": \"^1.17.1\",\n    \"pdf-parse\": \"^1.1.1\",\n    \"pdfjs-dist\": \"^4.9.155\",\n    \"react\": \"^19.0.0\",\n    \"react-colorful\": \"^5.6.1\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-dropzone\": \"^14.3.5\",\n    \"react-hook-form\": \"^7.54.2\",\n    \"react-hot-toast\": \"^2.4.1\",\n    \"react-markdown\": \"^9.0.1\",\n    \"react-pdf\": \"^9.1.1\",\n    \"react-rnd\": \"^10.4.13\",\n    \"react-textarea-autosize\": \"^8.5.5\",\n    \"react-tooltip\": \"^5.28.0\",\n    \"remark-gfm\": \"^4.0.0\",\n    \"reproject\": \"^1.2.7\",\n    \"rich-textarea\": \"^0.26.4\",\n    \"shpjs\": \"^6.1.0\",\n    \"swr\": \"^2.2.5\",\n    \"tailwind-merge\": \"^2.6.0\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"validator\": \"^13.12.0\",\n    \"zod\": \"^3.24.1\",\n    \"zustand\": \"^5.0.1\"\n  },\n  \"devDependencies\": {\n    \"@types/chroma-js\": \"^2.4.5\",\n    \"@types/lodash\": \"^4.17.13\",\n    \"@types/lodash.isequal\": \"^4.5.8\",\n    \"@types/mapbox__mapbox-gl-draw\": \"^1.4.8\",\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"19.0.1\",\n    \"@types/react-dom\": \"19.0.2\",\n    \"@types/uuid\": \"^10.0.0\",\n    \"@types/validator\": \"^13.12.2\",\n    \"eslint\": \"^8\",\n    \"eslint-config-next\": \"15.1.0\",\n    \"postcss\": \"^8\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"typescript\": \"^5\"\n  },\n  \"overrides\": {\n    \"react\": \"$react\",\n    \"react-dom\": \"$react-dom\",\n    \"@types/react\": \"19.0.1\",\n    \"@types/react-dom\": \"19.0.2\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.mjs",
    "content": "/** @type {import('postcss-load-config').Config} */\nconst config = {\n  plugins: {\n    tailwindcss: {},\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "stores/use-buttons-store.ts",
    "content": "import { create } from \"zustand\";\n\ntype ButtonGroup = \"draw_point\" | \"draw_polygon\" | \"simple_select\" | null; // Drawing mode\ntype BuildCenterButton =\n  | \"AiAssistantBuilder\"\n  | \"WorkflowBuilder\"\n  | \"AiAppBuilder\"\n  | null; // BuildCenter buttons\n\n// Define the types for the workflow action buttons\ntype WorkflowActionButton = \"stop\" | \"run\" | \"queue\"; // Active workflow action button\n\n// Define the types for the AiAppBuilder action buttons\ntype AiAppBuilderActionButton = \"run\" | \"stop\" | \"queue\" | \"save\"; // Active AiAppBuilder action button\n\ninterface ButtonsStore {\n  activeDrawingMode: ButtonGroup; // Current active drawing mode\n  setDrawingMode: (mode: ButtonGroup) => void; // Function to set the drawing mode\n\n  isArtifactsSidebarOpen: boolean;\n  toggleArtifactsSidebar: () => void;\n\n  buildCenterButtons: BuildCenterButton; // Active BuildCenter button\n  setBuildCenterButton: (button: BuildCenterButton) => void; // Function to set the BuildCenter button\n\n  workflowActionButtonsState: WorkflowActionButton; // Active workflow action button\n  setWorkflowActionButton: (button: WorkflowActionButton) => void; // Function to set the workflow action button\n\n  aiAppBuilderActionButtonsState: Record<AiAppBuilderActionButton, boolean>; // Active states for AiAppBuilder action buttons\n  toggleAiAppBuilderActionButton: (button: AiAppBuilderActionButton) => void; // Function to toggle AiAppBuilder action button state\n\n  // New state for sidebar collapse\n  isSidebarCollapsed: boolean; // State for whether the sidebar is collapsed\n  toggleSidebarCollapse: () => void; // Function to toggle the sidebar collapse state\n\n  activeBasemap: BasemapType;\n  toggleBasemap: () => void;\n\n  reset(): void;\n}\n\nexport const useButtonsStore = create<ButtonsStore>((set) => ({\n  // Existing state for drawing mode\n  activeDrawingMode: null,\n  setDrawingMode: (mode) =>\n    set((state) => ({\n      activeDrawingMode: state.activeDrawingMode === mode ? null : mode, // Toggle drawing mode\n    })),\n\n  // State for BuildCenter buttons\n  buildCenterButtons: null, // Default no active button\n  setBuildCenterButton: (button) =>\n    set((state) => ({\n      buildCenterButtons: state.buildCenterButtons === button ? null : button, // Toggle BuildCenter button\n    })),\n\n  // State for workflow action buttons\n  workflowActionButtonsState: \"run\", // Default no active workflow action button\n  setWorkflowActionButton: (button) =>\n    set((state) => ({\n      workflowActionButtonsState:\n        state.workflowActionButtonsState === button ? \"run\" : button, // Toggle workflow action button\n    })),\n\n  // State for AiAppBuilder action buttons\n  aiAppBuilderActionButtonsState: {\n    run: false,\n    stop: false,\n    queue: false,\n    save: false,\n  }, // All buttons are initially inactive\n  toggleAiAppBuilderActionButton: (button) =>\n    set((state) => ({\n      aiAppBuilderActionButtonsState: {\n        ...state.aiAppBuilderActionButtonsState,\n        [button]: !state.aiAppBuilderActionButtonsState[button], // Toggle the specific button state\n      },\n    })),\n\n  isArtifactsSidebarOpen: false,\n  toggleArtifactsSidebar: () =>\n    set((state) => ({\n      isArtifactsSidebarOpen: !state.isArtifactsSidebarOpen,\n    })),\n  closeArtifactsSidebar: () =>\n    set(() => ({\n      isArtifactsSidebarOpen: false,\n    })),\n\n  // State for sidebar collapse\n  isSidebarCollapsed: false, // Default to expanded\n  toggleSidebarCollapse: () =>\n    set((state) => ({\n      isSidebarCollapsed: !state.isSidebarCollapsed, // Toggle collapse state\n    })),\n\n  // Existing state for basemap toggle\n  activeBasemap: \"satellite\",\n  toggleBasemap: () =>\n    set((state) => ({\n      activeBasemap: state.activeBasemap === \"satellite\" ? \"osm\" : \"satellite\",\n    })),\n\n  reset: () =>\n    set(() => ({\n      activeDrawingMode: null,\n      isArtifactsSidebarOpen: false,\n      activeBasemap: \"satellite\",\n    })),\n}));\n"
  },
  {
    "path": "stores/use-integration-store.ts",
    "content": "import { create } from \"zustand\";\nimport { devtools } from \"zustand/middleware\";\nimport { INTEGRATION_SERVICES } from \"@/custom-configs/integrations\";\n\ninterface IntegrationState {\n  services: IntegrationService[];\n  updateServiceStatus: (serviceId: ServiceType, status: ServiceStatus) => void;\n  updateLastSync: (serviceId: ServiceType, lastSync: string) => void;\n  getServiceStatus: (serviceId: ServiceType) => ServiceStatus;\n  getLastSync: (serviceId: ServiceType) => string | undefined;\n}\n\nexport const useIntegrationStore = create<IntegrationState>()(\n  devtools(\n    (set, get) => ({\n      services: INTEGRATION_SERVICES,\n\n      updateServiceStatus: (serviceId, status) =>\n        set((state) => ({\n          services: state.services.map((service) =>\n            service.id === serviceId ? { ...service, status } : service\n          ),\n        })),\n\n      updateLastSync: (serviceId, lastSync) =>\n        set((state) => ({\n          services: state.services.map((service) =>\n            service.id === serviceId ? { ...service, lastSync } : service\n          ),\n        })),\n\n      getServiceStatus: (serviceId) =>\n        get().services.find((service) => service.id === serviceId)?.status ??\n        \"not_connected\",\n\n      getLastSync: (serviceId) =>\n        get().services.find((service) => service.id === serviceId)?.lastSync,\n    }),\n    {\n      name: \"integration-store\",\n    }\n  )\n);\n"
  },
  {
    "path": "stores/use-loading-store.ts",
    "content": "import { create } from \"zustand\";\n\ninterface LoadingState {\n  isLoading: boolean;\n  isLocal: boolean;\n  setLoading: (loading: boolean, local?: boolean) => void;\n}\n\nconst useLoadingStore = create<LoadingState>((set) => ({\n  isLoading: false,\n  isLocal: false,\n  setLoading: (loading, local = false) =>\n    set({ isLoading: loading, isLocal: local }),\n}));\n\nexport default useLoadingStore;\n"
  },
  {
    "path": "stores/use-sidebar-button-stores.ts",
    "content": "import { create } from \"zustand\";\n\n// Define the possible pages as an enum\nexport enum Pages {\n  NewChat = \"/\",\n  ChatHistory = \"chat-history\",\n  BuildCenter = \"build-center\",\n  KnowledgeBase = \"knowledge-base\",\n  Integrations = \"integrations\",\n  DraftedReports = \"drafted-reports\",\n  Profile = \"profile\",\n  Settings = \"settings\",\n}\n\n// Define the store's state and actions\ninterface SidebarButtonStoreState {\n  pageToOpen: Pages | null; // The currently active page\n  setPageToOpen: (page: Pages | null) => void; // Action to change the active page\n}\n\n// Create the Zustand store\nconst useSidebarButtonStores = create<SidebarButtonStoreState>((set) => ({\n  pageToOpen: null, // Default page\n  setPageToOpen: (page) => set({ pageToOpen: page }),\n}));\n\nexport default useSidebarButtonStores;\n"
  },
  {
    "path": "stores/use-toast-message-store.ts",
    "content": "// zustand store\nimport { create } from \"zustand\";\n\ninterface ToastStore {\n  toastMessage: string;\n  toastType: \"success\" | \"error\" | \"warning\";\n  toastId: number;\n  setToastMessage: (\n    toastMessage: string,\n    toastType?: \"success\" | \"error\" | \"warning\"\n  ) => void;\n}\n\nconst useToastMessageStore = create<ToastStore>((set) => ({\n  toastMessage: \"\",\n  toastType: \"success\",\n  toastId: 0,\n\n  setToastMessage: (\n    toastMessage: string,\n    toastType: \"success\" | \"error\" | \"warning\" = \"success\"\n  ) =>\n    set({\n      toastMessage,\n      toastType,\n      toastId: Date.now(),\n    }),\n}));\n\nexport default useToastMessageStore;\n"
  },
  {
    "path": "stores/use-user-profile-store.ts",
    "content": "import { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\n\ninterface UserState {\n  userName: string;\n  userEmail: string;\n  userRole: string;\n  userOrganization: string;\n  licenseStartDate: string;\n  licenseEndDate: string;\n\n  usageRequests: number;\n  usageDocs: number;\n  maxRequests: number;\n  maxDocs: number;\n  maxArea: number;\n\n  setUserData: (\n    userName: string,\n    userEmail: string,\n    userRole: string,\n    userOrganization: string,\n    licenseStartDate: string,\n    licenseEndDate: string\n  ) => void;\n\n  setUsage: (\n    usageRequests: number,\n    usageDocs: number,\n    maxRequests: number,\n    maxDocs: number,\n    maxArea: number\n  ) => void;\n\n  resetUserData: () => void;\n\n  fetchAndSetUsage: () => Promise<void>;\n}\n\nexport const useUserStore = create<UserState>()(\n  persist(\n    (set, get) => ({\n      // ------------------------------------\n      // Basic user fields\n      // ------------------------------------\n      userName: \"\",\n      userEmail: \"\",\n      userRole: \"\",\n      userOrganization: \"\",\n      licenseStartDate: \"\",\n      licenseEndDate: \"\",\n\n      // ------------------------------------\n      // Usage data & limits\n      // ------------------------------------\n      usageRequests: 0,\n      usageDocs: 0,\n      maxRequests: 0,\n      maxDocs: 0,\n      maxArea: 0,\n\n      // ------------------------------------\n      // Set user data\n      // ------------------------------------\n      setUserData: (\n        userName,\n        userEmail,\n        userRole,\n        userOrganization,\n        licenseStartDate,\n        licenseEndDate\n      ) => {\n        set({\n          userName,\n          userEmail,\n          userRole,\n          userOrganization,\n          licenseStartDate,\n          licenseEndDate,\n        });\n      },\n\n      // ------------------------------------\n      // Set usage data\n      // ------------------------------------\n      setUsage: (usageRequests, usageDocs, maxRequests, maxDocs, maxArea) => {\n        set({\n          usageRequests,\n          usageDocs,\n          maxRequests,\n          maxDocs,\n          maxArea,\n        });\n      },\n\n      // ------------------------------------\n      // Reset user data\n      // ------------------------------------\n      resetUserData: () => {\n        set({\n          userName: \"\",\n          userEmail: \"\",\n          userRole: \"\",\n          userOrganization: \"\",\n          licenseStartDate: \"\",\n          licenseEndDate: \"\",\n          usageRequests: 0,\n          usageDocs: 0,\n          maxRequests: 0,\n          maxDocs: 0,\n          maxArea: 0,\n        });\n      },\n\n      // ------------------------------------\n      // Fetch usage from /api/user-usage\n      // ------------------------------------\n      fetchAndSetUsage: async () => {\n        try {\n          const response = await fetch(\"/api/user-usage\", {\n            method: \"GET\",\n            credentials: \"include\",\n          });\n          if (!response.ok) {\n            const data = await response.json();\n            throw new Error(data.error || \"Failed to fetch usage\");\n          }\n          const usageData = await response.json();\n\n          set({\n            usageRequests: usageData.requests_count ?? 0,\n            usageDocs: usageData.knowledge_base_docs_count ?? 0,\n            maxRequests: usageData.maxRequests ?? 0,\n            maxDocs: usageData.maxDocs ?? 0,\n            maxArea: usageData.maxArea ?? 0,\n          });\n        } catch (err) {\n          console.error(\"Error fetching usage data:\", err);\n        }\n      },\n    }),\n    {\n      name: \"user-store\",\n    }\n  )\n);\n"
  },
  {
    "path": "tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nexport default {\n  darkMode: [\"class\"],\n  content: [\n    \"./pages/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./components/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./app/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./features/**/*.{js,ts,jsx,tsx,mdx}\",\n  ],\n  theme: {\n    extend: {\n      fontFamily: {\n        sans: [\"var(--font-geist-sans)\", \"var(--font-geist-mono)\"],\n      },\n      colors: {\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        warning: {\n          DEFAULT: \"hsl(var(--warning))\",\n          foreground: \"hsl(var(--warning-foreground))\",\n        },\n        info: {\n          DEFAULT: \"hsl(var(--info))\",\n          foreground: \"hsl(var(--info-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n        \"primary-blue\": {\n          DEFAULT: \"hsl(var(--primary-blue))\",\n          foreground: \"hsl(var(--primary-blue-foreground))\",\n        },\n        \"primary-green\": {\n          DEFAULT: \"hsl(var(--primary-green))\",\n          foreground: \"hsl(var(--primary-green-foreground))\",\n        },\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n    },\n  },\n\n  plugins: [require(\"@tailwindcss/typography\"), require(\"tailwindcss-animate\")],\n} satisfies Config;\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "types/global.d.ts",
    "content": "////////////////////////////////////////////////////////////\n// 1. Module Declarations\n////////////////////////////////////////////////////////////\n\ndeclare module \"@google/earthengine\" {\n  const ee: any;\n  export default ee;\n}\n\n////////////////////////////////////////////////////////////\n// 2. Type Definitions\n////////////////////////////////////////////////////////////\n\n/**\n * Multi-Analysis Related Types\n */\ntype MultiAnalysisOptionsType =\n  | \"Aerosols\"\n  | \"Flood Risk\"\n  | \"Urban Heat Island (UHI)\"\n  | \"CO\"\n  | \"NO2\"\n  | \"CH4\";\n\ntype MultiAnalysisFunctionNames =\n  | \"Air Pollutants Analysis\"\n  | \"Vulnerability Map Builder\";\n\ntype MultiAnalysisOptionsTypeForVulnerabilityMapBuilderType =\n  | \"Air Pollutants\"\n  | \"Flood Risk\"\n  | \"Urban Heat Island (UHI)\";\n\ntype MultiAnalysisOptionsTypeForAirPollutantsAnalysisType =\n  | \"CO\"\n  | \"NO2\"\n  | \"CH4\"\n  | \"Aerosols\";\n\ntype AirPollutantsUnits = \"mol/m²\" | \"ppm\" | \"ppb\" | \"index\";\n\n/**\n * MultiAnalysis Configuration Types\n */\ntype MultiAnalysisFunctionsConfigType = {\n  roiFieldRequired: boolean;\n  aggregationMethodRequired: boolean;\n  biTemporalOptions: boolean;\n  options: { name: string; description: string }[];\n  optionsHeader: string;\n  optionsWeights?: { [key: string]: number }[];\n};\n\n/**\n * Aggregation Methods\n */\ntype AggregationMethodType =\n  | \"Median\"\n  | \"Mean\"\n  | \"Max\"\n  | \"Min\"\n  | \"Mode\"\n  | \"90th Percentile\";\n\ntype AggregationMethodTypeCategorical = \"Mode\" | \"90th Percentile\";\n\ntype AggregationMethodTypeNumerical = \"Mean\" | \"Median\" | \"Max\" | \"Min\";\n\n/**\n * AI Assistants\n */\ntype AI_ASSISTANTS_TYPE = {\n  default: string[];\n  custom: string[];\n  icons: { [key: string]: string };\n};\n\n/**\n * Basemaps\n */\ntype BasemapType = \"satellite\" | \"osm\";\n\n/**\n * Citation Badge\n */\ntype CitationBadgeProps = {\n  citations: string[];\n  citationSource:\n    | \"uploadedDocument\"\n    | \"OneDrive\"\n    | \"GDrive\"\n    | \"Notion\"\n    | \"Confluence\";\n};\n\n/**\n * UHIMetrics\n */\ntype UHIMetrics = UHIMetric[];\n\n/**\n * Document Processing\n */\ntype ProcessDocumentFileProps = {\n  file: File;\n  folderId: string | null;\n};\n\n/**\n * Menu Position\n */\ntype MenuPosition = {\n  top: number;\n  left: number;\n  caret: number;\n};\n\n/**\n * Service Types & Status\n */\ntype ServiceType =\n  | \"arcgis\"\n  | \"google-drive\"\n  | \"microsoft-onedrive\"\n  | \"notion\"\n  | \"confluence\"\n  | \"postgresql\"\n  | \"aws-s3\";\n\ntype ServiceStatus = \"connected\" | \"not_connected\";\n\n////////////////////////////////////////////////////////////\n// 3. Interface Definitions\n////////////////////////////////////////////////////////////\n\n/**\n * Multi Analysis Settings Modal\n */\ninterface MultiAnalysisSettingsModalProps {\n  functionName: string;\n  optionsConfig: MultiAnalysisFunctionsConfigType;\n}\n\n/**\n * UHI Metric\n */\ninterface UHIMetric {\n  Metric: string;\n  Value: number;\n  Unit: string;\n  Description: string;\n}\n\n/**\n * GEE Output\n */\ninterface GeeOutputItem {\n  layerName: string;\n  urlFormat: string | undefined;\n  legendConfig: any;\n  mapStats: Record<string, any>;\n  uhiMetrics: UHIMetrics | null;\n}\n\n/**\n * Source (for citations)\n */\ninterface Source {\n  documentName: string;\n  pages: Page[];\n}\n\n/**\n * Legend Config\n */\ninterface LegendConfig {\n  labelNames: string[];\n  labelNamesStats?: string[];\n  palette: string[];\n  statsPalette?: string[];\n}\n\n/**\n * Geospatial Analysis Result\n */\ninterface GeospatialAnalysisResult {\n  urlFormat?: string;\n  legendConfig?: any;\n  mapStats?: any;\n  layerName?: string;\n  uhiMetrics?: any;\n  functionType?: string;\n}\n\n/**\n * Tool Calling Message Results\n */\ninterface ToolCallingMessageResults {\n  geospatialAnalysis?: GeospatialAnalysisResult;\n  geospatialData?: any;\n  citationSources?: Source[];\n  draftedReport?: string;\n  toolCallTitle?: string;\n  reportFileName?: string;\n}\n\n/**\n * Chat History\n */\ninterface ChatHistory {\n  chatId: string;\n  title: string;\n  createdAt: string;\n}\n\n/**\n * Chat Response Box\n */\ninterface ChatResponseBoxProps {\n  chatId: string;\n  initialMessages: any;\n}\n\n/**\n * Table Column\n */\ninterface TableColumn<T> {\n  key: keyof T;\n  header: string;\n  width?: string;\n  render?: (value: any, item: T) => React.ReactNode;\n}\n\n/**\n * User\n */\ninterface User {\n  id: number;\n  name: string;\n  createdAt: string;\n}\n\n/**\n * Document File\n */\ninterface DocumentFile {\n  id?: number | undefined;\n  name: string;\n  owner: string;\n  numberOfPages?: number | undefined;\n  folder_id: string | null;\n  created_at?: Date | undefined;\n}\n\n/**\n * Table Action\n */\ninterface TableAction<T> {\n  label: string;\n  icon?: React.ReactNode;\n  onClick: (item: T) => void;\n  className?: string;\n}\n\n/**\n * Text Editor Props\n */\ninterface TextEditorProps {\n  inputText?: string;\n}\n\n/**\n * Chat Input Box\n */\ninterface ChatInputBoxProps {\n  onSendMessage: () => void;\n  inputValue: string;\n  handleInputChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;\n  handleKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;\n  isStreaming: boolean;\n}\n\n/**\n * ROI Geometry\n */\ninterface ROIGeometry {\n  id: string;\n  name: string;\n  geometry: any;\n  source: \"drawn\" | \"arcgis\" | \"attached\";\n}\n\n/**\n * Integration Service\n */\ninterface IntegrationService {\n  id: ServiceType;\n  name: string;\n  description: string;\n  icon: string;\n  status: ServiceStatus;\n  lastSync?: string;\n}\n\n/**\n * Feature\n */\ninterface Feature {\n  UID: string;\n  maplibreFeatureId?: string;\n  geometry: \"Point\" | \"Polygon\";\n  drawnFeatureName: string;\n  lat?: number;\n  lon?: number;\n  coordinates?: number[][][];\n  query?: any;\n  rasterLayerName: string;\n  [key: string]: any;\n}\n\n/**\n * ArcGIS Layer\n */\ninterface ArcGISLayer {\n  name: string;\n  type: string;\n  url: string;\n  data_url?: string;\n}\n\n/**\n * Map Layer\n */\ninterface MapLayer {\n  id: string;\n  name: string;\n  visible: boolean;\n  type: \"raster\" | \"roi\";\n  layerOpacity?: number;\n  mapStats?: Record<string, any>;\n  uhiMetrics?: UHIMetrics | null;\n  layerFunctionType?: string;\n  roiName: string | null;\n}\n"
  },
  {
    "path": "utils/general/document-utils.ts",
    "content": "\"use server\";\nimport mammoth from \"mammoth\";\nimport { WebPDFLoader } from \"@langchain/community/document_loaders/web/pdf\";\n\nexport const handlePdfFile = async (file: File) => {\n  try {\n    const loader = new WebPDFLoader(file, { splitPages: false });\n    const output = await loader.load();\n    const pageCount = output[0].metadata.pdf.totalPages.toString();\n\n    return pageCount;\n  } catch (error) {\n    console.error(\"Error reading PDF:\", error);\n    return \"Unknown\";\n  }\n};\n\nexport const handleTextFile = async (file: File) => {\n  try {\n    const textContent = await file.text();\n    const lineCount = textContent.split(\"\\n\").length;\n    return Math.ceil(lineCount / 50).toString();\n  } catch (error) {\n    console.error(\"Error reading text file:\", error);\n    return \"Unknown\";\n  }\n};\n\nexport const handleDocxFile = async (file: File) => {\n  try {\n    const arrayBuffer = await file.arrayBuffer();\n    const result = await mammoth.extractRawText({ arrayBuffer });\n    const textContent = result.value || \"\";\n    const wordCount = textContent.split(/\\s+/).length;\n    return Math.ceil(wordCount / 300).toString();\n  } catch (error) {\n    console.error(\"Error reading DOCX file:\", error);\n    return \"Unknown\";\n  }\n};\n"
  },
  {
    "path": "utils/general/general-utils.ts",
    "content": "import dayjs from \"dayjs\";\nimport relativeTime from \"dayjs/plugin/relativeTime\";\nimport utc from \"dayjs/plugin/utc\";\nimport * as path from \"path\";\n\ndayjs.extend(utc);\ndayjs.extend(relativeTime);\n\nexport function extractYear(dateString: string): number {\n  const date = new Date(dateString);\n  return date.getUTCFullYear();\n}\n\nexport const dateToString = (date: Date | null): string => {\n  const year = date?.getFullYear();\n  const month = date ? String(date.getMonth() + 1).padStart(2, \"0\") : \"\";\n  const day = date ? String(date.getDate()).padStart(2, \"0\") : \"\";\n  return `${year}-${month}-${day}`;\n};\n\nexport const formatDbDate = (isoString: string): string => {\n  const date = dayjs.utc(isoString); // Parse the timestamp in UTC\n  if (!date.isValid()) {\n    return \"N/A\";\n  }\n  return date.fromNow(); // e.g., \"5 minutes ago\"\n};\nexport function cleanString(str: string): string {\n  return str.replace(/\\x00/g, \"\");\n}\n\nexport function removeExtension(filename: string): string {\n  const parsed = path.parse(filename);\n  return parsed.name;\n}\n"
  },
  {
    "path": "utils/reset-chat-stores.ts",
    "content": "import useROIStore from \"@/features/maps/stores/use-roi-store\";\nimport useQueryOutputReadyFromVectorLayer from \"@/features/maps/stores/map-queries-stores/useQueryOutputReadyFromVectorLayerStore\";\nimport useQueryRasterFromVectorLayerStore from \"@/features/maps/stores/map-queries-stores/useQueryRasterFromVectorLayerStore\";\nimport useQueryStore from \"@/features/maps/stores/map-queries-stores/useQueryReadyStore\";\nimport useChartRequestedTypeStore from \"@/features/maps/stores/plots-stores/useChartRequestedTypeStore\";\nimport usePlotReadyDataStore from \"@/features/maps/stores/plots-stores/usePlotReadyDataStore\";\nimport usePlotReadyDataFromVectorLayerStore from \"@/features/maps/stores/plots-stores/usePlotReadyFromVectorLayerStore\";\nimport { useAgolLayersStore } from \"@/features/maps/stores/use-agol-layers-store\";\nimport useColorPickerStore from \"@/features/maps/stores/use-color-picker-store\";\nimport useCursorStore from \"@/features/maps/stores/use-cursor-store\";\nimport useDrawnFeatureOnMapStore from \"@/features/maps/stores/use-drawn-feature-on-map-store\";\nimport useFunctionStore from \"@/features/maps/stores/use-function-store\";\nimport { useGeeOutputStore } from \"@/features/maps/stores/use-gee-ouput-store\";\nimport useGeojsonStore from \"@/features/maps/stores/use-geojson-store\";\nimport useLayerSelectionStore from \"@/features/maps/stores/use-layer-selection-store\";\nimport useMapDisplayStore from \"@/features/maps/stores/use-map-display-store\";\nimport useMapLayersStore from \"@/features/maps/stores/use-map-layer-store\";\nimport useZoomRequestStore from \"@/features/maps/stores/use-map-zoom-request-store\";\nimport useMapLegendStore from \"@/features/maps/stores/use-map-legend-store\";\nimport useTableStore from \"@/features/maps/stores/use-table-store\";\nimport { useAttachmentStore } from \"@/features/chat/stores/use-attachments-store\";\nimport useChatSourcesStore from \"@/features/chat/stores/use-chat-response-sources-store\";\nimport useDraftedReportStore from \"@/features/chat/stores/use-drafted-report-store\";\nimport { useButtonsStore } from \"@/stores/use-buttons-store\";\n\nexport function resetChatStores() {\n  useROIStore.getState().reset();\n  useQueryOutputReadyFromVectorLayer.getState().reset();\n  useQueryRasterFromVectorLayerStore.getState().reset();\n  useQueryStore.getState().reset();\n  useChartRequestedTypeStore.getState().reset();\n  usePlotReadyDataStore.getState().reset();\n  usePlotReadyDataFromVectorLayerStore.getState().reset();\n  useButtonsStore.getState().reset();\n  useColorPickerStore.getState().reset();\n  useCursorStore.getState().reset();\n  useDrawnFeatureOnMapStore.getState().reset();\n  useFunctionStore.getState().reset();\n  useGeeOutputStore.getState().reset();\n  useGeojsonStore.getState().reset();\n  useLayerSelectionStore.getState().reset();\n  useMapDisplayStore.getState().reset();\n  useMapLayersStore.getState().reset();\n  useZoomRequestStore.getState().reset();\n  useMapLegendStore.getState().reset();\n  useTableStore.getState().reset();\n  useAttachmentStore.getState().reset();\n  useChatSourcesStore.getState().reset();\n  useDraftedReportStore.getState().reset();\n}\n"
  },
  {
    "path": "utils/service-handlers/esri.ts",
    "content": "export const handleArcGISAuth = () => {\n  const authorizationUrl = `/api/auth/esri/authorize`;\n\n  try {\n    window.open(\n      authorizationUrl,\n      \"ArcGIS Auth\",\n      \"width=600,height=500,left=200,top=200\"\n    );\n  } catch (error) {\n    console.error(\"Failed to open ArcGIS Auth window:\", error);\n  }\n};\n"
  },
  {
    "path": "utils/supabase/client.ts",
    "content": "import { createBrowserClient } from \"@supabase/ssr\";\n\nexport function createClient() {\n  // Create a supabase client on the browser with project's credentials\n  return createBrowserClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!\n  );\n}\n"
  },
  {
    "path": "utils/supabase/middleware.ts",
    "content": "import { createServerClient } from \"@supabase/ssr\";\nimport { NextResponse, type NextRequest } from \"next/server\";\n\nexport async function updateSession(request: NextRequest) {\n  let supabaseResponse = NextResponse.next({\n    request,\n  });\n\n  const supabase = createServerClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n    {\n      cookies: {\n        getAll() {\n          return request.cookies.getAll();\n        },\n        setAll(cookiesToSet) {\n          cookiesToSet.forEach(({ name, value, options }) =>\n            request.cookies.set(name, value)\n          );\n          supabaseResponse = NextResponse.next({\n            request,\n          });\n          cookiesToSet.forEach(({ name, value, options }) =>\n            supabaseResponse.cookies.set(name, value, options)\n          );\n        },\n      },\n    }\n  );\n\n  // refreshing the auth token\n  await supabase.auth.getUser();\n\n  return supabaseResponse;\n}\n"
  },
  {
    "path": "utils/supabase/server.ts",
    "content": "import { createServerClient } from \"@supabase/ssr\";\nimport { cookies } from \"next/headers\";\n\nexport async function createClient() {\n  const cookieStore = await cookies();\n\n  return createServerClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n    {\n      cookies: {\n        getAll() {\n          return cookieStore.getAll();\n        },\n        setAll(cookiesToSet) {\n          try {\n            cookiesToSet.forEach(({ name, value, options }) =>\n              cookieStore.set(name, value, options)\n            );\n          } catch {\n            console.error(\"Failed to set cookies\");\n          }\n        },\n      },\n    }\n  );\n}\n"
  },
  {
    "path": "utils/validation-utils/validation-utils.ts",
    "content": "import validator from \"validator\";\n\nexport function isValidUrl(url: string): boolean {\n  return validator.isURL(url, {\n    protocols: [\"http\", \"https\"],\n    require_protocol: true,\n    allow_trailing_dot: false,\n    allow_protocol_relative_urls: false,\n  });\n}\n\nexport function sanitizeUrl(url: string): string {\n  return validator.escape(url); // Escape potentially harmful characters\n}\n\nexport function isValidLayerName(name: string): boolean {\n  return validator.matches(name, /^[a-zA-Z0-9_-]+$/);\n}\n\nexport function sanitizeLayerName(name: string): string {\n  return validator.escape(name); // Escape potentially harmful characters\n}\n\nexport function validateAndSanitizeUrl(url: string): string | null {\n  if (!isValidUrl(url)) {\n    console.warn(\"Invalid URL:\", url);\n    return null;\n  }\n  return sanitizeUrl(url);\n}\n\nexport function validateAndSanitizeLayerName(name: string): string | null {\n  if (!isValidLayerName(name)) {\n    console.warn(\"Invalid layer name:\", name);\n    return null;\n  }\n  return sanitizeLayerName(name);\n}\n"
  }
]