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
869 changes: 103 additions & 766 deletions app/analyze/page.tsx

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions components/analyze/AnalyzeErrorScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import { motion } from "framer-motion";
import { Layers, Terminal, ArrowLeft } from "lucide-react";
import { useRouter } from "next/navigation";

interface AnalyzeErrorScreenProps {
error: string;
isRateLimit: boolean;
}

export default function AnalyzeErrorScreen({
error,
isRateLimit,
}: AnalyzeErrorScreenProps) {
const router = useRouter();

return (
<div className="h-screen w-screen bg-[#0a0a0a] flex flex-col items-center justify-center font-satoshi p-4">
<div
className={`glass-card p-8 rounded-2xl max-w-lg w-full text-center border ${
isRateLimit ? "border-amber-500/20" : "border-red-500/10"
}`}
>
<div
className={`w-12 h-12 mx-auto rounded-xl flex items-center justify-center mb-6 border ${
isRateLimit
? "bg-amber-500/10 border-amber-500/20"
: "bg-red-500/10 border-red-500/20"
}`}
>
{isRateLimit ? (
<Layers className="w-6 h-6 text-amber-400" />
) : (
<Terminal className="w-6 h-6 text-red-400" />
)}
</div>

<h2 className="cabinet text-2xl font-bold text-white mb-4">
{isRateLimit ? "Daily Limit Reached" : "Autopsy Failed"}
</h2>

<div className="w-full max-h-[200px] overflow-y-auto custom-scrollbar p-4 mb-8 rounded-lg bg-[#0e0e0e] border border-white/5 shadow-inner text-left">
<p className="text-xs leading-relaxed font-mono text-red-400 break-words whitespace-pre-wrap">
{error}
</p>
</div>

<div className="flex flex-col gap-3">
{isRateLimit && (
<motion.button
whileTap={{ scale: 0.97 }}
onClick={() => router.push("/pricing")}
className="w-full px-6 py-3 rounded-xl bg-white text-black font-bold text-sm hover:scale-[1.02] transition-transform"
>
View Upgrade Options
</motion.button>
)}
<motion.button
whileTap={{ scale: 0.97 }}
onClick={() => router.back()}
className="w-full px-6 py-3 rounded-xl bg-white/5 hover:bg-white/10 border border-white/10 text-sm font-bold text-white transition-all flex items-center justify-center gap-2"
>
<ArrowLeft className="w-4 h-4" /> Go Back
</motion.button>
</div>
</div>
</div>
);
}
146 changes: 146 additions & 0 deletions components/analyze/AnalyzeHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"use client";

import { motion, AnimatePresence } from "framer-motion";
import {
ArrowLeft,
Terminal,
LayoutGrid,
ThumbsUp,
ThumbsDown,
CheckCircle2,
} from "lucide-react";
import { FaGithub } from "react-icons/fa";
import Link from "next/link";
import ExportButton from "@/components/ExportButton";
import ShareButton from "@/components/ShareButton";
import { RepoData } from "@/lib/types/analyze";

interface AnalyzeHeaderProps {
data: RepoData;
source: string | null;
isAiStreaming: boolean;
feedbackSubmitted: boolean;
hideFeedback: boolean;
onBack: () => void;
onFeedback: (isHelpful: boolean) => void;
}

export default function AnalyzeHeader({
data,
source,
isAiStreaming,
feedbackSubmitted,
hideFeedback,
onBack,
onFeedback,
}: AnalyzeHeaderProps) {
return (
<header className="h-16 flex-shrink-0 border-b border-white/5 bg-[#0e0e0e] flex items-center justify-between px-4 lg:px-6 z-20">
{/* ── Left: Back + Repo Identity ── */}
<div className="flex items-center gap-4 sm:gap-6 min-w-0">
<button
onClick={onBack}
aria-label="Go back"
className="p-2 rounded-lg hover:bg-white/5 text-slate-400 hover:text-white transition-colors group flex-shrink-0"
>
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
</button>

<div className="flex items-center gap-3 min-w-0">
<FaGithub className="w-5 h-5 text-slate-400 hidden sm:block flex-shrink-0" />

<h1 className="cabinet text-lg md:text-xl font-bold text-slate-100 flex items-center gap-2 truncate">
{source === "local" ? (
"Local Codebase"
) : (
<div className="truncate">
<span className="text-slate-500 font-medium hidden sm:inline">
{data.owner}
</span>
<span className="text-slate-600 hidden sm:inline">/</span>
<span className="truncate">{data.repo}</span>
</div>
)}
</h1>

<div className="hidden md:flex items-center gap-2 ml-4 px-3 py-1 rounded-full bg-white/[0.02] border border-white/5 flex-shrink-0">
<Terminal className="w-3 h-3 text-slate-500" />
<span className="mono text-[10px] text-slate-400 font-bold uppercase">
{data.language}
</span>
</div>

{/* AI streaming indicator */}
{isAiStreaming && (
<div className="hidden md:flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex-shrink-0">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-ping" />
<span className="mono text-[9px] text-emerald-400 font-bold uppercase tracking-widest">
AI Live
</span>
</div>
)}
</div>
</div>

{/* ── Right: Actions ── */}
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
<Link
href="/dashboard"
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-slate-300 hover:text-white transition-all font-mono text-[10px] uppercase tracking-widest font-bold h-9 flex-shrink-0"
>
<LayoutGrid className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Dashboard</span>
</Link>

{/* Inline feedback widget */}
<AnimatePresence mode="wait">
{!hideFeedback && (
<motion.div
key={feedbackSubmitted ? "thanks" : "form"}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5, filter: "blur(4px)" }}
className="hidden sm:flex items-center mr-2"
>
{!feedbackSubmitted ? (
<div className="flex items-center gap-1 bg-white/[0.02] border border-white/5 rounded-lg p-1 h-9 flex-shrink-0">
<span className="text-[10px] uppercase font-mono text-slate-500 px-3 font-bold whitespace-nowrap">
Helpful?
</span>
<div className="flex items-center gap-1 px-1">
<button
onClick={() => onFeedback(true)}
aria-label="Mark as helpful"
className="p-1.5 rounded hover:bg-white/10 text-slate-400 hover:text-green-400 transition-colors"
>
<ThumbsUp className="w-3.5 h-3.5" />
</button>
<button
onClick={() => onFeedback(false)}
aria-label="Mark as not helpful"
className="p-1.5 rounded hover:bg-white/10 text-slate-400 hover:text-red-400 transition-colors"
>
<ThumbsDown className="w-3.5 h-3.5" />
</button>
</div>
</div>
) : (
<div className="flex items-center gap-3 px-4 py-2 rounded-lg bg-green-500/10 border border-green-500/20 h-9 flex-shrink-0">
<CheckCircle2 className="w-3.5 h-3.5 text-green-400" />
<span className="text-[10px] uppercase font-mono text-green-400 font-bold whitespace-nowrap">
Thanks!
</span>
</div>
)}
</motion.div>
)}
</AnimatePresence>

