From 8644f1923c5c4cc5213de6c8ee5a85e2630cdf8b Mon Sep 17 00:00:00 2001 From: Justin Florentine Date: Thu, 18 Dec 2025 18:04:41 -0500 Subject: [PATCH 1/3] feat: add delete insight functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ability to delete insights from sources with a confirmation dialog. Uses AlertDialog component for a native React experience instead of browser confirm(). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../components/source/SourceDetailContent.tsx | 60 ++++++++++++++++++- frontend/src/lib/api/insights.ts | 4 ++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/source/SourceDetailContent.tsx b/frontend/src/components/source/SourceDetailContent.tsx index 7e966c5..e7ccf90 100644 --- a/frontend/src/components/source/SourceDetailContent.tsx +++ b/frontend/src/components/source/SourceDetailContent.tsx @@ -24,6 +24,16 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' import { Select, SelectContent, @@ -80,6 +90,8 @@ export function SourceDetailContent({ const [isDownloadingFile, setIsDownloadingFile] = useState(false) const [fileAvailable, setFileAvailable] = useState(null) const [selectedInsight, setSelectedInsight] = useState(null) + const [insightToDelete, setInsightToDelete] = useState(null) + const [deletingInsight, setDeletingInsight] = useState(false) const fetchSource = useCallback(async () => { try { @@ -152,6 +164,23 @@ export function SourceDetailContent({ } } + const handleDeleteInsight = async () => { + if (!insightToDelete) return + + try { + setDeletingInsight(true) + await insightsApi.delete(insightToDelete) + toast.success('Insight deleted successfully') + setInsightToDelete(null) + await fetchInsights() + } catch (err) { + console.error('Failed to delete insight:', err) + toast.error('Failed to delete insight') + } finally { + setDeletingInsight(false) + } + } + const handleUpdateTitle = async (title: string) => { if (!source || title === source.title) return @@ -573,10 +602,18 @@ export function SourceDetailContent({

{insight.content.slice(0, 180)}{insight.content.length > 180 ? '…' : ''}

