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
324 changes: 167 additions & 157 deletions client/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"clsx": "^2.1.1",
"dagre": "^0.8.5",
"lucide-react": "^0.562.0",
"prismjs": "^1.30.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-redux": "^9.2.0",
Expand Down
9 changes: 8 additions & 1 deletion client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import Layout from '@/components/layout/Layout';

import { LandingPage, LoginPage, SignupPage } from '@/features/auth';
import { DashboardPage } from '@/features/dashboard';
import { AnalyzePage, GraphPage } from '@/features/graph';
import { UploadRepoPage, GraphPage } from '@/features/graph';
import { AnalyzeFilePage, AnalyzePage } from '@/features/analyze';
import { AskPage } from '@/features/ai';

function RootRedirect() {
const { isAuthenticated, loading } = useAuth();
Expand Down Expand Up @@ -39,8 +41,13 @@ function AppRoutes() {
<Route element={<PrivateGuard />}>
<Route element={<Layout />}>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/upload-repo" element={<UploadRepoPage />} />
<Route path="/analyze" element={<AnalyzePage />} />
<Route path="/analyze/file" element={<AnalyzeFilePage />} />
<Route path="/analyze/:dir_name" element={<AnalyzePage />} />
<Route path="/analyze/:dir_name/file" element={<AnalyzeFilePage />} />
<Route path="/graph" element={<GraphPage />} />
<Route path="/ask" element={<AskPage />} />
</Route>
</Route>

Expand Down
2 changes: 2 additions & 0 deletions client/src/app/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import themeReducer from '@/features/theme/slices/themeSlice';
import graphReducer from '@/features/graph/slices/graphSlice';
import dashboardReducer from '@/features/dashboard/slices/dashboardSlice';
import aiReducer from '@/features/ai/slices/aiSlice';
import analyzeReducer from '@/features/analyze/slices/analyzeSlice';

export const store = configureStore({
reducer: {
theme: themeReducer,
graph: graphReducer,
dashboard: dashboardReducer,
ai: aiReducer,
analyze: analyzeReducer,
},
devTools: import.meta.env.DEV,
});
Expand Down
17 changes: 15 additions & 2 deletions client/src/components/layout/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
Share2,
ChevronLeft,
ChevronRight,
UploadIcon,
GitGraphIcon,
MessageSquare,
} from 'lucide-react';
import { cn } from '@/lib/utils';

Expand All @@ -16,16 +19,26 @@ const NAV_ITEMS = [
icon: <LayoutDashboard className="size-4 shrink-0" />,
label: 'Dashboard',
},
{
to: '/upload-repo',
icon: <UploadIcon className="size-4 shrink-0" />,
label: 'Upload Repo',
},
{
to: '/analyze',
icon: <Network className="size-4 shrink-0" />,
icon: <GitGraphIcon className="size-4 shrink-0" />,
label: 'Analyze',
},
{
to: '/graph',
icon: <Share2 className="size-4 shrink-0" />,
icon: <Network className="size-4 shrink-0" />,
label: 'Graph',
},
{
to: '/ask',
icon: <MessageSquare className="size-4 shrink-0" />,
label: 'Ask',
},
];

