diff --git a/.env.example b/.env.example index 67c4fcd..a1f8f78 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# Copy to .env.local and fill in values. See README for provider setup. +# .env.local'a kopyalayın ve değerleri doldurun. Daha Fazlası için README dosyasını okuyun.. # --- Supabase (optional: response caching is off if these are empty) --- SUPABASE_URL= @@ -18,9 +18,10 @@ OPENROUTER_API_KEY= # OPENROUTER_HTTP_REFERER=https://yoursite.example # OPENROUTER_APP_TITLE=gitreverse -# Cache TTL for reverse-prompt results in Supabase (hours; default 24) -# CACHE_TTL_HOURS=24 - # REQUIRED in production — generate with: openssl rand -hex 32 (or PowerShell random hex) # Without this, the app will refuse to start in production. VIEWS_IP_SALT= + +# --- Custom reverse (optional) --- +# Backend base URL for Custom reverse (local or your own deployment). +# CUSTOM_REVERSE_SERVICE_URL=http://localhost:3001 diff --git a/.gitignore b/.gitignore index 7b8da95..d1cf3ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# Dosyaları yok sayma hakkında daha fazla bilgi için https://help.github.com/articles/ignoring-files/ adresini ziyaret edin. -# dependencies +# dependencies / bağımlılıklar /node_modules /.pnp .pnp.* @@ -10,27 +10,27 @@ !.yarn/releases !.yarn/versions -# testing +# testing / Testler /coverage # next.js /.next/ /out/ -# production +# production / Yayınlama /build -# misc +# misc / diğerleri .DS_Store *.pem -# debug +# debug / hata ayıklama npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) +# env files (can opt-in for committing if needed) / env dosyaları (gerekirse commit işlemine dahil edilebilir) .env* !.env.example diff --git a/AGENTS.md b/AGENTS.md index 8bd0e39..bc1d8f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,5 @@ -# This is NOT the Next.js you know +# Bu Next.js DEĞİL, Biliyorsun. -This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. +Bu Sürümde uyumsuz değişiklikler bulunmaktadır — API'ler, kurallar ve dosya yapısı, eğitim verilerinizden farklı olabilir. Herhangi bir kod yazmadan önce `node_modules/next/dist/docs/` ilgili kılavuzu **okuyun.** Kullanımdan kaldırılacak özelliklere ilişkin uyarıları dikkate alın. diff --git a/README.md b/README.md index 3d6ec82..89b8e7f 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,58 @@ -# GitReverse +# GitReverse Turkish (Translated by mehmetdemir-tr) https://github.com/user-attachments/assets/f0cdb7b2-c6f0-4483-8a01-153170479f2e -Turn a **public GitHub repository** into a **single synthetic user prompt** that someone might paste into Cursor, Claude Code, Codex, etc. to vibe code the project from scratch. +Bir **Geliştirilmeye açık GitHub deposunu** birisinin Cursor, Claude Code, Codex vb. uygulamalara yapıştırarak projeyi sıfırdan kodlayabileceği **tek sentetik yapay zeka istem satırına** dönüştürün. -The app pulls **repo metadata**, a **root file tree** (depth 1), and the **README**, then uses an LLM via [OpenRouter](https://openrouter.ai/) to produce one short, conversational prompt grounded in that context. +Bu uygulama, **repo meta verilerini**, bir **kök dosya ağacını** (derinlik yani depth 1) ve **README** dosyasını alır; ardından [OpenRouter](https://openrouter.ai/) aracılığıyla bir LLM kullanarak bu bağlamda temellendirilmiş kısa ve sohbet tarzında bir istem satırı oluşturur. -Paste a GitHub URL or `owner/repo` on the home page. You can also open **`/owner/repo`** (e.g. `/vercel/next.js`) for a shareable link that runs the same flow. +Ana sayfaya bir Github URL'si veya 'owner/repo' (sahip/depo) yazın. Aynı iş akışını çalıştıran paylaşılabilir bir bağlantı için **'/sahip/depo'** (ör. '/vercel/next.js) adresinide açabilirsiniz. -## Stack +Bu sitedeki GitHub tarzı **'/owner/repo/tree/...'** URL'leri, 404 hatası vermemeleri için **`/owner/repo`** adresine yönlendirilir. Ters akış şimdilik hala tüm depoyu kullanmaktadır; **alt klasörleri algılayan** bağlam (o yolun kapsamına dahil) daha sonraki bir güncellemede eklenecektir. -Next.js (App Router), React, TypeScript, Tailwind CSS, GitHub API, OpenRouter. +## Yığın (Stack) +Next.js (Uygulama Yönlendiricisi), React, TypeScript, Tailwind CSS, Github API Ve OpenRouter. -## Configuration +## Yapılandırma +`.env.example` dosyasını `.env.local` dosyasına kopyalayın. **`OPENROUTER_API_KEY`**'e ihtiyacınız var. İsteğe bağlı: `OPENROUTER_MODEL` (varsayılan olarak `google/gemini-2.5-pro`), daha iyi GitHub hız sınırları için `GITHUB_TOKEN` ve sunucu tarafında önbellekleme istiyorsanız örnek dosyadaki Supabase ortam değişkenleri. -Copy `.env.example` to `.env.local`. You need **`OPENROUTER_API_KEY`**. Optional: `OPENROUTER_MODEL` (defaults to `google/gemini-2.5-pro`), `GITHUB_TOKEN` for better GitHub rate limits, and Supabase env vars from the example file if you want server-side caching. +### Özel ters yönlendirme (isteğe bağlı) + + + +**deep / focus** istemleri için, **custom_reverse** hizmetini (ayrı bir TypeScript projesi; `README` dosyasına bakın; `pnpm dev`, varsayılan bağlantı noktası **3001**) yerel olarak çalıştırın veya kendi arka ucunuza dağıtın. `.env.local` dosyasında şunu ayarlayın: + + + +`CUSTOM_REVERSE_SERVICE_URL=http://localhost:3001` + + + +`SUPABASE_URL` ve `SUPABASE_PUBLISHABLE_KEY` ayarlandığında başarılı çalıştırmalar Supabase'de (`custom_prompt_cache`) saklanır; bunlar genel kütüphanede **görünmez**. + + + +Ardından ana sayfada **Özel tersine çevirme** seçeneğini etkinleştirin ve neyin tersine mühendislik yapılacağını açıklayın. + + + +## Geliştirme -## Development ```bash + pnpm install + pnpm dev + ``` -Open [http://localhost:3000](http://localhost:3000). -```bash -pnpm build -pnpm start -pnpm lint -``` -Shout out to [GitIngest](http://github.com/coderamp-labs/gitingest) for inspiration. +[http://localhost:3000](http://localhost:3000) adresini açın. + + +[GitIngest](http://github.com/coderamp-labs/gitingest)'e fikir ve ilham için teşekkürler! :) + +***Çeviri: mehmetdemir-tr (Mehmet Demir)*** + diff --git a/app/[owner]/[repo]/[focus]/page.tsx b/app/[owner]/[repo]/[focus]/page.tsx new file mode 100644 index 0000000..63673bf --- /dev/null +++ b/app/[owner]/[repo]/[focus]/page.tsx @@ -0,0 +1,69 @@ +import { notFound } from "next/navigation"; +import { connection } from "next/server"; +import { ReversePromptHome } from "@/components/reverse-prompt-home"; +import { focusFingerprint } from "@/lib/focus-fingerprint"; +import { isValidGitHubRepoPath, normalizeRepoSegment } from "@/lib/parse-github-repo"; +import { getSupabase } from "@/lib/supabase"; + +type PageProps = { + params: Promise<{ owner: string; repo: string; focus: string }>; +}; + +export default async function RepoFocusPage({ params }: PageProps) { + await connection(); + const { owner: ownerRaw, repo: repoRaw, focus: focusRaw } = await params; + const owner = decodeURIComponent(ownerRaw); + const repo = decodeURIComponent(repoRaw); + let focus: string; + try { + focus = decodeURIComponent(focusRaw); + } catch { + notFound(); + } + + if (!isValidGitHubRepoPath(owner, repo)) { + notFound(); + } + + const trimmedFocus = focus.trim(); + if (!trimmedFocus) { + notFound(); + } + + const repoNorm = normalizeRepoSegment(repo); + const initialRepoInput = `${owner}/${repoNorm}`; + const fp = focusFingerprint(trimmedFocus); + + let cachedPrompt: string | undefined; + try { + const supabase = getSupabase(); + if (supabase) { + const { data } = await supabase + .from("custom_prompt_cache") + .select("prompt") + .eq("owner", owner) + .eq("repo", repoNorm) + .eq("focus_fingerprint", fp) + .maybeSingle(); + if (data?.prompt) { + cachedPrompt = data.prompt as string; + } + } + } catch { + // fall back to client auto-submit + } + + return ( + + ); +} diff --git a/app/[owner]/[repo]/deep/page.tsx b/app/[owner]/[repo]/deep/page.tsx new file mode 100644 index 0000000..6b0d833 --- /dev/null +++ b/app/[owner]/[repo]/deep/page.tsx @@ -0,0 +1,57 @@ +import { notFound } from "next/navigation"; +import { connection } from "next/server"; +import { ReversePromptHome } from "@/components/reverse-prompt-home"; +import { DEEP_REVERSE_FOCUS, focusFingerprint } from "@/lib/focus-fingerprint"; +import { isValidGitHubRepoPath, normalizeRepoSegment } from "@/lib/parse-github-repo"; +import { getSupabase } from "@/lib/supabase"; + +type PageProps = { + params: Promise<{ owner: string; repo: string }>; +}; + +export default async function RepoDeepPage({ params }: PageProps) { + await connection(); + const { owner: ownerRaw, repo: repoRaw } = await params; + const owner = decodeURIComponent(ownerRaw); + const repo = decodeURIComponent(repoRaw); + + if (!isValidGitHubRepoPath(owner, repo)) { + notFound(); + } + + const repoNorm = normalizeRepoSegment(repo); + const initialRepoInput = `${owner}/${repoNorm}`; + const fp = focusFingerprint(DEEP_REVERSE_FOCUS); + + let cachedPrompt: string | undefined; + try { + const supabase = getSupabase(); + if (supabase) { + const { data } = await supabase + .from("custom_prompt_cache") + .select("prompt") + .eq("owner", owner) + .eq("repo", repoNorm) + .eq("focus_fingerprint", fp) + .maybeSingle(); + if (data?.prompt) { + cachedPrompt = data.prompt as string; + } + } + } catch { + // fall back to client auto-submit + } + + return ( + + ); +} diff --git a/app/[owner]/[repo]/page.tsx b/app/[owner]/[repo]/page.tsx index ac451b4..0aaa5ad 100644 --- a/app/[owner]/[repo]/page.tsx +++ b/app/[owner]/[repo]/page.tsx @@ -46,6 +46,7 @@ export default async function RepoPage({ params }: PageProps) { initialPrompt={cachedPrompt} owner={owner} repo={repoNorm} + initialGenerationKind={cachedPrompt ? "quick" : undefined} /> ); } diff --git a/app/[owner]/[repo]/tree/[[...path]]/page.tsx b/app/[owner]/[repo]/tree/[[...path]]/page.tsx new file mode 100644 index 0000000..ba1019c --- /dev/null +++ b/app/[owner]/[repo]/tree/[[...path]]/page.tsx @@ -0,0 +1,25 @@ +import { notFound, redirect } from "next/navigation"; +import { connection } from "next/server"; +import { isValidGitHubRepoPath, normalizeRepoSegment } from "@/lib/parse-github-repo"; + +type PageProps = { + params: Promise<{ owner: string; repo: string; path?: string[] }>; +}; + +/** + * GitHub-style `/owner/repo/tree/branch/...` → `/owner/repo` (avoids 404). + * Subfolder-scoped reverse context: planned for later; see README. + */ +export default async function RepoTreeRedirectPage({ params }: PageProps) { + await connection(); + const { owner: ownerRaw, repo: repoRaw } = await params; + const owner = decodeURIComponent(ownerRaw); + const repo = decodeURIComponent(repoRaw); + + if (!isValidGitHubRepoPath(owner, repo)) { + notFound(); + } + + const repoNorm = normalizeRepoSegment(repo); + redirect(`/${encodeURIComponent(owner)}/${encodeURIComponent(repoNorm)}`); +} diff --git a/app/api/custom-reverse/route.ts b/app/api/custom-reverse/route.ts new file mode 100644 index 0000000..6e26c75 --- /dev/null +++ b/app/api/custom-reverse/route.ts @@ -0,0 +1,473 @@ +import { NextRequest, NextResponse } from "next/server"; +import http from "node:http"; +import https from "node:https"; +import { URL } from "node:url"; +import { DEEP_REVERSE_FOCUS, focusFingerprint } from "@/lib/focus-fingerprint"; +import { parseGitHubRepoInput } from "@/lib/parse-github-repo"; +import { getSupabase } from "@/lib/supabase"; + +export const runtime = "nodejs"; + +const DEFAULT_CUSTOM_REVERSE_URL = "http://localhost:3001"; + +function getServiceUrl(): string { + return ( + process.env.CUSTOM_REVERSE_SERVICE_URL?.trim() || DEFAULT_CUSTOM_REVERSE_URL + ); +} + +/** 15 min hard cap — route-level abort. */ +const ROUTE_TIMEOUT_MS = 900_000; + +const inFlight = new Map>(); + +function buildInFlightKey(owner: string, repo: string, focus: string): string { + return `${owner}/${repo}:${focusFingerprint(focus)}`; +} + +/** + * Raw http.request with no socket/headers timeout (Node default is 0 = none). + * Needed because global fetch (Undici) has a 5-minute headersTimeout by default, + * which causes "fetch failed" on runs that take longer before sending any response. + */ +function httpPost( + url: string, + body: string +): Promise<{ status: number; json: unknown }> { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const isHttps = parsed.protocol === "https:"; + const lib = isHttps ? https : http; + + const req = lib.request( + { + hostname: parsed.hostname, + port: parsed.port || (isHttps ? 443 : 80), + path: parsed.pathname + parsed.search, + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(body), + }, + timeout: 0, // no socket inactivity timeout + }, + (res) => { + const chunks: Buffer[] = []; + res.on("data", (chunk: Buffer) => chunks.push(chunk)); + res.on("end", () => { + const text = Buffer.concat(chunks).toString("utf8"); + let json: unknown; + try { + json = JSON.parse(text); + } catch { + reject(new Error(`Upstream returned non-JSON: ${text.slice(0, 200)}`)); + return; + } + resolve({ status: res.statusCode ?? 0, json }); + }); + res.on("error", reject); + } + ); + + req.on("error", reject); + req.write(body); + req.end(); + }); +} + +/** Background-parse SSE for `event: done` and persist; errors ignored. */ +async function parseSseStreamForDonePersist( + body: ReadableStream, + repoUrl: string, + focus: string +): Promise { + const reader = body.getReader(); + const dec = new TextDecoder(); + let buf = ""; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buf += dec.decode(value, { stream: true }); + for (;;) { + const idx = buf.indexOf("\n\n"); + if (idx < 0) break; + const block = buf.slice(0, idx); + buf = buf.slice(idx + 2); + if (!block.includes("event: done")) continue; + const dataLine = block + .split("\n") + .find((l) => l.startsWith("data: ")); + if (!dataLine) continue; + try { + const json = JSON.parse( + dataLine.slice(5).trim() as string + ) as { prompt?: string }; + if (typeof json.prompt === "string" && json.prompt) { + persistCustomPromptCache({ + repoUrl: repoUrl.trim(), + focus, + prompt: json.prompt, + }); + } + } catch { + // ignore + } + } + } + } catch { + // ignore + } finally { + try { + reader.releaseLock(); + } catch { + // ignore + } + } +} + +function persistCustomPromptCache(opts: { + repoUrl: string; + focus: string; + prompt: string; +}): void { + const sb = getSupabase(); + if (!sb) return; + const parsed = parseGitHubRepoInput(opts.repoUrl); + if (!parsed) return; + const fp = focusFingerprint(opts.focus); + void sb + .from("custom_prompt_cache") + .upsert( + { + owner: parsed.owner, + repo: parsed.repo, + focus: opts.focus, + focus_fingerprint: fp, + prompt: opts.prompt, + cached_at: new Date().toISOString(), + }, + { onConflict: "owner,repo,focus_fingerprint" } + ) + .then(({ error }) => { + if (error) { + console.error( + "[custom-reverse] Supabase upsert failed:", + error.message + ); + } + }); +} + +async function executeCustomReverse(opts: { + repoUrl: string; + customPrompt: string | undefined; + isDeep: boolean; + focus: string; + parsed: { owner: string; repo: string } | null; +}): Promise { + const { repoUrl, customPrompt, isDeep, focus, parsed } = opts; + const fp = focusFingerprint(focus); + + if (parsed) { + const supabase = getSupabase(); + if (supabase) { + try { + const { data, error } = await supabase + .from("custom_prompt_cache") + .select("prompt") + .eq("owner", parsed.owner) + .eq("repo", parsed.repo) + .eq("focus_fingerprint", fp) + .maybeSingle(); + if (!error && data?.prompt) { + return NextResponse.json({ + prompt: data.prompt as string, + fromCache: true, + }); + } + } catch { + // cache miss — continue to upstream + } + } + } + + const base = getServiceUrl().replace(/\/$/, ""); + + const upstreamBody: { repoUrl: string; customPrompt?: string; mode?: "deep" } = + { + repoUrl: repoUrl.trim(), + }; + if (isDeep) { + upstreamBody.mode = "deep"; + } else { + upstreamBody.customPrompt = customPrompt!.trim(); + } + + let upstreamStatus: number; + let data: unknown; + try { + const timer = new Promise((_, reject) => + setTimeout( + () => reject(new Error("__timeout__")), + ROUTE_TIMEOUT_MS + ) + ); + const result = await Promise.race([ + httpPost(`${base}/reverse`, JSON.stringify(upstreamBody)), + timer, + ]); + upstreamStatus = result.status; + data = result.json; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + const isTimeout = msg === "__timeout__"; + return NextResponse.json( + { + error: isTimeout + ? "Manual control timed out. Try a smaller repo or a narrower prompt." + : `Manual control service unreachable (${msg}). Check CUSTOM_REVERSE_SERVICE_URL and that the service is running.`, + }, + { status: 503 } + ); + } + + if (upstreamStatus < 200 || upstreamStatus >= 300) { + const err = + data && + typeof data === "object" && + "error" in data && + typeof (data as { error: unknown }).error === "string" + ? (data as { error: string }).error + : `Request failed (${upstreamStatus})`; + return NextResponse.json( + { error: err }, + { status: upstreamStatus >= 400 && upstreamStatus < 600 ? upstreamStatus : 502 } + ); + } + + const prompt = + data && + typeof data === "object" && + "prompt" in data && + typeof (data as { prompt: unknown }).prompt === "string" + ? (data as { prompt: string }).prompt + : null; + + if (!prompt) { + return NextResponse.json( + { error: "Manual control service did not return a prompt." }, + { status: 502 } + ); + } + + persistCustomPromptCache({ + repoUrl: repoUrl.trim(), + focus, + prompt, + }); + + return NextResponse.json({ prompt }, { status: 200 }); +} + +/** + * Server-Sent Events proxy to `custom_reverse` /reverse/stream, with cache short-circuit. + * Tee response to persist the final `done` event in Supabase. + */ +async function executeCustomReverseStream(opts: { + repoUrl: string; + customPrompt: string | undefined; + isDeep: boolean; + focus: string; + parsed: { owner: string; repo: string } | null; +}): Promise { + const { repoUrl, customPrompt, isDeep, focus, parsed } = opts; + const fp = focusFingerprint(focus); + + if (parsed) { + const supabase = getSupabase(); + if (supabase) { + try { + const { data, error } = await supabase + .from("custom_prompt_cache") + .select("prompt") + .eq("owner", parsed.owner) + .eq("repo", parsed.repo) + .eq("focus_fingerprint", fp) + .maybeSingle(); + if (!error && data?.prompt) { + return NextResponse.json({ + prompt: data.prompt as string, + fromCache: true, + }); + } + } catch { + // cache miss + } + } + } + + const base = getServiceUrl().replace(/\/$/, ""); + const upstreamBody: { repoUrl: string; customPrompt?: string; mode?: "deep" } = { + repoUrl: repoUrl.trim(), + }; + if (isDeep) { + upstreamBody.mode = "deep"; + } else { + upstreamBody.customPrompt = customPrompt!.trim(); + } + + let upstream: Response; + try { + const ac = new AbortController(); + const timer = setTimeout(() => ac.abort(), ROUTE_TIMEOUT_MS); + upstream = await fetch(`${base}/reverse/stream`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(upstreamBody), + signal: ac.signal, + }); + clearTimeout(timer); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + const isAbortOrTimeout = + /abort/i.test(msg) || msg === "The operation was aborted."; + return NextResponse.json( + { + error: isAbortOrTimeout + ? "Manual control timed out. Try a smaller repo or a narrower prompt." + : `Manual control service unreachable (${msg}). Check CUSTOM_REVERSE_SERVICE_URL and that the service is running.`, + }, + { status: 503 } + ); + } + + if (!upstream.ok) { + let err = `Request failed (${upstream.status})`; + try { + const j = (await upstream.json()) as { error?: string }; + if (j.error) err = j.error; + } catch { + // ignore + } + return NextResponse.json( + { error: err }, + { status: upstream.status >= 400 && upstream.status < 600 ? upstream.status : 502 } + ); + } + + if (!upstream.body) { + return NextResponse.json( + { error: "Manual control service returned an empty body." }, + { status: 502 } + ); + } + + const [toClient, toParse] = upstream.body.tee(); + void parseSseStreamForDonePersist( + toParse, + repoUrl.trim(), + focus + ); + + return new NextResponse(toClient, { + status: 200, + headers: { + "Content-Type": "text/event-stream; charset=utf-8", + "Cache-Control": "no-cache, no-transform", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }, + }); +} + +export async function POST(request: NextRequest) { + let body: { + repoUrl?: string; + customPrompt?: string; + mode?: string; + stream?: boolean; + }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + + const repoUrl = body.repoUrl; + const customPrompt = body.customPrompt; + const isDeep = body.mode === "deep"; + const useStream = body.stream === true; + + if (typeof repoUrl !== "string" || !repoUrl.trim()) { + return NextResponse.json( + { error: "repoUrl is required (string)" }, + { status: 400 } + ); + } + if ( + !isDeep && + (typeof customPrompt !== "string" || !customPrompt.trim()) + ) { + return NextResponse.json( + { error: "customPrompt is required (string)" }, + { status: 400 } + ); + } + + const trimmedUrl = repoUrl.trim(); + const focus = isDeep ? DEEP_REVERSE_FOCUS : customPrompt!.trim(); + const parsed = parseGitHubRepoInput(trimmedUrl); + const parsedForCache = parsed + ? { owner: parsed.owner, repo: parsed.repo } + : null; + + if (useStream) { + if (!parsedForCache) { + return executeCustomReverseStream({ + repoUrl: trimmedUrl, + customPrompt, + isDeep, + focus, + parsed: null, + }); + } + return executeCustomReverseStream({ + repoUrl: trimmedUrl, + customPrompt, + isDeep, + focus, + parsed: parsedForCache, + }); + } + + if (!parsedForCache) { + return executeCustomReverse({ + repoUrl: trimmedUrl, + customPrompt, + isDeep, + focus, + parsed: null, + }); + } + + const key = buildInFlightKey(parsedForCache.owner, parsedForCache.repo, focus); + const existing = inFlight.get(key); + if (existing) { + return await existing; + } + + const promise = executeCustomReverse({ + repoUrl: trimmedUrl, + customPrompt, + isDeep, + focus, + parsed: parsedForCache, + }); + inFlight.set(key, promise); + try { + return await promise; + } finally { + inFlight.delete(key); + } +} diff --git a/app/api/increment-views/route.ts b/app/api/increment-views/route.ts index 7a2a807..d7a83fd 100644 --- a/app/api/increment-views/route.ts +++ b/app/api/increment-views/route.ts @@ -1,6 +1,5 @@ import { createHash } from "crypto"; import { NextRequest, NextResponse } from "next/server"; -import { isHomeExampleRepo } from "@/lib/home-example-repos"; import { isValidGitHubRepoPath, normalizeRepoSegment, @@ -23,7 +22,7 @@ if ( ) { throw new Error( "[increment-views] VIEWS_IP_SALT is not set. " + - "Set a random secret (openssl rand -hex 32) in your deployment env." + "Deployment ortamında rastgele bir secret ayarlayın (openssl rand -hex 32)." ); } @@ -95,10 +94,6 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "Invalid owner or repo." }, { status: 400 }); } - if (isHomeExampleRepo(owner, repo)) { - return NextResponse.json({ ok: true }); - } - const supabase = getSupabase(); if (!supabase) { return NextResponse.json({ error: "Database unavailable." }, { status: 503 }); diff --git a/app/api/reverse-prompt/route.ts b/app/api/reverse-prompt/route.ts index 8238158..a43a304 100644 --- a/app/api/reverse-prompt/route.ts +++ b/app/api/reverse-prompt/route.ts @@ -81,11 +81,6 @@ function buildUserMessage( ].join("\n"); } -function cacheTtlHours(): number { - const n = Number(process.env.CACHE_TTL_HOURS); - return Number.isFinite(n) && n > 0 ? n : 24; -} - /** Maps to client 429 handling → “Browse the library” (same as GitHub/rate limits). */ function isExhaustedCreditsOrQuotaMessage(msg: string): boolean { const lower = msg.toLowerCase(); @@ -187,26 +182,16 @@ export async function POST(request: NextRequest) { const promise = (async () => { const supabase = getSupabase(); - let stalePrompt: string | null = null; if (supabase) { try { - const ttlHours = cacheTtlHours(); const { data, error } = await supabase .from("prompt_cache") - .select("prompt, cached_at") + .select("prompt") .eq("owner", owner) .eq("repo", repo) .maybeSingle(); if (!error && data?.prompt) { - if (data.cached_at) { - const ageHours = - (Date.now() - new Date(data.cached_at).getTime()) / 36e5; - if (ageHours < ttlHours) { - return { prompt: data.prompt as string }; - } - } - // Entry exists but is stale — keep as fallback - stalePrompt = data.prompt as string; + return { prompt: data.prompt as string }; } } catch { // cache miss — continue to GitHub + LLM @@ -319,9 +304,6 @@ export async function POST(request: NextRequest) { isExhaustedCreditsOrQuotaMessage(msg); if (creditsExhausted) { - if (stalePrompt) { - return { prompt: stalePrompt }; - } return NextResponse.json( { error: "Service is currently over capacity. Try again later." }, { status: 429 } diff --git a/app/history/page.tsx b/app/history/page.tsx index 2a41012..d835a32 100644 --- a/app/history/page.tsx +++ b/app/history/page.tsx @@ -1,8 +1,8 @@ import { HistoryPage } from "@/components/history-page"; export const metadata = { - title: "History — GitReverse", - description: "Repositories you recently viewed on GitReverse.", + title: "Geçmiş — GitReverse", + description: "GitReverse'de görüntülediğin Depoların geçmişini görüntüle.", }; export default function HistoryRoute() { diff --git a/app/layout.tsx b/app/layout.tsx index 7a46845..fef2160 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -16,7 +16,7 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { title: "GitReverse", description: - "Turn a GitHub repository into a plain-language coding agent prompt.", + "Herhangi bir GitHub deposunu düz metin olarak hazırlanmış bir isteme dönüştürün.", }; export default function RootLayout({ diff --git a/app/library/page.tsx b/app/library/page.tsx index 04f65ec..4e8548b 100644 --- a/app/library/page.tsx +++ b/app/library/page.tsx @@ -5,9 +5,9 @@ import { LibraryPage } from "@/components/library-page"; export const dynamic = "force-dynamic"; export const metadata = { - title: "Prompt Library — GitReverse", + title: "Komut Kütüphanesi — GitReverse", description: - "Browse 1,000+ reverse-engineered prompts from real GitHub repositories.", + "1,000+'den fazla oluşturulmuş komut kütüphanesini gezin.", }; const INITIAL_LIMIT = 24; diff --git a/components/history-page.tsx b/components/history-page.tsx index 3d82b1b..5a652ff 100644 --- a/components/history-page.tsx +++ b/components/history-page.tsx @@ -10,9 +10,30 @@ type HistoryEntry = { owner: string; repo: string; visitedAt: string; + /** `quick`, `deep`, or `m:${focus}`; omitted in older localStorage rows (= quick). */ + historySlot?: string; promptPreview?: string; + lastGenerationType?: "quick" | "deep" | "manual"; + lastManualFocus?: string; }; +function historySlotOf(e: { historySlot?: string }): string { + return e.historySlot ?? "quick"; +} + +function historyHref(entry: HistoryEntry): string { + const o = encodeURIComponent(entry.owner); + const r = encodeURIComponent(entry.repo); + const slot = historySlotOf(entry); + if (slot === "deep") return `/${o}/${r}/deep`; + if (slot.startsWith("m:")) { + const focus = entry.lastManualFocus?.trim() || slot.slice(2); + if (!focus) return `/${o}/${r}`; + return `/${o}/${r}/${encodeURIComponent(focus)}`; + } + return `/${o}/${r}`; +} + function isHistoryEntry(x: unknown): x is HistoryEntry { if ( typeof x !== "object" || @@ -24,21 +45,34 @@ function isHistoryEntry(x: unknown): x is HistoryEntry { return false; } const pv = (x as HistoryEntry).promptPreview; - return pv === undefined || typeof pv === "string"; + if (pv !== undefined && typeof pv !== "string") return false; + const gt = (x as HistoryEntry).lastGenerationType; + if ( + gt !== undefined && + gt !== "hızlı" && + gt !== "derin" && + gt !== "manuel" + ) { + return false; + } + const mf = (x as HistoryEntry).lastManualFocus; + if (mf !== undefined && typeof mf !== "string") return false; + const hs = (x as HistoryEntry).historySlot; + return hs === undefined || typeof hs === "string"; } function relativeTime(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const s = Math.floor(diff / 1000); - if (s < 60) return "just now"; + if (s < 60) return "Şimdi"; const m = Math.floor(s / 60); - if (m < 60) return `${m}m ago`; + if (m < 60) return `${m}dk önce`; const h = Math.floor(m / 60); - if (h < 24) return `${h}h ago`; + if (h < 24) return `${h}saat önce`; const d = Math.floor(h / 24); - if (d < 30) return `${d}d ago`; + if (d < 30) return `${d}gün önce`; const mo = Math.floor(d / 30); - return `${mo}mo ago`; + return `${mo}ay önce`; } export function HistoryPage() { @@ -57,7 +91,8 @@ export function HistoryPage() { return; } const list = parsed.filter(isHistoryEntry); - const sorted = [...list].sort( + const withPrompt = list.filter((e) => e.promptPreview?.trim()); + const sorted = [...withPrompt].sort( (a, b) => new Date(b.visitedAt).getTime() - new Date(a.visitedAt).getTime() ); @@ -103,7 +138,7 @@ export function HistoryPage() { History