-
+
+
))} @@ -744,6 +781,27 @@ export function SourceDetailContent({ }} insight={selectedInsight ?? undefined} /> + + setInsightToDelete(null)}> + + + Delete Insight? + + This action cannot be undone. This insight will be permanently deleted. + + + + Cancel + + {deletingInsight ? 'Deleting...' : 'Delete'} + + + + ) } diff --git a/frontend/src/lib/api/insights.ts b/frontend/src/lib/api/insights.ts index d59f854..1f6677c 100644 --- a/frontend/src/lib/api/insights.ts +++ b/frontend/src/lib/api/insights.ts @@ -30,5 +30,9 @@ export const insightsApi = { data ) return response.data + }, + + delete: async (insightId: string) => { + await apiClient.delete(`/insights/${insightId}`) } } \ No newline at end of file From c7de8ecd86027ae97d699a1325b539a82c6ebd1f Mon Sep 17 00:00:00 2001 From: Justin Florentine Date: Fri, 19 Dec 2025 20:19:02 -0500 Subject: [PATCH 2/3] fix: address PR feedback for delete insight feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix AlertDialogAction closing before async delete completes by using asChild with Button and event.preventDefault() - Add delete button to SourceInsightDialog header for consistent UX - Add confirmation dialog in SourceInsightDialog - Pass onDelete callback from SourceDetailContent to SourceInsightDialog Addresses PR #334 feedback from lfnovo and cubic-dev-ai. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../components/source/SourceDetailContent.tsx | 23 ++- .../components/source/SourceInsightDialog.tsx | 134 +++++++++++++----- 2 files changed, 116 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/source/SourceDetailContent.tsx b/frontend/src/components/source/SourceDetailContent.tsx index e7ccf90..fe350ac 100644 --- a/frontend/src/components/source/SourceDetailContent.tsx +++ b/frontend/src/components/source/SourceDetailContent.tsx @@ -164,7 +164,8 @@ export function SourceDetailContent({ } } - const handleDeleteInsight = async () => { + const handleDeleteInsight = async (e?: React.MouseEvent) => { + e?.preventDefault() if (!insightToDelete) return try { @@ -780,6 +781,12 @@ export function SourceDetailContent({ } }} insight={selectedInsight ?? undefined} + onDelete={async (insightId) => { + await insightsApi.delete(insightId) + toast.success('Insight deleted successfully') + setSelectedInsight(null) + await fetchInsights() + }} /> setInsightToDelete(null)}> @@ -792,12 +799,14 @@ export function SourceDetailContent({ Cancel - - {deletingInsight ? 'Deleting...' : 'Delete'} + + diff --git a/frontend/src/components/source/SourceInsightDialog.tsx b/frontend/src/components/source/SourceInsightDialog.tsx index 6103e7d..d7ded43 100644 --- a/frontend/src/components/source/SourceInsightDialog.tsx +++ b/frontend/src/components/source/SourceInsightDialog.tsx @@ -1,7 +1,10 @@ 'use client' -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { useState, useEffect } from 'react' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Trash2 } from 'lucide-react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { useInsight } from '@/lib/hooks/use-insights' @@ -15,9 +18,13 @@ interface SourceInsightDialogProps { content?: string created?: string } + onDelete?: (insightId: string) => Promise } -export function SourceInsightDialog({ open, onOpenChange, insight }: SourceInsightDialogProps) { +export function SourceInsightDialog({ open, onOpenChange, insight, onDelete }: SourceInsightDialogProps) { + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) + // Ensure insight ID has 'source_insight:' prefix for API calls const insightIdWithPrefix = insight?.id ? (insight.id.includes(':') ? insight.id : `source_insight:${insight.id}`) @@ -28,49 +35,108 @@ export function SourceInsightDialog({ open, onOpenChange, insight }: SourceInsig // Use fetched data if available, otherwise fall back to passed-in insight const displayInsight = fetchedInsight ?? insight + // Reset delete state when dialog closes + useEffect(() => { + if (!open) { + setShowDeleteConfirm(false) + setIsDeleting(false) + } + }, [open]) + + const handleDelete = async () => { + if (!insight?.id || !onDelete) return + + try { + setIsDeleting(true) + await onDelete(insight.id) + // Parent's onDelete callback handles closing the dialog via setSelectedInsight(null) + } catch { + // Only reset state if delete failed - dialog stays open + setShowDeleteConfirm(false) + setIsDeleting(false) + } + } + return ( Source Insight - {displayInsight?.insight_type && ( - - {displayInsight.insight_type} - - )} +
+ {displayInsight?.insight_type && ( + + {displayInsight.insight_type} + + )} + {onDelete && insight?.id && !showDeleteConfirm && ( + + )} +
-
- {isLoading ? ( -
- Loading insight… -
- ) : displayInsight ? ( -
- ( -
- {children}
-
- ), - thead: ({ children }) => {children}, - tbody: ({ children }) => {children}, - tr: ({ children }) => {children}, - th: ({ children }) => {children}, - td: ({ children }) => {children}, - }} + {showDeleteConfirm ? ( +
+

+ Are you sure you want to delete this insight?
+ This action cannot be undone. +

+
+ +
- ) : ( -

No insight selected.

- )} -
+
+ ) : ( +
+ {isLoading ? ( +
+ Loading insight… +
+ ) : displayInsight ? ( +
+ ( +
+ {children}
+
+ ), + thead: ({ children }) => {children}, + tbody: ({ children }) => {children}, + tr: ({ children }) => {children}, + th: ({ children }) => {children}, + td: ({ children }) => {children}, + }} + > + {displayInsight.content} +
+
+ ) : ( +

No insight selected.

+ )} +
+ )}
) From 8fdb0f0539791e8ee7d923be132d44675e94e1fc Mon Sep 17 00:00:00 2001 From: Luis Novo Date: Fri, 19 Dec 2025 22:48:09 -0300 Subject: [PATCH 3/3] Update frontend/src/components/source/SourceDetailContent.tsx Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../src/components/source/SourceDetailContent.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/source/SourceDetailContent.tsx b/frontend/src/components/source/SourceDetailContent.tsx index fe350ac..8757141 100644 --- a/frontend/src/components/source/SourceDetailContent.tsx +++ b/frontend/src/components/source/SourceDetailContent.tsx @@ -782,10 +782,15 @@ export function SourceDetailContent({ }} insight={selectedInsight ?? undefined} onDelete={async (insightId) => { - await insightsApi.delete(insightId) - toast.success('Insight deleted successfully') - setSelectedInsight(null) - await fetchInsights() + try { + await insightsApi.delete(insightId) + toast.success('Insight deleted successfully') + setSelectedInsight(null) + await fetchInsights() + } catch (err) { + console.error('Failed to delete insight:', err) + toast.error('Failed to delete insight') + } }} />