2026-03-16 13:35:11 +01:00
|
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
|
|
import { Brain, Pencil, Trash2, ChevronDown, ChevronUp, Check, X } from 'lucide-react'
|
|
|
|
|
|
import { api } from '../utils/api'
|
|
|
|
|
|
import { useAuth } from '../context/AuthContext'
|
|
|
|
|
|
import Markdown from '../utils/Markdown'
|
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
|
import 'dayjs/locale/de'
|
|
|
|
|
|
dayjs.locale('de')
|
|
|
|
|
|
|
|
|
|
|
|
const SLUG_LABELS = {
|
|
|
|
|
|
gesamt: '🔍 Gesamtanalyse',
|
|
|
|
|
|
koerper: '🫧 Körperkomposition',
|
|
|
|
|
|
ernaehrung: '🍽️ Ernährung',
|
|
|
|
|
|
aktivitaet: '🏋️ Aktivität',
|
|
|
|
|
|
gesundheit: '❤️ Gesundheitsindikatoren',
|
|
|
|
|
|
ziele: '🎯 Zielfortschritt',
|
|
|
|
|
|
pipeline: '🔬 Mehrstufige Gesamtanalyse',
|
|
|
|
|
|
pipeline_body: '🔬 Pipeline: Körper-Analyse (JSON)',
|
|
|
|
|
|
pipeline_nutrition: '🔬 Pipeline: Ernährungs-Analyse (JSON)',
|
|
|
|
|
|
pipeline_activity: '🔬 Pipeline: Aktivitäts-Analyse (JSON)',
|
|
|
|
|
|
pipeline_synthesis: '🔬 Pipeline: Synthese',
|
|
|
|
|
|
pipeline_goals: '🔬 Pipeline: Zielabgleich',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function InsightCard({ ins, onDelete, defaultOpen=false }) {
|
|
|
|
|
|
const [open, setOpen] = useState(defaultOpen)
|
|
|
|
|
|
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}}>
|
|
|
|
|
|
{SLUG_LABELS[ins.scope] || ins.scope}
|
|
|
|
|
|
</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>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function PromptEditor({ prompt, onSave, onCancel }) {
|
|
|
|
|
|
const [template, setTemplate] = useState(prompt.template)
|
|
|
|
|
|
const [name, setName] = useState(prompt.name)
|
|
|
|
|
|
const [desc, setDesc] = useState(prompt.description||'')
|
|
|
|
|
|
|
|
|
|
|
|
const VARS = ['{{name}}','{{geschlecht}}','{{height}}','{{goal_weight}}','{{goal_bf_pct}}',
|
|
|
|
|
|
'{{weight_trend}}','{{weight_aktuell}}','{{kf_aktuell}}','{{caliper_summary}}',
|
|
|
|
|
|
'{{circ_summary}}','{{nutrition_summary}}','{{nutrition_detail}}',
|
|
|
|
|
|
'{{protein_ziel_low}}','{{protein_ziel_high}}','{{activity_summary}}',
|
|
|
|
|
|
'{{activity_kcal_summary}}','{{activity_detail}}']
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="card section-gap">
|
|
|
|
|
|
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:12}}>
|
|
|
|
|
|
<div className="card-title" style={{margin:0}}>Prompt bearbeiten</div>
|
|
|
|
|
|
<button style={{background:'none',border:'none',cursor:'pointer',color:'var(--text3)'}}
|
|
|
|
|
|
onClick={onCancel}><X size={16}/></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<label className="form-label">Name</label>
|
|
|
|
|
|
<input type="text" className="form-input" value={name} onChange={e=>setName(e.target.value)}/>
|
|
|
|
|
|
<span className="form-unit"/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row">
|
|
|
|
|
|
<label className="form-label">Beschreibung</label>
|
|
|
|
|
|
<input type="text" className="form-input" value={desc} onChange={e=>setDesc(e.target.value)}/>
|
|
|
|
|
|
<span className="form-unit"/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{marginBottom:8}}>
|
|
|
|
|
|
<div style={{fontSize:12,fontWeight:600,color:'var(--text3)',marginBottom:6}}>
|
|
|
|
|
|
Variablen (antippen zum Einfügen):
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{display:'flex',flexWrap:'wrap',gap:4}}>
|
|
|
|
|
|
{VARS.map(v=>(
|
|
|
|
|
|
<button key={v} onClick={()=>setTemplate(t=>t+v)}
|
|
|
|
|
|
style={{fontSize:10,padding:'2px 7px',borderRadius:4,border:'1px solid var(--border2)',
|
|
|
|
|
|
background:'var(--surface2)',cursor:'pointer',fontFamily:'monospace',color:'var(--accent)'}}>
|
|
|
|
|
|
{v}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<textarea value={template} onChange={e=>setTemplate(e.target.value)}
|
|
|
|
|
|
style={{width:'100%',minHeight:280,padding:10,fontFamily:'monospace',fontSize:12,
|
|
|
|
|
|
background:'var(--surface2)',border:'1.5px solid var(--border2)',borderRadius:8,
|
|
|
|
|
|
color:'var(--text1)',resize:'vertical',lineHeight:1.5,boxSizing:'border-box'}}/>
|
|
|
|
|
|
<div style={{display:'flex',gap:8,marginTop:8}}>
|
|
|
|
|
|
<button className="btn btn-primary" style={{flex:1}}
|
|
|
|
|
|
onClick={()=>onSave({name,description:desc,template})}>
|
|
|
|
|
|
<Check size={14}/> Speichern
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button className="btn btn-secondary" style={{flex:1}} onClick={onCancel}>Abbrechen</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function Analysis() {
|
|
|
|
|
|
const { canUseAI, isAdmin } = useAuth()
|
|
|
|
|
|
const [prompts, setPrompts] = useState([])
|
|
|
|
|
|
const [allInsights, setAllInsights] = useState([])
|
|
|
|
|
|
const [loading, setLoading] = useState(null)
|
|
|
|
|
|
const [error, setError] = useState(null)
|
|
|
|
|
|
const [editing, setEditing] = useState(null)
|
|
|
|
|
|
const [tab, setTab] = useState('run')
|
|
|
|
|
|
const [newResult, setNewResult] = useState(null)
|
|
|
|
|
|
const [pipelineLoading, setPipelineLoading] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
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:[])
|
|
|
|
|
|
}
|
|
|
|
|
|
useEffect(()=>{ loadAll() },[])
|
|
|
|
|
|
|
|
|
|
|
|
const runPipeline = async () => {
|
|
|
|
|
|
setPipelineLoading(true); setError(null); setNewResult(null)
|
|
|
|
|
|
try {
|
2026-03-16 16:52:36 +01:00
|
|
|
|
const result = await api.insightPipeline()
|
2026-03-16 13:35:11 +01:00
|
|
|
|
setNewResult(result)
|
|
|
|
|
|
await loadAll()
|
|
|
|
|
|
setTab('run')
|
|
|
|
|
|
} catch(e) {
|
|
|
|
|
|
setError('Pipeline-Fehler: ' + e.message)
|
|
|
|
|
|
} finally { setPipelineLoading(false) }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const runPrompt = async (slug) => {
|
|
|
|
|
|
setLoading(slug); setError(null); setNewResult(null)
|
|
|
|
|
|
try {
|
2026-03-16 16:52:36 +01:00
|
|
|
|
const result = await api.runInsight(slug)
|
2026-03-16 13:35:11 +01:00
|
|
|
|
setNewResult(result) // show immediately
|
|
|
|
|
|
await loadAll() // refresh lists
|
|
|
|
|
|
setTab('run') // stay on run tab to see result
|
|
|
|
|
|
} catch(e) {
|
|
|
|
|
|
setError('Fehler: ' + e.message)
|
|
|
|
|
|
} finally { setLoading(null) }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const savePrompt = async (promptId, data) => {
|
2026-03-19 06:36:37 +01:00
|
|
|
|
const token = localStorage.getItem('bodytrack_token')||''
|
2026-03-16 13:35:11 +01:00
|
|
|
|
await fetch(`/api/prompts/${promptId}`, {
|
2026-03-19 06:36:37 +01:00
|
|
|
|
method:'PUT',
|
|
|
|
|
|
headers:{'Content-Type':'application/json', 'X-Auth-Token': token},
|
|
|
|
|
|
body:JSON.stringify(data)
|
2026-03-16 13:35:11 +01:00
|
|
|
|
})
|
|
|
|
|
|
setEditing(null); await loadAll()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const activePrompts = prompts.filter(p=>p.active && !p.slug.startsWith('pipeline_'))
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
{isAdmin && <button className={'tab'+(tab==='prompts'?' active':'')} onClick={()=>setTab('prompts')}>Prompts</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}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Pipeline button */}
|
|
|
|
|
|
<div className="card" style={{marginBottom:16,borderColor:'var(--accent)',borderWidth:2}}>
|
|
|
|
|
|
<div style={{display:'flex',alignItems:'flex-start',gap:12}}>
|
|
|
|
|
|
<div style={{flex:1}}>
|
|
|
|
|
|
<div style={{fontWeight:700,fontSize:15,color:'var(--accent)'}}>🔬 Mehrstufige Gesamtanalyse</div>
|
|
|
|
|
|
<div style={{fontSize:12,color:'var(--text2)',marginTop:3,lineHeight:1.5}}>
|
|
|
|
|
|
3 spezialisierte KI-Calls parallel (Körper + Ernährung + Aktivität),
|
|
|
|
|
|
dann Synthese + Zielabgleich. Detaillierteste Auswertung.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{allInsights.find(i=>i.scope==='pipeline') && (
|
|
|
|
|
|
<div style={{fontSize:11,color:'var(--text3)',marginTop:3}}>
|
|
|
|
|
|
Letzte Analyse: {dayjs(allInsights.find(i=>i.scope==='pipeline').created).format('DD.MM.YYYY, HH:mm')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button className="btn btn-primary" style={{flexShrink:0,minWidth:100}}
|
|
|
|
|
|
onClick={runPipeline} disabled={!!loading||pipelineLoading}>
|
|
|
|
|
|
{pipelineLoading
|
|
|
|
|
|
? <><div className="spinner" style={{width:13,height:13}}/> Läuft…</>
|
|
|
|
|
|
: <><Brain size={13}/> Starten</>}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{!canUseAI && <div style={{fontSize:11,color:'#D85A30',marginTop:4}}>🔒 KI nicht freigeschaltet</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{pipelineLoading && (
|
|
|
|
|
|
<div style={{marginTop:10,padding:'8px 12px',background:'var(--accent-light)',
|
|
|
|
|
|
borderRadius:8,fontSize:12,color:'var(--accent-dark)'}}>
|
|
|
|
|
|
⚡ Stufe 1: 3 parallele Analyse-Calls… dann Synthese… dann Zielabgleich
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{canUseAI && <p style={{fontSize:13,color:'var(--text2)',marginBottom:14,lineHeight:1.6}}>
|
|
|
|
|
|
Oder wähle eine Einzelanalyse:
|
|
|
|
|
|
</p>}
|
|
|
|
|
|
|
|
|
|
|
|
{activePrompts.map(p => {
|
|
|
|
|
|
// Show latest existing insight for this prompt
|
|
|
|
|
|
const existing = allInsights.find(i=>i.scope===p.slug)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={p.id} className="card section-gap">
|
|
|
|
|
|
<div style={{display:'flex',alignItems:'flex-start',gap:12}}>
|
|
|
|
|
|
<div style={{flex:1}}>
|
|
|
|
|
|
<div style={{fontWeight:600,fontSize:15}}>{SLUG_LABELS[p.slug]||p.name}</div>
|
|
|
|
|
|
{p.description && <div style={{fontSize:12,color:'var(--text3)',marginTop:2}}>{p.description}</div>}
|
|
|
|
|
|
{existing && (
|
|
|
|
|
|
<div style={{fontSize:11,color:'var(--text3)',marginTop:3}}>
|
|
|
|
|
|
Letzte Auswertung: {dayjs(existing.created).format('DD.MM.YYYY, HH:mm')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button className="btn btn-primary" style={{flexShrink:0,minWidth:90}}
|
|
|
|
|
|
onClick={()=>runPrompt(p.slug)} disabled={!!loading||!canUseAI}>
|
|
|
|
|
|
{loading===p.slug
|
|
|
|
|
|
? <><div className="spinner" style={{width:13,height:13}}/> Läuft…</>
|
|
|
|
|
|
: <><Brain size={13}/> {existing?'Neu erstellen':'Starten'}</>}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/* Show existing result collapsed */}
|
|
|
|
|
|
{existing && newResult?.id !== existing.id && (
|
|
|
|
|
|
<div style={{marginTop:8,borderTop:'1px solid var(--border)',paddingTop:8}}>
|
|
|
|
|
|
<InsightCard ins={existing} onDelete={deleteInsight} defaultOpen={false}/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
{activePrompts.length===0 && (
|
|
|
|
|
|
<div className="empty-state"><p>Keine aktiven Prompts. Aktiviere im Tab "Prompts".</p></div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</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}}>
|
|
|
|
|
|
{SLUG_LABELS[scope]||scope} ({ins.length})
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{ins.map(i => <InsightCard key={i.id} ins={i} onDelete={deleteInsight}/>)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* ── Prompts ── */}
|
|
|
|
|
|
{tab==='prompts' && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p style={{fontSize:13,color:'var(--text2)',marginBottom:14,lineHeight:1.6}}>
|
|
|
|
|
|
Passe Prompts an. Variablen wie{' '}
|
|
|
|
|
|
<code style={{fontSize:11,background:'var(--surface2)',padding:'1px 4px',borderRadius:3}}>{'{{name}}'}</code>{' '}
|
|
|
|
|
|
werden automatisch mit deinen Daten befüllt.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{editing ? (
|
|
|
|
|
|
<PromptEditor prompt={editing}
|
|
|
|
|
|
onSave={(data)=>savePrompt(editing.id,data)}
|
|
|
|
|
|
onCancel={()=>setEditing(null)}/>
|
|
|
|
|
|
) : (() => {
|
|
|
|
|
|
const singlePrompts = prompts.filter(p=>!p.slug.startsWith('pipeline_'))
|
|
|
|
|
|
const pipelinePrompts = prompts.filter(p=>p.slug.startsWith('pipeline_'))
|
|
|
|
|
|
const jsonSlugs = ['pipeline_body','pipeline_nutrition','pipeline_activity']
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Single prompts */}
|
|
|
|
|
|
<div style={{fontSize:12,fontWeight:700,color:'var(--text3)',
|
|
|
|
|
|
textTransform:'uppercase',letterSpacing:'0.05em',marginBottom:8}}>
|
|
|
|
|
|
Einzelanalysen
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{singlePrompts.map(p=>(
|
2026-03-19 07:27:56 +01:00
|
|
|
|
<div key={p.id} className="card section-gap" style={{opacity:p.active?1:0.6}}>
|
2026-03-16 13:35:11 +01:00
|
|
|
|
<div style={{display:'flex',alignItems:'center',gap:10}}>
|
|
|
|
|
|
<div style={{flex:1}}>
|
|
|
|
|
|
<div style={{fontWeight:600,fontSize:14,display:'flex',alignItems:'center',gap:8}}>
|
|
|
|
|
|
{SLUG_LABELS[p.slug]||p.name}
|
2026-03-19 07:27:56 +01:00
|
|
|
|
{!p.active && <span style={{fontSize:10,color:'#D85A30',
|
|
|
|
|
|
background:'#FCEBEB',padding:'2px 8px',borderRadius:4,fontWeight:600}}>⏸ Deaktiviert</span>}
|
2026-03-16 13:35:11 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
{p.description && <div style={{fontSize:12,color:'var(--text3)',marginTop:1}}>{p.description}</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button className="btn btn-secondary" style={{padding:'5px 8px',fontSize:12}}
|
2026-03-19 06:36:37 +01:00
|
|
|
|
onClick={()=>{
|
|
|
|
|
|
const token = localStorage.getItem('bodytrack_token')||''
|
|
|
|
|
|
fetch(`/api/prompts/${p.id}`,{
|
|
|
|
|
|
method:'PUT',
|
|
|
|
|
|
headers:{'Content-Type':'application/json','X-Auth-Token':token},
|
2026-03-19 07:27:56 +01:00
|
|
|
|
body:JSON.stringify({active:!p.active})
|
2026-03-19 06:36:37 +01:00
|
|
|
|
}).then(loadAll)
|
|
|
|
|
|
}}>
|
2026-03-19 07:27:56 +01:00
|
|
|
|
{p.active?'Deaktivieren':'Aktivieren'}
|
2026-03-16 13:35:11 +01:00
|
|
|
|
</button>
|
|
|
|
|
|
<button className="btn btn-secondary" style={{padding:'5px 8px'}}
|
|
|
|
|
|
onClick={()=>setEditing(p)}><Pencil size={13}/></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{marginTop:8,padding:'8px 10px',background:'var(--surface2)',borderRadius:6,
|
|
|
|
|
|
fontSize:11,fontFamily:'monospace',color:'var(--text3)',maxHeight:60,overflow:'hidden',lineHeight:1.4}}>
|
|
|
|
|
|
{p.template.slice(0,200)}…
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Pipeline prompts */}
|
|
|
|
|
|
<div style={{fontSize:12,fontWeight:700,color:'var(--text3)',
|
|
|
|
|
|
textTransform:'uppercase',letterSpacing:'0.05em',margin:'20px 0 8px'}}>
|
|
|
|
|
|
Mehrstufige Pipeline
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{padding:'10px 12px',background:'var(--warn-bg)',borderRadius:8,
|
|
|
|
|
|
fontSize:12,color:'var(--warn-text)',marginBottom:12,lineHeight:1.6}}>
|
|
|
|
|
|
⚠️ <strong>Hinweis:</strong> Pipeline-Stage-1-Prompts müssen valides JSON zurückgeben.
|
|
|
|
|
|
Halte das JSON-Format im Prompt erhalten. Stage 2 + 3 können frei angepasst werden.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{pipelinePrompts.map(p=>{
|
|
|
|
|
|
const isJson = jsonSlugs.includes(p.slug)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={p.id} className="card section-gap"
|
2026-03-19 07:27:56 +01:00
|
|
|
|
style={{borderLeft:`3px solid ${isJson?'var(--warn)':'var(--accent)'}`,opacity:p.active?1:0.6}}>
|
2026-03-16 13:35:11 +01:00
|
|
|
|
<div style={{display:'flex',alignItems:'center',gap:10}}>
|
|
|
|
|
|
<div style={{flex:1}}>
|
|
|
|
|
|
<div style={{fontWeight:600,fontSize:14,display:'flex',alignItems:'center',gap:8}}>
|
|
|
|
|
|
{p.name}
|
|
|
|
|
|
{isJson && <span style={{fontSize:10,background:'var(--warn-bg)',
|
|
|
|
|
|
color:'var(--warn-text)',padding:'1px 6px',borderRadius:4}}>JSON-Output</span>}
|
2026-03-19 07:27:56 +01:00
|
|
|
|
{!p.active && <span style={{fontSize:10,color:'#D85A30',
|
|
|
|
|
|
background:'#FCEBEB',padding:'2px 8px',borderRadius:4,fontWeight:600}}>⏸ Deaktiviert</span>}
|
2026-03-16 13:35:11 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
{p.description && <div style={{fontSize:12,color:'var(--text3)',marginTop:1}}>{p.description}</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button className="btn btn-secondary" style={{padding:'5px 8px'}}
|
|
|
|
|
|
onClick={()=>setEditing(p)}><Pencil size={13}/></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{marginTop:8,padding:'8px 10px',background:'var(--surface2)',borderRadius:6,
|
|
|
|
|
|
fontSize:11,fontFamily:'monospace',color:'var(--text3)',maxHeight:80,overflow:'hidden',lineHeight:1.4}}>
|
|
|
|
|
|
{p.template.slice(0,300)}…
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)
|
|
|
|
|
|
})()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|