export default function Sidebar({
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/ui/button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"

const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[transform,background-color,border-color,text-color,box-shadow] duration-200 ease-[var(--ease-out)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:scale-[0.97]",
{
variants: {
variant: {
Expand All @@ -17,6 +17,7 @@ const buttonVariants = cva(
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
neumo: "bg-background shadow-neu-flat hover:shadow-neu-inset text-foreground transition-all duration-300",
},
size: {
default: "h-10 px-4 py-2",
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ui/input.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const Input = React.forwardRef(({ className, type, ...props }, ref) => {
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input bg-background/50 px-3 py-2 text-sm shadow-neu-inset transition-all duration-300 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:border-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ui/select.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function SelectTrigger({ className, children, ...props }) {
<SelectPrimitive.Trigger
data-slot="select-trigger"
className={cn(
'border-input data-placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex h-10 w-full items-center justify-between rounded-md border bg-background px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-10 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*=text-])]:text-muted-foreground [&_svg:not([class*=size-])]:size-4 [&_svg]:shrink-0',
'border-input data-placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex h-10 w-full items-center justify-between rounded-md border bg-background/50 px-3 py-2 text-sm whitespace-nowrap shadow-neu-inset transition-all duration-300 outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-10 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg:not([class*=text-])]:text-muted-foreground [&_svg:not([class*=size-])]:size-4 [&_svg]:shrink-0',
className,
)}
{...props}
Expand Down
96 changes: 91 additions & 5 deletions client/src/features/ai/components/AiPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { X, AlertTriangle, Loader2, Zap } from 'lucide-react';
import { X, AlertTriangle, Loader2, Zap, Wrench } from 'lucide-react';
import {
analyzeImpact,
selectAiImpactState,
Expand All @@ -16,6 +16,9 @@ export default function AiPanel({ nodeId, graph, onClose }) {
const [streamedText, setStreamedText] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [streamError, setStreamError] = useState('');
const [isLoadingRefactor, setIsLoadingRefactor] = useState(false);
const [refactorError, setRefactorError] = useState('');
const [refactorSuggestion, setRefactorSuggestion] = useState(null);

const nodeData = nodeId ? graph?.[nodeId] : null;

Expand All @@ -33,6 +36,8 @@ export default function AiPanel({ nodeId, graph, onClose }) {
setStreamedText('');
setIsStreaming(true);
setStreamError('');
setRefactorSuggestion(null);
setRefactorError('');

aiService
.streamExplain({
Expand Down Expand Up @@ -78,8 +83,29 @@ export default function AiPanel({ nodeId, graph, onClose }) {
dispatch(analyzeImpact({ jobId, filePath: nodeId }));
};

const handleSuggestRefactor = async () => {
if (!jobId || !nodeId || isLoadingRefactor) return;

setIsLoadingRefactor(true);
setRefactorError('');

try {
const result = await aiService.suggestRefactor({
jobId,
filePath: nodeId,
});

setRefactorSuggestion(result);
} catch (error) {
setRefactorSuggestion(null);
setRefactorError(error?.response?.data?.error || error?.message || 'Failed to load suggestions.');
} finally {
setIsLoadingRefactor(false);
}
};

return (
<div className="absolute top-2 right-2 z-10 w-80 max-h-[85vh] overflow-y-auto rounded-xl border border-border bg-card/95 backdrop-blur-sm p-4 text-xs shadow-xl transition-all">
<div className="w-full rounded-xl border border-border bg-card/95 backdrop-blur-sm p-4 text-xs shadow-xl transition-all">
<div className="flex items-start justify-between gap-2 mb-3">
<div className="flex items-center gap-1.5 min-w-0">
<span className="font-mono font-semibold text-foreground truncate">{nodeId}</span>
Expand Down Expand Up @@ -124,6 +150,66 @@ export default function AiPanel({ nodeId, graph, onClose }) {
)}
</div>

<div className="mb-3 rounded-lg border border-border bg-background/40 p-3">
<div className="mb-2 flex items-center justify-between gap-2">
<p className="text-muted-foreground/60 uppercase tracking-wider text-[10px] flex items-center gap-1">
<Wrench className="size-3" /> Refactor Suggestions
</p>
<button
type="button"
onClick={handleSuggestRefactor}
disabled={!jobId || isLoadingRefactor}
className="text-[10px] text-primary/70 hover:text-primary disabled:opacity-40 transition-colors"
>
{isLoadingRefactor ? 'Analyzing...' : 'Suggest refactor ->'}
</button>
</div>

{isLoadingRefactor && (
<div className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="size-3 animate-spin" />
<span>Evaluating architecture risk...</span>
</div>
)}

{refactorError && (
<p className="text-red-400 flex items-center gap-1">
<AlertTriangle className="size-3" /> {refactorError}
</p>
)}

{refactorSuggestion && !isLoadingRefactor && !refactorError && (
<div className="space-y-2">
<p className="text-foreground/80">
Priority: <span className="uppercase">{refactorSuggestion.priority || 'medium'}</span>
{' '}· Effort: <span>{refactorSuggestion.estimatedEffort || 'unknown'}</span>
</p>

{refactorSuggestion.concerns?.length > 0 && (
<div>
<p className="mb-1 text-muted-foreground/60 uppercase tracking-wider text-[10px]">Concerns</p>
<ul className="list-disc pl-4 space-y-1 text-foreground/90">
{refactorSuggestion.concerns.map((item, index) => (
<li key={`concern-${index}`}>{item}</li>
))}
</ul>
</div>
)}

{refactorSuggestion.suggestions?.length > 0 && (
<div>
<p className="mb-1 text-muted-foreground/60 uppercase tracking-wider text-[10px]">Suggestions</p>
<ul className="list-disc pl-4 space-y-1 text-foreground/90">
{refactorSuggestion.suggestions.map((item, index) => (
<li key={`suggestion-${index}`}>{item}</li>
))}
</ul>
</div>
)}
</div>
)}
</div>

{declarations.length > 0 && (
<div className="mb-3">
<p className="mb-1 text-muted-foreground/60 uppercase tracking-wider text-[10px]">
Expand Down Expand Up @@ -154,7 +240,7 @@ export default function AiPanel({ nodeId, graph, onClose }) {

{impactedFiles.length > 0 && (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-2">
<ul className="flex flex-col gap-0.5 max-h-24 overflow-y-auto custom-scrollbar">
<ul className="flex flex-col gap-0.5 custom-scrollbar">
{impactedFiles.map((file) => (
<li key={file} className="font-mono text-amber-200/80 truncate">{file}</li>
))}
Expand All @@ -166,7 +252,7 @@ export default function AiPanel({ nodeId, graph, onClose }) {
{deps.length > 0 && (
<div className="mb-3">
<p className="mb-1 text-muted-foreground/60 uppercase tracking-wider text-[10px]">Imports ({deps.length})</p>
<ul className="flex flex-col gap-0.5 max-h-28 overflow-y-auto custom-scrollbar">
<ul className="flex flex-col gap-0.5 custom-scrollbar">
{deps.map((dep) => (
<li key={dep} className="font-mono text-gold/80 truncate">{dep}</li>
))}
Expand All @@ -177,7 +263,7 @@ export default function AiPanel({ nodeId, graph, onClose }) {
{usedBy.length > 0 && (
<div className="mb-3">
<p className="mb-1 text-muted-foreground/60 uppercase tracking-wider text-[10px]">Used By ({usedBy.length})</p>
<ul className="flex flex-col gap-0.5 max-h-28 overflow-y-auto custom-scrollbar">
<ul className="flex flex-col gap-0.5 custom-scrollbar">
{usedBy.map((file) => (
<li key={file} className="font-mono text-foreground/70 truncate">{file}</li>
))}
Expand Down
20 changes: 10 additions & 10 deletions client/src/features/ai/components/QueryBar.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Search, X, CheckCircle, AlertCircle } from 'lucide-react';
import { Search, X, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
import { queryGraph, resetAiState, selectAiQueryState } from '../slices/aiSlice';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
Expand Down Expand Up @@ -53,10 +53,10 @@ export default function QueryBar({ jobId }) {
<div className="relative w-full">
{/* Minimalist Query Input */}
<div
className={`transition-all duration-300 ease-out ${
className={`transition-all duration-500 ease-[var(--ease-out)] ${
expanded || hasResult
? 'ring-1 ring-primary/20 rounded-lg bg-background'
: 'bg-muted/40 hover:bg-muted/60 rounded-full'
? 'shadow-neu-flat rounded-xl bg-background/60'
: 'shadow-neu-inset rounded-full bg-background/40 hover:bg-background/60'
}`}
>
<form onSubmit={handleAsk} className="flex items-center gap-2 px-4 py-3">
Expand Down Expand Up @@ -87,28 +87,28 @@ export default function QueryBar({ jobId }) {
<Button
type="submit"
size="sm"
variant="default"
variant="neumo"
disabled={!question.trim() || !jobId || isLoading}
className="ml-auto"
>
{isLoading ? (
<span className="flex items-center gap-2">
<span className="size-3 border-1.5 border-current border-t-transparent rounded-full animate-spin" />
<Loader2 className="size-3.5 animate-spin" />
Searching
Comment on lines 94 to 97
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loader2 is used in the button content, but it isn’t imported from lucide-react (current import only includes Search, X, CheckCircle, AlertCircle). This will throw at runtime / fail the build. Add Loader2 to the icon import (or remove the usage).

Copilot uses AI. Check for mistakes.
</span>
) : (
'Ask'
'Ask AI'
)}
</Button>
</form>

{/* Results Display */}
{(hasResult || hasError) && (
<div
className={`border-t transition-all duration-300 ${
className={`transition-all duration-500 animate-in fade-in slide-in-from-top-2 ${
hasResult
? 'border-muted bg-gradient-to-b from-background via-background to-muted/20'
: 'border-destructive/20 bg-destructive/5'
? 'border-t border-border/10 bg-gradient-to-b from-transparent to-background/20'
: 'border-t border-destructive/10 bg-destructive/5'
}`}
>
<div className="px-4 py-3 space-y-3">
Expand Down
24 changes: 12 additions & 12 deletions client/src/features/ai/components/QueryHistory.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,25 +94,25 @@ export default function QueryHistory({ jobId }) {
if (!jobId) return null;

return (
<div className="mt-2 rounded-lg border border-border/70 bg-background/40">
<div className="mt-2 rounded-xl shadow-neu-inset border-none bg-background/20 transition-all duration-500 overflow-hidden">
<button
type="button"
onClick={() => setIsOpen((open) => !open)}
className="flex w-full items-center justify-between px-3 py-2 text-left"
className="flex w-full items-center justify-between px-4 py-3 text-left transition-all duration-300 hover:bg-background/40 active:scale-[0.99]"
>
<span className="flex items-center gap-2 text-xs uppercase tracking-wide text-muted-foreground">
<span className="flex items-center gap-2.5 text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60">
<History className="size-3.5" />
Recent queries
Recent Queries
</span>
<span className="flex items-center gap-2">
{isLoading && <Loader2 className="size-3.5 animate-spin text-muted-foreground" />}
<span className="flex items-center gap-3">
{isLoading && <Loader2 className="size-3.5 animate-spin text-gold/60" />}
{hasQueries && (
<span className="rounded-full border border-border bg-muted px-2 py-0.5 text-[10px] text-muted-foreground">
<span className="rounded-full shadow-neu-inset border-none bg-background/50 px-2.5 py-0.5 text-[9px] font-bold text-gold/80">
{queries.length}
</span>
)}
<ChevronDown
className={`size-3.5 text-muted-foreground transition-transform ${isOpen ? 'rotate-180' : ''}`}
className={`size-3.5 text-muted-foreground/40 transition-transform duration-500 ease-[var(--ease-out)] ${isOpen ? 'rotate-180' : ''}`}
/>
</span>
</button>
Expand All @@ -128,7 +128,7 @@ export default function QueryHistory({ jobId }) {
)}

{!error && visibleQueries.length > 0 && (
<ul className="flex flex-col gap-1">
<ul className="flex flex-col gap-1.5 animate-in fade-in slide-in-from-top-2 duration-500">
{visibleQueries.map((queryItem) => (
<li key={queryItem.id}>
<button
Expand All @@ -137,10 +137,10 @@ export default function QueryHistory({ jobId }) {
if (!queryItem.question) return;
dispatch(queryGraph({ question: queryItem.question, jobId }));
}}
className="group flex w-full items-start justify-between gap-3 rounded-md px-2 py-1.5 text-left hover:bg-muted/60"
className="group flex w-full items-start justify-between gap-4 rounded-xl px-3 py-2.5 text-left transition-all duration-300 hover:bg-background shadow-neu-flat active:scale-[0.98]"
>
<span className="line-clamp-2 text-xs text-foreground/90">{queryItem.question}</span>
<span className="shrink-0 text-[10px] text-muted-foreground">
<span className="line-clamp-2 text-xs font-medium text-foreground/80 group-hover:text-gold transition-colors">{queryItem.question}</span>
<span className="shrink-0 text-[9px] font-bold uppercase tracking-tighter text-muted-foreground/30 mt-0.5">
{formatRelativeDate(queryItem.createdAt)}
</span>
</button>
Expand Down
1 change: 1 addition & 0 deletions client/src/features/ai/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export { aiService } from './services/aiService';
export { default as QueryBar } from './components/QueryBar';
export { default as QueryHistory } from './components/QueryHistory';
export { default as AiPanel } from './components/AiPanel';
export { default as AskPage } from './pages/AskPage';
Loading
Loading