diff --git a/src/app/dashboard/prompt-chains/page.tsx b/src/app/dashboard/prompt-chains/page.tsx new file mode 100644 index 0000000..4b34a3e --- /dev/null +++ b/src/app/dashboard/prompt-chains/page.tsx @@ -0,0 +1,281 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface ChainStep { + id: string; name: string; prompt: string; + useOutputOf?: string; // step id whose output to inject as {prev_output} + model?: string; +} + +interface Chain { + id?: string; name: string; description: string; + steps: ChainStep[]; tags: string[]; +} + +interface StepResult { stepId: string; name: string; output: string; elapsed: number; } + +const TEMPLATES: Chain[] = [ + { + name: 'Research → Summary → Action Plan', + description: 'Cercetare profundă → rezumat executiv → plan acționabil', + tags: ['research', 'strategy'], + steps: [ + { id: 's1', name: 'Research', prompt: 'Cercetează în detaliu: {topic}\nFurnizează fapte, date, surse și context.' }, + { id: 's2', name: 'Executive Summary', prompt: 'Rezumă în maxim 200 cuvinte pentru un CEO:\n\n{prev_output}', useOutputOf: 's1' }, + { id: 's3', name: 'Action Plan', prompt: 'Pe baza rezumatului următor, creează un plan de acțiune în 5 pași cu deadline-uri:\n\n{prev_output}', useOutputOf: 's2' }, + ], + }, + { + name: 'Pitch Deck Generator', + description: 'Din descriere companie → toate slide-urile pitch deck', + tags: ['fundraising', 'pitch'], + steps: [ + { id: 'p1', name: 'Problem & Solution', prompt: 'Pentru această companie: {company_description}\nGenerează Slide 1 (Problem) și Slide 2 (Solution) pentru pitch deck. Format: titlu + 3 bullet points per slide.' }, + { id: 'p2', name: 'Market & Business Model', prompt: 'Pe baza:\n{prev_output}\n\nGenerează Slide 3 (Market Size: TAM/SAM/SOM) și Slide 4 (Business Model). Include cifre estimate.', useOutputOf: 'p1' }, + { id: 'p3', name: 'Team & The Ask', prompt: 'Pe baza pitch deck-ului de până acum:\n{prev_output}\n\nGenerează Slide 5 (Team - placeholder) și Slide 6 (The Ask - ce și cât se ridică). Format consistent.', useOutputOf: 'p2' }, + ], + }, + { + name: 'Trade Analysis Chain', + description: 'Analiza unui produs: piață → concurență → oportunitate import/export', + tags: ['trade', 'analysis'], + steps: [ + { id: 't1', name: 'Product Market Analysis', prompt: 'Analizează piața globală pentru: {product}\nInclude: dimensiune piață, top țări producătoare/consumatoare, tendințe 2024-2026.' }, + { id: 't2', name: 'Competition & Trade Routes', prompt: 'Pe baza analizei:\n{prev_output}\n\nIdentifică: principalele rute comerciale, competitori cheie, bariere tarifare/non-tarifare, oportunități arbitraj.', useOutputOf: 't1' }, + { id: 't3', name: 'Opportunity Report', prompt: 'Pe baza analizei complete:\n{prev_output}\n\nGenerează un raport de oportunitate: recomandare go/no-go, piețe țintă, strategie intrare, risc estimat.', useOutputOf: 't2' }, + ], + }, +]; + +function uid() { return Math.random().toString(36).slice(2, 9); } + +export default function PromptChainPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + + const [chain, setChain] = useState({ name: 'Lanț nou', description: '', steps: [], tags: [] }); + const [variables, setVariables] = useState>({}); + const [results, setResults] = useState([]); + const [runningStep, setRunningStep] = useState(null); + const [showTemplates, setShowTemplates] = useState(true); + + // Save chain as observation + const saveMut = useMutation({ + mutationFn: () => apiFetch('/v1/observations', { + tenantId, method: 'POST', + body: { + metric: 'prompt-chain', + value: chain.name, + subjectType: 'chain', + unit: `${chain.steps.length} steps`, + confidence: 1, + source: JSON.stringify({ chain }), + }, + }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['chains', tenantId] }), + }); + + // Saved chains + const { data: savedObs = [] } = useQuery({ + queryKey: ['chains', tenantId], + queryFn: () => apiFetch('/v1/observations', { tenantId }), + select: (d: any[]) => d.filter(o => o.metric === 'prompt-chain'), + enabled: Boolean(tenantId), + }); + + // Run single step + const runStep = useCallback(async (step: ChainStep, allResults: StepResult[], vars: Record) => { + setRunningStep(step.id); + const t0 = Date.now(); + + let prompt = step.prompt; + // Inject variables + for (const [k, v] of Object.entries(vars)) { + prompt = prompt.replace(new RegExp(`\\{${k}\\}`, 'g'), v); + } + // Inject prev_output + if (step.useOutputOf) { + const prev = allResults.find(r => r.stepId === step.useOutputOf); + if (prev) prompt = prompt.replace(/\{prev_output\}/g, prev.output); + } + + try { + const r = await apiFetch<{ reply: string }>('/v1/ai/ask', { + tenantId, method: 'POST', + body: { + messages: [{ role: 'user', content: prompt }], + context: JSON.stringify({ chain: chain.name, step: step.name }), + }, + }); + return { stepId: step.id, name: step.name, output: r.reply, elapsed: Date.now() - t0 }; + } catch (err: any) { + return { stepId: step.id, name: step.name, output: `Error: ${err.message}`, elapsed: Date.now() - t0 }; + } finally { + setRunningStep(null); + } + }, [tenantId, chain.name]); + + async function runAll() { + setResults([]); + const accumulated: StepResult[] = []; + for (const step of chain.steps) { + const result = await runStep(step, accumulated, variables); + accumulated.push(result); + setResults([...accumulated]); + } + } + + function loadTemplate(t: Chain) { + setChain({ ...t, id: undefined }); + // Detect variables in steps + const vars: Record = {}; + for (const step of t.steps) { + const matches = step.prompt.matchAll(/\{([^}]+)\}/g); + for (const m of matches) { + if (!['prev_output'].includes(m[1])) vars[m[1]] = ''; + } + } + setVariables(vars); + setShowTemplates(false); + setResults([]); + } + + function addStep() { + setChain(c => ({ ...c, steps: [...c.steps, { id: uid(), name: `Step ${c.steps.length + 1}`, prompt: '' }] })); + } + + function updateStep(id: string, patch: Partial) { + setChain(c => ({ ...c, steps: c.steps.map(s => s.id === id ? { ...s, ...patch } : s) })); + } + + function removeStep(id: string) { + setChain(c => ({ ...c, steps: c.steps.filter(s => s.id !== id) })); + } + + const detectedVars = [...new Set(chain.steps.flatMap(s => [...s.prompt.matchAll(/\{([^}]+)\}/g)].map(m => m[1])))].filter(v => v !== 'prev_output'); + + return ( +
+
+
+

Prompt Chain Builder

+

Construiește și rulează lanțuri de prompts — output-ul unui step alimentează următorul

+
+ +
+ + {/* Templates */} + {showTemplates && ( +
+ {TEMPLATES.map(t => ( + + ))} +
+ )} + +
+ {/* Builder */} +
+
+ setChain(c => ({...c, name: e.target.value}))} + className="w-full rounded-lg border bg-background px-3 py-2 text-sm font-semibold text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ + {/* Variables */} + {detectedVars.length > 0 && ( +
+

Variabile detectate

+ {detectedVars.map(v => ( +
+ {`{${v}}`} + setVariables(vs => ({...vs, [v]: e.target.value}))} + placeholder={`Valoare pentru ${v}…`} + className="flex-1 rounded border bg-white px-2 py-1 text-xs text-ink focus:outline-none focus:ring-1 focus:ring-amber-400" /> +
+ ))} +
+ )} + + {/* Steps */} +
+ {chain.steps.map((step, i) => ( +
+
+ {i+1} + updateStep(step.id, {name: e.target.value})} + className="flex-1 rounded border bg-background px-2 py-1 text-xs font-medium text-ink focus:outline-none focus:ring-1 focus:ring-ring" /> + +
+