grants/src/components/StudioSection.tsx
gdegelas a05331128b Atlas Green Morocco — grant strategy platform
- Full grant strategy framework for renewable energy & green hydrogen
- AI-powered grant studio, partner outreach, financial modeling
- Umami analytics with data-performance tracking
- Live Degelas metrics connected to solar.degelas.be
- Trilingual (EN/FR/AR) with i18n support
- Dockerized with Nginx frontend + Express API proxy
2026-06-01 09:44:03 +00:00

967 lines
51 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from "react";
import { Section } from "./Section";
import { useI18n } from "../i18n";
import { cn } from "../utils/cn";
import { useEffect } from "react";
import { trackEvent, trackAI, trackExport } from "../lib/analytics";
import { ai } from "../lib/ai";
import { vault } from "../lib/vault";
import { profileStore, projectStore } from "../lib/profile";
import { grantPrograms } from "../data/grants";
import { zoneDetails } from "../data/zones";
import type {
GenerateStudioResponse,
GenerateOutreachResponse,
GenerateFinancialResponse,
GenerateReviewResponse,
} from "../types/ai";
import { StudioLoader, CopyButton, Block, downloadText } from "./ai/studioShared";
type TabId = "grant" | "outreach" | "financial" | "review";
function SavedBadge({ show, label }: { show: boolean; label: string }) {
if (!show) return null;
return (
<span className="inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-semibold text-emerald-700 border border-emerald-200">
{label}
</span>
);
}
export function StudioSection() {
const { t, locale } = useI18n();
const [tab, setTab] = useState<TabId>("grant");
const [lastDraftForAudit, setLastDraftForAudit] = useState("");
const [lastDraftGrantId, setLastDraftGrantId] = useState("");
// Re-render when active project changes so the profile banner refreshes
const [activeProject, setActiveProject] = useState(() => projectStore.getActive());
useEffect(() => {
const handler = () => setActiveProject(projectStore.getActive());
window.addEventListener("atlasgreen:project-changed", handler);
window.addEventListener("atlasgreen:profile-updated", handler);
return () => {
window.removeEventListener("atlasgreen:project-changed", handler);
window.removeEventListener("atlasgreen:profile-updated", handler);
};
}, []);
const tabDefs: { id: TabId; label: string; desc: string; icon: string }[] = [
{ id: "grant", label: t.studio.tabGrant, desc: t.studio.tabGrantDesc, icon: "📝" },
{ id: "review", label: t.studio.tabReview, desc: t.studio.tabReviewDesc, icon: "🔍" },
{ id: "outreach", label: t.studio.tabOutreach, desc: t.studio.tabOutreachDesc, icon: "🤝" },
{ id: "financial", label: t.studio.tabFinancial, desc: t.studio.tabFinancialDesc, icon: "📊" },
];
const vaultCount = vault.countForProject(activeProject.id);
const switchToAudit = (draftText: string, grantId: string) => {
setLastDraftForAudit(draftText);
setLastDraftGrantId(grantId);
setTab("review");
};
return (
<Section id="studio" eyebrow={t.studio.eyebrow} title={t.studio.title} intro={t.studio.intro}>
{/* Dashboard metrics strip */}
<div className="mb-6 flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-start flex-1 min-w-[200px]">
<span className="text-xl">{activeProject.emoji}</span>
<div className="min-w-0 flex-1">
<span className="block text-xs font-semibold text-emerald-700">{t.studio.usingProfile}</span>
<span className="block truncate text-xs font-bold text-emerald-900">{activeProject.name}</span>
<span className="block truncate text-[10px] text-emerald-600">{activeProject.profile.tagline || activeProject.profile.product?.slice(0,40)}</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="rounded-2xl border border-slate-200 bg-white px-4 py-3 text-center">
<span className="text-xl font-bold text-emerald-600">{tabDefs.length}</span>
<span className="ml-1.5 text-xs text-slate-500">{t.studio.toolsAvailable}</span>
</div>
<div className="rounded-2xl border border-slate-200 bg-white px-4 py-3 text-center">
<span className="text-xl font-bold text-emerald-600">{vaultCount}</span>
<span className="ml-1.5 text-xs text-slate-500">{t.studio.docsGenerated}</span>
</div>
<div className="rounded-2xl border border-slate-200 bg-white px-4 py-3 text-center">
<span className="text-xl">{locale === "darija" ? "🇲🇦" : locale === "fr" ? "🇫🇷" : "🇬🇧"}</span>
</div>
</div>
</div>
{/* The Pipeline Visual */}
<div className="mb-8 hidden items-center justify-between rounded-full border border-slate-200 bg-white px-8 py-3 shadow-sm md:flex">
<div className={cn("flex items-center gap-2 text-xs font-bold transition-colors", tab === "financial" ? "text-emerald-600" : "text-slate-400")}>
<span className="text-lg">📊</span> {t.studio.pipe1}
</div>
<span className="text-slate-300"></span>
<div className={cn("flex items-center gap-2 text-xs font-bold transition-colors", tab === "grant" ? "text-emerald-600" : "text-slate-400")}>
<span className="text-lg">📝</span> {t.studio.pipe2}
</div>
<span className="text-slate-300"></span>
<div className={cn("flex items-center gap-2 text-xs font-bold transition-colors", tab === "review" ? "text-emerald-600" : "text-slate-400")}>
<span className="text-lg">🔍</span> {t.studio.pipe3}
</div>
<span className="text-slate-300"></span>
<div className={cn("flex items-center gap-2 text-xs font-bold transition-colors", tab === "outreach" ? "text-emerald-600" : "text-slate-400")}>
<span className="text-lg">🤝</span> {t.studio.pipe4}
</div>
</div>
{/* Tab cards with descriptions */}
<div className="mb-8 grid grid-cols-2 gap-3 sm:grid-cols-4">
{tabDefs.map((tb) => (
<button
key={tb.id}
id={`studio-tab-${tb.id}`}
onClick={() => { setTab(tb.id); trackEvent("studio_tab", { tab: tb.id }); }}
className={cn(
"flex flex-col items-center gap-1.5 rounded-2xl border p-4 text-center transition",
tab === tb.id
? "border-emerald-400 bg-emerald-950 text-white shadow-lg ring-1 ring-emerald-400"
: "border-slate-200 bg-white text-slate-700 hover:border-emerald-200 hover:bg-emerald-50/30"
)}
>
<span className="text-2xl">{tb.icon}</span>
<span className="text-sm font-bold">{tb.label}</span>
<span className={cn(
"text-[10px] leading-tight",
tab === tb.id ? "text-emerald-200/80" : "text-slate-400"
)}>
{tb.desc}
</span>
</button>
))}
</div>
{tab === "grant" && <GrantStudioTab locale={locale} onAudit={switchToAudit} />}
{tab === "review" && <ReviewerTab locale={locale} prefillDraft={lastDraftForAudit} prefillGrantId={lastDraftGrantId} />}
{tab === "outreach" && <OutreachTab locale={locale} />}
{tab === "financial" && <FinancialTab locale={locale} />}
</Section>
);
}
// ── Tab 1: Grant Studio ───────────────────────────────────────────────────────
function GrantStudioTab({ locale, onAudit }: { locale: "en" | "darija" | "fr"; onAudit?: (draftText: string, grantId: string) => void }) {
const { t } = useI18n();
const [grantId, setGrantId] = useState(grantPrograms[0].id);
const [status, setStatus] = useState<"idle" | "loading" | "done" | "error">("idle");
const [result, setResult] = useState<GenerateStudioResponse | null>(null);
const [error, setError] = useState("");
const [parentDocId, setParentDocId] = useState<string | null>(null); // tracks version lineage
const [projectTitle, setProjectTitle] = useState("");
const [fundingAmount, setFundingAmount] = useState("");
const [zone, setZone] = useState("");
const [duration, setDuration] = useState("24 months");
const [tone, setTone] = useState<"technical" | "commercial" | "impact">("technical");
const [trlCurrent, setTrlCurrent] = useState("TRL 6");
const [trlTarget, setTrlTarget] = useState("TRL 8");
const grant = grantPrograms.find((g) => g.id === grantId) || grantPrograms[0];
const run = async () => {
setStatus("loading"); setError(""); trackAI("generate-grant-studio", grant.name);
try {
const data = await ai.draftGrantStudio({
grantId, grantName: grant.name, founder: profileStore.getActive(), locale,
config: { projectTitle, fundingAmount, zone, duration, tone, trlCurrent, trlTarget },
});
setResult(data); setStatus("done");
const saveParentId = status === "done" ? parentDocId : undefined;
const docId = vault.save({ type: "grant_studio", title: projectTitle || grant.name, subtitle: "Grant Application", locale: locale as "en"|"darija"|"fr", projectId: projectStore.getActiveId(), data, parentId: saveParentId ?? undefined });
if (!parentDocId) setParentDocId(docId);
} catch (e) {
setError(e instanceof Error ? e.message : "error"); setStatus("error");
}
};
const exportAll = () => {
if (!result) return;
const txt = [
`# ${result.programName}`,
`\n## ${t.studio.execSummary}\n${result.executiveSummary}`,
`\n## ${t.studio.problem}\n${result.problemStatement}`,
`\n## ${t.studio.solution}\n${result.solutionDescription}`,
`\n## ${t.studio.innovation}\n${result.innovationStatement}`,
`\n## ${t.studio.technical}\n${result.technicalApproach}`,
`\n## ${t.studio.workPackages}\n${result.workPackages.map((w) => `- ${w.name} (${w.months}): ${w.focus}${w.deliverable}`).join("\n")}`,
`\n## ${t.studio.consortium}\n${result.consortium.map((c) => `- ${c.partner}${c.role} (${c.country})`).join("\n")}`,
`\n## ${t.studio.budget}\n${result.budgetBreakdown.map((b) => `- ${b.category} (${b.share}): ${b.justification}`).join("\n")}`,
`\n## ${t.studio.impact}\n${result.impactKpis.map((k) => `- ${k.metric}: ${k.target} (${k.timeframe})`).join("\n")}`,
`\n## ${t.studio.risks}\n${result.risks.map((r) => `- ${r.risk}${r.mitigation}`).join("\n")}`,
`\n## ${t.studio.timeline}\n${result.timeline.map((m) => `- ${m.month}: ${m.milestone}`).join("\n")}`,
].join("\n");
downloadText(`grant-application-${grant.id}.md`, txt);
};
const inputCls = "w-full rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100";
return (
<div>
{/* Project configuration panel */}
<div className="mb-6 rounded-2xl border border-slate-200 bg-slate-50/60 p-5 text-start">
<h4 className="mb-4 text-xs font-bold uppercase tracking-wider text-slate-500"> {t.studio.cfgTitle}</h4>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.selectGrant}</label>
<select value={grantId} onChange={(e) => setGrantId(e.target.value)} className={cn(inputCls, "cursor-pointer")}>
{grantPrograms.map((g) => (
<option key={g.id} value={g.id}>{g.name}</option>
))}
</select>
</div>
<div>
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.cfgProjectTitle}</label>
<input value={projectTitle} onChange={(e) => setProjectTitle(e.target.value)} placeholder="Noor RWA forecasting pilot" className={inputCls} />
</div>
<div>
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.cfgFunding}</label>
<input value={fundingAmount} onChange={(e) => setFundingAmount(e.target.value)} placeholder="€300,000" className={inputCls} />
</div>
<div>
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.cfgZone}</label>
<select value={zone} onChange={(e) => setZone(e.target.value)} className={cn(inputCls, "cursor-pointer")}>
<option value="">{t.studio.cfgZoneAny}</option>
{zoneDetails.map((z) => (
<option key={z.id} value={z.name}>{z.name}</option>
))}
</select>
</div>
<div>
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.cfgDuration}</label>
<select value={duration} onChange={(e) => setDuration(e.target.value)} className={cn(inputCls, "cursor-pointer")}>
<option value="12 months">12 months</option>
<option value="18 months">18 months</option>
<option value="24 months">24 months</option>
<option value="36 months">36 months</option>
</select>
</div>
<div>
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.cfgTone}</label>
<select value={tone} onChange={(e) => setTone(e.target.value as typeof tone)} className={cn(inputCls, "cursor-pointer")}>
<option value="technical">{t.studio.cfgToneTechnical}</option>
<option value="commercial">{t.studio.cfgToneCommercial}</option>
<option value="impact">{t.studio.cfgToneImpact}</option>
</select>
</div>
<div>
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.cfgTrlCurrent}</label>
<select value={trlCurrent} onChange={(e) => setTrlCurrent(e.target.value)} className={cn(inputCls, "cursor-pointer")}>
{["TRL 3", "TRL 4", "TRL 5", "TRL 6", "TRL 7", "TRL 8"].map((x) => <option key={x} value={x}>{x}</option>)}
</select>
</div>
<div>
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.cfgTrlTarget}</label>
<select value={trlTarget} onChange={(e) => setTrlTarget(e.target.value)} className={cn(inputCls, "cursor-pointer")}>
{["TRL 5", "TRL 6", "TRL 7", "TRL 8", "TRL 9"].map((x) => <option key={x} value={x}>{x}</option>)}
</select>
</div>
</div>
</div>
<div className="mb-6 flex flex-wrap items-center gap-4">
<button
onClick={run}
disabled={status === "loading"}
className="rounded-full bg-emerald-500 px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-emerald-500/25 transition hover:bg-emerald-400 disabled:opacity-50"
>
{status === "loading" ? t.studio.generating : status === "done" ? t.studio.regenerate : `${t.studio.generate}`}
</button>
{result && (
<button onClick={() => { exportAll(); trackExport("grant-studio-md"); }} className="rounded-full border border-emerald-300 bg-emerald-50 px-4 py-2.5 text-sm font-semibold text-emerald-700 transition hover:bg-emerald-100">
{t.studio.download}
</button>
)}
<SavedBadge show={status === "done"} label={t.vault.autoSaved} />
</div>
{/* Audit CTA — appears after a draft is generated */}
{status === "done" && result && onAudit && (
<div className="mb-6 flex items-center justify-between gap-3 rounded-2xl border-2 border-amber-300 bg-amber-50 px-5 py-3">
<p className="text-sm text-amber-900">
<strong>{result.programName}</strong> {t.studio.tabReviewDesc}
</p>
<button
onClick={() => {
const txt = [
result.executiveSummary,
result.problemStatement,
result.solutionDescription,
result.innovationStatement,
result.technicalApproach,
].join("\n\n");
onAudit(txt, grantId);
}}
className="shrink-0 rounded-full bg-amber-400 px-5 py-2 text-sm font-bold text-amber-950 transition hover:bg-amber-300"
>
{t.studio.auditThisDraft}
</button>
</div>
)}
{status === "loading" && <StudioLoader label={t.studio.generating} />}
{status === "error" && <p className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">{error}</p>}
{status === "done" && result && (
<div className="space-y-4">
<Block title={t.studio.execSummary} accent><p className="text-sm leading-relaxed text-emerald-900">{result.executiveSummary}</p></Block>
<div className="grid gap-4 md:grid-cols-2">
<Block title={t.studio.problem}><p className="text-sm leading-relaxed text-slate-700">{result.problemStatement}</p></Block>
<Block title={t.studio.solution}><p className="text-sm leading-relaxed text-slate-700">{result.solutionDescription}</p></Block>
<Block title={t.studio.innovation}><p className="text-sm leading-relaxed text-slate-700">{result.innovationStatement}</p></Block>
<Block title={t.studio.technical}><p className="text-sm leading-relaxed text-slate-700">{result.technicalApproach}</p></Block>
</div>
<Block title={t.studio.workPackages}>
<div className="space-y-2">
{result.workPackages.map((w, i) => (
<div key={i} className="rounded-xl border border-slate-100 bg-white p-3">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-semibold text-emerald-950">{w.name}</span>
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-bold text-emerald-700">{w.months}</span>
</div>
<p className="mt-1 text-xs text-slate-600">{w.focus}</p>
<p className="mt-1 text-xs text-slate-500"> {w.deliverable}</p>
</div>
))}
</div>
</Block>
<div className="grid gap-4 md:grid-cols-2">
<Block title={t.studio.consortium}>
<ul className="space-y-1.5">
{result.consortium.map((c, i) => (
<li key={i} className="text-sm text-slate-700"><strong>{c.partner}</strong> {c.role} <span className="text-slate-400">({c.country})</span></li>
))}
</ul>
</Block>
<Block title={t.studio.budget}>
<ul className="space-y-1.5">
{result.budgetBreakdown.map((b, i) => (
<li key={i} className="flex items-start justify-between gap-2 text-sm text-slate-700">
<span>{b.category} <span className="text-slate-400"> {b.justification}</span></span>
<span className="shrink-0 font-bold text-emerald-600">{b.share}</span>
</li>
))}
</ul>
</Block>
</div>
<Block title={t.studio.impact}>
<div className="grid gap-2 sm:grid-cols-2">
{result.impactKpis.map((k, i) => (
<div key={i} className="rounded-xl border border-teal-100 bg-teal-50/50 p-3 text-sm">
<span className="font-semibold text-teal-900">{k.metric}</span>
<span className="block text-xs text-teal-700">{k.target} · {k.timeframe}</span>
</div>
))}
</div>
</Block>
<div className="grid gap-4 md:grid-cols-2">
<Block title={t.studio.risks}>
<ul className="space-y-2">
{result.risks.map((r, i) => (
<li key={i} className="text-sm text-slate-700">🔸 {r.risk}<span className="block text-xs text-emerald-700"> {r.mitigation}</span></li>
))}
</ul>
</Block>
<Block title={t.studio.compliance}>
<ul className="space-y-1.5">
{result.complianceChecklist.map((c, i) => (
<li key={i} className="flex items-center justify-between gap-2 text-sm text-slate-700">
{c.requirement}
<span className={cn("shrink-0 text-xs font-bold", c.met === "yes" ? "text-emerald-600" : c.met === "partial" ? "text-amber-600" : "text-rose-600")}>
{c.met === "yes" ? "✓" : c.met === "partial" ? "~" : "✗"}
</span>
</li>
))}
</ul>
</Block>
</div>
<Block title={t.studio.timeline}>
<div className="flex flex-wrap gap-2">
{result.timeline.map((m, i) => (
<span key={i} className="rounded-full bg-slate-100 px-3 py-1 text-xs text-slate-600"><strong>{m.month}</strong> · {m.milestone}</span>
))}
</div>
</Block>
</div>
)}
</div>
);
}
// ── Tab 2: Partner Outreach ───────────────────────────────────────────────────
const PARTNER_PRESETS = ["UM6P", "IRESEN", "OCP Group", "Masen / ONEE", "EBRD Partner Bank", "EU Utility", "Solar IPP"];
function OutreachTab({ locale }: { locale: "en" | "darija" | "fr" }) {
const { t } = useI18n();
const [partnerName, setPartnerName] = useState(PARTNER_PRESETS[0]);
const [partnerType, setPartnerType] = useState("Academic / Research");
const [ask, setAsk] = useState("Explore an R&D + asset-validation partnership for our RWA layer.");
const [channel, setChannel] = useState<"cold_email" | "warm_intro" | "conference" | "linkedin">("cold_email");
const [tone, setTone] = useState<"formal" | "friendly" | "executive">("formal");
const [outputLanguage, setOutputLanguage] = useState<"en" | "fr" | "darija">("en");
const [status, setStatus] = useState<"idle" | "loading" | "done" | "error">("idle");
const [result, setResult] = useState<GenerateOutreachResponse | null>(null);
const [error, setError] = useState("");
const [parentDocId, setParentDocId] = useState<string | null>(null);
const run = async () => {
setStatus("loading"); setError(""); trackAI("generate-outreach", partnerName);
try {
const data = await ai.generateOutreach({
partnerName, partnerType, ask, founder: profileStore.getActive(),
config: { channel, tone, outputLanguage },
locale,
});
setResult(data); setStatus("done");
const saveParentId = status === "done" ? parentDocId : undefined;
const docId = vault.save({ type: "partner_outreach", title: `${partnerName} Outreach`, subtitle: partnerType, locale: locale as "en"|"darija"|"fr", projectId: projectStore.getActiveId(), data, parentId: saveParentId ?? undefined });
if (!parentDocId) setParentDocId(docId);
} catch (e) {
setError(e instanceof Error ? e.message : "error"); setStatus("error");
}
};
const inputCls = "w-full rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100";
return (
<div>
<div className="mb-6 grid gap-4 sm:grid-cols-3 lg:grid-cols-6">
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.partner}</label>
<input list="partner-presets" value={partnerName} onChange={(e) => setPartnerName(e.target.value)} className={inputCls} />
<datalist id="partner-presets">{PARTNER_PRESETS.map((p) => <option key={p} value={p} />)}</datalist>
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.partnerType}</label>
<input value={partnerType} onChange={(e) => setPartnerType(e.target.value)} className={inputCls} />
</div>
<div className="text-start sm:col-span-2">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.ask}</label>
<input value={ask} onChange={(e) => setAsk(e.target.value)} className={inputCls} />
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.outreachChannel}</label>
<select value={channel} onChange={(e) => setChannel(e.target.value as typeof channel)} className={cn(inputCls, "cursor-pointer")}>
<option value="cold_email">{t.studio.outreachChannelCold}</option>
<option value="warm_intro">{t.studio.outreachChannelWarm}</option>
<option value="conference">{t.studio.outreachChannelConf}</option>
<option value="linkedin">{t.studio.outreachChannelLinked}</option>
</select>
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.outreachTone}</label>
<select value={tone} onChange={(e) => setTone(e.target.value as typeof tone)} className={cn(inputCls, "cursor-pointer")}>
<option value="formal">{t.studio.outreachToneFormal}</option>
<option value="friendly">{t.studio.outreachToneFriendly}</option>
<option value="executive">{t.studio.outreachToneExec}</option>
</select>
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.outreachLang}</label>
<select value={outputLanguage} onChange={(e) => setOutputLanguage(e.target.value as typeof outputLanguage)} className={cn(inputCls, "cursor-pointer")}>
<option value="en">{t.studio.outreachLangEn}</option>
<option value="fr">{t.studio.outreachLangFr}</option>
<option value="darija">{t.studio.outreachLangDarija}</option>
</select>
</div>
</div>
<button onClick={run} disabled={status === "loading"} className="mb-6 rounded-full bg-emerald-500 px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-emerald-500/25 transition hover:bg-emerald-400 disabled:opacity-50">
{status === "loading" ? t.studio.generating : status === "done" ? t.studio.regenerate : `${t.studio.generate}`}
</button>
{status === "loading" && <StudioLoader label={t.studio.generating} />}
{status === "error" && <p className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">{error}</p>}
{status === "done" && result && (
<div className="space-y-4">
<Block title={t.studio.subject}>
<div className="flex items-start justify-between gap-3">
<p className="text-sm font-semibold text-emerald-900">{result.subject}</p>
<CopyButton text={result.subject} copyLabel={t.studio.copy} copiedLabel={t.studio.copied} />
</div>
</Block>
<Block title={t.studio.email} accent>
<div className="flex justify-end"><CopyButton text={result.emailBody} copyLabel={t.studio.copy} copiedLabel={t.studio.copied} /></div>
<p className="mt-2 whitespace-pre-line text-sm leading-relaxed text-emerald-900">{result.emailBody}</p>
</Block>
<div className="grid gap-4 md:grid-cols-2">
<Block title={t.studio.linkedin}>
<div className="flex justify-end"><CopyButton text={result.linkedinMessage} copyLabel={t.studio.copy} copiedLabel={t.studio.copied} /></div>
<p className="mt-2 text-sm leading-relaxed text-slate-700">{result.linkedinMessage}</p>
</Block>
<Block title={t.studio.followUp}>
<div className="flex justify-end"><CopyButton text={result.followUpEmail} copyLabel={t.studio.copy} copiedLabel={t.studio.copied} /></div>
<p className="mt-2 text-sm leading-relaxed text-slate-700">{result.followUpEmail}</p>
</Block>
</div>
<div className="grid gap-4 md:grid-cols-2">
<Block title={t.studio.agenda}>
<ul className="space-y-1.5">{result.meetingAgenda.map((a, i) => <li key={i} className="flex gap-2 text-sm text-slate-700"><span className="text-emerald-500">{i + 1}.</span>{a}</li>)}</ul>
</Block>
<Block title={t.studio.talking}>
<ul className="space-y-1.5">{result.talkingPoints.map((p, i) => <li key={i} className="flex items-start gap-2 text-sm text-slate-700"><span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-emerald-400" />{p}</li>)}</ul>
</Block>
</div>
<Block title={t.studio.mou}>
<div className="space-y-2">
{result.mouOutline.map((m, i) => (
<div key={i} className="rounded-xl border border-slate-100 bg-white p-3">
<span className="text-sm font-semibold text-emerald-950">{m.section}</span>
<p className="mt-1 text-xs text-slate-600">{m.content}</p>
</div>
))}
</div>
</Block>
</div>
)}
</div>
);
}
// ── Tab 3: Financial Model ────────────────────────────────────────────────────
function FinancialTab({ locale }: { locale: "en" | "darija" | "fr" }) {
const { t } = useI18n();
const [businessModel, setBusinessModel] = useState("Real-World Assets (solar + storage) backed by forecasting");
const [market, setMarket] = useState("Morocco + Spain");
const [pricing, setPricing] = useState("Asset co-ownership + SaaS + service fees");
const [scenario, setScenario] = useState<"conservative" | "base" | "aggressive">("base");
const [initialCapital, setInitialCapital] = useState("");
const [teamSize, setTeamSize] = useState("");
const [burnRate, setBurnRate] = useState("");
const [currency, setCurrency] = useState<"EUR" | "MAD" | "USD">("EUR");
const [status, setStatus] = useState<"idle" | "loading" | "done" | "error">("idle");
const [result, setResult] = useState<GenerateFinancialResponse | null>(null);
const [error, setError] = useState("");
const [parentDocId, setParentDocId] = useState<string | null>(null);
const run = async () => {
setStatus("loading"); setError(""); trackAI("generate-financial", market);
try {
const data = await ai.generateFinancialModel({
businessModel, market, pricingModel: pricing,
grantStack: ["marocpme-tatweer", "ebrd-geff", "horizon-europe", "afdb-sefa"],
founder: profileStore.getActive(),
config: { scenario, initialCapital, teamSize, burnRate, currency },
locale,
});
setResult(data); setStatus("done");
const saveParentId = status === "done" ? parentDocId : undefined;
const docId = vault.save({ type: "financial_model", title: `${market} Financial Model`, subtitle: businessModel, locale: locale as "en"|"darija"|"fr", projectId: projectStore.getActiveId(), data, parentId: saveParentId ?? undefined });
if (!parentDocId) setParentDocId(docId);
} catch (e) {
setError(e instanceof Error ? e.message : "error"); setStatus("error");
}
};
const inputCls = "w-full rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100";
return (
<div>
<div className="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-7">
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.finBusinessModel}</label>
<input value={businessModel} onChange={(e) => setBusinessModel(e.target.value)} className={inputCls} />
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.finMarket}</label>
<input value={market} onChange={(e) => setMarket(e.target.value)} className={inputCls} />
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.finPricing}</label>
<input value={pricing} onChange={(e) => setPricing(e.target.value)} className={inputCls} />
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.finScenario}</label>
<select value={scenario} onChange={(e) => setScenario(e.target.value as typeof scenario)} className={cn(inputCls, "cursor-pointer")}>
<option value="conservative">{t.studio.finScenarioConservative}</option>
<option value="base">{t.studio.finScenarioBase}</option>
<option value="aggressive">{t.studio.finScenarioAggressive}</option>
</select>
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.finInitialCapital}</label>
<input value={initialCapital} onChange={(e) => setInitialCapital(e.target.value)} placeholder="€500,000" className={inputCls} />
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.finTeamSize}</label>
<input value={teamSize} onChange={(e) => setTeamSize(e.target.value)} placeholder="8" className={inputCls} />
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.finCurrency}</label>
<select value={currency} onChange={(e) => setCurrency(e.target.value as typeof currency)} className={cn(inputCls, "cursor-pointer")}>
<option value="EUR">EUR</option>
<option value="MAD">MAD</option>
<option value="USD">USD</option>
</select>
</div>
</div>
<div className="mb-6 grid gap-4 sm:grid-cols-2">
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.finBurnRate}</label>
<input value={burnRate} onChange={(e) => setBurnRate(e.target.value)} placeholder="€15,000/month" className={inputCls} />
</div>
</div>
<button onClick={run} disabled={status === "loading"} className="mb-6 rounded-full bg-emerald-500 px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-emerald-500/25 transition hover:bg-emerald-400 disabled:opacity-50">
{status === "loading" ? t.studio.generating : status === "done" ? t.studio.regenerate : `${t.studio.generate}`}
</button>
{status === "loading" && <StudioLoader label={t.studio.generating} />}
{status === "error" && <p className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">{error}</p>}
{status === "done" && result && (
<div className="space-y-4">
<Block title={t.studio.summary} accent><p className="text-sm leading-relaxed text-emerald-900">{result.summary}</p></Block>
{/* Revenue table */}
<Block title={t.studio.revenue}>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200 text-start text-xs text-slate-400">
<th className="py-2 text-start font-semibold">{t.studio.year}</th>
<th className="py-2 text-start font-semibold">{t.studio.customers}</th>
<th className="py-2 text-start font-semibold">{t.studio.arr}</th>
<th className="py-2 text-start font-semibold">{t.studio.revenueCol}</th>
</tr>
</thead>
<tbody>
{result.revenueProjection.map((r, i) => (
<tr key={i} className="border-b border-slate-50">
<td className="py-2 font-semibold text-emerald-950">{r.year}</td>
<td className="py-2 text-slate-600">{r.customers}</td>
<td className="py-2 text-slate-600">{r.arr}</td>
<td className="py-2 font-semibold text-emerald-600">{r.revenue}</td>
</tr>
))}
</tbody>
</table>
</div>
</Block>
<div className="grid gap-4 md:grid-cols-2">
<Block title={t.studio.assumptions}>
<ul className="space-y-1.5">
{result.assumptions.map((a, i) => (
<li key={i} className="flex items-start justify-between gap-2 text-sm text-slate-700">
<span>{a.label}</span><span className="shrink-0 font-semibold text-emerald-700">{a.value}</span>
</li>
))}
</ul>
</Block>
<Block title={t.studio.metrics}>
<div className="grid grid-cols-2 gap-2">
{result.keyMetrics.map((m, i) => (
<div key={i} className="rounded-xl border border-slate-100 bg-white p-3 text-center">
<div className="text-sm font-bold text-emerald-600">{m.value}</div>
<div className="text-[10px] text-slate-500">{m.metric}</div>
</div>
))}
</div>
</Block>
</div>
{/* Cost structure */}
<Block title={t.studio.costs}>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200 text-xs text-slate-400">
<th className="py-2 text-start font-semibold">{t.studio.category}</th>
<th className="py-2 text-start font-semibold">Y1</th>
<th className="py-2 text-start font-semibold">Y2</th>
<th className="py-2 text-start font-semibold">Y3</th>
</tr>
</thead>
<tbody>
{result.costStructure.map((c, i) => (
<tr key={i} className="border-b border-slate-50">
<td className="py-2 font-medium text-slate-700">{c.category}</td>
<td className="py-2 text-slate-600">{c.y1}</td>
<td className="py-2 text-slate-600">{c.y2}</td>
<td className="py-2 text-slate-600">{c.y3}</td>
</tr>
))}
</tbody>
</table>
</div>
</Block>
<Block title={t.studio.funding}>
<div className="flex flex-wrap gap-2">
{result.fundingStack.map((f, i) => (
<div key={i} className="rounded-xl border border-emerald-100 bg-emerald-50 px-3 py-2 text-xs">
<span className="font-semibold text-emerald-800">{f.source}</span>
<span className="mx-1 text-emerald-600">·</span>
<span className="text-emerald-700">{f.amount}</span>
<span className="block text-[10px] text-emerald-500">{f.stage}</span>
</div>
))}
</div>
</Block>
<Block title={t.studio.milestones}>
<ol className="relative space-y-2 border-s-2 border-emerald-200 ps-5">
{result.milestones.map((m, i) => (
<li key={i} className="text-sm">
<span className="font-bold text-emerald-700">{m.month}</span>
<span className="mx-1"></span>{m.milestone}
<span className="ms-2 rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-bold text-emerald-700">{m.arrTarget}</span>
</li>
))}
</ol>
</Block>
{/* Post-generation CTA */}
<div className="mt-8 flex items-center justify-between gap-3 rounded-2xl border-2 border-emerald-300 bg-emerald-50 px-5 py-4">
<p className="text-sm text-emerald-900 text-start">
{t.studio.nextDraftGrant}
</p>
<button
onClick={() => {
const el = document.getElementById("studio-tab-grant");
if (el) el.click();
trackEvent("studio_next_step", { from: "financial", to: "grant" });
}}
className="shrink-0 rounded-full bg-emerald-500 px-5 py-2 text-sm font-bold text-white transition hover:bg-emerald-400"
>
{t.studio.btnDraftGrant}
</button>
</div>
</div>
)}
</div>
);
}
// ── Tab 4: Grant Reviewer (AI Critic) ─────────────────────────────────────────
function ReviewerTab({ locale, prefillDraft, prefillGrantId }: { locale: "en" | "darija" | "fr"; prefillDraft?: string; prefillGrantId?: string }) {
const { t } = useI18n();
const [grantId, setGrantId] = useState(prefillGrantId || grantPrograms[0].id);
const [draftContent, setDraftContent] = useState(prefillDraft || "");
const [strictness, setStrictness] = useState<"lenient" | "realistic" | "brutal">("realistic");
const [focusArea, setFocusArea] = useState<"whole_draft" | "innovation_only" | "budget_only" | "impact_only">("whole_draft");
const [status, setStatus] = useState<"idle" | "loading" | "done" | "error">("idle");
const [result, setResult] = useState<GenerateReviewResponse | null>(null);
const [error, setError] = useState("");
const grant = grantPrograms.find((g) => g.id === grantId) || grantPrograms[0];
const run = async () => {
if (!draftContent.trim()) return;
setStatus("loading"); setError(""); trackAI("review-grant", grant.name);
try {
const data = await ai.reviewGrantApplication({
grantId,
grantName: grant.name,
draftContent,
founder: profileStore.getActive(),
config: { strictness, focusArea },
locale,
});
setResult(data); setStatus("done");
} catch (e) {
setError(e instanceof Error ? e.message : "error"); setStatus("error");
}
};
const inputCls = "w-full rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100";
return (
<div>
<div className="mb-6 grid gap-4 sm:grid-cols-3">
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.reviewGrant}</label>
<select
value={grantId}
onChange={(e) => setGrantId(e.target.value)}
className={inputCls}
>
{grantPrograms.map((g) => (
<option key={g.id} value={g.id}>{g.name}</option>
))}
</select>
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.reviewStrictness}</label>
<select value={strictness} onChange={(e) => setStrictness(e.target.value as typeof strictness)} className={cn(inputCls, "cursor-pointer")}>
<option value="realistic">{t.studio.reviewStrictRealistic}</option>
<option value="brutal">{t.studio.reviewStrictBrutal}</option>
<option value="lenient">{t.studio.reviewStrictLenient}</option>
</select>
</div>
<div className="text-start">
<label className="mb-1.5 block text-xs font-semibold text-slate-700">{t.studio.reviewFocus}</label>
<select value={focusArea} onChange={(e) => setFocusArea(e.target.value as typeof focusArea)} className={cn(inputCls, "cursor-pointer")}>
<option value="whole_draft">{t.studio.reviewFocusWhole}</option>
<option value="innovation_only">{t.studio.reviewFocusInnovation}</option>
<option value="budget_only">{t.studio.reviewFocusBudget}</option>
<option value="impact_only">{t.studio.reviewFocusImpact}</option>
</select>
</div>
<div className="sm:col-span-3 text-start">
<div className="flex items-center justify-between mb-1.5">
<label className="block text-xs font-semibold text-slate-700">{t.studio.reviewPaste}</label>
<button
onClick={() => {
const docs = vault.getAll().filter(d => d.type === "grant_studio");
if(docs.length === 0) {
alert(t.studio.reviewLoadVaultEmpty);
return;
}
const doc = docs[0]; // load most recent
const rawData = doc.data as any;
const text = [
rawData.executiveSummary,
rawData.problemStatement,
rawData.solutionDescription,
rawData.innovationStatement,
rawData.technicalApproach,
].filter(Boolean).join("\\n\\n");
setDraftContent(text);
alert(`Loaded: ${doc.title}`);
}}
className="text-[10px] font-semibold text-emerald-600 hover:underline"
>
📥 {t.studio.reviewLoadVault}
</button>
</div>
<textarea
rows={4}
value={draftContent}
onChange={(e) => setDraftContent(e.target.value)}
placeholder="e.g. Degelas solar forecasting will expand to provide remote predictive analysis for the Noor solar complex..."
className={cn(inputCls, "resize-y")}
/>
<p className="mt-1 text-[11px] text-slate-400">{t.studio.reviewPasteHint}</p>
</div>
</div>
<button
onClick={run}
disabled={status === "loading" || !draftContent.trim()}
className="mb-6 rounded-full bg-emerald-500 px-6 py-2.5 text-sm font-semibold text-white shadow-lg shadow-emerald-500/25 transition hover:bg-emerald-400 disabled:opacity-50"
>
{status === "loading" ? t.studio.reviewing : status === "done" ? t.studio.regenerate : `${t.studio.reviewBtn}`}
</button>
{status === "loading" && <StudioLoader label={t.studio.reviewing} />}
{status === "error" && <p className="rounded-xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">{error}</p>}
{status === "done" && result && (
<div className="space-y-6">
{/* Header overall evaluation */}
<div className="flex flex-wrap items-center justify-between gap-4 rounded-3xl border border-slate-100 bg-white p-6 shadow-sm">
<div className="text-start">
<p className="text-xs text-slate-400">{t.studio.reviewOverall}</p>
<h3 className="text-xl font-bold text-emerald-950">{result.programName}</h3>
</div>
<div className="flex items-center gap-3">
<div className={cn(
"flex h-16 w-16 flex-col items-center justify-center rounded-2xl border-2 font-bold",
result.overallScore >= 80 ? "bg-emerald-50 border-emerald-400 text-emerald-700" :
result.overallScore >= 50 ? "bg-amber-50 border-amber-400 text-amber-700" :
"bg-rose-50 border-rose-400 text-rose-700"
)}>
<span className="text-2xl">{result.overallScore}</span>
<span className="text-[9px] uppercase font-semibold text-slate-400">/100</span>
</div>
</div>
</div>
<Block title={t.studio.reviewVerdict} accent>
<p className="text-sm leading-relaxed text-emerald-950 whitespace-pre-line">{result.summaryVerdict}</p>
</Block>
{result.topWeaknesses.length > 0 && (
<Block title={t.studio.reviewGaps}>
<ul className="space-y-2">
{result.topWeaknesses.map((w, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-rose-800">
<span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-rose-500" />
{w}
</li>
))}
</ul>
</Block>
)}
{/* Section-by-section audit */}
<div>
<h4 className="mb-3 text-xs font-bold uppercase tracking-wider text-slate-500 text-start">{t.studio.reviewSection}</h4>
<div className="space-y-4">
{result.sectionFeedback.map((sec, i) => (
<div key={i} className="rounded-2xl border border-slate-200 bg-white p-5 text-start shadow-sm">
<div className="flex items-center justify-between gap-3 border-b border-slate-100 pb-3">
<h5 className="font-bold text-emerald-950">{sec.sectionName}</h5>
<span className={cn(
"rounded-full px-2.5 py-0.5 text-xs font-bold",
sec.score >= 80 ? "bg-emerald-100 text-emerald-800" :
sec.score >= 50 ? "bg-amber-100 text-amber-800" :
"bg-rose-100 text-rose-800"
)}>
{sec.score}/100
</span>
</div>
<div className="mt-3 space-y-3">
<div>
<span className="block text-[10px] font-bold uppercase tracking-wider text-slate-400">{t.studio.reviewCritique}</span>
<p className="mt-0.5 text-xs text-slate-700 leading-relaxed">{sec.critique}</p>
</div>
{sec.criticalGaps.length > 0 && (
<div>
<span className="block text-[10px] font-bold uppercase tracking-wider text-rose-400">{t.studio.reviewEvidenceGaps}</span>
<ul className="mt-1 space-y-1">
{sec.criticalGaps.map((g, gi) => (
<li key={gi} className="text-xs text-rose-900 leading-relaxed">
{g}
</li>
))}
</ul>
</div>
)}
{sec.polishedRewrite && (
<div className="rounded-xl bg-emerald-50/70 p-3 border border-emerald-100">
<span className="block text-[10px] font-bold uppercase tracking-wider text-emerald-600"> {t.studio.reviewOptimal}</span>
<p className="mt-1 text-xs text-emerald-950 leading-relaxed italic">"{sec.polishedRewrite}"</p>
</div>
)}
</div>
</div>
))}
</div>
</div>
<Block title={t.studio.reviewStrategic}>
<p className="text-xs text-slate-700 leading-relaxed">{result.strategicAlignmentNote}</p>
</Block>
{/* Post-audit CTA */}
<div className="mt-8 flex items-center justify-between gap-3 rounded-2xl border-2 border-emerald-300 bg-emerald-50 px-5 py-4">
<p className="text-sm text-emerald-900 text-start">
{t.studio.nextSecurePartner}
</p>
<button
onClick={() => {
const el = document.getElementById("studio-tab-outreach");
if (el) el.click();
trackEvent("studio_next_step", { from: "review", to: "outreach" });
}}
className="shrink-0 rounded-full bg-emerald-500 px-5 py-2 text-sm font-bold text-white transition hover:bg-emerald-400"
>
{t.studio.btnSecurePartner}
</button>
</div>
</div>
)}
</div>
);
}