tahamajs/IE / Projects /front /src /components /grant /KnowledgeBasePanel.tsx
tahamajs's picture
download
raw
9.91 kB
// src/components/grant/KnowledgeBasePanel.tsx
import { useState } from "react";
import type { SuccessfulProposal } from "@/types/grant-assistant";
import { BookOpen, Award, ExternalLink } from "lucide-react";
interface KnowledgeBasePanelProps {
proposals: SuccessfulProposal[];
}
export function KnowledgeBasePanel({ proposals }: KnowledgeBasePanelProps) {
return (
<div className="p-4">
<h4 className="text-sm font-semibold flex items-center gap-2 mb-3">
<BookOpen className="w-4 h-4 text-primary" /> Similar Successful Proposals
</h4>
{proposals.length === 0 ? (
<p className="text-xs text-gray-400 py-8 text-center">
No similar proposals found. Upload an RFP to search the knowledge base.
</p>
) : (
<div className="space-y-3">
{proposals.map((p) => (
<div key={p.id} className="p-3 bg-gray-50 dark:bg-gray-700/50 rounded-xl border">
<p className="text-sm font-semibold">{p.title}</p>
<p className="text-xs text-gray-500 mt-1">{p.agency} · {p.year}</p>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1.5 line-clamp-3">{p.abstract}</p>
<div className="flex items-center gap-2 mt-2">
<Award className="w-3 h-3 text-amber-500" />
<span className="text-xs font-semibold">${p.amount.toLocaleString()}</span>
</div>
</div>
))}
</div>
)}
</div>
);
}
// src/components/grant/BudgetCalculator.tsx
import type { BudgetEstimate } from "@/types/grant-assistant";
import { DollarSign } from "lucide-react";
interface BudgetCalculatorProps {
estimate: BudgetEstimate;
}
export function BudgetCalculator({ estimate }: BudgetCalculatorProps) {
const items = [
{ label: "Personnel", value: estimate.personnel, color: "bg-blue-500" },
{ label: "Equipment", value: estimate.equipment, color: "bg-purple-500" },
{ label: "Supplies", value: estimate.supplies, color: "bg-green-500" },
{ label: "Travel", value: estimate.travel, color: "bg-amber-500" },
{ label: "Overhead", value: estimate.overhead, color: "bg-rose-500" },
];
return (
<div className="p-4">
<h4 className="text-sm font-semibold flex items-center gap-2 mb-3">
<DollarSign className="w-4 h-4 text-primary" /> Budget Estimate
</h4>
<div className="text-center mb-4">
<p className="text-3xl font-extrabold text-primary">
${estimate.total.toLocaleString()}
</p>
<p className="text-xs text-gray-500">{estimate.currency}</p>
</div>
<div className="space-y-3">
{items.map((item) => (
<div key={item.label} className="space-y-1">
<div className="flex justify-between text-xs">
<span>{item.label}</span>
<span className="font-semibold">${item.value.toLocaleString()}</span>
</div>
<div className="h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full">
<div
className={`h-full rounded-full ${item.color}`}
style={{ width: `${estimate.total > 0 ? (item.value / estimate.total) * 100 : 0}%` }}
/>
</div>
</div>
))}
</div>
{estimate.notes && (
<p className="text-xs text-gray-500 mt-4 p-2 bg-gray-50 dark:bg-gray-700/50 rounded-lg">{estimate.notes}</p>
)}
</div>
);
}
// src/components/grant/TimelineBuilder.tsx
import type { TimelineItem } from "@/types/grant-assistant";
import { Clock, Calendar } from "lucide-react";
interface TimelineBuilderProps { items: TimelineItem[]; }
export function TimelineBuilder({ items }: TimelineBuilderProps) {
if (items.length === 0) {
return (
<div className="p-4 text-center text-gray-400">
<Clock className="w-8 h-8 mx-auto mb-2 opacity-30" />
<p className="text-xs">No timeline items yet. Generate a proposal first.</p>
</div>
);
}
const totalMonths = Math.max(...items.map((i) => i.startMonth + i.durationMonths), 1);
return (
<div className="p-4">
<h4 className="text-sm font-semibold flex items-center gap-2 mb-4">
<Calendar className="w-4 h-4 text-primary" /> Project Timeline
</h4>
<div className="relative">
{/* Month scale */}
<div className="flex justify-between text-[9px] text-gray-400 mb-2">
{Array.from({ length: Math.ceil(totalMonths / 3) + 1 }, (_, i) => (
<span key={i}>M{i * 3}</span>
))}
</div>
{/* Bars */}
<div className="space-y-2">
{items.map((item) => (
<div key={item.id} className="space-y-0.5">
<p className="text-[10px] font-medium text-gray-700 dark:text-gray-300 truncate">{item.task}</p>
<div className="relative h-6 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className="absolute h-full bg-primary/70 rounded-full flex items-center px-2"
style={{
left: `${(item.startMonth / totalMonths) * 100}%`,
width: `${(item.durationMonths / totalMonths) * 100}%`,
}}
>
<span className="text-[9px] text-white font-medium truncate">
{item.durationMonths} mo
</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
// src/components/grant/ComplianceChecker.tsx
import type { RfpDocument, Proposal } from "@/types/grant-assistant";
import { CheckCircle2, XCircle, AlertTriangle, CheckCheck } from "lucide-react";
import { useMemo } from "react";
interface ComplianceCheckerProps { rfp: RfpDocument; proposal: Proposal; }
export function ComplianceChecker({ rfp, proposal }: ComplianceCheckerProps) {
const checks = useMemo(() => {
const results: { label: string; passed: boolean; note?: string }[] = [];
// Check all required sections are present
rfp.sections.filter((s) => s.required).forEach((section) => {
const found = proposal.sections.find((ps) => ps.name === section.name);
results.push({
label: `Section "${section.name}" present`,
passed: !!found && found.wordCount > 0,
note: found ? `${found.wordCount} words` : "Missing",
});
});
// Page limit checks (assuming ~500 words/page)
rfp.sections.forEach((section) => {
if (section.pageLimit) {
const found = proposal.sections.find((ps) => ps.name === section.name);
const pages = (found?.wordCount ?? 0) / 500;
results.push({
label: `"${section.name}" under ${section.pageLimit} pages`,
passed: pages <= section.pageLimit,
note: `~${pages.toFixed(1)} pages`,
});
}
});
// Deadline not passed
const deadline = new Date(rfp.deadline);
results.push({
label: "Deadline not passed",
passed: deadline > new Date(),
note: deadline.toLocaleDateString(),
});
return results;
}, [rfp, proposal]);
const passedCount = checks.filter((c) => c.passed).length;
return (
<div className="p-4">
<h4 className="text-sm font-semibold flex items-center gap-2 mb-3">
<CheckCheck className="w-4 h-4 text-primary" /> Compliance Check
<span className="text-xs font-normal text-gray-400">
({passedCount}/{checks.length})
</span>
</h4>
<div className="space-y-2">
{checks.map((check, i) => (
<div
key={i}
className={`flex items-start gap-2 p-2.5 rounded-lg text-xs ${
check.passed
? "bg-green-50 dark:bg-green-900/10 border border-green-200 dark:border-green-800"
: "bg-red-50 dark:bg-red-900/10 border border-red-200 dark:border-red-800"
}`}
>
{check.passed ? (
<CheckCircle2 className="w-4 h-4 text-green-500 mt-0.5 shrink-0" />
) : (
<XCircle className="w-4 h-4 text-red-500 mt-0.5 shrink-0" />
)}
<div>
<p className={check.passed ? "text-green-800 dark:text-green-300" : "text-red-800 dark:text-red-300"}>
{check.label}
</p>
{check.note && (
<p className="text-gray-500 mt-0.5">{check.note}</p>
)}
</div>
</div>
))}
</div>
</div>
);
}
// src/components/grant/ExportPanel.tsx — simple export button
import { Download, FileText, Loader2 } from "lucide-react";
import { toast } from "react-toastify";
import { saveAs } from "file-saver";
interface ExportPanelProps {
onExport: (format: "docx" | "pdf") => Promise<Blob | null>;
}
export function ExportPanel({ onExport }: ExportPanelProps) {
const [exporting, setExporting] = useState<string | null>(null);
const handleExport = async (format: "docx" | "pdf") => {
setExporting(format);
const blob = await onExport(format);
if (blob) {
saveAs(blob, `grant-proposal.${format === "docx" ? "docx" : "pdf"}`);
toast.success(`Exported as ${format.toUpperCase()}`);
}
setExporting(null);
};
return (
<div className="p-4 flex gap-2">
<button onClick={() => handleExport("docx")} disabled={!!exporting} className="btn btn-primary btn-sm flex-1 gap-1">
{exporting === "docx" ? <Loader2 className="w-4 h-4 animate-spin" /> : <Download className="w-4 h-4" />}
DOCX
</button>
<button onClick={() => handleExport("pdf")} disabled={!!exporting} className="btn btn-secondary btn-sm flex-1 gap-1">
{exporting === "pdf" ? <Loader2 className="w-4 h-4 animate-spin" /> : <Download className="w-4 h-4" />}
PDF
</button>
</div>
);
}

Xet Storage Details

Size:
9.91 kB
·
Xet hash:
c51fc27ae3ccaa8bae78496ab15fbd17b3425106ade07aee4339b0143ac57c39

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.