2026-03-16 13:35:11 +01:00
|
|
|
import { useState, useEffect } from 'react'
|
2026-03-26 07:50:13 +01:00
|
|
|
import { Brain, Trash2, ChevronDown, ChevronUp } from 'lucide-react'
|
2026-03-16 13:35:11 +01:00
|
|
|
import { api } from '../utils/api'
|
|
|
|
|
import { useAuth } from '../context/AuthContext'
|
|
|
|
|
import Markdown from '../utils/Markdown'
|
2026-03-21 06:43:10 +01:00
|
|
|
import UsageBadge from '../components/UsageBadge'
|
2026-03-16 13:35:11 +01:00
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
import 'dayjs/locale/de'
|
|
|
|
|
dayjs.locale('de')
|
|
|
|
|
|
2026-03-26 07:50:13 +01:00
|
|
|
// Legacy fallback labels (display_name takes precedence)
|
2026-03-16 13:35:11 +01:00
|
|
|
const SLUG_LABELS = {
|
2026-03-26 07:50:13 +01:00
|
|
|
pipeline: '🔬 Mehrstufige Gesamtanalyse'
|
2026-03-16 13:35:11 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-25 06:44:22 +01:00
|
|
|
function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
|
2026-03-16 13:35:11 +01:00
|
|
|
const [open, setOpen] = useState(defaultOpen)
|
2026-03-25 06:44:22 +01:00
|
|
|
|
|
|
|
|
// Find matching prompt to get display_name
|
|
|
|
|
const prompt = prompts.find(p => p.slug === ins.scope)
|
|
|
|
|
const displayName = prompt?.display_name || SLUG_LABELS[ins.scope] || ins.scope
|
|
|
|
|
|
2026-03-16 13:35:11 +01:00
|
|
|
return (
|
|
|
|
|
<div className="card section-gap" style={{borderLeft:`3px solid var(--accent)`}}>
|
|
|
|
|
<div style={{display:'flex',alignItems:'center',gap:8,marginBottom:open?12:0,cursor:'pointer'}}
|
|
|
|
|
onClick={()=>setOpen(o=>!o)}>
|
|
|
|
|
<div style={{flex:1}}>
|
|
|
|
|
<div style={{fontSize:13,fontWeight:600}}>
|
2026-03-25 06:44:22 +01:00
|
|
|
{displayName}
|
2026-03-16 13:35:11 +01:00
|
|
|
</div>
|
|
|
|
|
<div style={{fontSize:11,color:'var(--text3)'}}>
|
|
|
|
|
{dayjs(ins.created).format('DD. MMMM YYYY, HH:mm')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button style={{background:'none',border:'none',cursor:'pointer',color:'var(--text3)',padding:4}}
|
|
|
|
|
onClick={e=>{e.stopPropagation();onDelete(ins.id)}}>
|
|
|
|
|
<Trash2 size={13}/>
|
|
|
|
|
</button>
|
|
|
|
|
{open ? <ChevronUp size={16} color="var(--text3)"/> : <ChevronDown size={16} color="var(--text3)"/>}
|
|
|
|
|
</div>
|
|
|
|
|
{open && <Markdown text={ins.content}/>}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default function Analysis() {
|
2026-03-26 07:50:13 +01:00
|
|
|
const { canUseAI } = useAuth()
|
2026-03-16 13:35:11 +01:00
|
|
|
const [prompts, setPrompts] = useState([])
|
|
|
|
|
const [allInsights, setAllInsights] = useState([])
|
|
|
|
|
const [loading, setLoading] = useState(null)
|
|
|
|
|
const [error, setError] = useState(null)
|
|
|
|
|
const [tab, setTab] = useState('run')
|
2026-03-26 07:50:13 +01:00
|
|
|
const [newResult, setNewResult] = useState(null)
|
|
|
|
|
const [aiUsage, setAiUsage] = useState(null)
|
2026-03-16 13:35:11 +01:00
|
|
|
|
|
|
|
|
const loadAll = async () => {
|
|
|
|
|
const [p, i] = await Promise.all([
|
2026-03-16 16:52:36 +01:00
|
|
|
api.listPrompts(),
|
2026-03-16 13:35:11 +01:00
|
|
|
api.listInsights()
|
|
|
|
|
])
|
|
|
|
|
setPrompts(Array.isArray(p)?p:[])
|
|
|
|
|
setAllInsights(Array.isArray(i)?i:[])
|
|
|
|
|
}
|
2026-03-21 06:43:10 +01:00
|
|
|
|
|
|
|
|
useEffect(()=>{
|
|
|
|
|
loadAll()
|
|
|
|
|
// Load feature usage for badges
|
|
|
|
|
api.getFeatureUsage().then(features => {
|
|
|
|
|
const aiFeature = features.find(f => f.feature_id === 'ai_calls')
|
|
|
|
|
setAiUsage(aiFeature)
|
|
|
|
|
}).catch(err => console.error('Failed to load usage:', err))
|
|
|
|
|
},[])
|
2026-03-16 13:35:11 +01:00
|
|
|
|
|
|
|
|
const runPrompt = async (slug) => {
|
|
|
|
|
setLoading(slug); setError(null); setNewResult(null)
|
|
|
|
|
try {
|
2026-03-26 09:38:58 +01:00
|
|
|
// Use new unified executor with save=true
|
|
|
|
|
const result = await api.executeUnifiedPrompt(slug, null, null, false, true)
|
|
|
|
|
|
|
|
|
|
// Transform result to match old format for InsightCard
|
|
|
|
|
let content = ''
|
|
|
|
|
if (result.type === 'pipeline') {
|
|
|
|
|
// For pipeline, extract final output
|
|
|
|
|
const finalOutput = result.output || {}
|
|
|
|
|
if (typeof finalOutput === 'object' && Object.keys(finalOutput).length === 1) {
|
|
|
|
|
content = Object.values(finalOutput)[0]
|
|
|
|
|
} else {
|
|
|
|
|
content = JSON.stringify(finalOutput, null, 2)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// For base prompts, use output directly
|
|
|
|
|
content = typeof result.output === 'string' ? result.output : JSON.stringify(result.output, null, 2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setNewResult({ scope: slug, content })
|
2026-03-26 07:50:13 +01:00
|
|
|
await loadAll()
|
|
|
|
|
setTab('run')
|
2026-03-16 13:35:11 +01:00
|
|
|
} catch(e) {
|
|
|
|
|
setError('Fehler: ' + e.message)
|
|
|
|
|
} finally { setLoading(null) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deleteInsight = async (id) => {
|
|
|
|
|
if (!confirm('Analyse löschen?')) return
|
|
|
|
|
const pid = localStorage.getItem('bodytrack_active_profile')||''
|
|
|
|
|
await fetch(`/api/insights/${id}`, {
|
|
|
|
|
method:'DELETE', headers: pid ? {'X-Profile-Id':pid} : {}
|
|
|
|
|
})
|
|
|
|
|
if (newResult?.id === id) setNewResult(null)
|
|
|
|
|
await loadAll()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Group insights by scope for history view
|
|
|
|
|
const grouped = {}
|
|
|
|
|
allInsights.forEach(ins => {
|
|
|
|
|
const key = ins.scope || 'sonstige'
|
|
|
|
|
grouped[key] = grouped[key] || []
|
|
|
|
|
grouped[key].push(ins)
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-26 07:50:13 +01:00
|
|
|
// Show only active pipeline-type prompts
|
|
|
|
|
const pipelinePrompts = prompts.filter(p => p.active && p.type === 'pipeline')
|
2026-03-19 07:47:59 +01:00
|
|
|
|
2026-03-16 13:35:11 +01:00
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="page-title">KI-Analyse</h1>
|
|
|
|
|
|
|
|
|
|
<div className="tabs">
|
|
|
|
|
<button className={'tab'+(tab==='run'?' active':'')} onClick={()=>setTab('run')}>Analysen starten</button>
|
|
|
|
|
<button className={'tab'+(tab==='history'?' active':'')} onClick={()=>setTab('history')}>
|
|
|
|
|
Verlauf
|
|
|
|
|
{allInsights.length>0 && <span style={{marginLeft:4,fontSize:10,background:'var(--accent)',
|
|
|
|
|
color:'white',padding:'1px 5px',borderRadius:8}}>{allInsights.length}</span>}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<div style={{padding:'10px 14px',background:'#FCEBEB',borderRadius:8,fontSize:13,
|
|
|
|
|
color:'#D85A30',marginBottom:12,lineHeight:1.5}}>
|
|
|
|
|
{error.includes('nicht aktiviert') || error.includes('Limit')
|
|
|
|
|
? <>🔒 <strong>KI-Zugang eingeschränkt</strong><br/>
|
|
|
|
|
Dein Profil hat keinen Zugang zu KI-Analysen oder das Tageslimit wurde erreicht.
|
|
|
|
|
Bitte den Admin kontaktieren.</>
|
|
|
|
|
: error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ── Analysen starten ── */}
|
|
|
|
|
{tab==='run' && (
|
|
|
|
|
<div>
|
|
|
|
|
{/* Fresh result shown immediately */}
|
|
|
|
|
{newResult && (
|
|
|
|
|
<div style={{marginBottom:16}}>
|
|
|
|
|
<div style={{fontSize:12,fontWeight:600,color:'var(--accent)',marginBottom:8}}>
|
|
|
|
|
✅ Neue Analyse erstellt:
|
|
|
|
|
</div>
|
|
|
|
|
<InsightCard
|
|
|
|
|
ins={{...newResult, created: new Date().toISOString()}}
|
|
|
|
|
onDelete={deleteInsight}
|
|
|
|
|
defaultOpen={true}
|
2026-03-25 06:44:22 +01:00
|
|
|
prompts={prompts}
|
2026-03-16 13:35:11 +01:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!canUseAI && (
|
|
|
|
|
<div style={{padding:'14px 16px',background:'#FCEBEB',borderRadius:10,
|
|
|
|
|
border:'1px solid #D85A3033',marginBottom:16}}>
|
|
|
|
|
<div style={{fontSize:14,fontWeight:600,color:'#D85A30',marginBottom:4}}>
|
|
|
|
|
🔒 KI-Analysen nicht freigeschaltet
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{fontSize:13,color:'var(--text2)',lineHeight:1.5}}>
|
|
|
|
|
Dein Profil hat keinen Zugang zu KI-Analysen.
|
|
|
|
|
Bitte den Admin bitten, KI für dein Profil zu aktivieren
|
|
|
|
|
(Einstellungen → Admin → Profil bearbeiten).
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-26 07:50:13 +01:00
|
|
|
{canUseAI && pipelinePrompts.length > 0 && (
|
|
|
|
|
<p style={{fontSize:13,color:'var(--text2)',marginBottom:14,lineHeight:1.6}}>
|
|
|
|
|
Wähle eine mehrstufige KI-Analyse:
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{pipelinePrompts.map(p => {
|
2026-03-16 13:35:11 +01:00
|
|
|
const existing = allInsights.find(i=>i.scope===p.slug)
|
|
|
|
|
return (
|
2026-03-26 07:50:13 +01:00
|
|
|
<div key={p.id} className="card" style={{marginBottom:16,borderColor:'var(--accent)',borderWidth:2}}>
|
2026-03-16 13:35:11 +01:00
|
|
|
<div style={{display:'flex',alignItems:'flex-start',gap:12}}>
|
|
|
|
|
<div style={{flex:1}}>
|
2026-03-26 07:50:13 +01:00
|
|
|
<div className="badge-container-right" style={{fontWeight:700,fontSize:15,color:'var(--accent)'}}>
|
2026-03-25 06:31:25 +01:00
|
|
|
<span>{p.display_name || SLUG_LABELS[p.slug] || p.name}</span>
|
2026-03-21 06:43:10 +01:00
|
|
|
{aiUsage && <UsageBadge {...aiUsage} />}
|
|
|
|
|
</div>
|
2026-03-26 07:50:13 +01:00
|
|
|
{p.description && (
|
|
|
|
|
<div style={{fontSize:12,color:'var(--text2)',marginTop:3,lineHeight:1.5}}>
|
|
|
|
|
{p.description}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-16 13:35:11 +01:00
|
|
|
{existing && (
|
|
|
|
|
<div style={{fontSize:11,color:'var(--text3)',marginTop:3}}>
|
2026-03-26 07:50:13 +01:00
|
|
|
Letzte Analyse: {dayjs(existing.created).format('DD.MM.YYYY, HH:mm')}
|
2026-03-16 13:35:11 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-03-21 07:42:50 +01:00
|
|
|
<div
|
|
|
|
|
title={aiUsage && !aiUsage.allowed ? `Limit erreicht (${aiUsage.used}/${aiUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
|
|
|
|
|
style={{display:'inline-block'}}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
className="btn btn-primary"
|
2026-03-26 07:50:13 +01:00
|
|
|
style={{flexShrink:0,minWidth:100, cursor: (aiUsage && !aiUsage.allowed) ? 'not-allowed' : 'pointer'}}
|
2026-03-21 07:42:50 +01:00
|
|
|
onClick={()=>runPrompt(p.slug)}
|
|
|
|
|
disabled={!!loading||!canUseAI||(aiUsage && !aiUsage.allowed)}
|
|
|
|
|
>
|
|
|
|
|
{loading===p.slug
|
|
|
|
|
? <><div className="spinner" style={{width:13,height:13}}/> Läuft…</>
|
|
|
|
|
: (aiUsage && !aiUsage.allowed) ? '🔒 Limit'
|
2026-03-26 07:50:13 +01:00
|
|
|
: <><Brain size={13}/> Starten</>}
|
2026-03-21 07:42:50 +01:00
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-03-16 13:35:11 +01:00
|
|
|
</div>
|
|
|
|
|
{/* Show existing result collapsed */}
|
|
|
|
|
{existing && newResult?.id !== existing.id && (
|
|
|
|
|
<div style={{marginTop:8,borderTop:'1px solid var(--border)',paddingTop:8}}>
|
2026-03-25 06:44:22 +01:00
|
|
|
<InsightCard ins={existing} onDelete={deleteInsight} defaultOpen={false} prompts={prompts}/>
|
2026-03-16 13:35:11 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
ROLLBACK: complete removal of broken feature enforcement system
Reverts all feature enforcement changes (commits 3745ebd, cbad50a, cd4d912, 8415509)
to restore original working functionality.
Issues caused by feature enforcement implementation:
- Export buttons disappeared and never reappeared
- KI analysis counter not incrementing
- New analyses not saving
- Pipeline appearing twice
- Many core features broken
Restored files to working state before enforcement implementation (commit 0210844):
- Backend: auth.py, insights.py, exportdata.py, importdata.py, nutrition.py, activity.py
- Frontend: Analysis.jsx, SettingsPage.jsx, api.js
- Removed: FeatureGate.jsx, useFeatureAccess.js
The original simple AI limit system (ai_enabled, ai_limit_day) is now active again.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:19:56 +01:00
|
|
|
})}
|
2026-03-26 07:50:13 +01:00
|
|
|
|
|
|
|
|
{canUseAI && pipelinePrompts.length === 0 && (
|
|
|
|
|
<div className="empty-state">
|
|
|
|
|
<p>Keine aktiven Pipeline-Prompts verfügbar.</p>
|
|
|
|
|
<p style={{fontSize:12,color:'var(--text3)',marginTop:8}}>
|
|
|
|
|
Erstelle Pipeline-Prompts im Admin-Bereich (Einstellungen → Admin → KI-Prompts).
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
ROLLBACK: complete removal of broken feature enforcement system
Reverts all feature enforcement changes (commits 3745ebd, cbad50a, cd4d912, 8415509)
to restore original working functionality.
Issues caused by feature enforcement implementation:
- Export buttons disappeared and never reappeared
- KI analysis counter not incrementing
- New analyses not saving
- Pipeline appearing twice
- Many core features broken
Restored files to working state before enforcement implementation (commit 0210844):
- Backend: auth.py, insights.py, exportdata.py, importdata.py, nutrition.py, activity.py
- Frontend: Analysis.jsx, SettingsPage.jsx, api.js
- Removed: FeatureGate.jsx, useFeatureAccess.js
The original simple AI limit system (ai_enabled, ai_limit_day) is now active again.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:19:56 +01:00
|
|
|
)}
|
2026-03-16 13:35:11 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ── Verlauf gruppiert ── */}
|
|
|
|
|
{tab==='history' && (
|
|
|
|
|
<div>
|
|
|
|
|
{allInsights.length===0
|
|
|
|
|
? <div className="empty-state"><h3>Noch keine Analysen</h3></div>
|
|
|
|
|
: Object.entries(grouped).map(([scope, ins]) => (
|
|
|
|
|
<div key={scope} style={{marginBottom:20}}>
|
|
|
|
|
<div style={{fontSize:13,fontWeight:700,color:'var(--text3)',
|
|
|
|
|
textTransform:'uppercase',letterSpacing:'0.05em',marginBottom:8}}>
|
2026-03-25 06:44:22 +01:00
|
|
|
{prompts.find(p => p.slug === scope)?.display_name || SLUG_LABELS[scope] || scope} ({ins.length})
|
2026-03-16 13:35:11 +01:00
|
|
|
</div>
|
2026-03-25 06:44:22 +01:00
|
|
|
{ins.map(i => <InsightCard key={i.id} ins={i} onDelete={deleteInsight} prompts={prompts}/>)}
|
2026-03-16 13:35:11 +01:00
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|