- 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
967 lines
51 KiB
TypeScript
967 lines
51 KiB
TypeScript
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>
|
||
);
|
||
}
|