diff --git a/Backend/app/__init__.py b/Backend/app/__init__.py
index 434a2ac..2163748 100644
--- a/Backend/app/__init__.py
+++ b/Backend/app/__init__.py
@@ -101,9 +101,11 @@ def invalid_token_callback(error_string):
from app.auth.routes import auth as auth_blueprint
from app.profile.routes import profile as profile_blueprint
from app.main.routes import main as main_blueprint
+ from app.claude.routes import claude as claude_blueprint
app.register_blueprint(auth_blueprint)
app.register_blueprint(profile_blueprint)
app.register_blueprint(main_blueprint)
+ app.register_blueprint(claude_blueprint)
return app
diff --git a/Backend/app/claude/__init__.py b/Backend/app/claude/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/Backend/app/claude/routes.py b/Backend/app/claude/routes.py
new file mode 100644
index 0000000..6e2dd36
--- /dev/null
+++ b/Backend/app/claude/routes.py
@@ -0,0 +1,122 @@
+from flask import Blueprint, request, jsonify
+from flask_jwt_extended import jwt_required, get_jwt_identity
+from anthropic import Anthropic, APIError
+import httpx
+import os
+import logging
+import sys
+
+# Set up logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+claude = Blueprint("claude", __name__)
+
+# Get API key
+logger.info("Current directory: %s", os.getcwd())
+logger.info("Python path: %s", sys.path)
+logger.info("Environment variables: %s",
+ {k: v for k, v in os.environ.items() if 'KEY' in k or 'SECRET' in k or 'PASSWORD' in k})
+api_key = os.environ.get('ANTHROPIC_API_KEY')
+logger.info(f"API Key present: {bool(api_key)}")
+
+try:
+ client = httpx.Client()
+ anthropic = Anthropic(
+ api_key=api_key,
+ http_client=client
+ )
+ logger.info("Anthropic client initialized successfully")
+ logger.info(f"Anthropic client: {anthropic}")
+ logger.info("Api key: " + api_key)
+except Exception as e:
+ logger.error(f"Failed to initialize Anthropic client: {str(e)}")
+ anthropic = None
+
+@claude.route('/claude/generate', methods=['POST'])
+def generate_content():
+ """
+ Generate proposal content using Claude AI.
+ """
+ # Log request received
+ logger.info("Received generation request")
+
+ # Check API key first
+ if not api_key:
+ logger.error("No API key found")
+ return jsonify({
+ 'error': 'API key not configured',
+ 'api_status': 'missing'
+ }), 500
+
+ # Check client initialization
+ if not anthropic:
+ logger.error("Anthropic client not initialized")
+ return jsonify({
+ 'error': 'Anthropic client not initialized',
+ 'api_status': 'client_error'
+ }), 500
+
+ try:
+ # Get and validate request data
+ data = request.get_json()
+ if not data:
+ logger.error("No JSON data in request")
+ return jsonify({'error': 'No JSON data provided'}), 400
+
+ if 'prompt' not in data or 'section' not in data:
+ logger.error(f"Missing required fields. Received fields: {data.keys()}")
+ return jsonify({'error': 'Missing required fields: prompt and/or section'}), 400
+
+ logger.info(f"Generating content for section: {data['section']}")
+
+ try:
+ # Generate content using Claude
+ message = anthropic.messages.create(
+ model="claude-3-haiku-20240307",
+ max_tokens=1500,
+ messages=[{
+ "role": "user",
+ "content": data['prompt']
+ }],
+ temperature=0.7,
+ )
+
+ if not message.content:
+ logger.error("No content generated")
+ return jsonify({
+ 'error': 'Content generation failed',
+ 'api_status': 'generation_error'
+ }), 500
+
+ generated_content = message.content[0].text
+ logger.info("Content generated successfully")
+
+ return jsonify({
+ 'content': generated_content,
+ 'status': 'success'
+ }), 200
+
+ except APIError as ae:
+ logger.error(f"Anthropic API error: {str(ae)}")
+ return jsonify({
+ 'error': 'Anthropic API error',
+ 'details': str(ae),
+ 'api_status': 'api_error'
+ }), 500
+
+ except Exception as e:
+ logger.error(f"Generation error: {str(e)}")
+ return jsonify({
+ 'error': 'Content generation failed',
+ 'details': str(e),
+ 'api_status': 'generation_error'
+ }), 500
+
+ except Exception as e:
+ logger.error(f"Request processing error: {str(e)}")
+ return jsonify({
+ 'error': 'Request processing failed',
+ 'details': str(e),
+ 'api_status': 'request_error'
+ }), 500
\ No newline at end of file
diff --git a/Backend/app/config.py b/Backend/app/config.py
index 785afff..3b077b5 100644
--- a/Backend/app/config.py
+++ b/Backend/app/config.py
@@ -13,12 +13,12 @@ class AppConfig:
database_url = os.getenv("DATABASE_URL")
if database_url and database_url.startswith("postgres://"):
database_url = database_url.replace("postgres://", "postgresql://")
-
+
# Determine environment and database connection
IS_MIGRATION = os.getenv('IS_MIGRATION') == 'true'
IS_CLOUD_RUN = os.getenv('K_SERVICE') is not None
USE_CLOUD_SQL_PROXY = os.getenv('USE_CLOUD_SQL_PROXY') == 'true'
-
+
# Database URI Configuration
if IS_MIGRATION or USE_CLOUD_SQL_PROXY:
# Use Cloud SQL Proxy connection for migrations or when explicitly requested
@@ -29,13 +29,13 @@ class AppConfig:
else:
# Local development fallback
SQLALCHEMY_DATABASE_URI = database_url or "sqlite:///db.sqlite"
-
+
# Determine database type to set appropriate configuration
is_sqlite = SQLALCHEMY_DATABASE_URI.startswith('sqlite')
-
+
# Base SQLAlchemy configuration
SQLALCHEMY_TRACK_MODIFICATIONS = False
-
+
# Configure engine options based on database type
if is_sqlite:
SQLALCHEMY_ENGINE_OPTIONS = {
@@ -55,25 +55,25 @@ class AppConfig:
'connect_timeout': 30
} if not is_sqlite else {}
}
-
+
# Session configuration
SESSION_TYPE = "filesystem"
SESSION_COOKIE_SAMESITE = "None"
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_DOMAIN = '.onrender.com' if os.getenv('FLASK_ENV') == 'production' else None
-
+
# Mail configuration
MAIL_USERNAME = os.getenv("MAIL_USERNAME")
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD")
MAIL_SERVER = "smtp.googlemail.com"
MAIL_PORT = 587
MAIL_USE_TLS = True
-
+
# File upload configuration
UPLOAD_FOLDER = os.path.abspath("uploads")
ENV = os.getenv("FLASK_ENV", "production")
-
+
# JWT settings
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=1)
@@ -85,12 +85,14 @@ class AppConfig:
# Google Cloud configuration
GOOGLE_CLOUD_PROJECT = os.getenv("GOOGLE_CLOUD_PROJECT")
GOOGLE_APPLICATION_CREDENTIALS = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
+ GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME")
- GCS_BUCKET_NAME = os.environ.get('GCS_BUCKET_NAME')
+ # Anthropic API key
+ ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY')
class TestConfig(AppConfig):
SQLALCHEMY_DATABASE_URI = "sqlite://"
TESTING = True
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_pre_ping': True
- } # Simplified options for testing
\ No newline at end of file
+ } # Simplified options for testing
diff --git a/Backend/requirements.dev.txt b/Backend/requirements.dev.txt
index 7f8acc0..448d76f 100644
--- a/Backend/requirements.dev.txt
+++ b/Backend/requirements.dev.txt
@@ -1,4 +1,5 @@
# Core dependencies
+anthropic>=0.10.0
bcrypt>=4.0.1
Flask>=2.2.3
Flask-Bcrypt>=1.0.1
diff --git a/Backend/requirements.prod.txt b/Backend/requirements.prod.txt
index 8987514..851cf59 100644
--- a/Backend/requirements.prod.txt
+++ b/Backend/requirements.prod.txt
@@ -1,4 +1,5 @@
# Core dependencies
+anthropic>=0.10.0
bcrypt==4.0.1
Flask==2.2.3
Flask-Bcrypt==1.0.1
diff --git a/Frontend/src/App.js b/Frontend/src/App.js
index a6a9b2e..8bca853 100644
--- a/Frontend/src/App.js
+++ b/Frontend/src/App.js
@@ -17,7 +17,8 @@ import VolunteerPage from './pages/VolunteerPage';
import SelectUserTypePage from './pages/SelectUserTypePage';
import EstablishmentGuide from './pages/EstablishmentGuide';
import EditProfile from './pages/EditProfilePage';
-import ProfileImages from './components/OrgProfileFormComponents/ProfileImages';
+import ProfileImages from "./components/OrgProfileFormComponents/ProfileImages";
+import ProposalBuilderPage from './pages/ProposalBuilderPage';
function App() {
@@ -37,7 +38,6 @@ function App() {
} />
} />
} />
-
} />
} />
} />
@@ -45,11 +45,12 @@ function App() {
} />
} />
+ } />
-
+
-
-
+
+
);
}
diff --git a/Frontend/src/components/Header.js b/Frontend/src/components/Header.js
index ce1be62..d36b6fc 100644
--- a/Frontend/src/components/Header.js
+++ b/Frontend/src/components/Header.js
@@ -15,6 +15,7 @@ const navigation = [
{ name: "Home", href: "/" },
{ name: "Find GOs", href: "/find-gos" },
{ name: "Establishment Guide", href: "/establishment-guide" },
+ { name: "Proposal Builder", href: "/proposal-builder" },
{ name: "Volunteer", href: "/volunteer" },
{ name: "About", href: "/about" },
];
diff --git a/Frontend/src/components/ProposalBuilderComponents/ExecutiveSummarySection.js b/Frontend/src/components/ProposalBuilderComponents/ExecutiveSummarySection.js
new file mode 100644
index 0000000..f9ac08a
--- /dev/null
+++ b/Frontend/src/components/ProposalBuilderComponents/ExecutiveSummarySection.js
@@ -0,0 +1,218 @@
+import React, { useState, useEffect } from 'react';
+import {
+ DocumentTextIcon,
+ ArrowPathIcon,
+} from '@heroicons/react/24/outline';
+import { useApi } from '../../contexts/ApiProvider';
+import { useAuth } from '../../contexts/AuthProvider';
+import GeneratedContent from './GeneratedContent';
+
+/**
+ * ExecutiveSummarySection
+ *
+ * Creates a compelling executive summary that synthesizes all proposal sections.
+ * Written last but appears first in the final document.
+ * Pulls context from organization info, project narrative, and budget sections.
+ */
+export default function ExecutiveSummarySection() {
+ const [generatedContent, setGeneratedContent] = useState('');
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [error, setError] = useState(null);
+ const apiClient = useApi();
+ const { user } = useAuth();
+ const userId = user ? user.id : null;
+
+ const [inputs, setInputs] = useState({
+ keyHighlights: '', // Additional points to emphasize
+ uniqueValue: '', // What makes this project special
+ urgency: '', // Why now/why this project
+ impact: '' // Expected outcomes worth highlighting
+ });
+
+ // Load stored content from localStorage on mount
+ useEffect(() => {
+ const storedContent = localStorage.getItem(`${userId}_executiveSummary`);
+ if (storedContent) {
+ setGeneratedContent(storedContent);
+ }
+ }, []);
+
+ const handleInputChange = (field, value) => {
+ setInputs(prev => ({
+ ...prev,
+ [field]: value
+ }));
+ };
+
+ const handleGenerate = async () => {
+ setIsGenerating(true);
+ setError(null);
+
+ try {
+ // Get content from all previous sections
+ const orgContent = localStorage.getItem(`${userId}_organizationContent`);
+ const narrativeContent = localStorage.getItem(`${userId}_projectNarrative`);
+ const budgetContent = localStorage.getItem(`${userId}_proposalBudget`);
+
+ const prompt = `You are an expert grant writer. Create a compelling executive summary that synthesizes all sections of the grant proposal into a powerful opening statement.
+
+ Use these sections for context:
+
+ Organization Information:
+ ${orgContent}
+
+ Project Narrative:
+ ${narrativeContent}
+
+ Budget Information:
+ ${budgetContent}
+
+ Additional Emphasis Points:
+ Key Highlights: ${inputs.keyHighlights}
+ Unique Value: ${inputs.uniqueValue}
+ Urgency: ${inputs.urgency}
+ Impact: ${inputs.impact}
+
+ Create an executive summary that:
+ 1. Captures attention in the first paragraph
+ 2. Clearly states the problem and your solution
+ 3. Emphasizes your organization's unique capability
+ 4. Includes key financial figures and project timeline
+ 5. Highlights expected impact and outcomes
+ 6. Maintains professional tone while conveying urgency
+ 7. Stays under 500 words
+
+ Note: This summary will appear first in the proposal but synthesizes all sections.`;
+
+ const response = await apiClient.post('/claude/generate', {
+ prompt,
+ section: 'executiveSummary'
+ });
+
+ setGeneratedContent(response.body.content);
+ localStorage.setItem(`${userId}_executiveSummary`, response.body.content);
+
+
+ } catch (error) {
+ console.error('Generation failed:', error);
+ setError('Failed to generate executive summary. Please try again.');
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+
+ return (
+
+
Executive Summary
+
+ {/* Information Notice */}
+
+
+
+
This is your opening pitch - make every word count.
+
Write this section last but present it first in your proposal.
+
Focus on key points from each section of your proposal.
+
Keep it concise - aim for 500 words or less.
+
Remember: Many readers will only see this section.
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ Note: Before generating the executive summary, make sure you've completed all other sections of your proposal. The AI will use that context to create a comprehensive summary.
+
+ );
+};
+
+export default GeneratedContent;
\ No newline at end of file
diff --git a/Frontend/src/components/ProposalBuilderComponents/OrganizationInfoSection.js b/Frontend/src/components/ProposalBuilderComponents/OrganizationInfoSection.js
new file mode 100644
index 0000000..122c427
--- /dev/null
+++ b/Frontend/src/components/ProposalBuilderComponents/OrganizationInfoSection.js
@@ -0,0 +1,372 @@
+import React, { useState, useEffect } from 'react';
+import { BuildingOfficeIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
+import { useApi } from '../../contexts/ApiProvider';
+import { useAuth } from '../../contexts/AuthProvider';
+import GeneratedContent from './GeneratedContent';
+
+/**
+ * OrganizationInfoSection
+ *
+ * Displays and manages organization information for grant proposals.
+ * Loads data from the API and allows for proposal-specific additions.
+ *
+ * Features:
+ * - Uses auth context for user identification
+ * - Loads existing org profile data from API
+ * - Displays loading states and error handling
+ * - Allows additional proposal-specific details
+ * - Generates AI-enhanced content for grant proposals
+ * - Saves to localStorage for proposal persistence
+ */
+export default function OrganizationInfoSection() {
+ const apiClient = useApi();
+ const { user } = useAuth();
+ const userId = user ? user.id : null;
+
+ const [loading, setLoading] = useState(true);
+ const [orgData, setOrgData] = useState(null);
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [error, setError] = useState(null);
+ const [generatedContent, setGeneratedContent] = useState('');
+
+ // Load stored content from localStorage on mount
+ useEffect(() => {
+ const storedContent = localStorage.getItem(`${userId}_organizationContent`);
+ if (storedContent) {
+ setGeneratedContent(storedContent);
+ }
+ }, []);
+
+ // State for form inputs, will be populated with org data
+ const [inputs, setInputs] = useState({
+ // Basic Info (pre-populated)
+ orgName: '',
+ yearEstablished: '',
+ registrationNumber: '',
+ missionStatement: '',
+
+ // Contact Info (pre-populated)
+ location: {
+ country: '',
+ county: '',
+ district: ''
+ },
+ contact: {
+ email: '',
+ phone: '',
+ website: ''
+ },
+
+ // Grant-Specific Info (user input)
+ grantSpecific: {
+ orgCapacity: '',
+ previousGrants: '',
+ successStories: '',
+ teamQualifications: '',
+ partnershipHistory: '',
+ relevantInitiatives: '',
+ ongoingProjects: ''
+ }
+ });
+
+ // Load org data when userId is available
+ useEffect(() => {
+ const loadOrgData = async () => {
+ if (!userId) {
+ setLoading(false);
+ return;
+ }
+
+ try {
+ const response = await apiClient.get(`/profile/load_org?user_id=${userId}`);
+
+ if (response.ok) {
+ setOrgData(response.body);
+
+ // Update inputs with loaded data
+ const { orgProfile, orgInitiatives, orgProjects } = response.body;
+
+ setInputs(prev => ({
+ ...prev,
+ orgName: orgProfile.org_name,
+ yearEstablished: orgProfile.org_year_established,
+ registrationNumber: orgProfile.org_registration_number,
+ missionStatement: orgProfile.org_mission_statement,
+ location: {
+ country: orgProfile.org_country,
+ county: orgProfile.org_county,
+ district: orgProfile.org_district_town
+ },
+ contact: {
+ email: orgProfile.org_email,
+ phone: orgProfile.org_phone,
+ website: orgProfile.org_website || ''
+ },
+ // Pre-populate relevant initiatives and projects
+ grantSpecific: {
+ ...prev.grantSpecific,
+ relevantInitiatives: orgInitiatives
+ .map(init => `${init.initiative_name}: ${init.initiative_description}`)
+ .join('\n\n'),
+ ongoingProjects: orgProjects
+ .filter(proj => proj.project_status === 'ongoing')
+ .map(proj => `${proj.project_name}: ${proj.project_description}`)
+ .join('\n\n')
+ }
+ }));
+ } else {
+ setError("Failed to load organization data");
+ console.error("Error fetching data: ", response.body);
+ }
+ } catch (error) {
+ setError("Error connecting to server");
+ console.error("Error fetching data: ", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadOrgData();
+ }, [apiClient, userId]);
+
+ const handleInputChange = (section, field, value) => {
+ setInputs(prev => ({
+ ...prev,
+ [section]: section === 'grantSpecific' || section === 'location' || section === 'contact'
+ ? { ...prev[section], [field]: value }
+ : value
+ }));
+ };
+
+ const handleGenerate = async () => {
+ setIsGenerating(true);
+ setError(null);
+
+ try {
+ const prompt = `Create a professional organization background section for a grant proposal using the following information:
+
+ Organization Name: ${inputs.orgName}
+ Year Established: ${inputs.yearEstablished}
+ Registration: ${inputs.registrationNumber}
+ Mission: ${inputs.missionStatement}
+
+ Location: ${inputs.location.district}, ${inputs.location.county}, ${inputs.location.country}
+
+ Current Initiatives:
+ ${inputs.grantSpecific.relevantInitiatives}
+
+ Ongoing Projects:
+ ${inputs.grantSpecific.ongoingProjects}
+
+ Additional Context:
+ Organizational Capacity: ${inputs.grantSpecific.orgCapacity}
+ Previous Grants: ${inputs.grantSpecific.previousGrants}
+ Success Stories: ${inputs.grantSpecific.successStories}
+ Team Qualifications: ${inputs.grantSpecific.teamQualifications}
+ Partnership History: ${inputs.grantSpecific.partnershipHistory}
+
+ Please create a compelling narrative that:
+ 1. Establishes organizational credibility
+ 2. Highlights relevant experience and success
+ 3. Demonstrates capacity to implement projects
+ 4. Showcases strong governance and accountability
+ 5. Uses professional grant writing language`;
+
+ const response = await apiClient.post('/claude/generate', {
+ prompt,
+ section: 'organizationInfo'
+ });
+
+ setGeneratedContent(response.body.content);
+ localStorage.setItem(`${userId}_organizationContent`, response.body.content);
+
+ } catch (error) {
+ console.error('Generation failed:', error);
+ setError('Failed to generate content. Please try again.');
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
Organization Information
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {/* Information Notice */}
+
+
+ This section builds upon your organization's existing profile information. The following fields are optional
+ and should only be filled if they provide additional context specific to this grant application.
+ If the information is already covered in your organization profile, you may leave these fields blank.
+
+
+
+ {/* Pre-populated Organization Information */}
+
+ );
+}
\ No newline at end of file
diff --git a/Frontend/src/components/ProposalBuilderComponents/ProposalBudgetSection.js b/Frontend/src/components/ProposalBuilderComponents/ProposalBudgetSection.js
new file mode 100644
index 0000000..2decd49
--- /dev/null
+++ b/Frontend/src/components/ProposalBuilderComponents/ProposalBudgetSection.js
@@ -0,0 +1,295 @@
+import React, { useState, useEffect } from 'react';
+import {
+ CurrencyDollarIcon,
+ ArrowPathIcon,
+} from '@heroicons/react/24/outline';
+import { useApi } from '../../contexts/ApiProvider';
+import { useAuth } from '../../contexts/AuthProvider';
+import GeneratedContent from './GeneratedContent';
+
+/**
+ * ProposalBudgetSection
+ *
+ * A flexible budget component that handles both specific grant requirements
+ * and open-ended proposals. Adapts to different funding situations and
+ * generates appropriate budget narratives.
+ */
+export default function ProposalBudgetSection() {
+ const [generatedContent, setGeneratedContent] = useState('');
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [error, setError] = useState(null);
+ const apiClient = useApi();
+ const { user } = useAuth();
+ const userId = user ? user.id : null;
+
+ // Initialize form state
+ const [inputs, setInputs] = useState({
+ grantType: 'open', // 'open' or 'specific'
+ grantRequirements: '', // For pasting specific grant budget requirements
+ budget: {
+ totalAmount: '',
+ breakdown: '', // Flexible text area for budget breakdown
+ justification: '' // Explanation of costs
+ },
+ sustainability: {
+ additionalFunding: '', // Other funding sources or plans
+ continuationPlan: '' // How the project will continue after funding
+ }
+ });
+
+ // Load stored content from localStorage on mount
+ useEffect(() => {
+ const storedContent = localStorage.getItem(`${userId}_proposalBudget`);
+ if (storedContent) {
+ setGeneratedContent(storedContent);
+ }
+ }, []);
+
+ const handleInputChange = (section, field, value) => {
+ setInputs(prev => ({
+ ...prev,
+ [section]: {
+ ...prev[section],
+ [field]: value
+ }
+ }));
+ };
+
+ const handleGrandTypeChange = (type) => {
+ setInputs(prev => ({
+ ...prev,
+ grantType: type
+ }));
+ };
+
+ const handleGenerate = async () => {
+ setIsGenerating(true);
+ setError(null);
+
+ try {
+ // Get previous content for context
+ const orgContent = localStorage.getItem(`${userId}_organizationContent`);
+ const narrativeContent = localStorage.getItem(`${userId}_projectNarrative`);
+
+ const prompt = `You are an expert grant writer. Using the provided information, create a budget narrative that ${
+ inputs.grantType === 'specific'
+ ? 'addresses the specific grant requirements provided'
+ : 'follows standard grant writing best practices'
+ }.
+
+ Organization Context:
+ ${orgContent}
+
+ Project Narrative Context:
+ ${narrativeContent}
+
+ ${inputs.grantType === 'specific' ? `Grant Requirements:
+ ${inputs.grantRequirements}` : ''}
+
+ Budget Information:
+ Total Amount: ${inputs.budget.totalAmount}
+ Budget Breakdown: ${inputs.budget.breakdown}
+ Budget Justification: ${inputs.budget.justification}
+
+ Additional Funding: ${inputs.sustainability.additionalFunding}
+ Continuation Plan: ${inputs.sustainability.continuationPlan}
+
+ Generate a professional budget narrative that:
+ 1. ${inputs.grantType === 'specific' ? 'Directly addresses the grant requirements' : 'Follows standard grant writing best practices'}
+ 2. Clearly justifies all costs
+ 3. Demonstrates fiscal responsibility
+ 4. Shows alignment with project goals
+ 5. Explains sustainability plans`;
+
+ const response = await apiClient.post('/claude/generate', {
+ prompt,
+ section: 'proposalBudget'
+ });
+
+ setGeneratedContent(response.body.content);
+ localStorage.setItem(`${userId}_proposalBudget`, response.body.content);
+
+ } catch (error) {
+ console.error('Generation failed:', error);
+ setError('Failed to generate budget narrative. Please try again.');
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+
+ return (
+
+
Project Budget
+
+ {/* Grant Type Selection */}
+
+
+
+
+
Be detailed and specific with all cost estimates and line items.
+
Explain the rationale behind each budget category and how it supports project goals.
+
Include all funding sources and demonstrate responsible financial planning.
+
Show sustainability by explaining how the project will continue beyond the grant period.
+
Use clear, professional language and accurate calculations.
+
+
+
+
+
+
+
+
Choose your approach:
+
+
+ Specific Grant: Choose this if you're responding to a particular grant opportunity with defined requirements and guidelines. You'll be able to paste the grant's budget requirements and ensure your proposal aligns perfectly with the funder's expectations.
+
+
+ Open Proposal: Select this if you're creating a general funding proposal or planning to approach multiple funders. This will help you create a flexible budget narrative that follows grant writing best practices and can be adapted for different opportunities.
+
+
+
+
+
+
+
+
+
+
+ {/* Grant Requirements (if specific grant selected) */}
+ {inputs.grantType === 'specific' && (
+
+ );
+}
diff --git a/Frontend/src/pages/ProposalBuilderPage.js b/Frontend/src/pages/ProposalBuilderPage.js
new file mode 100644
index 0000000..d77cd06
--- /dev/null
+++ b/Frontend/src/pages/ProposalBuilderPage.js
@@ -0,0 +1,197 @@
+import React, { useState, useEffect } from 'react';
+import {
+ BuildingOfficeIcon,
+ DocumentIcon,
+ CurrencyDollarIcon,
+ DocumentTextIcon,
+ CheckCircleIcon,
+ ArrowDownIcon
+} from '@heroicons/react/24/outline';
+import ProjectNarrativeSection from '../components/ProposalBuilderComponents/ProjectNarrativeSection';
+import OrganizationInfoSection from '../components/ProposalBuilderComponents/OrganizationInfoSection';
+import ProposalBudgetSection from '../components/ProposalBuilderComponents/ProposalBudgetSection';
+import ExecutiveSummarySection from '../components/ProposalBuilderComponents/ExecutiveSummarySection';
+import Header from '../components/Header';
+import { useAuth } from '../contexts/AuthProvider';
+
+/**
+ * ProposalBuilder
+ *
+ * A step-by-step grant proposal generator that creates professional proposal content
+ * based on user inputs. The tool guides users through four main sections:
+ *
+ * 1. Organization Information - Details about your organization
+ * 2. Project Narrative - Core project description and goals
+ * 3. Budget - Financial requirements and justification
+ * 4. Executive Summary - Comprehensive overview (generated last, displayed first)
+ *
+ * Each section:
+ * - Collects specific inputs
+ * - Generates content using AI
+ * - Builds upon previous sections' content
+ * - Can be regenerated as needed
+ *
+ * Content is saved to localStorage for persistence between sessions.
+ */
+export default function ProposalBuilder() {
+ const [activeSection, setActiveSection] = useState('organizationInfo');
+ const [showTip, setShowTip] = useState(true);
+ const { user } = useAuth();
+ const userId = user ? user.id : null;
+ const [completedSections, setCompletedSections] = useState(() => {
+ // Initialize from localStorage
+ const saved = localStorage.getItem(`${userId}_completedSections`);
+ return saved ? JSON.parse(saved) : [];
+ });
+
+ const sections = [
+ { id: 'organizationInfo', label: 'Organization Information', icon: BuildingOfficeIcon },
+ { id: 'projectNarrative', label: 'Project Narrative', icon: DocumentIcon },
+ { id: 'proposalBudget', label: 'Budget', icon: CurrencyDollarIcon },
+ { id: 'executiveSummary', label: 'Executive Summary', icon: DocumentTextIcon }
+ ];
+
+ // Track section completion
+ useEffect(() => {
+ const handleStorageChange = () => {
+ const completed = [];
+ if (localStorage.getItem(`${userId}_organizationContent`)) completed.push('organizationInfo');
+ if (localStorage.getItem(`${userId}_projectNarrative`)) completed.push('projectNarrative');
+ if (localStorage.getItem(`${userId}_proposalBudget`)) completed.push('proposalBudget');
+ if (localStorage.getItem(`${userId}_executiveSummary`)) completed.push('executiveSummary');
+
+ setCompletedSections(completed);
+ localStorage.setItem(`${userId}_completedSection`, JSON.stringify(completed));
+ };
+
+ // Check on mount and when localStorage changes
+ handleStorageChange();
+ window.addEventListener('storage', handleStorageChange);
+ return () => window.removeEventListener('storage', handleStorageChange);
+ }, []);
+
+ return (
+
+
+
+
+ {/* Introduction section */}
+
+
+
+ AI-Powered Grant Proposal Builder
+
+
+
+ Transform your project ideas into professionally written grant proposals. Our AI assistant guides you through each section, ensuring comprehensive and compelling content.
+
+
+
+
Professional Quality
+
Generate polished, funder-ready content that follows grant writing best practices.
+
+
+
+
+
+
+ {/* Progress Overview */}
+ {showTip && (
+
+
+
+
+
+
+ Recommended order: Complete sections from left to right. The Executive Summary should be written last as it draws from all other sections.
+
+
+ Progress is automatically saved as you work.
+
+
+
+
+
+
+ )}
+
+ {/* Section Navigation */}
+
+
+
+
+
+ {!userId ? (
+
+
+ Please log in to access organization information.
+