feat: delete AI reports, Super Contexte versions, and KB entries

- database.py: add delete_ai_report(), delete_reasoning_state(), delete_kb_entry()
- reasoning.py: DELETE /api/reasoning/reports/{id}
- knowledge.py: DELETE /api/knowledge/history/{id} and /entries/{id}
- useApi.ts: useDeleteAiReport, useDeleteReasoningState, useDeleteKbEntry hooks
- RapportIA.tsx: trash icon on hover in archived reports sidebar
- SuperContexte.tsx: trash icon on hover for history versions and KB entries;
  both propagate onDelete through CategorySection down to KbEntry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-17 00:10:41 +02:00
parent 22687dfd03
commit a3fb486477
6 changed files with 174 additions and 33 deletions

View File

@@ -8,6 +8,7 @@ from services.database import (
get_kb_entries, get_all_kb_entries, save_kb_entry, update_kb_entry_status,
get_latest_reasoning_state, get_reasoning_history, get_reasoning_state_by_id,
save_reasoning_state, list_ai_reports, get_mtm_trades_with_traces, _trade_maturity,
delete_reasoning_state, delete_kb_entry,
)
router = APIRouter(prefix="/api/knowledge", tags=["knowledge"])
@@ -174,6 +175,24 @@ def get_state_version(state_id: int):
return {"state": state}
@router.delete("/history/{state_id}")
def delete_state_version(state_id: int):
"""Delete a reasoning state version by ID."""
deleted = delete_reasoning_state(state_id)
if not deleted:
raise HTTPException(404, "Version introuvable")
return {"deleted": True, "id": state_id}
@router.delete("/entries/{entry_id}")
def delete_entry(entry_id: int):
"""Permanently delete a KB entry."""
deleted = delete_kb_entry(entry_id)
if not deleted:
raise HTTPException(404, "Entrée introuvable")
return {"deleted": True, "id": entry_id}
@router.get("/entries")
def list_entries(status: str = "all"):
if status == "all":

View File

@@ -21,6 +21,7 @@ from services.database import (
get_trade_entry_by_id,
list_ai_reports,
save_ai_report,
delete_ai_report,
_trade_maturity,
)
@@ -369,3 +370,12 @@ def get_report(report_id: int):
if not report:
raise HTTPException(404, f"Report {report_id} not found")
return report
@router.delete("/reports/{report_id}")
def delete_report(report_id: int):
"""Delete an archived AI report by ID."""
deleted = delete_ai_report(report_id)
if not deleted:
raise HTTPException(404, f"Report {report_id} not found")
return {"deleted": True, "id": report_id}

View File

@@ -1451,3 +1451,27 @@ def get_reasoning_state_by_id(state_id: int) -> Optional[Dict[str, Any]]:
except Exception:
d["synthesis"] = {}
return d
def delete_ai_report(report_id: int) -> bool:
conn = get_conn()
cur = conn.execute("DELETE FROM ai_reports WHERE id=?", (report_id,))
conn.commit()
conn.close()
return cur.rowcount > 0
def delete_reasoning_state(state_id: int) -> bool:
conn = get_conn()
cur = conn.execute("DELETE FROM reasoning_state WHERE id=?", (state_id,))
conn.commit()
conn.close()
return cur.rowcount > 0
def delete_kb_entry(entry_id: int) -> bool:
conn = get_conn()
cur = conn.execute("DELETE FROM knowledge_base WHERE id=?", (entry_id,))
conn.commit()
conn.close()
return cur.rowcount > 0

View File

@@ -546,3 +546,30 @@ export const usePatchKbEntryStatus = () => {
onSuccess: () => qc.invalidateQueries({ queryKey: ['knowledge-entries'] }),
})
}
export const useDeleteAiReport = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: number) => api.delete(`/reasoning/reports/${id}`).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['ai-reports-list'] }),
})
}
export const useDeleteReasoningState = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: number) => api.delete(`/knowledge/history/${id}`).then(r => r.data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['knowledge-history'] })
qc.invalidateQueries({ queryKey: ['knowledge-state'] })
},
})
}
export const useDeleteKbEntry = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (id: number) => api.delete(`/knowledge/entries/${id}`).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['knowledge-entries'] }),
})
}

View File

