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:
@@ -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":
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'] }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user