Skip to content
Merged
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
16 changes: 14 additions & 2 deletions src/app/api/generate/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { NextResponse } from "next/server";
import { getGeminiModel } from "@/lib/gemini";
import { getRepoData, getRepoContents } from "@/lib/octokit";
import { SUPPORTED_LANGUAGES } from "@/constants/languages";

export const dynamic = "force-dynamic";

/**
* AI README Generation Endpoint
* Optimized for data accuracy and clean prompt interpolation.
* Optimized for data accuracy, clean prompt interpolation, and multi-language support.
*
* @param {Request} req - The incoming request object containing the repo URL and optional language.
* @returns {Promise<NextResponse>} A JSON response containing the generated Markdown or an error message.
*/
export async function POST(req: Request) {
let rawUrl: string;
let language: string;
try {
const body = await req.json();
rawUrl = body.url;
const rawLanguage =
typeof body.language === "string" ? body.language.trim() : "";
const normalized =
rawLanguage.charAt(0).toUpperCase() + rawLanguage.slice(1).toLowerCase();
language = (SUPPORTED_LANGUAGES as readonly string[]).includes(normalized)
? normalized
: "English";
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
Expand Down Expand Up @@ -96,7 +108,7 @@ export async function POST(req: Request) {
// Fix: Prompt updated with neutral fallbacks and dynamic license
const prompt = `
**Role**: You are a Principal Solutions Architect and World-Class Technical Writer.
**Task**: Generate a professional, high-conversion README.md for the GitHub repository: "${repo}".
**Task**: Generate a professional, high-conversion README.md for the GitHub repository: "${repo}" in the following language: **${language}**.

---
### 1. PROJECT CONTEXT (VERIFIED DATA)
Expand Down
7 changes: 5 additions & 2 deletions src/app/generate/GeneratePageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ export default function GeneratePageClient({ repoSlug }: GeneratePageProps) {
}
}, [repoSlug]);

const handleGenerate = async (githubUrl: string) => {
const handleGenerate = async (
githubUrl: string,
language: string = "English",
) => {
setIsLoading(true);
setMarkdown("");
try {
const response = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: githubUrl }),
body: JSON.stringify({ url: githubUrl, language }),
});

if (!response.ok) {
Expand Down
74 changes: 53 additions & 21 deletions src/components/Generator/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@
import React, { useState } from "react";
import { Loader2, Github, AlertCircle } from "lucide-react";
import { Button } from "../ui/Button";
import { SUPPORTED_LANGUAGES } from "@/constants/languages";

interface SearchInputProps {
onGenerate: (url: string) => void;
onGenerate: (url: string, language: string) => void;
isLoading: boolean;
initialValue?: string; // optional initial value
ariaLabel?: string; // optional aria-label for accessibility
}

/**
* SearchInput Component
* Renders a responsive form for GitHub URL input and language selection.
*
* @param {SearchInputProps} props - The component props.
* @returns {JSX.Element} The rendered search input form.
*/
export const SearchInput = ({
onGenerate,
isLoading,
Expand All @@ -18,6 +26,7 @@ export const SearchInput = ({
}: SearchInputProps) => {
// Initialize state directly from initialValue once
const [url, setUrl] = useState(initialValue || "");
const [language, setLanguage] = useState("English");
const [error, setError] = useState<string | null>(null);

const handleSubmit = (e: React.FormEvent) => {
Expand All @@ -28,36 +37,59 @@ export const SearchInput = ({
/^https?:\/\/(www\.)?github\.com\/[\w.-]+\/[\w.-]+\/?$/;

if (githubUrlPattern.test(url.trim())) {
onGenerate(url.trim());
onGenerate(url.trim(), language);
} else {
setError("Please enter a valid GitHub repository URL.");
}
};

return (
<div className="w-full max-w-4xl mx-auto">
<form onSubmit={handleSubmit} className="relative group">
<div className="absolute inset-y-0 left-5 flex items-center pointer-events-none text-gray-500 group-focus-within:text-blue-500 transition-colors">
<Github size={20} />
<div className="w-full max-w-4xl mx-auto space-y-4">
<form
onSubmit={handleSubmit}
className="relative group flex flex-col md:flex-row gap-4"
>
<div className="relative flex-grow">
<div className="absolute inset-y-0 left-5 flex items-center pointer-events-none text-gray-500 group-focus-within:text-blue-500 transition-colors">
<Github size={20} />
</div>
<input
type="text"
value={url}
onChange={(e) => {
setUrl(e.target.value);
if (error) setError(null);
}}
placeholder="https://github.com/username/repo"
aria-label={ariaLabel}
className={`w-full bg-zinc-900/50 border ${
error ? "border-red-500/50" : "border-white/10"
} rounded-2xl py-6 pl-14 pr-4 text-white placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all backdrop-blur-xl`}
/>
</div>
<input
type="text"
value={url}
onChange={(e) => {
setUrl(e.target.value);
if (error) setError(null);
}}
placeholder="https://github.com/username/repo"
aria-label={ariaLabel}
className={`w-full bg-zinc-900/50 border ${
error ? "border-red-500/50" : "border-white/10"
} rounded-2xl py-6 pl-14 pr-40 text-white placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all backdrop-blur-xl`}
/>
<div className="absolute inset-y-2 right-2 flex items-center">

<div className="flex gap-4">
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
aria-label="Select language for README generation"
className="bg-zinc-900/50 border border-white/10 rounded-2xl px-6 py-6 text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all backdrop-blur-xl appearance-none cursor-pointer min-w-[140px]"
>
{SUPPORTED_LANGUAGES.map((lang) => (
<option
key={lang}
value={lang}
className="bg-zinc-900 text-white"
>
{lang}
</option>
))}
</select>

<Button
type="submit"
disabled={isLoading || !url}
className="h-full px-8 shadow-lg shadow-blue-500/20"
className="h-full px-8 shadow-lg shadow-blue-500/20 whitespace-nowrap"
>
{isLoading ? (
<Loader2 className="animate-spin" size={18} />
Expand Down
15 changes: 15 additions & 0 deletions src/constants/languages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const SUPPORTED_LANGUAGES = [
"English",
"Spanish",
"French",
"German",
"Chinese",
"Japanese",
"Korean",
"Portuguese",
"Russian",
"Arabic",
"Turkish",
] as const;

export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
Loading