@@ -1,7 +1,7 @@
import { useState } from 'react'
import { Brain, TrendingUp, TrendingDown, RefreshCw, Zap, AlertTriangle, BookOpen, Target, Eye, Clock, ChevronRight } from 'lucide-react'
import { Brain, TrendingUp, TrendingDown, RefreshCw, Zap, AlertTriangle, BookOpen, Target, Eye, Clock, ChevronRight, Trash2 } from 'lucide-react'
import clsx from 'clsx'
import { usePortfolioReportData, useGeneratePortfolioReport, useAiReportsList, useAiReport } from '../hooks/useApi'
import { usePortfolioReportData, useGeneratePortfolioReport, useAiReportsList, useAiReport, useDeleteAiReport } from '../hooks/useApi'
import { useQueryClient } from '@tanstack/react-query'
const SCENARIO_META: Record<string, { label: string; color: string; emoji: string }> = {
@@ -202,6 +202,7 @@ export default function RapportIA() {
const { mutate: generate, isPending: generating, data: freshReportData, reset: resetFresh } = useGeneratePortfolioReport()
const { data: historyList } = useAiReportsList()
const { data: historicReport } = useAiReport(selectedHistoryId)
const { mutate: deleteReport } = useDeleteAiReport()
const history: any[] = (historyList as any)?.reports ?? []
@@ -247,29 +248,46 @@ export default function RapportIA() {
const date = r.created_at?.slice(0, 16).replace('T', ' ') ?? ''
const isActive = selectedHistoryId === r.id
return (
<button
<div
key={r.id}
onClick={() => { setSelectedHistoryId(r.id) }}
className={clsx(
'text-left px-3 py-2.5 hover:bg-dark-700/50 transition-colors',
'group relative text-left hover:bg-dark-700/50 transition-colors',
isActive && 'bg-blue-900/20 border-l-2 border-blue-500'
)}
>
<div className="flex items-center justify-between mb-0.5">
<span className="text-[11px] font-mono text-slate-500">{date}</span>
<span className="text-[10px] text-slate-600">{r.days}j</span>
</div>
{r.report?.headline && (
<p className="text-xs text-slate-400 line-clamp-2 leading-snug">{r.report.headline}</p>
)}
{avg != null && (
<span className={clsx('text-[11px] font-mono font-bold mt-1 block',
avg >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{avg >= 0 ? '+' : ''}{avg.toFixed(1)}% moy.
</span>
)}
{isActive && <ChevronRight className="w-3 h-3 text-blue-400 mt-1" />}
</button>
<button
onClick={() => { setSelectedHistoryId(r.id) }}
className="w-full text-left px-3 py-2.5 pr-8"
>
<div className="flex items-center justify-between mb-0.5">
<span className="text-[11px] font-mono text-slate-500">{date}</span>
<span className="text-[10px] text-slate-600">{r.days}j</span>
</div>
{r.report?.headline && (
<p className="text-xs text-slate-400 line-clamp-2 leading-snug">{r.report.headline}</p>
)}
{avg != null && (
<span className={clsx('text-[11px] font-mono font-bold mt-1 block',
avg >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{avg >= 0 ? '+' : ''}{avg.toFixed(1)}% moy.
</span>
)}
{isActive && <ChevronRight className="w-3 h-3 text-blue-400 mt-1" />}
</button>
<button
onClick={(e) => {
e.stopPropagation()
if (confirm('Supprimer ce rapport ?')) {
if (selectedHistoryId === r.id) setSelectedHistoryId(null)
deleteReport(r.id)
}
}}
className="absolute top-2 right-2 p-1 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 hover:bg-red-400/10 transition-all"
title="Supprimer ce rapport"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
)
})}
</div>

View File

@@ -3,11 +3,12 @@ import {
Brain, RefreshCw, Clock, ChevronDown, ChevronUp,
AlertTriangle, Target, TrendingUp, TrendingDown,
Shield, Zap, BookOpen, CheckCircle, XCircle, HelpCircle,
BarChart2, Activity,
BarChart2, Activity, Trash2,
} from 'lucide-react'
import {
useKnowledgeState, useKnowledgeHistory, useKnowledgeStateVersion,
useKnowledgeEntries, useSynthesizeKnowledge, usePatchKbEntryStatus,
useDeleteReasoningState, useDeleteKbEntry,
} from '../hooks/useApi'
// ─── Status badge ─────────────────────────────────────────────────────────────
@@ -39,10 +40,14 @@ function ConfidenceBar({ value }: { value: number }) {
}
// ─── KB Entry card ────────────────────────────────────────────────────────────
function KbEntry({ entry, onStatusChange }: { entry: any; onStatusChange: (id: number, s: string) => void }) {
function KbEntry({ entry, onStatusChange, onDelete }: {
entry: any
onStatusChange: (id: number, s: string) => void
onDelete: (id: number) => void
}) {
const [open, setOpen] = useState(false)
return (
<div className="bg-slate-800/60 border border-slate-700/50 rounded-lg p-3 space-y-2">
<div className="group bg-slate-800/60 border border-slate-700/50 rounded-lg p-3 space-y-2">
<div className="flex items-start gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
@@ -54,6 +59,15 @@ function KbEntry({ entry, onStatusChange }: { entry: any; onStatusChange: (id: n
<button onClick={() => setOpen(o => !o)} className="text-slate-500 hover:text-slate-300 mt-0.5">
{open ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
<button
onClick={() => {
if (confirm(`Supprimer "${entry.title}" ?`)) onDelete(entry.id)
}}
className="opacity-0 group-hover:opacity-100 p-1 rounded text-slate-600 hover:text-red-400 hover:bg-red-400/10 transition-all mt-0.5"
title="Supprimer cette entrée"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
{open && (
<div className="space-y-2 pt-1">
@@ -99,8 +113,13 @@ const CAT_META: Record<string, { icon: React.ReactNode; color: string; label: st
}
function CategorySection({
category, entries, onStatusChange,
}: { category: string; entries: any[]; onStatusChange: (id: number, s: string) => void }) {
category, entries, onStatusChange, onDelete,
}: {
category: string
entries: any[]
onStatusChange: (id: number, s: string) => void
onDelete: (id: number) => void
}) {
const [open, setOpen] = useState(true)
const meta = CAT_META[category] || CAT_META['général']
const active = entries.filter(e => e.status !== 'invalidated').length
@@ -120,7 +139,7 @@ function CategorySection({
{open && (
<div className="p-3 grid gap-2 bg-slate-900/40">
{entries.map(e => (
<KbEntry key={e.id} entry={e} onStatusChange={onStatusChange} />
<KbEntry key={e.id} entry={e} onStatusChange={onStatusChange} onDelete={onDelete} />
))}
</div>
)}
@@ -176,6 +195,8 @@ export default function SuperContexte() {
const { data: versionData } = useKnowledgeStateVersion(selectedHistoryId)
const synthesize = useSynthesizeKnowledge()
const patchStatus = usePatchKbEntryStatus()
const deleteState = useDeleteReasoningState()
const deleteEntry = useDeleteKbEntry()
const currentState = selectedHistoryId
? versionData?.state
@@ -202,6 +223,10 @@ export default function SuperContexte() {
patchStatus.mutate({ id, status })
}
const handleDeleteEntry = (id: number) => {
deleteEntry.mutate(id)
}
return (
<div className="min-h-screen bg-slate-950 text-white">
{/* Header */}
@@ -255,15 +280,32 @@ export default function SuperContexte() {
Dernière version
</button>
{history.map((h: any) => (
<button
<div
key={h.id}
onClick={() => setSelectedHistoryId(h.id)}
className={`w-full text-left px-3 py-2 rounded-lg text-xs transition-colors ${selectedHistoryId === h.id ? 'bg-violet-600/20 border border-violet-500/30 text-violet-300' : 'border border-slate-700/50 text-slate-400 hover:border-slate-600'}`}
className={`group relative rounded-lg border text-xs transition-colors ${selectedHistoryId === h.id ? 'bg-violet-600/20 border-violet-500/30 text-violet-300' : 'border-slate-700/50 text-slate-400 hover:border-slate-600'}`}
>
<div className="font-medium">v{h.version}</div>
<div className="text-slate-500">{h.created_at?.slice(0, 16)}</div>
<div className="text-slate-600">{h.reports_used} rapports · {h.trades_analyzed} trades</div>
</button>
<button
onClick={() => setSelectedHistoryId(h.id)}
className="w-full text-left px-3 py-2 pr-7"
>
<div className="font-medium">v{h.version}</div>
<div className="text-slate-500">{h.created_at?.slice(0, 16)}</div>
<div className="text-slate-600">{h.reports_used} rapports · {h.trades_analyzed} trades</div>
</button>
<button
onClick={(e) => {
e.stopPropagation()
if (confirm(`Supprimer la version v${h.version} du Super Contexte ?`)) {
if (selectedHistoryId === h.id) setSelectedHistoryId(null)
deleteState.mutate(h.id)
}
}}
className="absolute top-1.5 right-1.5 p-1 rounded opacity-0 group-hover:opacity-100 text-slate-600 hover:text-red-400 hover:bg-red-400/10 transition-all"
title="Supprimer cette version"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
))}
</div>
)}
@@ -466,6 +508,7 @@ export default function SuperContexte() {
category={cat}
entries={entries}
onStatusChange={handleStatusChange}
onDelete={handleDeleteEntry}
/>
))}
</div>