# Project export: Dashify

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2025
- Tagline: Turn spreadsheets into dynamic, interactive dashboards in seconds.
- Devpost: https://devpost.com/software/dashify
- GitHub: https://github.com/halliefaust/sheet2dash
- Video: https://www.youtube.com/embed/eS6-VfSwMvc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Hallie Faust (20 commits), Zain Merchant (11 commits), Sophia Krugler (7 commits)

## Devpost submission (written by the team)

### Inspiration

At work, visual dashboards are essential but tedious to create. Many engineers spend significant time building and maintaining internal dashboards, manually assembling components and struggling to determine the most relevant visualizations. Existing solutions are static, requiring users to recreate all visualizations whenever the data changes and manually fine-tune charts. We wanted to build a tool that simplifies this process—one that dynamically generates interactive dashboards from a Google Sheet, making data visualization effortless and adaptable.

### What it does

Dashify transforms a Google Sheet into an interactive, customizable dashboard. Users provide a link to their sheet and the tool: Generates dynamic charts based on spreadsheet data. Allows real-time customization of visualizations. Automatically updates charts when the source data changes. Supports drag-and-drop for easy chart rearrangement.

### How we built it

We started by identifying gaps in existing spreadsheet tools and brainstorming a solution to streamline data visualization. Our development process was guided by the expected user flow. Using Vercel’s v0, we created the initial app template and then integrated Google Sheets. We integrated with the OpenAI API allowing us to generate visualizations and reason through a user's data on the fly. Using Recharts, we rendered charts that update in real time as data changes.

### Challenges we ran into

One of our biggest challenges was managing the variability of LLM-generated output when dealing with highly diverse spreadsheet data. Enforcing a strict structured format was too rigid, while allowing open-ended responses led to excessive unpredictability. Striking the right balance required careful prompt engineering and iterative refinement. We also faced difficulties in identifying a clear gap in existing Google Sheets tools that our solution could uniquely address. Additionally, defining the project scope was challenging, as there were many potential directions we could explore.

### Accomplishments we're proud of

We are proud of coming up with an idea that addresses a common problem we all experience. We successfully built an easy-to-use user interface that makes interacting with the dashboard simple. We implemented a sync functionality that ensures the dashboard is updated on click when data changes. Finally, we were able to figure out how to get the LLM to reason over data, which was a complex challenge but a rewarding accomplishment.

### What we learned

We learned how to fine-tune prompts and structure LLM outputs to create reliable and meaningful chart recommendations. Keeping the dashboard in sync with Google Sheets was trickier than expected, requiring efficient polling and state management. We realized that a great UX is just as important as the AI itself—users need intuitive controls to refine charts

### What's next

User Accounts & Dashboard Management – Save your dashboards, revisit them anytime, and make edits effortlessly. Link to a user's Google account to enable dashboard creation for privately owned sheets. Leverage a wider range of chart types available on Recharts for richer data representation. Insights Tab – Get a concise, AI-powered breakdown of key insights from your data, making analysis more intuitive and actionable.

## README (from the GitHub repository)

# Dashify

## How to Run

### 1. Start the Backend

Run the following commands in your terminal:

```bash
export OPENAI_API_KEY=[YourOpenAPIKey]
export GOOGLE_API_KEY=[YourGoogleAPIKey]
```

```bash
python server.py
```

### 2. Start the Frontend

Open a new terminal window and enter:

```bash
npm run dev
```

### 3. Open the Application

Go to:

```
http://localhost:3000/
```

## How to Test

