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. +

+
+ + {/* Input Form */} +
+
+ +