- Repositories you opened recently on this device. + Önceki oluşturduğunuz istemler.

@@ -111,19 +146,19 @@ export function HistoryPage() {

Loading…

) : entries.length === 0 ? (

- No history yet. Check out the{" "} + Geçmiş yok (şimdilik) şuraya bir bak:{" "} - Prompt Library + istem Kütüphanesi .

) : (
    {entries.map((e) => ( -
  • +
  • ))} @@ -134,9 +169,30 @@ export function HistoryPage() { ); } +function generationBadge(entry: HistoryEntry): { label: string; title?: string } | null { + const t = entry.lastGenerationType; + if (t === "deep") { + return { label: "Derin", title: "Derin Tersine Mühendislik" }; + } + if (t === "manual") { + const f = entry.lastManualFocus?.trim(); + const short = + f && f.length > 48 ? `${f.slice(0, 48).trimEnd()}…` : f; + return { + label: short ? `Manuel: ${short}` : "Manuel", + title: f ?? "Manuel Kontrol", + }; + } + if (t === "quick") { + return { label: "Hızlı", title: "Hızlı tersine komut" }; + } + return null; +} + function HistoryCard({ entry }: { entry: HistoryEntry }) { const router = useRouter(); - const href = `/${encodeURIComponent(entry.owner)}/${encodeURIComponent(entry.repo)}`; + const href = historyHref(entry); + const badge = generationBadge(entry); return (
    {entry.repo}

    + {badge ? ( + + {badge.label} + + ) : null}
    = { - trending: "Trending", - newest: "Newest first", - oldest: "Oldest first", + trending: "Popüler", + newest: "En yenisi", + oldest: "En eskisi", }; const PAGE_SIZE = 24; @@ -28,15 +28,15 @@ const PAGE_SIZE = 24; function relativeTime(iso: string): string { const diff = Date.now() - new Date(iso).getTime(); const s = Math.floor(diff / 1000); - if (s < 60) return "just now"; + if (s < 60) return "şimdi"; const m = Math.floor(s / 60); - if (m < 60) return `${m}m ago`; + if (m < 60) return `${m}dk önce`; const h = Math.floor(m / 60); - if (h < 24) return `${h}h ago`; + if (h < 24) return `${h}saat önce`; const d = Math.floor(h / 24); - if (d < 30) return `${d}d ago`; + if (d < 30) return `${d}gün önce`; const mo = Math.floor(d / 30); - return `${mo}mo ago`; + return `${mo}ay önce`; } type LibraryPageProps = { @@ -132,7 +132,7 @@ export function LibraryPage({ initialData, initialTotal }: LibraryPageProps) { href="/history" className="font-semibold text-zinc-900 transition-transform hover:-translate-y-0.5" > - History + Geçmiş

    - Prompt Library + İstem Kütüphanesi

    - Reverse-engineered prompts from real GitHub repositories. + Tersine Mühendislikle oluşturan orijinal GitHub depolarının istemleri.

    @@ -275,8 +275,8 @@ export function LibraryPage({ initialData, initialTotal }: LibraryPageProps) { {entries.length === 0 ? (
    -

    No prompts found

    -

    Try a different search term.

    +

    Karaya vurdun! (404)

    {/* a little joke :D*/} +

    Aradığın istem bulunamadı, farklı bir şey arat.

    ) : (
    @@ -286,7 +286,7 @@ export function LibraryPage({ initialData, initialTotal }: LibraryPageProps) {
    )} - {/* Load more */} + {/* Az daha yükle */} {hasMore && (
    @@ -329,30 +329,25 @@ export function LibraryPage({ initialData, initialTotal }: LibraryPageProps) {
    ); diff --git a/components/reverse-generation-flavor-text.tsx b/components/reverse-generation-flavor-text.tsx new file mode 100644 index 0000000..c915e64 --- /dev/null +++ b/components/reverse-generation-flavor-text.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useEffect, useState } from "react"; + +const ELLIPSIS_MS = 450; +const FLAVOR_MS = 3000; + +/** Standard “Get Prompt” flow: light copy, no deep-dive or manual-control phrasing. */ +const QUICK_FLAVOR_LINES = [ + "Gathering repository metadata", + "Scanning the README and top-level files", + "Shaping a single prompt from what we find", + "Polishing the wording", + "Packaging it for your editor", + "Almost there", + "Putting on the finishing touches", +] as const; + +const ELLIPSIS_FRAMES = ["", ".", "..", "..."] as const; + +/** + * Presentational rotating lines for the quick (non-manual) prompt path. + * Manual / deep uses live SSE status instead of this component. + */ +export function ReverseGenerationFlavorText() { + const lines = QUICK_FLAVOR_LINES; + + const [flavorIndex, setFlavorIndex] = useState(0); + const [ellipsisIndex, setEllipsisIndex] = useState(0); + + useEffect(() => { + const ellipsisId = window.setInterval(() => { + setEllipsisIndex((i) => (i + 1) % ELLIPSIS_FRAMES.length); + }, ELLIPSIS_MS); + + const flavorId = window.setInterval(() => { + setFlavorIndex((i) => (i + 1) % lines.length); + }, FLAVOR_MS); + + return () => { + window.clearInterval(ellipsisId); + window.clearInterval(flavorId); + }; + }, [lines.length]); + + const line = lines[flavorIndex] ?? lines[0]; + const dots = ELLIPSIS_FRAMES[ellipsisIndex] ?? ""; + + return ( +

    + {line} + + {dots} + +

    + ); +} diff --git a/components/reverse-prompt-home.tsx b/components/reverse-prompt-home.tsx index 476fa55..e111abe 100644 --- a/components/reverse-prompt-home.tsx +++ b/components/reverse-prompt-home.tsx @@ -2,10 +2,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; -import { HOME_EXAMPLES, isHomeExampleRepo } from "@/lib/home-example-repos"; +import { ReverseGenerationFlavorText } from "@/components/reverse-generation-flavor-text"; +import { HOME_EXAMPLES } from "@/lib/home-example-repos"; import { parseGitHubRepoInput } from "@/lib/parse-github-repo"; const GITREVERSE_HISTORY_KEY = "gitreverse_history"; +const GITREVERSE_HISTORY_MAX = 20; const HISTORY_PROMPT_PREVIEW_LEN = 160; function historyPromptPreview(text: string): string { @@ -14,11 +16,48 @@ function historyPromptPreview(text: string): string { return `${t.slice(0, HISTORY_PROMPT_PREVIEW_LEN).trimEnd()}…`; } +/** Stable row id: `quick`, `deep`, or `m:${trimmedFocus}` (manual). */ +function historySlotOf(e: { historySlot?: string }): string { + return e.historySlot ?? "quick"; +} + +function historySlotFromProps( + preserveUrl: boolean, + autoSubmitDeep: boolean, + autoSubmitFocus: string | undefined, + initialManualFocus: string | undefined, + initialGenerationKind: "quick" | "deep" | "manual" | undefined +): string { + if (preserveUrl) { + if (autoSubmitDeep || initialGenerationKind === "deep") return "deep"; + const focus = + (autoSubmitFocus?.trim() || initialManualFocus?.trim()) ?? ""; + if (initialGenerationKind === "manual" || focus) { + return `m:${focus}`; + } + } + return "quick"; +} + +function historySlotFromGenerationState( + kind: "quick" | "deep" | "manual" | null, + manualFocus: string | null +): string | null { + if (kind == null) return null; + if (kind === "deep") return "deep"; + if (kind === "manual") return `m:${manualFocus?.trim() ?? ""}`; + return "quick"; +} + type GitreverseHistoryEntry = { owner: string; repo: string; visitedAt: string; + /** Distinguishes quick vs deep vs manual rows for the same repo. */ + historySlot?: string; promptPreview?: string; + lastGenerationType?: "quick" | "deep" | "manual"; + lastManualFocus?: string; }; type ReversePromptHomeProps = { @@ -27,6 +66,15 @@ type ReversePromptHomeProps = { initialPrompt?: string; owner?: string; repo?: string; + /** Auto-run Deep Reverse on mount (shareable `/owner/repo/deep`). */ + autoSubmitDeep?: boolean; + /** Auto-run manual control with this focus on mount (shareable `/owner/repo/`). */ + autoSubmitFocus?: string; + /** When true, do not rewrite the URL to `/:owner/:repo` after generation. */ + preserveUrl?: boolean; + /** When SSR provides a cached prompt, record how it was produced for history. */ + initialGenerationKind?: "quick" | "deep" | "manual"; + initialManualFocus?: string; }; export function ReversePromptHome({ @@ -35,13 +83,35 @@ export function ReversePromptHome({ initialPrompt, owner, repo, + autoSubmitDeep = false, + autoSubmitFocus, + preserveUrl = false, + initialGenerationKind, + initialManualFocus, }: ReversePromptHomeProps) { const [repoUrl, setRepoUrl] = useState(initialRepoInput); + const [customReverse, setCustomReverse] = useState(false); + const [customPrompt, setCustomPrompt] = useState(""); + /** Hides “Deep Reverse” after a custom or deep run (not needed for that result). */ + const [lastResultWasCustom, setLastResultWasCustom] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [rateLimited, setRateLimited] = useState(false); const [prompt, setPrompt] = useState(initialPrompt ?? ""); const [copied, setCopied] = useState(false); + /** Live line for manual/deep (SSE or cache); empty when idle. */ + const [manualStatusLine, setManualStatusLine] = useState(""); + /** + * Which request is in flight. Deep Reverse uses `runCustomReverse` without + * the Manual control checkbox, so we cannot key off `customReverse` alone. + */ + const [loadKind, setLoadKind] = useState<"none" | "quick" | "custom">("none"); + const [lastGenerationKind, setLastGenerationKind] = useState< + "quick" | "deep" | "manual" | null + >(() => initialGenerationKind ?? null); + const [lastManualFocus, setLastManualFocus] = useState(() => + initialManualFocus?.trim() ? initialManualFocus.trim() : null + ); const resultsRef = useRef(null); const autoSubmitStartedRef = useRef(false); @@ -50,6 +120,7 @@ export function ReversePromptHome({ setRateLimited(false); setPrompt(""); setCopied(false); + setLoadKind("quick"); setLoading(true); try { const res = await fetch("/api/reverse-prompt", { @@ -66,13 +137,16 @@ export function ReversePromptHome({ setRateLimited(true); return; } - setError(data.error ?? `Request failed (${res.status})`); + setError(data.error ?? `İstek Başarısız: (${res.status})`); return; } if (typeof data.prompt === "string") { setPrompt(data.prompt); + setLastResultWasCustom(false); + setLastGenerationKind("quick"); + setLastManualFocus(null); const parsed = parseGitHubRepoInput(input); - if (parsed && typeof window !== "undefined") { + if (parsed && typeof window !== "undefined" && !preserveUrl) { window.history.replaceState( null, "", @@ -83,31 +157,221 @@ export function ReversePromptHome({ setError("No prompt in response."); } } catch (err) { - setError(err instanceof Error ? err.message : "Request failed"); + setError(err instanceof Error ? err.message : "İstek Başarısız"); } finally { setLoading(false); + setLoadKind("none"); } - }, []); + }, [preserveUrl]); + + const runCustomReverse = useCallback( + async (input: string, focusOrDeep: string | { mode: "deep" }) => { + setError(null); + setRateLimited(false); + setPrompt(""); + setCopied(false); + setManualStatusLine("Bellektemi diye kontrol ediliyor…"); + setLoadKind("custom"); + setLoading(true); + try { + const isDeep = + typeof focusOrDeep === "object" && focusOrDeep.mode === "deep"; + const bodyObj = isDeep + ? { repoUrl: input, mode: "deep" as const, stream: true as const } + : { + repoUrl: input, + customPrompt: focusOrDeep as string, + stream: true as const, + }; + + const res = await fetch("/api/custom-reverse", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(bodyObj), + }); + + const ct = res.headers.get("content-type") ?? ""; + + if (ct.includes("application/json")) { + const data = (await res.json()) as { + prompt?: string; + error?: string; + fromCache?: boolean; + }; + if (!res.ok) { + if (res.status === 429) { + setRateLimited(true); + return; + } + setError(data.error ?? `İstek başarısız: (${res.status})`); + return; + } + if (typeof data.prompt === "string") { + if (data.fromCache) { + setManualStatusLine("Bellekten yüklendi."); + await new Promise((r) => setTimeout(r, 450)); + } + setPrompt(data.prompt); + setLastResultWasCustom(true); + if (isDeep) { + setLastGenerationKind("deep"); + setLastManualFocus(null); + } else { + setLastGenerationKind("manual"); + setLastManualFocus(String(focusOrDeep as string).trim()); + } + const parsed = parseGitHubRepoInput(input); + if (parsed && typeof window !== "undefined" && !preserveUrl) { + window.history.replaceState( + null, + "", + `/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}` + ); + } + } else { + setError("Cevapta İstem Metni yok."); + } + return; + } + + if (!res.ok) { + try { + const errData = (await res.json()) as { error?: string }; + setError(errData.error ?? `İstek başarısız: (${res.status})`); + } catch { + setError(`İstek başarısız: (${res.status})`); + } + return; + } + + if (!res.body) { + setError("Manuel Kontrolden gelen cevap gövdesi yok."); + return; + } + + const reader = res.body.getReader(); + const dec = new TextDecoder(); + let buffer = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += dec.decode(value, { stream: true }); + for (;;) { + const idx = buffer.indexOf("\n\n"); + if (idx < 0) break; + const block = buffer.slice(0, idx); + buffer = buffer.slice(idx + 2); + let event = "message"; + let dataStr = ""; + for (const line of block.split("\n")) { + if (line.startsWith("event:")) { + event = line.slice(6).trim(); + } else if (line.startsWith("data:")) { + dataStr = line.slice(5).trim(); + } + } + if (!dataStr) continue; + try { + if (event === "status") { + const j = JSON.parse(dataStr) as { message?: string }; + if (typeof j.message === "string" && j.message) { + setManualStatusLine(j.message); + } + } else if (event === "done") { + const j = JSON.parse(dataStr) as { prompt?: string }; + if (typeof j.prompt === "string") { + setPrompt(j.prompt); + setLastResultWasCustom(true); + if (isDeep) { + setLastGenerationKind("deep"); + setLastManualFocus(null); + } else { + setLastGenerationKind("manual"); + setLastManualFocus(String(focusOrDeep as string).trim()); + } + const parsed = parseGitHubRepoInput(input); + if (parsed && typeof window !== "undefined" && !preserveUrl) { + window.history.replaceState( + null, + "", + `/${encodeURIComponent(parsed.owner)}/${encodeURIComponent(parsed.repo)}` + ); + } + } else { + setError("Cevapta istem metni yok."); + } + } else if (event === "error") { + const j = JSON.parse(dataStr) as { error?: string }; + setError(j.error ?? "Request failed"); + } + } catch { + // ignore malformed SSE chunk + } + } + } + } catch (err) { + setError(err instanceof Error ? err.message : "İstek Başarısız"); + } finally { + setLoading(false); + setLoadKind("none"); + setManualStatusLine(""); + } + }, + [preserveUrl] + ); function onSubmit(e: React.FormEvent) { e.preventDefault(); - void runReversePrompt(repoUrl.trim()); + if (loading) return; + const trimmed = repoUrl.trim(); + if (customReverse) { + void runCustomReverse(trimmed, customPrompt.trim()); + } else { + void runReversePrompt(trimmed); + } + } + + function onCustomReverseCheckboxChange(wantsOn: boolean) { + setCustomReverse(wantsOn); } useEffect(() => { - if (!autoSubmit || autoSubmitStartedRef.current) return; + if (autoSubmitStartedRef.current) return; const trimmed = initialRepoInput?.trim() ?? ""; if (!trimmed || !parseGitHubRepoInput(trimmed)) return; - autoSubmitStartedRef.current = true; - void runReversePrompt(trimmed); - }, [autoSubmit, initialRepoInput, runReversePrompt]); + + if (autoSubmitDeep) { + autoSubmitStartedRef.current = true; + void runCustomReverse(trimmed, { mode: "deep" }); + return; + } + const focus = autoSubmitFocus?.trim() ?? ""; + if (focus) { + autoSubmitStartedRef.current = true; + void runCustomReverse(trimmed, focus); + return; + } + if (autoSubmit) { + autoSubmitStartedRef.current = true; + void runReversePrompt(trimmed); + } + }, [ + autoSubmit, + autoSubmitDeep, + autoSubmitFocus, + initialRepoInput, + runCustomReverse, + runReversePrompt, + ]); + + /* `/owner/repo` uses quick auto-submit; `/owner/repo/deep` and `/owner/repo/` use the branches above. */ useEffect(() => { if (typeof window === "undefined") return; const o = owner?.trim(); const r = repo?.trim(); if (!o || !r) return; - if (isHomeExampleRepo(o, r)) return; /* Server-side dedupes by IP hash, so we no longer need a localStorage gate. */ void fetch("/api/increment-views", { @@ -119,41 +383,6 @@ export function ReversePromptHome({ }); }, [owner, repo]); - useEffect(() => { - if (typeof window === "undefined") return; - const o = owner?.trim(); - const r = repo?.trim(); - if (!o || !r) return; - - const MAX = 20; - const raw = localStorage.getItem(GITREVERSE_HISTORY_KEY); - let arr: GitreverseHistoryEntry[] = []; - if (raw) { - try { - const parsed = JSON.parse(raw) as unknown; - arr = Array.isArray(parsed) ? (parsed as GitreverseHistoryEntry[]) : []; - } catch { - arr = []; - } - } - const idx = arr.findIndex((e) => e.owner === o && e.repo === r); - const prevPreview = idx !== -1 ? arr[idx]?.promptPreview : undefined; - const entry: GitreverseHistoryEntry = { - owner: o, - repo: r, - visitedAt: new Date().toISOString(), - ...(prevPreview != null && prevPreview !== "" - ? { promptPreview: prevPreview } - : {}), - }; - if (idx !== -1) arr.splice(idx, 1); - arr.unshift(entry); - localStorage.setItem( - GITREVERSE_HISTORY_KEY, - JSON.stringify(arr.slice(0, MAX)) - ); - }, [owner, repo]); - useEffect(() => { if (typeof window === "undefined") return; const o = owner?.trim(); @@ -172,11 +401,69 @@ export function ReversePromptHome({ return; } } - const idx = arr.findIndex((e) => e.owner === o && e.repo === r); - if (idx === -1 || arr[idx].promptPreview === preview) return; - arr[idx] = { ...arr[idx], promptPreview: preview }; + const slot = + historySlotFromGenerationState(lastGenerationKind, lastManualFocus) ?? + historySlotFromProps( + preserveUrl, + autoSubmitDeep, + autoSubmitFocus, + initialManualFocus, + initialGenerationKind + ); + + const idx = arr.findIndex( + (e) => e.owner === o && e.repo === r && historySlotOf(e) === slot + ); + + const gen = lastGenerationKind ?? undefined; + const focusMeta = + lastGenerationKind === "manual" && lastManualFocus?.trim() + ? lastManualFocus.trim() + : undefined; + + if (idx === -1) { + arr.unshift({ + owner: o, + repo: r, + historySlot: slot, + visitedAt: new Date().toISOString(), + promptPreview: preview, + ...(gen != null ? { lastGenerationType: gen } : {}), + lastManualFocus: focusMeta, + }); + localStorage.setItem( + GITREVERSE_HISTORY_KEY, + JSON.stringify(arr.slice(0, GITREVERSE_HISTORY_MAX)) + ); + return; + } + + const cur = arr[idx]; + const samePreview = cur.promptPreview === preview; + const sameGen = cur.lastGenerationType === gen; + const sameFocus = cur.lastManualFocus === focusMeta; + if (samePreview && sameGen && sameFocus) return; + + arr[idx] = { + ...cur, + historySlot: slot, + promptPreview: preview, + ...(gen != null ? { lastGenerationType: gen } : {}), + lastManualFocus: focusMeta, + }; localStorage.setItem(GITREVERSE_HISTORY_KEY, JSON.stringify(arr)); - }, [owner, repo, prompt]); + }, [ + owner, + repo, + prompt, + lastGenerationKind, + lastManualFocus, + preserveUrl, + autoSubmitDeep, + autoSubmitFocus, + initialManualFocus, + initialGenerationKind, + ]); useEffect(() => { if (!prompt) return; @@ -201,7 +488,7 @@ export function ReversePromptHome({ setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { - setError("Could not copy to clipboard."); + setError("Panoya kopyalanamadı."); } } @@ -218,13 +505,13 @@ export function ReversePromptHome({ href="/library" className="font-semibold text-zinc-900 transition-transform hover:-translate-y-0.5" > - Library + Kütüphane - History + Geçmiş - GitHub + GitHub Sayfası @@ -284,14 +571,14 @@ export function ReversePromptHome({

    - Repository to + Depodan ->
    - Prompt + İsteme

    - Reverse engineer a codebase{" "} - into a prompt - that likely created it. + Bir kod tabanını{" "} + Komut İstemine + dönüştürün.

    @@ -302,89 +589,142 @@ export function ReversePromptHome({ onSubmit={onSubmit} className="relative z-10 rounded-xl border-[3px] border-zinc-900 bg-[#fff4da] p-6" > -
    -
    -
    - setRepoUrl(e.target.value)} - required - /> +
    +
    +
    +
    + setRepoUrl(e.target.value)} + required + /> +
    +
    +
    + +
    -
    -
    - + + onCustomReverseCheckboxChange(e.target.checked) + } + /> + Manuel Kontrol + + {customReverse ? ( +
    +
    +