feat(FE): Money Raise Hub — template-uri, standarde, protocoale, roadmap-uri, legal, metrici

This commit is contained in:
admin-valentin 2026-08-26 19:12:26 +00:00
parent e84ac46de8
commit a489e614c5

View file

@ -0,0 +1,211 @@
'use client';
import { useState } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { apiFetch } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
interface KbEntry {
title: string; category: string; subcategory: string;
region: string; stage: string; content: string; format: string; tags: string;
}
const CATEGORIES = [
{ key: 'all', label: 'Tot', icon: '🗂️' },
{ key: 'template', label: 'Template-uri', icon: '📋' },
{ key: 'standard', label: 'Standarde', icon: '📜' },
{ key: 'protocol', label: 'Protocoale', icon: '🔄' },
{ key: 'roadmap', label: 'Roadmap-uri', icon: '🗺️' },
{ key: 'investor', label: 'Investitori', icon: '🏦' },
{ key: 'legal', label: 'Legal', icon: '⚖️' },
{ key: 'metric', label: 'Metrici', icon: '📊' },
];
const STAGES = ['all','pre_seed','seed','series_a','series_b','growth'];
const REGIONS = ['all','global','eu','dach','uk','us','ro'];
export default function MoneyRaisePage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const [cat, setCat] = useState('all');
const [stage, setStage] = useState('all');
const [region, setRegion] = useState('all');
const [search, setSearch] = useState('');
const [selected, setSelected] = useState<KbEntry | null>(null);
const [aiQ, setAiQ] = useState('');
const [aiReply, setAiReply] = useState('');
// Query money_raise knowledge base via IntelligenceService
const kbQuery = useMutation({
mutationFn: (params: { queryKey: string; params: Record<string, string> }) =>
apiFetch<{ data: KbEntry[]; rows: number; error?: string }>('/v1/intelligence/query', {
tenantId, method: 'POST', body: params,
}),
});
// AI Ask about fundraising
const aiAsk = useMutation({
mutationFn: (question: string) =>
apiFetch<{ reply: string }>('/v1/ai/ask', {
tenantId, method: 'POST',
body: {
messages: [{ role: 'user', content: question }],
context: JSON.stringify({ context: 'money_raise_hub', tenantId }),
},
}),
onSuccess: (d) => setAiReply(d.reply),
});
// Load entries on first render
const { data: allEntries = [], isLoading } = useQuery({
queryKey: ['mr-kb', tenantId],
queryFn: () => apiFetch<{ data: KbEntry[] }>('/v1/intelligence/query', {
tenantId, method: 'POST',
body: { queryKey: 'money_raise_all' },
}).then(r => r.data ?? []),
enabled: Boolean(tenantId),
staleTime: 300_000,
});
const filtered = allEntries.filter(e => {
if (cat !== 'all' && e.category !== cat) return false;
if (stage !== 'all' && e.stage !== stage && e.stage !== 'all') return false;
if (region !== 'all' && e.region !== region && e.region !== 'global') return false;
if (search && !e.title.toLowerCase().includes(search.toLowerCase()) &&
!e.content.toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
function stageLabel(s: string) {
return s.replace('_', ' ').replace(/\b\w/g, c => c.toUpperCase());
}
return (
<div className="max-w-6xl space-y-6 p-6">
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Money Raise Hub</h1>
<p className="text-sm text-ink-faint mt-1">
Template-uri · Standarde · Protocoale · Roadmap-uri · Legal · Global
</p>
</div>
<div className="rounded-xl border bg-primary/5 border-primary/20 px-4 py-2 text-center">
<p className="text-2xl font-bold text-primary">{allEntries.length}</p>
<p className="text-[10px] text-ink-faint">resurse indexate</p>
</div>
</div>
{/* AI Assistant */}
<div className="card p-4 space-y-3">
<p className="text-xs font-semibold text-ink"> Întreabă AI-ul despre fundraising</p>
<div className="flex gap-2">
<input value={aiQ} onChange={e => setAiQ(e.target.value)}
onKeyDown={e => e.key === 'Enter' && aiQ.trim() && aiAsk.mutate(aiQ)}
placeholder="ex: Cum negociez lichidation preference? Ce documente trebuie pentru Series A?"
className="flex-1 rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
<button onClick={() => aiAsk.mutate(aiQ)} disabled={!aiQ.trim() || aiAsk.isPending}
className="rounded-lg bg-primary px-4 py-2 text-sm text-white disabled:opacity-50">
{aiAsk.isPending ? '…' : 'Ask'}
</button>
</div>
{aiReply && (
<div className="rounded-xl bg-muted/40 p-3">
<p className="text-xs text-ink whitespace-pre-line">{aiReply}</p>
<button onClick={() => setAiReply('')} className="mt-2 text-[10px] text-ink-faint hover:text-ink"> Închide</button>
</div>
)}
</div>
{/* Setup notice if no data */}
{allEntries.length === 0 && !isLoading && (
<div className="card p-5 border-amber-200 bg-amber-50 space-y-2">
<p className="text-sm font-semibold text-amber-800">ClickHouse: money_raise.knowledge_base nu există încă</p>
<p className="text-xs text-amber-700">Rulează pe supersrv:</p>
<code className="block text-xs font-mono bg-amber-100 p-2 rounded">
cd ~/; git clone [forgejo]/ceo-api && bash ceo-api/money-raise/ingest.sh
</code>
<p className="text-xs text-amber-700">
Apoi adaugă în IntelligenceService query-ul <code>money_raise_all</code>.
</p>
</div>
)}
{/* Filters */}
<div className="space-y-3">
<div className="flex flex-wrap gap-2">
{CATEGORIES.map(c => (
<button key={c.key} onClick={() => setCat(c.key)}
className={`rounded-full px-3 py-1 text-xs border flex items-center gap-1 ${cat === c.key ? 'bg-primary text-white border-primary' : 'bg-background text-ink-faint border-border hover:border-primary/40'}`}>
<span>{c.icon}</span> {c.label}
{c.key !== 'all' && <span className="ml-1 opacity-60">({allEntries.filter(e => e.category === c.key).length})</span>}
</button>
))}
</div>
<div className="flex flex-wrap gap-2">
{STAGES.map(s => (
<button key={s} onClick={() => setStage(s)}
className={`rounded-full px-2.5 py-0.5 text-[11px] border ${stage === s ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-background text-ink-faint border-border hover:border-indigo-300'}`}>
{s === 'all' ? 'Toate etapele' : stageLabel(s)}
</button>
))}
{REGIONS.map(r => (
<button key={r} onClick={() => setRegion(r)}
className={`rounded-full px-2.5 py-0.5 text-[11px] border ${region === r ? 'bg-emerald-600 text-white border-emerald-600' : 'bg-background text-ink-faint border-border hover:border-emerald-300'}`}>
{r === 'all' ? 'Global' : r.toUpperCase()}
</button>
))}
</div>
<input value={search} onChange={e => setSearch(e.target.value)}
placeholder="Caută în titlu sau conținut…"
className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
</div>
<div className="flex gap-6">
{/* List */}
<div className={`space-y-2 ${selected ? 'w-1/3' : 'w-full'}`}>
{isLoading ? (
<p className="text-sm text-ink-faint text-center py-8">Se încarcă</p>
) : filtered.length === 0 ? (
<p className="text-sm text-ink-faint text-center py-8">Niciun rezultat.</p>
) : filtered.map((e, i) => (
<button key={i} onClick={() => setSelected(e === selected ? null : e)}
className={`w-full text-left rounded-xl border p-3 transition-all hover:border-primary/40 ${selected === e ? 'border-primary bg-primary/5' : 'bg-background border-border'}`}>
<div className="flex items-start gap-2">
<span className="shrink-0 text-sm">
{e.category === 'template' ? '📋' : e.category === 'standard' ? '📜' : e.category === 'protocol' ? '🔄' : e.category === 'roadmap' ? '🗺️' : e.category === 'investor' ? '🏦' : e.category === 'legal' ? '⚖️' : '📊'}
</span>
<div className="flex-1 min-w-0">
<p className="text-xs font-semibold text-ink truncate">{e.title}</p>
<div className="flex gap-1.5 mt-0.5 flex-wrap">
<span className="rounded px-1.5 py-0.5 text-[10px] bg-muted text-ink-faint">{e.category}</span>
{e.stage !== 'all' && <span className="rounded px-1.5 py-0.5 text-[10px] bg-indigo-50 text-indigo-600">{stageLabel(e.stage)}</span>}
{e.region !== 'global' && <span className="rounded px-1.5 py-0.5 text-[10px] bg-emerald-50 text-emerald-700">{e.region.toUpperCase()}</span>}
</div>
</div>
</div>
</button>
))}
</div>
{/* Detail */}
{selected && (
<div className="flex-1 card p-5 overflow-y-auto max-h-[80vh] space-y-3">
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-bold text-ink">{selected.title}</p>
<button onClick={() => setSelected(null)} className="text-ink-faint hover:text-ink text-sm"></button>
</div>
<div className="flex flex-wrap gap-1.5">
<span className="rounded-full bg-primary/10 text-primary px-2 py-0.5 text-[10px]">{selected.category}</span>
<span className="rounded-full bg-indigo-50 text-indigo-600 px-2 py-0.5 text-[10px]">{selected.subcategory}</span>
<span className="rounded-full bg-emerald-50 text-emerald-700 px-2 py-0.5 text-[10px]">{selected.region}</span>
<span className="rounded-full bg-amber-50 text-amber-700 px-2 py-0.5 text-[10px]">{stageLabel(selected.stage)}</span>
</div>
<div className="prose prose-sm max-w-none text-ink whitespace-pre-line text-xs leading-relaxed">
{selected.content}
</div>
</div>
)}
</div>
</div>
);
}