Compare commits
No commits in common. "c0a50dedcdc149c33c42038f3fd24f6c297919a4" and "7daa2e40c719f1e2d81065aaaeb4ea1e15451f84" have entirely different histories.
c0a50dedcd
...
7daa2e40c7
|
|
@ -1,7 +0,0 @@
|
||||||
-- Migration 021: Add metadata column to ai_insights for storing debug info
|
|
||||||
-- Date: 2026-03-26
|
|
||||||
-- Purpose: Store resolved placeholder values with descriptions for transparency
|
|
||||||
|
|
||||||
ALTER TABLE ai_insights ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT NULL;
|
|
||||||
|
|
||||||
COMMENT ON COLUMN ai_insights.metadata IS 'Debug info: resolved placeholders, descriptions, etc.';
|
|
||||||
|
|
@ -14,18 +14,14 @@ from db import get_db, get_cursor, r2d
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
|
||||||
def resolve_placeholders(template: str, variables: Dict[str, Any], debug_info: Optional[Dict] = None, catalog: Optional[Dict] = None) -> str:
|
def resolve_placeholders(template: str, variables: Dict[str, Any], debug_info: Optional[Dict] = None) -> str:
|
||||||
"""
|
"""
|
||||||
Replace {{placeholder}} with values from variables dict.
|
Replace {{placeholder}} with values from variables dict.
|
||||||
|
|
||||||
Supports modifiers:
|
|
||||||
- {{key|d}} - Include description in parentheses (requires catalog)
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
template: String with {{key}} or {{key|modifiers}} placeholders
|
template: String with {{key}} placeholders
|
||||||
variables: Dict of key -> value mappings
|
variables: Dict of key -> value mappings
|
||||||
debug_info: Optional dict to collect debug information
|
debug_info: Optional dict to collect debug information
|
||||||
catalog: Optional placeholder catalog for descriptions (from get_placeholder_catalog)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Template with placeholders replaced
|
Template with placeholders replaced
|
||||||
|
|
@ -34,13 +30,7 @@ def resolve_placeholders(template: str, variables: Dict[str, Any], debug_info: O
|
||||||
unresolved = []
|
unresolved = []
|
||||||
|
|
||||||
def replacer(match):
|
def replacer(match):
|
||||||
full_placeholder = match.group(1).strip()
|
key = match.group(1).strip()
|
||||||
|
|
||||||
# Parse key and modifiers (e.g., "weight_aktuell|d" -> key="weight_aktuell", modifiers="d")
|
|
||||||
parts = full_placeholder.split('|')
|
|
||||||
key = parts[0].strip()
|
|
||||||
modifiers = parts[1].strip() if len(parts) > 1 else ''
|
|
||||||
|
|
||||||
if key in variables:
|
if key in variables:
|
||||||
value = variables[key]
|
value = variables[key]
|
||||||
# Convert dict/list to JSON string
|
# Convert dict/list to JSON string
|
||||||
|
|
@ -49,19 +39,6 @@ def resolve_placeholders(template: str, variables: Dict[str, Any], debug_info: O
|
||||||
else:
|
else:
|
||||||
resolved_value = str(value)
|
resolved_value = str(value)
|
||||||
|
|
||||||
# Apply modifiers
|
|
||||||
if 'd' in modifiers and catalog:
|
|
||||||
# Add description from catalog
|
|
||||||
description = None
|
|
||||||
for cat_items in catalog.values():
|
|
||||||
matching = [item for item in cat_items if item['key'] == key]
|
|
||||||
if matching:
|
|
||||||
description = matching[0].get('description', '')
|
|
||||||
break
|
|
||||||
|
|
||||||
if description:
|
|
||||||
resolved_value = f"{resolved_value} ({description})"
|
|
||||||
|
|
||||||
# Track resolution for debug
|
# Track resolution for debug
|
||||||
if debug_info is not None:
|
if debug_info is not None:
|
||||||
resolved[key] = resolved_value[:100] + ('...' if len(resolved_value) > 100 else '')
|
resolved[key] = resolved_value[:100] + ('...' if len(resolved_value) > 100 else '')
|
||||||
|
|
@ -174,16 +151,13 @@ async def execute_prompt(
|
||||||
|
|
||||||
prompt_type = prompt.get('type', 'pipeline')
|
prompt_type = prompt.get('type', 'pipeline')
|
||||||
|
|
||||||
# Get catalog from variables if available (passed from execute_prompt_with_data)
|
|
||||||
catalog = variables.pop('_catalog', None) if '_catalog' in variables else None
|
|
||||||
|
|
||||||
if prompt_type == 'base':
|
if prompt_type == 'base':
|
||||||
# Base prompt: single execution with template
|
# Base prompt: single execution with template
|
||||||
return await execute_base_prompt(prompt, variables, openrouter_call_func, enable_debug, catalog)
|
return await execute_base_prompt(prompt, variables, openrouter_call_func, enable_debug)
|
||||||
|
|
||||||
elif prompt_type == 'pipeline':
|
elif prompt_type == 'pipeline':
|
||||||
# Pipeline prompt: multi-stage execution
|
# Pipeline prompt: multi-stage execution
|
||||||
return await execute_pipeline_prompt(prompt, variables, openrouter_call_func, enable_debug, catalog)
|
return await execute_pipeline_prompt(prompt, variables, openrouter_call_func, enable_debug)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise HTTPException(400, f"Unknown prompt type: {prompt_type}")
|
raise HTTPException(400, f"Unknown prompt type: {prompt_type}")
|
||||||
|
|
@ -193,8 +167,7 @@ async def execute_base_prompt(
|
||||||
prompt: Dict,
|
prompt: Dict,
|
||||||
variables: Dict[str, Any],
|
variables: Dict[str, Any],
|
||||||
openrouter_call_func,
|
openrouter_call_func,
|
||||||
enable_debug: bool = False,
|
enable_debug: bool = False
|
||||||
catalog: Optional[Dict] = None
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Execute a base-type prompt (single template)."""
|
"""Execute a base-type prompt (single template)."""
|
||||||
template = prompt.get('template')
|
template = prompt.get('template')
|
||||||
|
|
@ -203,8 +176,8 @@ async def execute_base_prompt(
|
||||||
|
|
||||||
debug_info = {} if enable_debug else None
|
debug_info = {} if enable_debug else None
|
||||||
|
|
||||||
# Resolve placeholders (with optional catalog for |d modifier)
|
# Resolve placeholders
|
||||||
prompt_text = resolve_placeholders(template, variables, debug_info, catalog)
|
prompt_text = resolve_placeholders(template, variables, debug_info)
|
||||||
|
|
||||||
if enable_debug:
|
if enable_debug:
|
||||||
debug_info['template'] = template
|
debug_info['template'] = template
|
||||||
|
|
@ -242,8 +215,7 @@ async def execute_pipeline_prompt(
|
||||||
prompt: Dict,
|
prompt: Dict,
|
||||||
variables: Dict[str, Any],
|
variables: Dict[str, Any],
|
||||||
openrouter_call_func,
|
openrouter_call_func,
|
||||||
enable_debug: bool = False,
|
enable_debug: bool = False
|
||||||
catalog: Optional[Dict] = None
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Execute a pipeline-type prompt (multi-stage).
|
Execute a pipeline-type prompt (multi-stage).
|
||||||
|
|
@ -310,7 +282,7 @@ async def execute_pipeline_prompt(
|
||||||
raise HTTPException(400, f"Inline prompt missing template in stage {stage_num}")
|
raise HTTPException(400, f"Inline prompt missing template in stage {stage_num}")
|
||||||
|
|
||||||
placeholder_debug = {} if enable_debug else None
|
placeholder_debug = {} if enable_debug else None
|
||||||
prompt_text = resolve_placeholders(template, context_vars, placeholder_debug, catalog)
|
prompt_text = resolve_placeholders(template, context_vars, placeholder_debug)
|
||||||
|
|
||||||
if enable_debug:
|
if enable_debug:
|
||||||
prompt_debug['source'] = 'inline'
|
prompt_debug['source'] = 'inline'
|
||||||
|
|
@ -396,7 +368,7 @@ async def execute_prompt_with_data(
|
||||||
Execution result dict
|
Execution result dict
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from placeholder_resolver import get_placeholder_example_values, get_placeholder_catalog
|
from placeholder_resolver import get_placeholder_example_values
|
||||||
|
|
||||||
# Build variables from data modules
|
# Build variables from data modules
|
||||||
variables = {
|
variables = {
|
||||||
|
|
@ -404,13 +376,6 @@ async def execute_prompt_with_data(
|
||||||
'today': datetime.now().strftime('%Y-%m-%d')
|
'today': datetime.now().strftime('%Y-%m-%d')
|
||||||
}
|
}
|
||||||
|
|
||||||
# Load placeholder catalog for |d modifier support
|
|
||||||
try:
|
|
||||||
catalog = get_placeholder_catalog(profile_id)
|
|
||||||
variables['_catalog'] = catalog # Will be popped in execute_prompt
|
|
||||||
except Exception:
|
|
||||||
catalog = None
|
|
||||||
|
|
||||||
# Add PROCESSED placeholders (name, weight_trend, caliper_summary, etc.)
|
# Add PROCESSED placeholders (name, weight_trend, caliper_summary, etc.)
|
||||||
# This makes old-style prompts work with the new executor
|
# This makes old-style prompts work with the new executor
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -812,69 +812,13 @@ async def execute_unified_prompt(
|
||||||
if isinstance(content, dict):
|
if isinstance(content, dict):
|
||||||
content = json.dumps(content, ensure_ascii=False)
|
content = json.dumps(content, ensure_ascii=False)
|
||||||
|
|
||||||
# Prepare metadata with resolved placeholders and descriptions
|
# Save to database
|
||||||
from placeholder_resolver import get_placeholder_catalog
|
|
||||||
|
|
||||||
metadata = {
|
|
||||||
'prompt_type': result['type'],
|
|
||||||
'placeholders': {}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Collect all resolved placeholders from debug info
|
|
||||||
if result.get('debug'):
|
|
||||||
catalog = get_placeholder_catalog(profile_id)
|
|
||||||
|
|
||||||
if result['type'] == 'base':
|
|
||||||
# Base prompt: single set of placeholders
|
|
||||||
resolved = result['debug'].get('resolved_placeholders', {})
|
|
||||||
for key, value in resolved.items():
|
|
||||||
# Find description in catalog
|
|
||||||
desc = None
|
|
||||||
for cat_items in catalog.values():
|
|
||||||
matching = [item for item in cat_items if item['key'] == key]
|
|
||||||
if matching:
|
|
||||||
desc = matching[0].get('description', '')
|
|
||||||
break
|
|
||||||
|
|
||||||
metadata['placeholders'][key] = {
|
|
||||||
'value': value,
|
|
||||||
'description': desc or ''
|
|
||||||
}
|
|
||||||
|
|
||||||
elif result['type'] == 'pipeline':
|
|
||||||
# Pipeline: collect from all stages
|
|
||||||
stages_debug = result['debug'].get('stages', [])
|
|
||||||
for stage_debug in stages_debug:
|
|
||||||
for prompt_debug in stage_debug.get('prompts', []):
|
|
||||||
resolved = {}
|
|
||||||
# Check both direct and ref_debug
|
|
||||||
if 'resolved_placeholders' in prompt_debug:
|
|
||||||
resolved = prompt_debug['resolved_placeholders']
|
|
||||||
elif 'ref_debug' in prompt_debug and 'resolved_placeholders' in prompt_debug['ref_debug']:
|
|
||||||
resolved = prompt_debug['ref_debug']['resolved_placeholders']
|
|
||||||
|
|
||||||
for key, value in resolved.items():
|
|
||||||
if key not in metadata['placeholders']: # Avoid duplicates
|
|
||||||
# Find description in catalog
|
|
||||||
desc = None
|
|
||||||
for cat_items in catalog.values():
|
|
||||||
matching = [item for item in cat_items if item['key'] == key]
|
|
||||||
if matching:
|
|
||||||
desc = matching[0].get('description', '')
|
|
||||||
break
|
|
||||||
|
|
||||||
metadata['placeholders'][key] = {
|
|
||||||
'value': value,
|
|
||||||
'description': desc or ''
|
|
||||||
}
|
|
||||||
|
|
||||||
# Save to database with metadata
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(conn)
|
cur = get_cursor(conn)
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""INSERT INTO ai_insights (id, profile_id, scope, content, metadata, created)
|
"""INSERT INTO ai_insights (id, profile_id, scope, content, created)
|
||||||
VALUES (%s, %s, %s, %s, %s, CURRENT_TIMESTAMP)""",
|
VALUES (%s, %s, %s, %s, CURRENT_TIMESTAMP)""",
|
||||||
(str(uuid.uuid4()), profile_id, prompt_slug, content, json.dumps(metadata))
|
(str(uuid.uuid4()), profile_id, prompt_slug, content)
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
import { X, Plus, Trash2, MoveUp, MoveDown, Code } from 'lucide-react'
|
import { X, Plus, Trash2, MoveUp, MoveDown, Code } from 'lucide-react'
|
||||||
import PlaceholderPicker from './PlaceholderPicker'
|
import PlaceholderPicker from './PlaceholderPicker'
|
||||||
|
|
@ -39,8 +39,6 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [showPlaceholderPicker, setShowPlaceholderPicker] = useState(false)
|
const [showPlaceholderPicker, setShowPlaceholderPicker] = useState(false)
|
||||||
const [pickerTarget, setPickerTarget] = useState(null) // 'base' or {stage, promptIdx}
|
const [pickerTarget, setPickerTarget] = useState(null) // 'base' or {stage, promptIdx}
|
||||||
const [cursorPosition, setCursorPosition] = useState(null) // Track cursor position for insertion
|
|
||||||
const baseTemplateRef = useRef(null)
|
|
||||||
|
|
||||||
// Test functionality
|
// Test functionality
|
||||||
const [testing, setTesting] = useState(false)
|
const [testing, setTesting] = useState(false)
|
||||||
|
|
@ -516,12 +514,9 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div style={{ marginBottom: 24 }}>
|
||||||
<h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 12 }}>Template</h3>
|
<h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 12 }}>Template</h3>
|
||||||
<textarea
|
<textarea
|
||||||
ref={baseTemplateRef}
|
|
||||||
className="form-input"
|
className="form-input"
|
||||||
value={template}
|
value={template}
|
||||||
onChange={e => setTemplate(e.target.value)}
|
onChange={e => setTemplate(e.target.value)}
|
||||||
onClick={e => setCursorPosition(e.target.selectionStart)}
|
|
||||||
onKeyUp={e => setCursorPosition(e.target.selectionStart)}
|
|
||||||
rows={12}
|
rows={12}
|
||||||
placeholder="Prompt-Template mit {{placeholders}}..."
|
placeholder="Prompt-Template mit {{placeholders}}..."
|
||||||
style={{ width: '100%', textAlign: 'left', resize: 'vertical', fontFamily: 'monospace', fontSize: 12 }}
|
style={{ width: '100%', textAlign: 'left', resize: 'vertical', fontFamily: 'monospace', fontSize: 12 }}
|
||||||
|
|
@ -804,22 +799,10 @@ export default function UnifiedPromptModal({ prompt, onSave, onClose }) {
|
||||||
<PlaceholderPicker
|
<PlaceholderPicker
|
||||||
onSelect={(placeholder) => {
|
onSelect={(placeholder) => {
|
||||||
if (pickerTarget === 'base') {
|
if (pickerTarget === 'base') {
|
||||||
// Insert into base template at cursor position
|
// Insert into base template
|
||||||
const pos = cursorPosition ?? template.length
|
setTemplate(template + placeholder)
|
||||||
const newTemplate = template.slice(0, pos) + placeholder + template.slice(pos)
|
|
||||||
setTemplate(newTemplate)
|
|
||||||
|
|
||||||
// Restore focus and cursor position after insertion
|
|
||||||
setTimeout(() => {
|
|
||||||
if (baseTemplateRef.current) {
|
|
||||||
baseTemplateRef.current.focus()
|
|
||||||
const newPos = pos + placeholder.length
|
|
||||||
baseTemplateRef.current.setSelectionRange(newPos, newPos)
|
|
||||||
setCursorPosition(newPos)
|
|
||||||
}
|
|
||||||
}, 0)
|
|
||||||
} else if (pickerTarget && typeof pickerTarget === 'object') {
|
} else if (pickerTarget && typeof pickerTarget === 'object') {
|
||||||
// Insert into pipeline stage template (append at end for now)
|
// Insert into pipeline stage template
|
||||||
const { stage: stageNum, promptIdx } = pickerTarget
|
const { stage: stageNum, promptIdx } = pickerTarget
|
||||||
setStages(stages.map(s => {
|
setStages(stages.map(s => {
|
||||||
if (s.stage === stageNum) {
|
if (s.stage === stageNum) {
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,11 @@ const SLUG_LABELS = {
|
||||||
|
|
||||||
function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
|
function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
|
||||||
const [open, setOpen] = useState(defaultOpen)
|
const [open, setOpen] = useState(defaultOpen)
|
||||||
const [showValues, setShowValues] = useState(false)
|
|
||||||
|
|
||||||
// Find matching prompt to get display_name
|
// Find matching prompt to get display_name
|
||||||
const prompt = prompts.find(p => p.slug === ins.scope)
|
const prompt = prompts.find(p => p.slug === ins.scope)
|
||||||
const displayName = prompt?.display_name || SLUG_LABELS[ins.scope] || ins.scope
|
const displayName = prompt?.display_name || SLUG_LABELS[ins.scope] || ins.scope
|
||||||
|
|
||||||
// Parse metadata if available
|
|
||||||
const metadata = ins.metadata ? (typeof ins.metadata === 'string' ? JSON.parse(ins.metadata) : ins.metadata) : null
|
|
||||||
const placeholders = metadata?.placeholders || {}
|
|
||||||
const placeholderCount = Object.keys(placeholders).length
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card section-gap" style={{borderLeft:`3px solid var(--accent)`}}>
|
<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'}}
|
<div style={{display:'flex',alignItems:'center',gap:8,marginBottom:open?12:0,cursor:'pointer'}}
|
||||||
|
|
@ -44,61 +38,7 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
|
||||||
</button>
|
</button>
|
||||||
{open ? <ChevronUp size={16} color="var(--text3)"/> : <ChevronDown size={16} color="var(--text3)"/>}
|
{open ? <ChevronUp size={16} color="var(--text3)"/> : <ChevronDown size={16} color="var(--text3)"/>}
|
||||||
</div>
|
</div>
|
||||||
{open && (
|
{open && <Markdown text={ins.content}/>}
|
||||||
<>
|
|
||||||
<Markdown text={ins.content}/>
|
|
||||||
|
|
||||||
{/* Value Table */}
|
|
||||||
{placeholderCount > 0 && (
|
|
||||||
<div style={{ marginTop: 16, borderTop: '1px solid var(--border)', paddingTop: 12 }}>
|
|
||||||
<div
|
|
||||||
onClick={() => setShowValues(!showValues)}
|
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: 12,
|
|
||||||
color: 'var(--text2)',
|
|
||||||
fontWeight: 600,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 6
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{showValues ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
|
||||||
📊 Verwendete Werte ({placeholderCount})
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showValues && (
|
|
||||||
<div style={{ marginTop: 12, fontSize: 11 }}>
|
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
||||||
<thead>
|
|
||||||
<tr style={{ borderBottom: '1px solid var(--border)', textAlign: 'left' }}>
|
|
||||||
<th style={{ padding: '6px 8px', color: 'var(--text3)', fontWeight: 600 }}>Platzhalter</th>
|
|
||||||
<th style={{ padding: '6px 8px', color: 'var(--text3)', fontWeight: 600 }}>Wert</th>
|
|
||||||
<th style={{ padding: '6px 8px', color: 'var(--text3)', fontWeight: 600 }}>Beschreibung</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Object.entries(placeholders).map(([key, data]) => (
|
|
||||||
<tr key={key} style={{ borderBottom: '1px solid var(--border)' }}>
|
|
||||||
<td style={{ padding: '6px 8px', fontFamily: 'monospace', color: 'var(--accent)' }}>
|
|
||||||
{key}
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '6px 8px', fontFamily: 'monospace' }}>
|
|
||||||
{data.value}
|
|
||||||
</td>
|
|
||||||
<td style={{ padding: '6px 8px', color: 'var(--text3)', fontSize: 10 }}>
|
|
||||||
{data.description || '—'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -163,13 +103,12 @@ export default function Analysis() {
|
||||||
|
|
||||||
const deleteInsight = async (id) => {
|
const deleteInsight = async (id) => {
|
||||||
if (!confirm('Analyse löschen?')) return
|
if (!confirm('Analyse löschen?')) return
|
||||||
try {
|
const pid = localStorage.getItem('bodytrack_active_profile')||''
|
||||||
await api.deleteInsight(id)
|
await fetch(`/api/insights/${id}`, {
|
||||||
|
method:'DELETE', headers: pid ? {'X-Profile-Id':pid} : {}
|
||||||
|
})
|
||||||
if (newResult?.id === id) setNewResult(null)
|
if (newResult?.id === id) setNewResult(null)
|
||||||
await loadAll()
|
await loadAll()
|
||||||
} catch (e) {
|
|
||||||
setError('Löschen fehlgeschlagen: ' + e.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group insights by scope for history view
|
// Group insights by scope for history view
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,6 @@ export const api = {
|
||||||
insightPipeline: () => req('/insights/pipeline',{method:'POST'}),
|
insightPipeline: () => req('/insights/pipeline',{method:'POST'}),
|
||||||
listInsights: () => req('/insights'),
|
listInsights: () => req('/insights'),
|
||||||
latestInsights: () => req('/insights/latest'),
|
latestInsights: () => req('/insights/latest'),
|
||||||
deleteInsight: (id) => req(`/insights/${id}`, {method:'DELETE'}),
|
|
||||||
exportZip: async () => {
|
exportZip: async () => {
|
||||||
const res = await fetch(`${BASE}/export/zip`, {headers: hdrs()})
|
const res = await fetch(`${BASE}/export/zip`, {headers: hdrs()})
|
||||||
if (!res.ok) throw new Error('Export failed')
|
if (!res.ok) throw new Error('Export failed')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user