Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ cd ../backend
npm install
```

### 4. Configure environment variables

Create a `.env` file in `backend/` based on `.env.example` and add:

```
DATABASE_URL=postgresql://<user>:<password>@<host>:<port>/<database>
GEMINI_API_KEY=your-gemini-api-key
GEMINI_MODEL=gemini-2.5-flash # select model here
```

## Running the Application

### Start the frontend
Expand Down
5 changes: 4 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
DATABASE_URL=postgresql://<user>:<password>@<host>:<port>/<database>
DATABASE_URL=postgresql://<user>:<password>@<host>:<port>/<database>

GEMINI_API_KEY=your-gemini-api-key
GEMINI_MODEL=gemini-2.5-flash
10 changes: 10 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"license": "ISC",
"type": "module",
"dependencies": {
"@google/generative-ai": "^0.24.1",
"cors": "^2.8.6",
"express": "^5.2.1",
"pg": "^8.20.0"
Expand Down
30 changes: 30 additions & 0 deletions backend/src/controllers/geminiController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Request, Response } from "express";
import { runPrompt } from "../services/geminiService.js";

// Demo to test prompts for now
// Once tournament generation is complete, this will be integrated
export const runGeminiDemo = async (req: Request, res: Response) => {
const { prompt } = req.body ?? {};

if (typeof prompt !== "string" || prompt.trim().length === 0) {
return res.status(400).json({ error: "Prompt is required" });
}

try {
const result = await runPrompt(prompt);
return res.json(result);
} catch (err) {
console.error("GEMINI DEMO ERROR:", err);

const message =
err instanceof Error
? err.message
: "Gemini request failed. Verify GEMINI_API_KEY and GEMINI_MODEL.";

return res.status(500).json({
error: message,
hint:
"Ensure GEMINI_API_KEY is valid and GEMINI_MODEL (default gemini-2.5-flash) is supported. Try a smaller Flash/Pro variant if needed.",
});
}
};
4 changes: 3 additions & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import express from "express";
import cors from "cors";
import authRoutes from "./routes/authRoute.js";
import geminiRoutes from "./routes/geminiRoute.js";


const app = express();
Expand All @@ -26,4 +27,5 @@ app.get("/api/hello", (req, res) => {
res.json({message: "Hello world!"});
})

app.use("/api", authRoutes);
app.use("/api", authRoutes);
app.use("/api", geminiRoutes);
8 changes: 8 additions & 0 deletions backend/src/routes/geminiRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from "express";
import { runGeminiDemo } from "../controllers/geminiController.js";

const router = express.Router();

router.post("/llm/demo", runGeminiDemo);

export default router;
48 changes: 48 additions & 0 deletions backend/src/services/geminiService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { GoogleGenerativeAI } from "@google/generative-ai";

// Load environment variables from .env in non-production environments
if (process.env.NODE_ENV !== "production") {
const dotenv = await import("dotenv");
dotenv.config();
}

const apiKey = process.env.GEMINI_API_KEY;
// Allow overriding model, default to a supported flash model
const modelName = process.env.GEMINI_MODEL ?? "gemini-2.5-flash";

let gemini: GoogleGenerativeAI | null = null;

if (!apiKey) {
console.warn("GEMINI_API_KEY is not set, Gemini requests will fail.");
} else {
gemini = new GoogleGenerativeAI(apiKey);
}

export const runPrompt = async (prompt: string) => {
if (!gemini) {
throw new Error("Gemini API key not configured");
}

const model = gemini.getGenerativeModel({ model: modelName });

try {
// Send the prompt
const result = await model.generateContent(prompt);
const text = result.response.text();

return { text, model: modelName };

} catch (err: unknown) {
if (typeof err === "object" && err !== null && "status" in err) {
const status = (err as { status?: number }).status;
if (status === 404) {
throw new Error(
`Model ${modelName} not found. Update GEMINI_MODEL to a supported model for your API key.`
);
}
}

throw err;
}

};
27 changes: 27 additions & 0 deletions frontend/src/api/geminiApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type GeminiDemoResponse = {
text: string;
model: string;
};

export async function callGeminiDemo(prompt: string): Promise<GeminiDemoResponse> {
if (!prompt || prompt.trim().length === 0) {
throw new Error("Prompt cannot be empty");
}

const res = await fetch("/api/llm/demo", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ prompt }),
});

if (!res.ok) {
// Attempt to get server-provided error, otherwise fallback
const errBody = await res.json().catch(() => ({}));
const message = (errBody as { error?: string }).error ?? `Request failed with status ${res.status}`;
throw new Error(message);
}

return res.json();
}
76 changes: 76 additions & 0 deletions frontend/src/pages/geminiDemo/GeminiDemoPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { callGeminiDemo, type GeminiDemoResponse } from "../../api/geminiApi";
import "./styles/geminiDemo.css";

function GeminiDemoPage() {
const [prompt, setPrompt] = useState("");
const [result, setResult] = useState<GeminiDemoResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();

const handleSubmit = async () => {
const trimmed = prompt.trim();
if (!trimmed) {
setError("Please enter a prompt to try the demo.");
setResult(null);
return;
}

setLoading(true);
setError(null);
setResult(null);

try {
const data = await callGeminiDemo(trimmed);
setResult(data);
} catch (err) {
const message = err instanceof Error ? err.message : "Something went wrong";
setError(message);
} finally {
setLoading(false);
}
};

return (
<div className="gemini-page">
<div className="gemini-card">
<h1 className="gemini-title">Gemini LLM Demo</h1>
<p className="gemini-subtitle">
Enter a prompt to test the backend demo endpoint.
</p>

<textarea
className="gemini-textarea"
rows={6}
placeholder="e.g., Write a haiku about tournament brackets."
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
/>

<div className="gemini-actions">
<button className="gemini-button" onClick={handleSubmit} disabled={loading}>
{loading ? "Sending..." : "Send to Gemini"}
</button>
<button className="gemini-link" onClick={() => navigate("/")}>
Back to home
</button>
</div>

{error && <div className="gemini-error">{error}</div>}

{result && (
<div className="gemini-result">
<div className="gemini-result-label">Model</div>
<div className="gemini-chip">{result.model}</div>
<div className="gemini-result-label">Response</div>
<p className="gemini-result-text">{result.text}</p>
</div>
)}
</div>
</div>
);
}

export default GeminiDemoPage;
120 changes: 120 additions & 0 deletions frontend/src/pages/geminiDemo/styles/geminiDemo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
.gemini-page {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: radial-gradient(circle at 20% 20%, #f2e9dc, #e6c498);
padding: 24px;
box-sizing: border-box;
}

.gemini-card {
width: min(820px, 100%);
background: #ffffff;
border-radius: 16px;
padding: 28px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.06);
}

.gemini-title {
margin: 0 0 4px;
font-weight: 600;
font-size: 2rem;
}

.gemini-subtitle {
margin: 0 0 16px;
color: #4b5563;
}

.gemini-textarea {
width: 100%;
box-sizing: border-box;
padding: 12px;
border-radius: 10px;
border: 1px solid #cbd5e1;
font-family: inherit;
font-size: 1rem;
resize: vertical;
min-height: 120px;
background: #f9fafb;
}

.gemini-textarea:focus {
outline: 2px solid #007edf;
background: #fff;
}

.gemini-actions {
display: flex;
align-items: center;
gap: 12px;
margin: 16px 0 12px;
}

.gemini-button {
background-color: #007edf;
color: white;
border: none;
padding: 10px 20px;
font-size: 1rem;
font-family: inherit;
cursor: pointer;
border-radius: 10px;
}

.gemini-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}

.gemini-link {
background: transparent;
border: none;
color: #007edf;
font-size: 0.95rem;
cursor: pointer;
padding: 0;
}

.gemini-error {
background: #fef2f2;
border: 1px solid #fecaca;
color: #b91c1c;
padding: 10px 12px;
border-radius: 10px;
margin-bottom: 12px;
}

.gemini-result {
margin-top: 8px;
padding: 14px;
border-radius: 12px;
background: #f8fafc;
border: 1px solid #e2e8f0;
}

.gemini-result-label {
font-size: 0.9rem;
color: #475569;
margin-bottom: 4px;
}

.gemini-chip {
display: inline-flex;
align-items: center;
gap: 6px;
background: #e0f2fe;
color: #0369a1;
padding: 6px 10px;
border-radius: 20px;
font-size: 0.9rem;
margin-bottom: 12px;
}

.gemini-result-text {
margin: 0;
white-space: pre-wrap;
line-height: 1.4;
}
2 changes: 1 addition & 1 deletion frontend/src/pages/landing/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { callHelloAPI } from "../../api/helloApi";
import "./styles/landingStyles.css";
Expand Down