Use any publicly shared sample sheet, or use the following sample sheet URL: [Sample Google Sheet](https://docs.google.com/spreadsheets/d/1jyb_gnElCNhGg6SbNz45L_pz2YzA5PNKg6DSc6hyJQ0/edit?gid=1734362159#gid=1734362159)



## Detected evidence (automated analysis)

Indexed codebase: 63 recognized source files, 185 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (69 of 69)

```
.gitignore
app/create/page.tsx
app/dashboard/page.tsx
app/globals.css
app/layout.tsx
app/page.tsx
backend/server.py
components.json
components/theme-provider.tsx
components/ui/accordion.tsx
components/ui/alert-dialog.tsx
components/ui/alert.tsx
components/ui/aspect-ratio.tsx
components/ui/avatar.tsx
components/ui/badge.tsx
components/ui/breadcrumb.tsx
components/ui/button.tsx
components/ui/calendar.tsx
components/ui/card.tsx
components/ui/carousel.tsx
components/ui/chart.tsx
components/ui/checkbox.tsx
components/ui/collapsible.tsx
components/ui/command.tsx
components/ui/context-menu.tsx
components/ui/dialog.tsx
components/ui/drawer.tsx
components/ui/dropdown-menu.tsx
components/ui/form.tsx
components/ui/hover-card.tsx
components/ui/input-otp.tsx
components/ui/input.tsx
components/ui/label.tsx
components/ui/menubar.tsx
components/ui/navigation-menu.tsx
components/ui/pagination.tsx
components/ui/popover.tsx
components/ui/progress.tsx
components/ui/radio-group.tsx
components/ui/resizable.tsx
components/ui/scroll-area.tsx
components/ui/select.tsx
components/ui/separator.tsx
components/ui/sheet.tsx
components/ui/sidebar.tsx
components/ui/skeleton.tsx
components/ui/slider.tsx
components/ui/sonner.tsx
components/ui/switch.tsx
components/ui/table.tsx
components/ui/tabs.tsx
components/ui/textarea.tsx
components/ui/toast.tsx
components/ui/toaster.tsx
components/ui/toggle-group.tsx
components/ui/toggle.tsx
components/ui/tooltip.tsx
components/ui/use-mobile.tsx
components/ui/use-toast.ts
hooks/use-mobile.tsx
hooks/use-toast.ts
lib/utils.ts
next.config.mjs
package.json
postcss.config.mjs
README.md
styles/globals.css
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @hookform/resolvers@^3.9.1, @radix-ui/react-accordion@^1.2.2, @radix-ui/react-alert-dialog@^1.1.4, @radix-ui/react-aspect-ratio@^1.1.1, @radix-ui/react-avatar@^1.1.2, @radix-ui/react-checkbox@^1.1.3, @radix-ui/react-collapsible@^1.1.2, @radix-ui/react-context-menu@^2.2.4, @radix-ui/react-dialog@^1.1.4, @radix-ui/react-dropdown-menu@^2.1.4, @radix-ui/react-hover-card@^1.1.4, @radix-ui/react-label@^2.1.1, @radix-ui/react-menubar@^1.1.4, @radix-ui/react-navigation-menu@^1.2.3, @radix-ui/react-popover@^1.1.4, @radix-ui/react-progress@^1.1.1, @radix-ui/react-radio-group@^1.2.2, @radix-ui/react-scroll-area@^1.2.2, @radix-ui/react-select@^2.1.4, @radix-ui/react-separator@^1.1.1, @radix-ui/react-slider@^1.2.2, @radix-ui/react-slot@^1.1.1, @radix-ui/react-switch@^1.1.2, @radix-ui/react-tabs@^1.1.2, @radix-ui/react-toast@^1.2.4, @radix-ui/react-toggle@^1.1.1, @radix-ui/react-toggle-group@^1.1.1, @radix-ui/react-tooltip@^1.1.6, @types/node@^22, @types/react@^18, @types/react-dom@^18, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@1.0.4, date-fns@^3.6.0, embla-carousel-react@8.5.1, input-otp@1.4.1, jspdf@^2.5.2, lucide-react@^0.454.0, next@14.2.16, next-themes@^0.4.4, postcss@^8, react@^18, react-day-picker@8.10.1, react-dom@^18, react-grid-layout@^1.5.0, react-hook-form@^7.54.1, react-resizable-panels@^2.1.7, react-resize-detector@^12.0.2, recharts@latest, sonner@^1.7.1, tailwind-merge@^2.5.5, tailwindcss@^3.4.17, tailwindcss-animate@^1.0.7, typescript@^5, vaul@^0.9.6, zod@^3.24.1

### Recent commits (newest first)

- readme but without dependency instructions
- fixed typo
- fixed 500
- display loading
- fix double render
- moved handle
- loading
- fix button hover behavior
- center homepage
- change app name
- ui
- generate graphs on opening dashboard.
- Merge branch 'main' of github.com:halliefaust/sheet2dash
- remove backend call from homepage.
- sync working
- added modify functionality
- made request better
- pass user prompt to llm
- Merge branch 'main' of github.com:halliefaust/sheet2dash
- check json from ask llm function

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "my-v0-project",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@hookform/resolvers": "^3.9.1",
    "@radix-ui/react-accordion": "^1.2.2",
    "@radix-ui/react-alert-dialog": "^1.1.4",
    "@radix-ui/react-aspect-ratio": "^1.1.1",
    "@radix-ui/react-avatar": "^1.1.2",
    "@radix-ui/react-checkbox": "^1.1.3",
    "@radix-ui/react-collapsible": "^1.1.2",
    "@radix-ui/react-context-menu": "^2.2.4",
    "@radix-ui/react-dialog": "^1.1.4",
    "@radix-ui/react-dropdown-menu": "^2.1.4",
    "@radix-ui/react-hover-card": "^1.1.4",
    "@radix-ui/react-label": "^2.1.1",
    "@radix-ui/react-menubar": "^1.1.4",
    "@radix-ui/react-navigation-menu": "^1.2.3",
    "@radix-ui/react-popover": "^1.1.4",
    "@radix-ui/react-progress": "^1.1.1",
    "@radix-ui/react-radio-group": "^1.2.2",
    "@radix-ui/react-scroll-area": "^1.2.2",
    "@radix-ui/react-select": "^2.1.4",
    "@radix-ui/react-separator": "^1.1.1",
    "@radix-ui/react-slider": "^1.2.2",
    "@radix-ui/react-slot": "^1.1.1",
    "@radix-ui/react-switch": "^1.1.2",
    "@radix-ui/react-tabs": "^1.1.2",
    "@radix-ui/react-toast": "^1.2.4",
    "@radix-ui/react-toggle": "^1.1.1",
    "@radix-ui/react-toggle-group": "^1.1.1",
    "@radix-ui/react-tooltip": "^1.1.6",
    "autoprefixer": "^10.4.20",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "1.0.4",
    "date-fns": "^3.6.0",
    "embla-carousel-react": "8.5.1",
    "input-otp": "1.4.1",
    "jspdf": "^2.5.2",
    "lucide-react": "^0.454.0",
    "next": "14.2.16",
    "next-themes": "^0.4.4",
    "react": "^18",
    "react-day-picker": "8.10.1",
    "react-dom": "^18",
    "react-grid-layout": "^1.5.0",
    "react-hook-form": "^7.54.1",
    "react-resizable-panels": "^2.1.7",
    "react-resize-detector": "^12.0.2",
    "recharts": "latest",
    "sonner": "^1.7.1",
    "tailwind-merge": "^2.5.5",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^0.9.6",
    "zod": "^3.24.1"
  },
  "devDependencies": {
    "@types/node": "^22",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "postcss": "^8",
    "tailwindcss": "^3.4.17",
    "typescript": "^5"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'Dashify',
  description: 'Instantly create dynamic dashboard from sheets',
  icons: {
    icon: '/favicon.ico',
  },
  generator: 'v0.dev',
}

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode
}>) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

```

### app/page.tsx

```typescript
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { ArrowRight, Sparkles, BarChart3, Clock } from "lucide-react"

export default function Home() {
  return (
    <div className="flex flex-col min-h-screen">
      <header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex items-center justify-between h-16">
            <div className="flex items-center gap-2">
              <Sparkles className="h-6 w-6 text-primary" />
              <h1 className="text-xl font-bold tracking-tight">Dashify</h1>
            </div>
          </div>
        </div>
      </header>

      <main className="flex-grow">
        <div className="px-4 sm:px-6 lg:px-8 py-16 md:py-24">
          <div className="mx-auto max-w-5xl text-center">
            <h2 className="text-4xl font-bold tracking-tight mb-4 bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent sm:text-5xl">
              Turn your spreadsheets into beautiful dashboards in seconds
            </h2>

            <p className="text-xl text-muted-foreground mb-8">
              Connect your Google Sheets and create stunning, interactive dashboards without writing a single line of
              code.
            </p>

            <div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
              <Button size="lg" asChild>
                <Link href="/create" className="flex items-center gap-2">
                  Create Dashboard
                  <ArrowRight className="h-4 w-4" />
                </Link>
              </Button>
            </div>

            <div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-3">
              <div className="flex flex-col items-center">
                <BarChart3 className="h-10 w-10 text-primary mb-4" />
                <h3 className="text-lg font-semibold mb-2">Beautiful Charts</h3>
                <p className="text-muted-foreground text-sm">
                  Create stunning visualizations with our pre-built chart library
                </p>
              </div>
              <div className="flex flex-col items-center">
                <Clock className="h-10 w-10 text-primary mb-4" />
                <h3 className="text-lg font-semibold mb-2">Real-time Updates</h3>
                <p className="text-muted-foreground text-sm">Your dashboards stay in sync with your spreadsheet data</p>
              </div>
              <div className="flex flex-col items-center">
                <Sparkles className="h-10 w-10 text-primary mb-4" />
                <h3 className="text-lg font-semibold mb-2">No-code Builder</h3>
                <p className="text-muted-foreground text-sm">
                  Design professional dashboards with our intuitive builder
                </p>
              </div>
            </div>
          </div>
        </div>
      </main>
    </div>
  )
}


```

### backend/server.py

```python
import os
import re
import json
import requests
from flask import Flask, request, jsonify
from flask_cors import CORS
from openai import OpenAI

# Load API keys from environment variables
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")

if not OPENAI_API_KEY:
    raise ValueError("OpenAI API key is missing. Set OPENAI_API_KEY in environment variables.")
if not GOOGLE_API_KEY:
    raise ValueError("Google API key is missing. Set GOOGLE_API_KEY in environment variables.")

client = OpenAI(
    api_key=OPENAI_API_KEY
)

app = Flask(__name__)
CORS(app)  # Enable CORS for all routes

def extract_sheet_id(sheet_url):
    """Extracts the spreadsheet ID from a Google Sheets URL."""
    match = re.search(r'/d/([a-zA-Z0-9-_]+)', sheet_url)
    return match.group(1) if match else None

def fetch_sheet_data(sheet_id, sheet_range="A1:Z1000"):
    """Fetches data from a Google Sheet using the Sheets API."""
    api_url = f"https://sheets.googleapis.com/v4/spreadsheets/{sheet_id}/values/{sheet_range}?key={GOOGLE_API_KEY}"
    response = requests.get(api_url)
    if response.status_code != 200:
        raise Exception(f"Error fetching sheet data: {response.text}")
    return response.json()

def ask_llm_about_dashboard(processed_data, candidate_chart, user_prompt):
    """
    Passes the processed sheet data and candidate chart configuration
    to the LLM and asks it to suggest the best chart configuration.
    """
    prompt = f"""
I have the following Google Sheet data:
{json.dumps(processed_data, indent=2)}

I have prepared the following chart configuration options:
{json.dumps(candidate_chart, indent=2)}

Based on the data, please suggest the three to eight most appropriate chart configurations from the candidate options. 
If none of the options are appropriate, return an empty array for "charts". Here is the prompt provided by the user: {user_prompt} \n

Please respond in JSON format only.
"""
    try:
        response = client.chat.completions.create(
            model="gpt-4-turbo",
            messages=[
                {"role": "system", "content": "You are a data visualization expert. Respond in JSON format only."},
                {"role": "user", "content": prompt}
            ]
        )

        llm_response_content = response.choices[0].message.content.strip()
        return llm_response_content

    except Exception as e:
        error_msg = f'{{"charts": [], "error": "LLM API error: {str(e)}"}}'
        print("LLM Exception:", error_msg)  # <-- Debug print
        return error_msg

@app.route('/analyze-sheet', methods=['POST'])
def analyze_sheet():
    """
    Reads a Google Sheet, processes the data, constructs candidate chart configurations,
    asks the LLM to choose the best charts, and returns a JSON response that includes
    both the generated charts and the raw sheet data.
    """
    data = request.get_json()
    sheet_url = data.get('sheet_url')
    if not sheet_url:
        return jsonify({"error": "No sheet_url provided"}), 400

    sheet_id = extract_sheet_id(sheet_url)
    if not sheet_id:
        return jsonify({"error": "Invalid Google Sheets URL"}), 400

    try:
        sheet_data = fetch_sheet_data(sheet_id)
    except Exception as e:
        return jsonify({"error": str(e)}), 500

    values = sheet_data.get("values")
    if not values or len(values) < 2:
        return jsonify({"error": "Sheet data is insufficient for analysis"}), 400

    # Store the raw data for later use in the final JSON response.
    raw_data = values

    # Assume the first row contains headers (e.g., ["Month", "Alice", "Bob", "Charlie"])
    header = values[0]
    data_rows = []
    for row in values[1:]:
        row_dict = {}
        for i, key in enumerate(header):
            row_dict[key] = row[i] if i < len(row) else None
        data_rows.append(row_dict)

    # Convert numeric values (except for the first column) to numbers where possible.
    for row in data_rows:
        for key in header[1:]:
            try:
                row[key] = float(row[key]) if row[key] is not None else None
            except (ValueError, TypeError):
                pass

    # Build candidate chart configurations dynamically based on the sheet data.
    candidate_charts = {"charts": []}

    # 1. Line Chart
    line_chart = {
        "title": "Student Scores Over Time",
        "type": "line",
        "xAxis": header[0],
        "data": data_rows,
        "series": [{"key": col} for col in header[1:]]
    }
    candidate_charts["charts"].append(line_chart)

    # 2. Bar Chart
    bar_chart = {
        "title": "Student Scores Over Time (Bar Chart)",
        "type": "bar",
        "xAxis": header[0],
        "data": data_rows,
        "series": [{"key": col} for col in header[1:]]
    }
    candidate_charts["charts"].append(bar_chart)

    # 3. Pie Chart – if there is at least one data row, create a pie chart based on the last row.
    if data_rows:
        last_row = data_rows[-1]
        # Build pie chart data: use the first column value from the last row to name the chart.
        pie_data = []
        for col in header[1:]:
            pie_data.append({
                "Student": col,
                "Score": last_row.get(col)
            })
        pie_chart = {
            "title": f"Student Scores Distribution (Pie Chart for {last_row.get(header[0], 'N/A')})",
            "type": "pie",
            "xAxis": "Student",
            "data": pie_data,
            "series": [{"key": "Score"}]
        }
        candidate_charts["charts"].append(pie_chart)

    # Ask the LLM to select/improve the best chart configurations
    print("user prompt", data.get('prompt'))
    llm_answer_str = ask_llm_about_dashboard(data_rows, candidate_charts, data.get('prompt'))
    if not llm_answer_str:
        return jsonify({
            "error": "LLM returned an empty response."
        }), 500

    llm_answer_str = llm_answer_str.lstrip()
    if llm_answer_str.startswith("```")
[truncated — 5667 more characters]
```

### app/create/page.tsx

```typescript
"use client"

import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/use-toast"
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
import { ArrowLeft, Sparkles, TableProperties } from "lucide-react"
import Link from "next/link"

export default function CreateDashboard() {
  const [sheetUrl, setSheetUrl] = useState("")
  const [prompt, setPrompt] = useState("")
  const [isLoading, setIsLoading] = useState(false)
  const router = useRouter()

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    setIsLoading(true)

    // Redirect to the dashboard page with the sheet URL and prompt as query parameters
    router.push(`/dashboard?sheet_url=${encodeURIComponent(sheetUrl)}&prompt=${encodeURIComponent(prompt)}`);

  }

  return (
    <div className="flex flex-col min-h-screen bg-gray-50">
      <header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
        <div className="container flex items-center justify-between h-16">
          <div className="flex items-center gap-4">
            <Button variant="ghost" size="icon" asChild>
              <Link href="/">
                <ArrowLeft className="h-4 w-4" />
              </Link>
            </Button>
            <div className="flex items-center gap-2">
              <TableProperties className="h-5 w-5 text-primary" />
              <h1 className="text-xl font-semibold">Create New Dashboard</h1>
            </div>
          </div>
        </div>
      </header>

      <main className="flex-grow container mx-auto px-4 py-8">
        <Card className="max-w-xl mx-auto">
          <CardHeader>
            <CardTitle>Dashboard Configuration</CardTitle>
          </CardHeader>
          <CardContent>
            <form onSubmit={handleSubmit} className="space-y-6">
              <div className="space-y-2">
                <Label htmlFor="sheet-url">Google Sheet URL</Label>
                <Input
                  id="sheet-url"
                  type="url"
                  placeholder="https://docs.google.com/spreadsheets/d/..."
                  value={sheetUrl}
                  onChange={(e) => setSheetUrl(e.target.value)}
                  required
                  className="h-12"
                />
              </div>
              <div className="space-y-2">
                <Label htmlFor="prompt">Customization Preferences</Label>
                <Textarea
                  id="prompt"
                  placeholder="Describe your ideal dashboard. For example:
• Chart types you'd like to see
• Specific metrics to highlight
• Color scheme preferences
• Layout suggestions"
                  value={prompt}
                  onChange={(e) => setPrompt(e.target.value)}
                  className="min-h-[160px] resize-none"
                />
              </div>
              <Button 
                type="submit" 
                className="w-full h-12" 
                disabled={isLoading}
              >
                {isLoading ? (
                  <span className="flex items-center gap-2">
                    <Sparkles className="h-4 w-4 animate-spin" />
                    Generating Dashboard...
                  </span>
                ) : (
                  <span className="flex items-center gap-2">
                    <Sparkles className="h-4 w-4" />
                    Generate Dashboard
                  </span>
                )}
              </Button>
            </form>
          </CardContent>
        </Card>
      </main>
    </div>
  )
}
```

### app/dashboard/page.tsx

```typescript
"use client"

