feat(FE): Money Raise Hub — template-uri, standarde, protocoale, roadmap-uri, legal, metrici
This commit is contained in:
parent
e84ac46de8
commit
a489e614c5
1 changed files with 211 additions and 0 deletions
211
src/app/dashboard/money-raise/page.tsx
Normal file
211
src/app/dashboard/money-raise/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue