From 3cd4c47d88f0f1a074530f207cb66369029de2a8 Mon Sep 17 00:00:00 2001 From: SamanPandey-in Date: Mon, 30 Mar 2026 20:24:15 +0530 Subject: [PATCH 01/19] feat: add heatmap feature --- client/src/features/ai/components/AiPanel.jsx | 88 ++++++- client/src/features/ai/services/aiService.js | 22 ++ .../graph/components/GraphToolbar.jsx | 50 +++- .../features/graph/components/GraphView.jsx | 106 ++++++-- .../features/graph/services/graphService.js | 11 + .../src/features/graph/slices/graphSlice.js | 38 ++- server/package.json | 2 +- server/src/api/ai/routes/ai.routes.js | 86 +++++++ server/src/api/graph/routes/graph.routes.js | 34 +++ server/test/ai.suggest-refactor.test.js | 226 ++++++++++++++++++ server/test/graph.heatmap.test.js | 124 ++++++++++ 11 files changed, 759 insertions(+), 28 deletions(-) create mode 100644 server/test/ai.suggest-refactor.test.js create mode 100644 server/test/graph.heatmap.test.js diff --git a/client/src/features/ai/components/AiPanel.jsx b/client/src/features/ai/components/AiPanel.jsx index d5048c6..01146aa 100644 --- a/client/src/features/ai/components/AiPanel.jsx +++ b/client/src/features/ai/components/AiPanel.jsx @@ -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, @@ -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; @@ -33,6 +36,8 @@ export default function AiPanel({ nodeId, graph, onClose }) { setStreamedText(''); setIsStreaming(true); setStreamError(''); + setRefactorSuggestion(null); + setRefactorError(''); aiService .streamExplain({ @@ -78,6 +83,27 @@ 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 (
@@ -124,6 +150,66 @@ export default function AiPanel({ nodeId, graph, onClose }) { )}
+
+
+

+ Refactor Suggestions +

+ +
+ + {isLoadingRefactor && ( +
+ + Evaluating architecture risk... +
+ )} + + {refactorError && ( +

+ {refactorError} +

+ )} + + {refactorSuggestion && !isLoadingRefactor && !refactorError && ( +
+

+ Priority: {refactorSuggestion.priority || 'medium'} + {' '}· Effort: {refactorSuggestion.estimatedEffort || 'unknown'} +

+ + {refactorSuggestion.concerns?.length > 0 && ( +
+

Concerns

+
    + {refactorSuggestion.concerns.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ )} + + {refactorSuggestion.suggestions?.length > 0 && ( +
+

Suggestions

+
    + {refactorSuggestion.suggestions.map((item, index) => ( +
  • {item}
  • + ))} +
+
+ )} +
+ )} +
+ {declarations.length > 0 && (

diff --git a/client/src/features/ai/services/aiService.js b/client/src/features/ai/services/aiService.js index df616d6..dc52628 100644 --- a/client/src/features/ai/services/aiService.js +++ b/client/src/features/ai/services/aiService.js @@ -107,6 +107,28 @@ export const aiService = { return data; }, + async suggestRefactor({ jobId, filePath }) { + const normalizedJobId = normalizeText(jobId); + const normalizedFilePath = normalizeText(filePath); + + if (!normalizedJobId || !normalizedFilePath) { + throw new Error('suggestRefactor requires jobId and filePath.'); + } + + const { data } = await aiClient.post('/api/ai/suggest-refactor', { + jobId: normalizedJobId, + filePath: normalizedFilePath, + }); + + return { + filePath: normalizeText(data?.filePath) || normalizedFilePath, + concerns: Array.isArray(data?.concerns) ? data.concerns : [], + suggestions: Array.isArray(data?.suggestions) ? data.suggestions : [], + priority: normalizeText(data?.priority) || 'medium', + estimatedEffort: normalizeText(data?.estimatedEffort) || 'unknown', + }; + }, + async streamExplain({ question, jobId, onChunk, onDone, onError, signal } = {}) { const normalizedQuestion = normalizeText(question); const normalizedJobId = normalizeText(jobId); diff --git a/client/src/features/graph/components/GraphToolbar.jsx b/client/src/features/graph/components/GraphToolbar.jsx index e06f7e5..17ce167 100644 --- a/client/src/features/graph/components/GraphToolbar.jsx +++ b/client/src/features/graph/components/GraphToolbar.jsx @@ -6,6 +6,7 @@ import { Code2, FolderOpen, FileCode2, + Flame, Maximize2, Minimize2, Share2, @@ -13,7 +14,13 @@ import { } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { graphService } from '../services/graphService'; -import { clearGraph, selectGraphData } from '../slices/graphSlice'; +import { + clearGraph, + selectGraphData, + selectHeatmapMode, + setHeatmapHotspots, + setHeatmapMode, +} from '../slices/graphSlice'; async function copyToClipboard(value) { if (navigator?.clipboard?.writeText) { @@ -36,8 +43,10 @@ export default function GraphToolbar({ graphContainerId = 'graph-container' }) { const dispatch = useDispatch(); const navigate = useNavigate(); const data = useSelector(selectGraphData); + const heatmapMode = useSelector(selectHeatmapMode); const [isFullscreen, setIsFullscreen] = useState(false); const [isSharing, setIsSharing] = useState(false); + const [isHeatmapLoading, setIsHeatmapLoading] = useState(false); const [shareFeedback, setShareFeedback] = useState(null); useEffect(() => { @@ -108,6 +117,30 @@ export default function GraphToolbar({ graphContainerId = 'graph-container' }) { } }; + const handleHeatmapToggle = async () => { + if (!jobId || isHeatmapLoading) return; + + if (heatmapMode) { + dispatch(setHeatmapMode(false)); + return; + } + + setIsHeatmapLoading(true); + + try { + const { hotspots } = await graphService.getHeatmap(jobId); + dispatch(setHeatmapHotspots(hotspots)); + dispatch(setHeatmapMode(true)); + } catch (error) { + setShareFeedback({ + type: 'error', + message: error?.response?.data?.error || error?.message || 'Failed to load heatmap data.', + }); + } finally { + setIsHeatmapLoading(false); + } + }; + return (

@@ -143,6 +176,21 @@ export default function GraphToolbar({ graphContainerId = 'graph-container' }) { {shareFeedback.message} )} + @@ -105,10 +105,10 @@ export default function QueryBar({ jobId }) { {/* Results Display */} {(hasResult || hasError) && (
diff --git a/client/src/features/ai/components/QueryHistory.jsx b/client/src/features/ai/components/QueryHistory.jsx index 2abc625..712195b 100644 --- a/client/src/features/ai/components/QueryHistory.jsx +++ b/client/src/features/ai/components/QueryHistory.jsx @@ -94,25 +94,25 @@ export default function QueryHistory({ jobId }) { if (!jobId) return null; return ( -
+
@@ -128,7 +128,7 @@ export default function QueryHistory({ jobId }) { )} {!error && visibleQueries.length > 0 && ( -
    +
      {visibleQueries.map((queryItem) => (
    • diff --git a/client/src/features/dashboard/pages/DashboardPage.jsx b/client/src/features/dashboard/pages/DashboardPage.jsx index bfdacf6..24c89d4 100644 --- a/client/src/features/dashboard/pages/DashboardPage.jsx +++ b/client/src/features/dashboard/pages/DashboardPage.jsx @@ -132,7 +132,7 @@ function MetricCard({ icon, title, value, helper, index = 0 }) { >
      -
      +
      {icon}
      {title} @@ -416,7 +416,7 @@ export default function DashboardPage() { >
      -
      +
      {action.icon}
      {action.title} @@ -469,7 +469,7 @@ export default function DashboardPage() {
      - +
      @@ -624,7 +624,7 @@ export default function DashboardPage() { {repo.source} | {repo.branch || 'unknown'}

      - + {repo.status}
      @@ -742,7 +742,7 @@ export default function DashboardPage() { return (
      diff --git a/client/src/features/graph/components/AnalyzeForm.jsx b/client/src/features/graph/components/AnalyzeForm.jsx index 4d08f57..13e2763 100644 --- a/client/src/features/graph/components/AnalyzeForm.jsx +++ b/client/src/features/graph/components/AnalyzeForm.jsx @@ -45,13 +45,13 @@ function toErrorMessage(err, fallback) { function SourceToggle({ value, onChange, disabled }) { return ( -
      +
      @@ -821,9 +821,10 @@ export default function AnalyzeForm() { )} {selectedOwnedRepo && ( -
      -

      - Selected repository: {selectedOwnedRepo.fullName} +

      +

      Selected repository

      +

      + {selectedOwnedRepo.fullName}

      )} @@ -836,13 +837,13 @@ export default function AnalyzeForm() { )} {selectedOwnedRepo && ( -
      - +
      +