{source !== "local" && (
<ShareButton owner={data.owner} repo={data.repo} />
)}
<ExportButton data={data} />
</div>
</header>
);
}
56 changes: 56 additions & 0 deletions components/analyze/AnalyzeLoadingScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import { motion } from "framer-motion";
import { Cpu } from "lucide-react";
import { LOADING_PHRASES } from "@/components/analyze/constants";
import type { LoadingControls } from "@/hooks/analyze/useLoadingAnimation";

interface AnalyzeLoadingScreenProps {
repoUrl: string | null;
source: string | null;
loadingStep: number;
loadingControls: LoadingControls;
}

export default function AnalyzeLoadingScreen({
repoUrl,
source,
loadingStep,
loadingControls,
}: AnalyzeLoadingScreenProps) {
return (
<div className="h-screen w-screen bg-[#0a0a0a] flex flex-col items-center justify-center relative overflow-hidden text-slate-200 font-satoshi">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] h-[400px] bg-white/[0.01] rounded-full blur-[80px] animate-pulse" />
<div className="relative z-10 glass-card p-8 rounded-2xl border border-white/5 max-w-sm w-full mx-4 shadow-2xl">
<div className="flex items-center gap-4 mb-6 pb-6 border-b border-white/5">
<div className="w-10 h-10 rounded-xl bg-[#141414] border border-white/10 flex items-center justify-center shadow-inner">
<Cpu className="w-5 h-5 text-slate-400" />
</div>
<div>
<h3 className="cabinet font-bold text-lg text-white">
Performing Autopsy
</h3>
<p className="mono text-[10px] text-slate-500 tracking-widest uppercase truncate max-w-[200px]">
{source === "local"
? "Local.zip Upload"
: repoUrl?.split("/").slice(-2).join("/") || "Repository"}
</p>
</div>
</div>
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="w-3 h-3 rounded flex items-center justify-center bg-emerald-500/20 border border-emerald-500/50">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-ping" />
</div>
<motion.span
animate={loadingControls}
className="mono text-xs text-emerald-400 font-medium"
>
{LOADING_PHRASES[loadingStep]}
</motion.span>
</div>
</div>
</div>
</div>
);
}
55 changes: 55 additions & 0 deletions components/analyze/AnalyzeTabBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { motion } from "framer-motion";
import { TAB_CONFIG, TabType } from "@/components/analyze/constants";

interface AnalyzeTabBarProps {
activeTab: TabType;
onTabChange: (tab: TabType) => void;
}

export default function AnalyzeTabBar({
activeTab,
onTabChange,
}: AnalyzeTabBarProps) {
return (
<div
role="tablist"
aria-label="Analysis Tools"
className="h-16 flex-shrink-0 border-b border-white/5 flex items-center justify-center px-4 sm:px-6 bg-[#0a0a0a]/50 backdrop-blur-sm z-20 overflow-hidden"
>
<div className="flex overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none] items-center gap-2 bg-[#141414]/90 backdrop-blur-xl p-1.5 rounded-xl border border-white/20 shadow-[0_0_30px_rgba(255,255,255,0.15)] ring-1 ring-black/50 transition-all hover:shadow-[0_0_40px_rgba(255,255,255,0.25)] w-auto">
{TAB_CONFIG.map((tab) => {
const isActive = activeTab === tab.id;
return (
<button
type="button"
key={tab.id}
role="tab"
aria-selected={isActive ? "true" : "false"}
aria-controls={`tabpanel-${tab.id}`}
tabIndex={isActive ? 0 : -1}
onClick={() => onTabChange(tab.id)}
className={`relative flex-shrink-0 whitespace-nowrap px-4 py-2 rounded-lg text-[11px] font-bold uppercase tracking-widest font-mono transition-colors flex items-center gap-2 ${
isActive
? "text-white"
: "text-slate-400 hover:text-white hover:bg-white/5"
}`}
>
{isActive && (
<motion.div
layoutId="active-tab-highlight"
className="absolute inset-0 bg-white/10 rounded-lg shadow-inner border border-white/10"
initial={false}
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/>
)}
<tab.icon className="w-3.5 h-3.5 relative z-10" />
<span className="relative z-10">{tab.label}</span>
</button>
);
})}
</div>
</div>
);
}
Loading
Loading