# Project export: Sustaina-Blob

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: Cal Hacks 12.0
- Tagline: Like a Fitbit for AI, our chatbot tracks your query’s carbon footprint, showing how green your searches are and helping you chat consciously and more sustainably
- Devpost: https://devpost.com/software/sustaina-bot
- GitHub: https://github.com/davidcayapan/Calhacks12.git
- Video: https://www.youtube.com/embed/zZlYGIYZ45g?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — pragati-chaturvedi (4 commits), David Cayapan (2 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 19 recognized source files, 55 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (27 of 27)

```
.DS_Store
backend/.DS_Store
backend/.gitignore
backend/app.js
backend/config/index.js
backend/config/rules.json
backend/controllers/ai.controller.js
backend/controllers/analyze.controller.js
backend/package.json
backend/routes/ai.routes.js
backend/routes/analyze.routes.js
backend/server.js
backend/services/ai.service.js
backend/services/analyzer.service.js
backend/utils/text.utils.js
frontend/.env
frontend/.gitignore
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/README.md
frontend/src/api.js
frontend/src/App.css
frontend/src/App.jsx
frontend/src/index.css
frontend/src/main.jsx
frontend/vite.config.js
```

### Dependencies

- backend/package.json: @google/genai@^1.27.0, cors@^2.8.5, dotenv@^17.2.3, express@^5.1.0, express-rate-limit@^8.1.0, helmet@^8.1.0, morgan@^1.10.1, node-fetch@^3.3.2
- frontend/package.json: @eslint/js@^9.36.0, @types/react@^19.1.16, @types/react-dom@^19.1.9, @vitejs/plugin-react@^4.3.1, eslint@^9.36.0, eslint-plugin-react-hooks@^5.2.0, eslint-plugin-react-refresh@^0.4.22, globals@^16.4.0, react@^19.1.1, react-dom@^19.1.1, vite@^5.4.10

### Recent commits (newest first)

- Updated the name
- Issues outlined
- removed chat feature llm
- Connected frontend with backend endpoints
- pushing front end files
- backend and monorepo setup
- Initial commit: React Vite setup

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

### backend/package.json

```
{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs",
  "dependencies": {
    "@google/genai": "^1.27.0",
    "cors": "^2.8.5",
    "dotenv": "^17.2.3",
    "express": "^5.1.0",
    "express-rate-limit": "^8.1.0",
    "helmet": "^8.1.0",
    "morgan": "^1.10.1",
    "node-fetch": "^3.3.2"
  }
}

```

### frontend/package.json

```
{
  "name": "my-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.1.1",
    "react-dom": "^19.1.1"
  },
  "devDependencies": {
    "@eslint/js": "^9.36.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
    "@vitejs/plugin-react": "^4.3.1",
    "eslint": "^9.36.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.22",
    "globals": "^16.4.0",
    "vite": "^5.4.10"
  }
}

```

### backend/server.js

```javascript
/**
 * ================================
 * Entry Point: GreenPrompt Backend
 * ================================
 * Purpose:
 *  - Load environment variables
 *  - Import Express app
 *  - Start the HTTP server
 */

require('dotenv').config();          // Loads .env variables (e.g., PORT)
const app = require('./app');        // Import the configured Express app

// Use PORT from .env, or default to 3000
const port = process.env.PORT || 3000;

// Start the server
app.listen(port, () => {
    console.log(`✅ GreenPrompt backend is running on http://localhost:${port}`);
});

```

### backend/app.js

```javascript
import { useRef, useState } from "react";
import "./App.css";
import { analyzePrompt as analyzeAPI, chatWithLLM } from "./api";