import { useSearchParams } from "next/navigation"
import { useState, useEffect } from "react"
import { Responsive, WidthProvider } from "react-grid-layout"
import { useResizeDetector } from "react-resize-detector"
import { LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid, BarChart, Bar, PieChart, Pie, Cell } from "recharts"
import { Card, CardHeader, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import jsPDF from 'jspdf'
import html2canvas from 'html2canvas'
import { Pencil, RefreshCw, Send, X, Download, Loader2 } from "lucide-react"

import "react-grid-layout/css/styles.css"
import "react-resizable/css/styles.css"
import { toast } from "@/components/ui/use-toast"

const ResponsiveGridLayout = WidthProvider(Responsive)

export default function Dashboard() {
  const [isLoading, setIsLoading] = useState(false)
  const searchParams = useSearchParams()
  const sheetUrl = searchParams.get("sheet_url")
  const userPrompt = searchParams.get("prompt")

  const [mounted, setMounted] = useState(false)

  useEffect(() => {
    setMounted(true) // Prevents hydration mismatch
  }, [])

  const [data, setChartData] = useState({
    charts: [], // Initialize with an empty string or some default value
  })

  if (!sheetUrl) {
    return <div>Error: No url provided</div>
  }

  const [showChat, setShowChat] = useState(false)
  const [prompt, setPrompt] = useState("")

  // Fetch data when the page loads
  useEffect(() => {
    let isCancelled = false; // Prevent duplicate calls
  
    const fetchData = async () => {
      if (!sheetUrl) {
        toast({
          title: "Error",
          description: "URL or prompt is missing in search parameters.",
          variant: "destructive",
        });
        return;
      }
  
      setIsLoading(true);
      console.log("Fetching data...");
  
      try {
        const response = await fetch("http://127.0.0.1:5000/analyze-sheet", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ sheet_url: sheetUrl, prompt: userPrompt }),
        });
  
        if (!response.ok) {
          throw new Error("Failed to fetch data");
        }
  
        const result = await response.json();
        if (!isCancelled) {
          setChartData(result);
          toast({
            title: "Success",
            description: "Data successfully loaded.",
            variant: "default",
          });
        }
      } catch (error) {
        if (!isCancelled) {
          toast({
            title: "Error",
            description: "Failed to fetch data. Please try again.",
            variant: "destructive",
          });
          console.error(error);
        }
      } finally {
        if (!isCancelled) setIsLoading(false);
      }
    };
  
    fetchData();
  
    return () => {
      isCancelled = true; // Cleanup function to prevent duplicate calls
    };
  }, [sheetUrl, userPrompt]);
  

  const handleSync = async () => {
    // Implement sync functionality here
    console.log("Syncing data...")

    try {
      // Create a JSON object to send
      // TODO: clear the chart data
      const response = await fetch("http://127.0.0.1:5000/resync", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ sheet_url: sheetUrl, data: data }),
      })

      if (!response.ok) {
        throw new Error("Failed to resync data")
      }

      const result = await response.json()

      toast({
        title: "Success",
        description: "Data successfully resynced.",
        variant: "default",
      })

      // Update chart data
      setChartData(result)
    } catch (error) {
      toast({
        title: "Error",
        description: "Failed to resync data. Please try again.",
        variant: "destructive",
      })
      console.error(error)
    }
  }

  const handleRegenerate = async () => {
    if (!sheetUrl) {
      toast({
        title: "Error",
        description: "Sheet URL is missing. Please provide a valid sheet URL.",
        variant: "destructive",
      })
      return
    }

    console.log("Modifying data...")
    try {
      const response = await fetch("http://127.0.0.1:5000/analyze-sheet", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ sheet_url: sheetUrl, prompt: prompt }),
      })

      console.log("Received response!") // TODO: delete later

      if (!response.ok) {
        throw new Error("Failed to fetch data")
      }

      const result = await response.json()
      setChartData(result)

      setShowChat(false)
      setPrompt("")
      toast({
        title: "Success",
        description: "Dashboard modified successfully.",
        variant: "default",
      })
    } catch (error) {
      toast({
        title: "Error",
        description: "Failed to create dashboard. Please try again.",
        variant: "destructive",
      })
    }
  }

  const layouts = data.charts.map((chart, index) => ({
    i: index.toString(),
    x: (index * 2) % 6,
    y: Math.floor(index / 3) * 2,
    w: 3,
    h: 2,
    minW: 2,
    minH: 2,
  }))

  if (!mounted) {
    return <div>Loading...</div> // Prevents hydration mismatch
  }

  return (
    <div className="flex flex-col min-h-screen bg-gray-50">

      <header className="sticky top-0 z-50 bg-white border-b">
        <div className="container mx-auto px-4 py-4 flex justify-between items-center">
          <div className="flex items-center gap-3">
            <h1 className="text-2xl font-bold text-gray-800">Dashboard</h1>
            {isLoading && (
              <div className="flex items-center gap-2 text-sm text-muted-foreground">
                <Loader2 className="h-4 w-4 animate-spin" />
                Loading...
              </div>
            )}
          </div>
 
[truncated — 6859 more characters]
```

### tailwind.config.ts

```typescript
import type { Config } from "tailwindcss";

const config: Config = {
    darkMode: ["class"],
    content: [
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "*.{js,ts,jsx,tsx,mdx}"
  ],
  theme: {
  	extend: {
  		colors: {
  			background: 'hsl(var(--background))',
  			foreground: 'hsl(var(--foreground))',
  			card: {
  				DEFAULT: 'hsl(var(--card))',
  				foreground: 'hsl(var(--card-foreground))'
  			},
  			popover: {
  				DEFAULT: 'hsl(var(--popover))',
  				foreground: 'hsl(var(--popover-foreground))'
  			},
  			primary: {
  				DEFAULT: 'hsl(var(--primary))',
  				foreground: 'hsl(var(--primary-foreground))'
  			},
  			secondary: {
  				DEFAULT: 'hsl(var(--secondary))',
  				foreground: 'hsl(var(--secondary-foreground))'
  			},
  			muted: {
  				DEFAULT: 'hsl(var(--muted))',
  				foreground: 'hsl(var(--muted-foreground))'
  			},
  			accent: {
  				DEFAULT: 'hsl(var(--accent))',
  				foreground: 'hsl(var(--accent-foreground))'
  			},
  			destructive: {
  				DEFAULT: 'hsl(var(--destructive))',
  				foreground: 'hsl(var(--destructive-foreground))'
  			},
  			border: 'hsl(var(--border))',
  			input: 'hsl(var(--input))',
  			ring: 'hsl(var(--ring))',
  			chart: {
  				'1': 'hsl(var(--chart-1))',
  				'2': 'hsl(var(--chart-2))',
  				'3': 'hsl(var(--chart-3))',
  				'4': 'hsl(var(--chart-4))',
  				'5': 'hsl(var(--chart-5))'
  			},
  			sidebar: {
  				DEFAULT: 'hsl(var(--sidebar-background))',
  				foreground: 'hsl(var(--sidebar-foreground))',
  				primary: 'hsl(var(--sidebar-primary))',
  				'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
  				accent: 'hsl(var(--sidebar-accent))',
  				'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
  				border: 'hsl(var(--sidebar-border))',
  				ring: 'hsl(var(--sidebar-ring))'
  			}
  		},
  		borderRadius: {
  			lg: 'var(--radius)',
  			md: 'calc(var(--radius) - 2px)',
  			sm: 'calc(var(--radius) - 4px)'
  		},
  		keyframes: {
  			'accordion-down': {
  				from: {
  					height: '0'
  				},
  				to: {
  					height: 'var(--radix-accordion-content-height)'
  				}
  			},
  			'accordion-up': {
  				from: {
  					height: 'var(--radix-accordion-content-height)'
  				},
  				to: {
  					height: '0'
  				}
  			}
  		},
  		animation: {
  			'accordion-down': 'accordion-down 0.2s ease-out',
  			'accordion-up': 'accordion-up 0.2s ease-out'
  		}
  	}
  },
  plugins: [require("tailwindcss-animate")],
};
export default config;

```

### lib/utils.ts

```typescript
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

```

### components/theme-provider.tsx

```typescript
'use client'

import * as React from 'react'
import {
  ThemeProvider as NextThemesProvider,
  type ThemeProviderProps,
} from 'next-themes'

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

```

### hooks/use-mobile.tsx

```typescript
import * as React from "react"

const MOBILE_BREAKPOINT = 768

export function useIsMobile() {
  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

  React.useEffect(() => {
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
    const onChange = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    }
    mql.addEventListener("change", onChange)
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
    return () => mql.removeEventListener("change", onChange)
  }, [])

  return !!isMobile
}

```

[53 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]