Skip to content
Open
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
25 changes: 4 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
<h1 align="center">InstantCoder</h1>
<h1 align="center">Codi Mini</h1>
<p align="center">
Generate small apps with one prompt. Powered by the Gemini API.
Generate small apps with one prompt. Powered by the Codinera
</p>

Try it in https://huggingface.co/spaces/osanseviero/InstantCoder

This project is fully based on [llamacoder](https://github.com/Nutlope/llamacoder). Please follow [Nutlope](https://github.com/Nutlope) and give them a star.

## Tech stack

- [Gemini API](https://ai.google.dev/gemini-api/docs) to use Gemini 1.5 Pro, Gemini 1.5 Flash, and Gemini 2.0 Flash Experimental
- [Sandpack](https://sandpack.codesandbox.io/) for the code sandbox
- Next.js app router with Tailwind

You can also experiment with Gemini in [Google AI Studio](https://aistudio.google.com/).

## Cloning & running

1. Clone the repo: `git clone https://github.com/osanseviero/GemCoder`
2. Create a `.env` file and add your [Google AI Studio API key](https://aistudio.google.com/app/apikey): `GOOGLE_AI_API_KEY=`
3. Run `npm install` and `npm run dev` to install dependencies and run locally

**This is a personal project and not a Google official project**


19 changes: 13 additions & 6 deletions app/(main)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export default function Layout({
children: React.ReactNode;
}>) {
return (
<body className="bg-brand dark:bg-dark antialiased dark:text-gray-100">
// Removed flex properties from body
<body className="bg-brand dark:bg-dark antialiased dark:text-gray-100 relative">
{/* Background elements */}
<div className="absolute inset-0 dark:bg-dark-radial" />
<div className="absolute inset-x-0 flex justify-center">
<Image
Expand All @@ -21,12 +23,17 @@ export default function Layout({
/>
</div>

<div className="isolate relative">
<div className="mx-auto flex min-h-screen max-w-7xl flex-col items-center justify-center py-2">
<div className="fixed right-4 top-4 z-50">
<ThemeToggle />
</div>
{/* Main content container - Removed min-h-screen */}
<div className="relative isolate flex flex-col items-center py-2">
{/* Theme Toggle */}
<div className="fixed right-4 top-4 z-50">
<ThemeToggle />
</div>

{/* Width constraint and centering for content */}
<div className="mx-auto flex w-full max-w-7xl flex-col items-center">
<Header />
{/* Children (page content) */}
{children}
<Footer />
</div>
Expand Down
282 changes: 236 additions & 46 deletions app/(main)/page.tsx

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions app/api/downloadProject/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { NextResponse } from 'next/server';
import archiver from 'archiver';
import path from 'path';
import fs from 'fs';
import { PassThrough } from 'stream';

// Define files/directories to exclude from the zip
const excludedPaths = [
'node_modules',
'.next',
'.git',
'.env',
'prisma/migrations',
// Add any other files/directories to exclude
// e.g., 'dist', 'build', '*.log'
];

// Function to check if a path should be excluded
const isExcluded = (filePath: string, projectRoot: string): boolean => {
const relativePath = path.relative(projectRoot, filePath);
return excludedPaths.some(excluded => {
// Check for exact match or if it's a directory prefix
if (relativePath === excluded || relativePath.startsWith(excluded + path.sep)) {
return true;
}
// Check for .env files specifically
if (excluded === '.env' && path.basename(relativePath).startsWith('.env')) {
return true;
}
// Add more complex glob patterns if needed
return false;
});
};

export async function GET() {
const projectRoot = process.cwd(); // Gets the root directory (/Users/mertk/Documents/GitHub/InstantCoder-main)
const archive = archiver('zip', {
zlib: { level: 9 }, // Sets the compression level.
});

// Use PassThrough stream to pipe the archive data
const passThrough = new PassThrough();
archive.pipe(passThrough);

// Handle archive errors
archive.on('error', (err) => {
console.error('Archive creation error:', err);
// Cannot send response here as headers might already be sent
});

// Add files to the archive, excluding specified paths
archive.glob('**/*', {
cwd: projectRoot,
ignore: excludedPaths, // Use archiver's built-in ignore
dot: true, // Include dotfiles unless explicitly excluded
});

// Finalize the archive asynchronously
archive.finalize().catch(err => {
console.error('Error finalizing archive:', err);
// Handle finalization error if needed, though response might be sent
});

// Return the stream as the response
return new NextResponse(passThrough as any, { // Cast needed for type compatibility
status: 200,
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename="instantcoder-project.zip"',
},
});
}
30 changes: 25 additions & 5 deletions app/api/generateCode/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,34 @@ export async function POST(req: Request) {

let { model, messages } = result.data;
let systemPrompt = getSystemPrompt();
const geminiModel = genAI.getGenerativeModel({ model: model });

const geminiModel = genAI.getGenerativeModel({model: model});
// Map messages to the format expected by the Gemini API
// Prepend system prompt to the first user message
const geminiMessages = messages.map((msg, index) => {
const role = msg.role === "assistant" ? "model" : "user";
let content = msg.content;
// Add system prompt and specific instructions to the first user message
// Add system prompt and specific instructions to the first user message
if (msg.role === "user" && index === 0) {
content = systemPrompt + "\n\nUser Prompt: " + content + "\nPlease ONLY return code, NO backticks or language names. Don't start with ```typescript or ```javascript or ```tsx or ```.";
} else if (msg.role === 'user' && index > 0) {
// For subsequent user messages (modifications), explicitly ask to modify previous code
content = "Based on the previous code provided by the assistant, please apply the following modification: " + content + "\nPlease ONLY return the *complete, modified* React code, NO backticks or language names.";
}
return {
role: role,
parts: [{ text: content }],
};
});

const geminiStream = await geminiModel.generateContentStream(
messages[0].content + systemPrompt + "\nPlease ONLY return code, NO backticks or language names. Don't start with \`\`\`typescript or \`\`\`javascript or \`\`\`tsx or \`\`\`."
);
console.log("Sending messages to Gemini:", JSON.stringify(geminiMessages, null, 2));

console.log(messages[0].content + systemPrompt + "\nPlease ONLY return code, NO backticks or language names. Don't start with \`\`\`typescript or \`\`\`javascript or \`\`\`tsx or \`\`\`.")
// Pass the entire mapped message history
const geminiStream = await geminiModel.generateContentStream({
contents: geminiMessages,
// Optional: Add generationConfig if needed, e.g., temperature, maxOutputTokens
});

const readableStream = new ReadableStream({
async start(controller) {
Expand Down
14 changes: 5 additions & 9 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import type { Metadata } from "next";
import "./globals.css";
import { ThemeProvider } from "@/components/ThemeProvider";

let title = "Gemini Coder – AI Code Generator";
let title = "Codinera – AI Code Generator";
let description = "Generate your next app with Gemini";
let url = "https://llamacoder.io/";
let ogimage = "https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg";
let sitename = "geminicoder.io";
let sitename = "combocoder.com";

export const metadata: Metadata = {
metadataBase: new URL(url),
Expand All @@ -24,12 +24,7 @@ export const metadata: Metadata = {
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
images: [ogimage],
title,
description,
},

};

export default function RootLayout({
Expand All @@ -38,7 +33,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en" className="h-full">
// Removed h-full from html tag
<html lang="en" suppressHydrationWarning>
<ThemeProvider>
{children}
</ThemeProvider>
Expand Down
51 changes: 9 additions & 42 deletions components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,22 @@ import Link from "next/link";
export default function Footer() {
return (
<footer className="mb-3 mt-5 flex h-16 w-full flex-col items-center justify-between space-y-3 px-3 pt-4 text-center sm:mb-0 sm:h-20 sm:flex-row sm:pt-2">
<div>
{/* Ensure text color adapts to dark mode */}
<div className="text-gray-700 dark:text-gray-300">
<div className="font-medium">
Built with{" "}
<a
href="https://ai.google.dev/gemini-api/docs"
className="font-semibold text-blue-600 underline-offset-4 transition hover:text-gray-700 hover:underline"
href="www.codinera.com"
// Updated link colors
className="font-semibold text-primary-600 dark:text-primary-400 underline-offset-4 transition hover:text-primary-500 dark:hover:text-primary-300 hover:underline"
target="_blank"
>
Gemini API
Codinera
</a>{" "}
and{" "}
<a
href="https://github.com/nutlope/llamacoder"
className="font-semibold text-blue-600 underline-offset-4 transition hover:text-gray-700 hover:underline"
target="_blank"
>
Inspired on Llamacoder
</a>
. This is not an official Google product.
</div>
</div>
<div className="flex space-x-4 pb-4 sm:pb-0">
<Link
href="https://twitter.com/osanseviero"
className="group"
aria-label=""
target="_blank"
>
<svg
aria-hidden="true"
className="h-6 w-6 fill-gray-500 group-hover:fill-gray-700"
>
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0 0 22 5.92a8.19 8.19 0 0 1-2.357.646 4.118 4.118 0 0 0 1.804-2.27 8.224 8.224 0 0 1-2.605.996 4.107 4.107 0 0 0-6.993 3.743 11.65 11.65 0 0 1-8.457-4.287 4.106 4.106 0 0 0 1.27 5.477A4.073 4.073 0 0 1 2.8 9.713v.052a4.105 4.105 0 0 0 3.292 4.022 4.093 4.093 0 0 1-1.853.07 4.108 4.108 0 0 0 3.834 2.85A8.233 8.233 0 0 1 2 18.407a11.615 11.615 0 0 0 6.29 1.84" />
</svg>
</Link>
<Link
href="https://github.com/osanseviero/geminicoder"
className="group"
aria-label="TaxPal on GitHub"
target="_blank"
>
<svg
aria-hidden="true"
className="h-6 w-6 fill-slate-500 group-hover:fill-slate-700"
>
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
</svg>
</Link>

</div>
</div>

</footer>
);
}
27 changes: 17 additions & 10 deletions components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import Image from "next/image";
import Link from "next/link";
import logo from "../public/logo.svg";
import logo from "../public/logo.png";
import GithubIcon from "./github-icon";

export default function Header() {
return (
<header className="relative mx-auto mt-5 flex w-full items-center justify-center px-2 pb-7 sm:px-4">
<Link href="/" className="absolute flex items-center gap-2">
<Image alt="header text" src={logo} className="h-5 w-5" />
<h1 className="text-xl tracking-tight">
<span className="text-blue-600">Gemini</span>Coder
</h1>
</Link>
<Link href="/" className="absolute flex flex-col items-center gap-1 text-gray-900 dark:text-gray-100">
<div className="flex items-center gap-2">
<Image alt="header text" src={logo} className="h-5 w-5" />
<h1 className="text-xl tracking-tight">
<span className="text-primary-600 dark:text-primary-400">Codinera</span> Mini
</h1>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400">
Powered by <a href="https://codinera.com" className="hover:underline text-primary-600 dark:text-primary-400">Codinera</a>
</p>
</Link>
<a
href="https://github.com/osanseviero/geminicoder"
href="https://github.com/mertksk/Codinera-mini"
target="_blank"
className="ml-auto hidden items-center gap-3 rounded-2xl bg-white dark:bg-[#1E293B] dark:text-gray-100 px-6 py-2 sm:flex border border-gray-200 dark:border-gray-700"
// Updated background, text, and border colors
className="ml-auto hidden items-center gap-3 rounded-2xl bg-white dark:bg-primary-900/60 text-gray-700 dark:text-gray-300 px-6 py-2 sm:flex border border-gray-300 dark:border-primary-700 hover:bg-gray-50 dark:hover:bg-primary-800/60"
>
<GithubIcon className="h-4 w-4 dark:text-gray-100" />
{/* Updated icon color */}
<GithubIcon className="h-4 w-4 text-gray-500 dark:text-gray-400" />
<span>GitHub Repo</span>
</a>
</header>
Expand Down
34 changes: 27 additions & 7 deletions components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,46 @@ type ThemeContextType = {
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
// Initialize state without assuming a default theme to avoid initial mismatch
const [theme, setTheme] = useState<Theme | null>(null);

// Effect to determine the initial theme on mount (client-side only)
useEffect(() => {
const savedTheme = localStorage.getItem("theme") as Theme;
const savedTheme = localStorage.getItem("theme") as Theme | null;
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;

if (savedTheme) {
setTheme(savedTheme);
document.documentElement.classList.toggle("dark", savedTheme === "dark");
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
} else if (prefersDark) {
setTheme("dark");
document.documentElement.classList.add("dark");
} else {
setTheme("light"); // Default to light if nothing else is set
}
}, []);

// Effect to apply the theme class to the HTML element *after* state is set
useEffect(() => {
if (theme) {
document.documentElement.classList.toggle("dark", theme === "dark");
}
}, [theme]); // Run whenever the theme state changes

const toggleTheme = () => {
// Ensure theme is not null before toggling
if (!theme) return;

const newTheme = theme === "light" ? "dark" : "light";
setTheme(newTheme);
localStorage.setItem("theme", newTheme);
document.documentElement.classList.toggle("dark", newTheme === "dark");
// The useEffect above will handle the class toggle
};

// Prevent rendering children until the theme is determined to avoid flash/mismatch
if (!theme) {
return null; // Or a loading indicator, but null avoids rendering potentially mismatched content
}

// Pass the non-null theme to the context
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
Expand All @@ -45,4 +65,4 @@ export function useTheme() {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
}
5 changes: 3 additions & 2 deletions components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export default function ThemeToggle() {
return (
<button
onClick={toggleTheme}
className="flex h-8 w-8 items-center justify-center rounded-lg bg-gray-200 transition-colors hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600"
// Updated background colors to use primary (purple) theme
className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-100 text-primary-600 transition-colors hover:bg-primary-200 dark:bg-primary-800 dark:text-primary-300 dark:hover:bg-primary-700"
aria-label="Toggle dark mode"
>
{theme === "light" ? (
Expand Down Expand Up @@ -44,4 +45,4 @@ export default function ThemeToggle() {
)}
</button>
);
}
}
Loading