// Robust tip extractor: handles strings or objects; fallbacks if needed.
function pickTip(report) {
    const arr = report?.tips;
    if (Array.isArray(arr) && arr.length > 0) {
        const first = arr[0];
        if (typeof first === "string") return first.trim();
        if (first && typeof first === "object") {
            const t = (first.text || first.description || "").trim();
            if (t) return t;
        }
    }
    if (typeof report?.suggestedPrompt === "string" && report.suggestedPrompt.trim()) {
        return `Try this: ${report.suggestedPrompt.trim()}`;
    }
    const af = report?.autofixes?.[0];
    const afText = (af?.prompt || af?.text || af?.value || "").trim();
    if (afText) return `Suggested rewrite: ${afText}`;
    return "Tip: Ask for concise, structured output (e.g., 3 bullets or JSON) and set a hard length cap.";
}

export default function App() {
    const [query, setQuery] = useState("");
    const [report, setReport] = useState(null);   // flattened for UI
    const [messages, setMessages] = useState([]); // {role:'user'|'assistant', content:string}[]
    const [loadingAnalyze, setLoadingAnalyze] = useState(false);
    const [loadingSend, setLoadingSend] = useState(false);
    const [error, setError] = useState("");
    const inputRef = useRef(null);
    const chatRef = useRef(null);

    const handleAnalyze = async () => {
        const text = query.trim();
        if (!text || loadingAnalyze || loadingSend) return;

        setLoadingAnalyze(true);
        setError("");
        setReport(null);

        try {
            const res = await analyzeAPI(text, { language: "en" });
            const r = res?.report || res;

            const estTokens = r?.metrics?.input?.tokens ?? 0;
            const score = r?.score ?? 0;

            // backend gives kg; UI shows grams
            const co2kg = r?.impact_estimate?.CO2e_kg ?? 0;
            const co2g = Number(co2kg) * 1000;

            const kWh = r?.impact_estimate?.kWh ?? 0;
            const water_L = r?.impact_estimate?.water_L ?? 0;

            const usageLabel =
                estTokens > 200 ? "Very High Token Usage." :
                    estTokens > 100 ? "High Token Usage." :
                        estTokens > 40 ? "Moderate Token Usage." :
                            "Low Token Usage.";

            const tipText = pickTip(r);

            setReport({
                score,
                estTokens,
                co2g,
                kWh,
                water_L,
                usageLabel,
                tipText,
                _raw: r
            });

            // Scroll the report into view (above the input)
            setTimeout(() => {
                document.getElementById("reportCard")?.scrollIntoView({ behavior: "smooth", block: "start" });
            }, 0);
        } catch (e) {
            console.error(e);
            setError(e.message || "Analyze failed.");
        } finally {
            setLoadingAnalyze(false);
            inputRef.current?.focus();
        }
    };

    const handleSend = async () => {
        const text = query.trim();
        if (!text || loadingSend || loadingAnalyze) return;

        setLoadingSend(true);
        setError("");
        setReport(prev => prev); // keep last report visible

        // Push user message
        setMessages(prev => [...prev, { role: "user", content: text }]);
        setQuery("");

        try {
            const res = await chatWithLLM(text, { temperature: 0.3 });
            const reply = (res?.response ?? "").toString();
            setMessages(prev => [...prev, { role: "assistant", content: reply || "(no response)" }]);

            // Scroll chat window to bottom
            setTimeout(() => {
                chatRef.current?.scrollTo({ top: chatRef.current.scrollHeight, behavior: "smooth" });
            }, 0);
        } catch (e) {
            console.error(e);
            setError(e.message || "Send failed.");
        } finally {
            setLoadingSend(false);
            inputRef.current?.focus();
        }
    };

    // Gauge math (unchanged)
    const size = 140;
    const stroke = 14;
    const rsize = (size - stroke) / 2;
    const C = 2 * Math.PI * rsize;
    const pct = report?.score ?? 0;
    const dash = (pct / 100) * C;

    // Green bubble text: live tip if we have a report, else greeting
    const bubbleText =
        report?.tipText ||
        "Hi there! I'm Sustaina-Bot. Let’s make your AI queries greener!";

    return (
        <div className="shell">
            {/* Mascot & tip bubble */}
            <div className="mascotWrap" aria-hidden>
                <div className="bubble">{bubbleText}</div>
                <div className="mascot"><div className="eyes"><span /><span /></div></div>
            </div>

            {/* Hero */}
            <header className="hero">
                <h1 className="brand">
                    <span className="leaf" aria-hidden>
                        <svg viewBox="0 0 24 24" width="32" height="32">
                            <path d="M19.5 3.5c-7.5 0-12 4.2-14 9.6a7 7 0 0 0 9.9 9c4.8-2.3 8.1-8.1 8.1-15.6 0-1.3-.9-3-4-3Z" fill="currentColor" />
                        </svg>
                    </span>
                    Sustaina-Bot
                </h1>
                <p className="sub"></p>
            </header>

            {/* Chat window (appears above input; input is at the very bottom) */}
            <section className="chatSection">
                <h2 className="reportTitle">Chat</h2>
                <div className="chatWindow" ref={chatRef} aria-live="polite">
                    {messages.length === 0 ? (
                        <div className="chatEmpty">Start a conversation or run an analysis.</div>
                    ) : (
                        messages.map((m, i) => (
                          
[truncated — 6650 more characters]
```

### frontend/src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### backend/config/index.js

```javascript
/**
 * ===================
 * Config Loader
 * ===================
 * Purpose:
 *  - Load environment variables and static configuration files.
 *  - Make them accessible across modules.
 */

const fs = require('fs');
const path = require('path');

// Read and parse the rules.json config
const rulesPath = path.join(__dirname, 'rules.json');
const RULES = JSON.parse(fs.readFileSync(rulesPath, 'utf-8'));

// Export both .env values and rule definitions
module.exports = {
    env: process.env.NODE_ENV || 'development',
    rules: RULES
};

```

### frontend/src/App.jsx

```javascript
import { useRef, useState } from "react";
import "./App.css";
import { analyzePrompt as analyzeAPI } from "./api";

/** ------------------ Helper functions ------------------ **/

// Normalize backend tips (string or object) → array of strings
function normalizeTips(r) {
  const arr = r?.tips;
  if (!Array.isArray(arr)) return [];
  return arr
    .map((t) => {
      if (typeof t === "string") return t.trim();
      if (t && typeof t === "object") return String(t.text || t.description || "").trim();
      return "";
    })
    .filter(Boolean);
}

export default function App() {
  const [query, setQuery] = useState("");
  const [report, setReport] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
  const inputRef = useRef(null);

  const handleAnalyze = async () => {
    if (!query.trim() || loading) return;
    setLoading(true);
    setError("");
    setReport(null);

    try {
      const res = await analyzeAPI(query, { max_tokens: 200, temperature: 0.3 });
      const r = res.report || res;

      const estTokens = r?.metrics?.input?.tokens ?? 0;
      const score = r?.score ?? 0;
      const co2kg = r?.impact_estimate?.CO2e_kg ?? 0;
      const co2g = Number(co2kg) * 1000;
      const kWh = r?.impact_estimate?.kWh ?? 0;
      const water_L = r?.impact_estimate?.water_L ?? 0;

      const usageLabel =
        estTokens > 200 ? "Very High Token Usage." :
          estTokens > 100 ? "High Token Usage." :
            estTokens > 40 ? "Moderate Token Usage." :
              "Low Token Usage.";

      const allTips = normalizeTips(r);
      const tipText =
        allTips[0] ||
        r?.suggestedPrompt ||
        "Tip: Ask for concise, structured output (e.g., 3 bullets or JSON) and set a hard length cap.";

      setReport({
        score,
        estTokens,
        co2g,
        kWh,
        water_L,
        usageLabel,
        tipText,
        tips: allTips,
        issues: Array.isArray(r?.issues) ? r.issues : [],
        _raw: r
      });

      inputRef.current?.focus();
      setTimeout(() => {
        document.getElementById("reportCard")?.scrollIntoView({ behavior: "smooth", block: "start" });
      }, 0);
    } catch (e) {
      console.error(e);
      setError(e.message || "Analyze failed.");
    } finally {
      setLoading(false);
    }
  };

  /** Ring math for circular gauge **/
  const size = 140;
  const stroke = 14;
  const rsize = (size - stroke) / 2;
  const C = 2 * Math.PI * rsize;
  const pct = report?.score ?? 0;
  const dash = (pct / 100) * C;

  /** Bubble text **/
  const bubbleText =
    report?.tipText ||
    "Hi there! I'm Sustaina-Blob. Let’s make your AI queries greener!";

  return (
    <div className="shell">
      {/* Mascot */}
      <div className="mascotWrap" aria-hidden>
        <div className="bubble">{bubbleText}</div>
        <div className="mascot"><div className="eyes"><span /><span /></div></div>
      </div>

      {/* Hero */}
      <header className="hero">
        <h1 className="brand">
          <span className="leaf" aria-hidden>
            <svg viewBox="0 0 24 24" width="32" height="32">
              <path
                d="M19.5 3.5c-7.5 0-12 4.2-14 9.6a7 7 0 0 0 9.9 9c4.8-2.3 8.1-8.1 8.1-15.6 0-1.3-.9-3-4-3Z"
                fill="currentColor"
              />
            </svg>
          </span>
          Sustaina-Blob
        </h1>
      </header>

      {/* Input panel */}
      <section className="panel">
        <label className="fieldLabel" htmlFor="queryInput">
          Your Query:
        </label>
        <div className="inputRow">
          <input
            id="queryInput"
            ref={inputRef}
            className="queryInput"
            type="text"
            placeholder="Enter your prompt here ..."
            value={query}
            onChange={(e) => {
              setQuery(e.target.value);
              if (report) setReport(null);
              if (error) setError("");
            }}
          />
          <button
            className="analyzeBtn"
            onClick={handleAnalyze}
            disabled={!query.trim() || loading}
            aria-label="Analyze"
            title="Analyze"
          >
            <span className="bolt" aria-hidden>
              <svg viewBox="0 0 24 24" width="22" height="22">
                <path d="M13 2 3 14h7l-1 8 10-12h-7l1-8Z" fill="currentColor" />
              </svg>
            </span>
            {loading ? "Analyzing…" : "Analyze"}
          </button>
        </div>
        {error && <div style={{ color: "#f87171", marginTop: 8 }}>{error}</div>}
      </section>

      {/* Sustainability Report */}
      {report && (
        <section id="reportCard" className="reportCard" aria-live="polite">
          <h2 className="reportTitle">Sustainability Report</h2>

          <div className="gaugeRow">
            <div className="gauge">
              <svg
                width={size}
                height={size}
                viewBox={`0 0 ${size} ${size}`}
                role="img"
                aria-label={`Score ${pct}%`}
              >
                <circle
                  cx={size / 2}
                  cy={size / 2}
                  r={rsize}
                  fill="none"
                  stroke="rgba(255,255,255,.08)"
                  strokeWidth={stroke}
                />
                <circle
                  className="gaugeProgress"
                  cx={size / 2}
                  cy={size / 2}
                  r={rsize}
                  fill="none"
                  stroke="#10b981"
                  strokeWidth={stroke}
                  strokeLinecap="round"
                  strokeDasharray={`${dash} ${C - dash}`}
                  transform={`rotate(-90 ${size / 2} ${size / 2})`}
                />
              </svg>
              <div className="gaugeCenter">
                <div className="gaugeValue">{pct}%</div>
                <div className="gaugeLabel">Score</di
[truncated — 3638 more characters]
```

### frontend/vite.config.js

```javascript
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    host: true,
    port: 4173,      // or 5174 if you prefer
    strictPort: true,
    // Optional if Live Share relay breaks HMR:
    // hmr: { protocol: 'wss', clientPort: 443 }
  }
})

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>my-app</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

### frontend/eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{js,jsx}'],
    extends: [
      js.configs.recommended,
      reactHooks.configs['recommended-latest'],
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        ecmaVersion: 'latest',
        ecmaFeatures: { jsx: true },
        sourceType: 'module',
      },
    },
    rules: {
      'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
    },
  },
])

```

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