import gradio as gr import os import time import pandas as pd import numpy as np from typing import List from supabase import create_client from dotenv import load_dotenv from openai import OpenAI load_dotenv() client = OpenAI(api_key=os.environ.get("API_TOKEN", "")) supabase = create_client(os.getenv("SUPABASE_URL", ""), os.getenv("SUPABASE_KEY", "")) def get_embedding(text: str) -> List[float]: r = client.embeddings.create(model="text-embedding-3-small", input=text) return [float(x) for x in r.data[0].embedding] def cosine_similarity(a: List[float], b: List[float]) -> float: return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) def sign_up_school( username, password, name, address_line1, city, state, zip_code, school_type, educational_level, enrollment, title_i_status, free_reduced_lunch_percentage, contact_email, additional_info ): try: auth_response = supabase.auth.sign_up({"email": username, "password": password}) user_id = auth_response.user.id profile_text = f""" School: {name} Type: {school_type} Level: {educational_level} Title I: {title_i_status} Free/Reduced Lunch: {free_reduced_lunch_percentage}% Additional Info: {additional_info} """ profile_embedding = get_embedding(profile_text) if len(profile_embedding) != 1536: raise ValueError(f"Expected embedding dimension 1536, got {len(profile_embedding)}") school_data = { "user_id": user_id, "name": name, "address_line1": address_line1, "city": city, "state": state, "zip_code": zip_code, "school_type": school_type, "educational_level": educational_level, "enrollment": enrollment, "title_i_status": title_i_status, "free_reduced_lunch_percentage": free_reduced_lunch_percentage, "contact_email": contact_email, "additional_info": additional_info, "embedding": profile_embedding } supabase.table('schools').insert(school_data).execute() return True, "School profile created successfully!", user_id except Exception as e: return False, f"Error: {str(e)}", None def login(username, password): try: r = supabase.auth.sign_in_with_password({"email": username, "password": password}) user_id = r.user.id school_response = supabase.table('schools').select("*").eq('user_id', user_id).execute() school_id = None if school_response.data and len(school_response.data) > 0: school_id = school_response.data[0]['user_id'] return True, user_id, school_id except Exception as e: return False, f"Error: {str(e)}", None def get_school_name(school_id): if not school_id: return "No school selected" try: r = supabase.table('schools').select("name").eq('user_id', school_id).execute() if r.data and len(r.data) > 0: return r.data[0]['name'] return "Unknown school" except: return "Error loading school" def search_grants(query, school_id=None): try: headers = ["Title", "Provider", "Due_date", "Description", "Funding_amount", "Requirements", "Eligibility", "Categories", "Score"] if not query or query.strip() == "": return pd.DataFrame(columns=headers) query_embedding = get_embedding(query) school_data = None if school_id and str(school_id).strip().lower() != 'none': try: sr = supabase.table('schools').select("*").eq('user_id', school_id).execute() if sr.data: school_data = sr.data[0] if school_data.get('embedding'): try: school_data['embedding'] = eval(school_data['embedding']) except: school_data['embedding'] = None except: pass grants_response = supabase.table('grants').select("*").execute() if not grants_response.data: return pd.DataFrame(columns=headers) grants = grants_response.data scored_grants = [] for g in grants: try: if g.get('embedding'): try: grant_embedding = eval(g['embedding']) except: continue q_sim = cosine_similarity(query_embedding, grant_embedding) s_sim = 0 if school_data and school_data.get('embedding'): s_sim = cosine_similarity(school_data['embedding'], grant_embedding) context_score = 0 scored = 0.4 * q_sim + 0.3 * s_sim + 0.3 * context_score if scored > 0.1: scored_grants.append({ 'Title': g.get('title', ''), 'Provider': g.get('provider', ''), 'Due_date': g.get('due_date', ''), 'Description': g.get('description', ''), 'Funding_amount': g.get('funding_amount', ''), 'Requirements': g.get('requirements', ''), 'Eligibility': g.get('eligibility', ''), 'Categories': g.get('categories', ''), 'Score': round(scored, 3) }) except: continue if not scored_grants: return pd.DataFrame(columns=headers) scored_grants.sort(key=lambda x: x['Score'], reverse=True) df = pd.DataFrame(scored_grants) return df except: return pd.DataFrame(columns=headers) def chat_with_ai(message, history, grant_id=None, school_id=None): print(f"Chat with AI called with message: {message}, grant_id: {grant_id}, school_id: {school_id}") try: grant_data = None school_data = None school_profile = None if school_id and str(school_id).strip().lower() != 'none': sr = supabase.table('schools').select("*").eq('user_id', school_id).execute() if sr.data: school_data = sr.data[0] school_profile = f""" School Profile: Name: {school_data.get('name', '')} Type: {school_data.get('school_type', '')} Level: {school_data.get('educational_level', '')} Title I Status: {school_data.get('title_i_status', '')} Free/Reduced Lunch: {school_data.get('free_reduced_lunch_percentage', '')}% Enrollment: {school_data.get('enrollment', '')} Location: {school_data.get('city', '')}, {school_data.get('state', '')} Additional Info: {school_data.get('additional_info', '')} """ if grant_id and str(grant_id).strip().lower() != 'none': grr = supabase.table('grants').select("*").eq('grant_id', grant_id).execute() if not grr.data: grr = supabase.table('grants').select("*").eq('grant_id', grant_id).execute() if grr.data: grant_data = grr.data[0] system_prompt = f""" School Profile: {school_profile if school_profile else 'No school profile available'} Grant: {grant_data['title'] if grant_data else 'No specific grant selected'} You are GrantGPT, a grant writing assistant. Begin by summarizing the selected grant: "You are applying for [Grant Name], which provides [Funding Amount] for [Purpose]. The deadline is [Date]." Ask if they have a project in mind or need suggestions. Guide users through sections: - Project Abstract/Summary - Statement of Need - Program Description - Budget & Funding - Evaluation Plan Keep responses focused on grant writing assistance. Redirect unrelated questions to appropriate resources. """ messages = [{"role": "system", "content": system_prompt}] if history: for user_msg, assistant_msg in history: messages.append({"role": "user", "content": user_msg}) messages.append({"role": "assistant", "content": assistant_msg}) messages.append({"role": "user", "content": message}) response = client.chat.completions.create(model="gpt-4", messages=messages, max_tokens=2000, temperature=0.7) full_message = response.choices[0].message.content if (grant_id and str(grant_id).strip().lower() != 'none' and school_id and str(school_id).strip().lower() != 'none'): try: app_response = supabase.table('applications').select("id").eq('grant_id', grant_id).eq('school_id', school_id).execute() if not app_response.data: app_response = supabase.table('applications').insert({ 'grant_id': grant_id, 'school_id': school_id, 'status': 'Draft' }).execute() if app_response.data: application_id = app_response.data[0]['id'] supabase.table('application_chat_history').insert([ {'application_id': application_id, 'message_type': 'user', 'content': message}, {'application_id': application_id, 'message_type': 'assistant', 'content': full_message} ]).execute() except: pass return full_message except Exception as e: return f"Error: {str(e)}" custom_css = """ body { margin: 0; background: linear-gradient(to bottom, #c2e9fb, #e2fcff); font-family: 'Helvetica Neue', sans-serif; min-height: 100vh; } .gradio-container { max-width: 100% !important; margin: auto; padding: 0; display: flex; flex-direction: column; justify-content: center; min-height: 100vh; } .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.2); z-index: 999; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .auth-modal { background: #fff; padding: 40px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); z-index: 1000; width: 400px !important; max-width: 90% !important; height: auto !important; min-height: 300px !important; max-height: 400px !important; overflow-y: auto; margin: 0 auto; display: flex !important; flex-direction: column !important; justify-content: flex-start !important; } .landing-wrapper { display: flex; justify-content: center; align-items: center; min-height: 100vh; width: 100%; padding: 20px; box-sizing: border-box; } .landing-buttons { display: flex; flex-direction: column; gap: 15px; width: 100%; margin-top: 20px; } .landing-button { background-color: #8ecae6 !important; color: #2c3e50 !important; border: none !important; border-radius: 25px !important; padding: 12px 20px !important; font-size: 16px !important; font-weight: 500 !important; cursor: pointer !important; transition: background-color 0.3s ease !important; width: 100% !important; text-align: center !important; } .landing-button:hover { background-color: #76b5da !important; } @media (max-width: 768px) { .auth-modal { width: 90% !important; padding: 20px; } } .auth-modal h1 { color: #2c3e50 !important; margin: 0 0 20px 0 !important; text-align: center; flex-shrink: 0; } .auth-modal button { background-color: #ffffff; color: #2c3e50; border: 1px solid #ccc; border-radius: 25px; padding: 0.6rem 1.2rem; font-size: 1rem; margin: 10px; cursor: pointer; } .auth-modal button:hover { background-color: #f0f0f0; } .login-form, .signup-form { margin-top: 20px; } .gr-text-input input[type=text], .gr-text-input input[type=password], textarea { background-color: #fff !important; color: #333 !important; border: 1px solid #ccc !important; border-radius: 4px !important; } h1, h2, h3, h4, h5, h6 { color: #2c3e50; } .grant-card { background: #ffffff; border-radius: 10px; padding: 20px; margin-bottom: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); cursor: pointer; } .grant-card:hover { box-shadow: 0 4px 8px rgba(0,0,0,0.15); } .nav-bar { display: flex; justify-content: space-between; align-items: center; background: #fff; padding: 10px 20px; border-bottom: 1px solid #ccc; } .nav-bar button { background: none; border: none; cursor: pointer; font-weight: bold; color: #2c3e50; } .nav-bar button:hover { text-decoration: underline; } .grants-container { max-height: 600px; overflow-y: auto; padding: 20px; background: rgba(255, 255, 255, 0.5); border-radius: 10px; margin: 20px 0; } .grants-container::-webkit-scrollbar { width: 8px; } .grants-container::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .grants-container::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; } .grants-container::-webkit-scrollbar-thumb:hover { background: #555; } .help-button-container { display: flex; justify-content: center; align-items: center; margin-top: 2rem; margin-bottom: 2rem; } .centered-button { min-width: 200px; background-color: #2c3e50; color: white; border: none; border-radius: 25px; padding: 1rem 2rem; font-size: 1.1rem; cursor: pointer; transition: background-color 0.3s ease; } .centered-button:hover { background-color: #34495e; } .grant-details { background: white; padding: 2rem; border-radius: 10px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); color: #333; margin: 1rem; max-height: 70vh; overflow-y: auto; } .grant-details::-webkit-scrollbar { width: 8px; } .grant-details::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .grant-details::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; } .grant-details::-webkit-scrollbar-thumb:hover { background: #555; } .grant-details h1, .grant-details h2, .grant-details h3, .grant-details h4, .grant-details h5, .grant-details h6 { color: #2c3e50; } .grant-details p { color: #333; line-height: 1.6; } .grant-details strong { color: #2c3e50; } .grant-details ul, .grant-details ol { color: #333; } .grant-details li { color: #333; line-height: 1.6; } .grant-details .loading { display: flex; justify-content: center; align-items: center; padding: 2rem; } .grant-details .loading::after { content: ""; width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #2c3e50; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } """ def build_login_form(): with gr.Column(elem_classes="login-form"): email = gr.Textbox(label="Email") password = gr.Textbox(label="Password", type="password") login_button = gr.Button("Log In") error = gr.Markdown(visible=False) return email, password, login_button, error def build_signup_form(): with gr.Column(elem_classes="signup-form"): email = gr.Textbox(label="Email") password = gr.Textbox(label="Password", type="password") name = gr.Textbox(label="School Name") with gr.Row(): address_line1 = gr.Textbox(label="Address Line 1") city = gr.Textbox(label="City") with gr.Row(): state = gr.Dropdown(["CA", "NY", "TX"], label="State") zip_code = gr.Textbox(label="ZIP Code") with gr.Row(): school_type = gr.Dropdown(["Public", "Private", "Charter", "Nonprofit", "Other"], label="School Type") educational_level = gr.Dropdown(["K-12", "Higher Education", "Both"], label="Educational Level") with gr.Row(): enrollment = gr.Number(label="Enrollment") title_i_status = gr.Checkbox(label="Title I Status") free_reduced_lunch_percentage = gr.Number(label="Free/Reduced Lunch %") contact_email = gr.Textbox(label="Contact Email") additional_info = gr.Textbox(label="Additional Info", lines=3) signup_button = gr.Button("Create Profile") error = gr.Markdown(visible=False) return ( email, password, name, address_line1, city, state, zip_code, school_type, educational_level, enrollment, title_i_status, free_reduced_lunch_percentage, contact_email, additional_info, signup_button, error ) with gr.Blocks(css=custom_css) as demo: page_state = gr.State("landing") current_school_id = gr.State(None) selected_grant = gr.State(None) selected_grant_id = gr.State(None) is_authenticated = gr.State(False) def router(page): return { landing_page: gr.update(visible=(page == "landing")), login_page: gr.update(visible=(page == "login")), signup_page: gr.update(visible=(page == "signup")), home_page: gr.update(visible=(page == "home")), grant_details_page: gr.update(visible=(page == "grant_details")), chat_page: gr.update(visible=(page == "chat")) } with gr.Column(visible=True) as landing_page: with gr.Column(elem_classes="modal-overlay"): with gr.Column(elem_classes="auth-modal"): gr.Markdown("# Welcome to GrantRight!") with gr.Column(elem_classes="landing-buttons"): create_account_btn = gr.Button("CREATE ACCOUNT", elem_classes="landing-button") login_btn = gr.Button("LOG INTO EXISTING ACCOUNT", elem_classes="landing-button") def go_signup(): return "signup" def go_login(): return "login" create_account_btn.click(go_signup, None, page_state) login_btn.click(go_login, None, page_state) with gr.Column(visible=False) as login_page: with gr.Column(elem_classes="auth-modal"): gr.Markdown("## Log In") login_email, login_password, login_submit, login_error = build_login_form() def handle_login_click(email, pwd): success, result, school_id = login(email, pwd) if success: if school_id: return { is_authenticated: True, current_school_id: school_id, page_state: "home", login_error: gr.update(visible=False) } return { is_authenticated: True, current_school_id: None, page_state: "home", login_error: gr.update(visible=False) } else: return {login_error: gr.update(visible=True, value=result)} login_submit.click( handle_login_click, inputs=[login_email, login_password], outputs=[is_authenticated, current_school_id, page_state, login_error] ) with gr.Column(visible=False) as signup_page: with gr.Column(elem_classes="auth-modal"): gr.Markdown("## Create School Profile") ( su_email, su_password, su_name, su_address, su_city, su_state, su_zip, su_type, su_level, su_enroll, su_titlei, su_lunch, su_contact, su_info, su_submit, su_error ) = build_signup_form() def handle_signup_click( username, password, name, address_line1, city, state, zip_code, school_type, educational_level, enrollment, title_i_status, free_reduced_lunch_percentage, contact_email, additional_info ): success, msg, school_id = sign_up_school( username, password, name, address_line1, city, state, zip_code, school_type, educational_level, enrollment, title_i_status, free_reduced_lunch_percentage, contact_email, additional_info ) if success: return { is_authenticated: True, current_school_id: school_id, page_state: "home", su_error: gr.update(visible=False) } else: return {su_error: gr.update(visible=True, value=msg)} su_submit.click( handle_signup_click, inputs=[ su_email, su_password, su_name, su_address, su_city, su_state, su_zip, su_type, su_level, su_enroll, su_titlei, su_lunch, su_contact, su_info ], outputs=[is_authenticated, current_school_id, page_state, su_error] ) with gr.Column(visible=False) as home_page: with gr.Row(elem_classes="nav-bar"): nav_home_btn = gr.Button("Home") school_label = gr.Markdown("") search_bar = gr.Textbox(placeholder="Example: We're looking for STEM education grants to establish a robotics program...") search_button = gr.Button("Search") search_loading_indicator = gr.Markdown("", visible=False, elem_classes="loading") grants_container = gr.Column(elem_classes="grants-container") grant_dropdown = gr.Dropdown( choices=[], label="Select a Grant", visible=False ) selected_grant_id = gr.State(None) view_grant_btn = gr.Button( "View Grant Details", visible=False ) def update_school_label(sid): if sid: return get_school_name(sid) return "No School Selected" current_school_id.change(update_school_label, current_school_id, school_label) def do_search(q, sid): df = search_grants(q, sid) if df.empty: return { grants_container: gr.update(value="

No grants found matching your search.

"), grant_dropdown: gr.update(choices=[], visible=False), view_grant_btn: gr.update(visible=False), selected_grant_id: None } df = df.nlargest(30, 'Score') html_content = "" for _, row in df.iterrows(): html_content += f"""

{row['Title']}

{row['Description'][:80]}...

Due: {row['Due_date']}

Amount: {row['Funding_amount']}

Score: {row['Score']}

""" grants_data = {} for _, row in df.iterrows(): grants_data[row['Title']] = row['id'] if 'id' in row else None choices = [(row['Title'], row['Title']) for _, row in df.iterrows()] return { grants_container: gr.update(value=html_content), grant_dropdown: gr.update(choices=choices, visible=True), view_grant_btn: gr.update(visible=True), selected_grant_id: None } def handle_grant_selection(title, sid): if not title: return "home", None, None r = supabase.table('grants').select("grant_id").eq('title', title).execute() grant_id = None if r.data and len(r.data) > 0: grant_id = r.data[0]['grant_id'] print(f"Selected grant ID: {grant_id} for title: {title}") else: print(f"No grant ID found for title: {title}") return "grant_details", title, grant_id search_button.click( lambda: gr.update(visible=True), None, search_loading_indicator, queue=False ).then( do_search, inputs=[search_bar, current_school_id], outputs=[grants_container, grant_dropdown, view_grant_btn, selected_grant_id] ).then( lambda: gr.update(visible=False), None, search_loading_indicator, queue=False ) view_grant_btn.click( handle_grant_selection, inputs=[grant_dropdown, current_school_id], outputs=[page_state, selected_grant, selected_grant_id] ) with gr.Column(visible=False) as grant_details_page: with gr.Row(elem_classes="nav-bar"): nav_home_btn2 = gr.Button("Home") school_label2 = gr.Markdown("") with gr.Column(elem_classes="grant-details"): grant_title = gr.Markdown("") grant_info = gr.Markdown("") loading_indicator = gr.Markdown("", visible=False) with gr.Row(elem_classes="help-button-container"): help_write_btn = gr.Button("Help Me Write", size="lg", elem_classes="centered-button") def load_grant_details(title): if not title: return "", "", gr.update(visible=True) return "", gr.update(visible=True), gr.update(visible=True) def process_grant_details(title): if not title: return "", "", gr.update(visible=False) r = supabase.table('grants').select("*").eq('title', title).execute() if not r.data: return "", "", gr.update(visible=False) g = r.data[0] t = f"# {g.get('title', '')}" description = g.get('description', '') try: response = client.chat.completions.create( model="gpt-4", messages=[ {"role": "system", "content": "You are a grant writing assistant. Convert the following grant description into clear, concise sentences that highlight the most important information for grant applicants. Focus on key details about the grant's purpose, requirements, and benefits. Keep the language simple and direct."}, {"role": "user", "content": description} ], max_tokens=500, temperature=0.3 ) processed_description = response.choices[0].message.content except: processed_description = description d = f""" ### Grant Details **Description** {processed_description} **Important Information** - Due Date: {g.get('due_date', '')} - Amount: {g.get('funding_amount', '')} - Provider: {g.get('provider', '')} """ return t, d, gr.update(visible=False) selected_grant.change( load_grant_details, selected_grant, [grant_title, grant_info, loading_indicator] ).then( process_grant_details, selected_grant, [grant_title, grant_info, loading_indicator] ) def go_chat(): return "chat" help_write_btn.click(go_chat, None, page_state) with gr.Column(visible=False) as chat_page: with gr.Row(elem_classes="nav-bar"): nav_home_btn3 = gr.Button("Home") school_label3 = gr.Markdown("") chat_grant_id = gr.Textbox(visible=False) chat_school_id = gr.Textbox(visible=False) def set_chat_vars(grant_id, school_id): print(f"Setting chat variables - Grant ID: {grant_id}, School ID: {school_id}") return grant_id, school_id def chat_wrapper(message, history): grant_id = chat_grant_id.value school_id = chat_school_id.value print(f"Chat wrapper called with grant_id: {grant_id}, school_id: {school_id}") return chat_with_ai(message, history, grant_id, school_id) help_write_btn.click( set_chat_vars, inputs=[selected_grant_id, current_school_id], outputs=[chat_grant_id, chat_school_id], queue=False ).then( lambda: "chat", None, page_state ) chat = gr.ChatInterface(fn=chat_wrapper) def go_home(): return "home" nav_home_btn.click(go_home, None, page_state) nav_home_btn2.click(go_home, None, page_state) nav_home_btn3.click(go_home, None, page_state) page_state.change(router, page_state, [landing_page, login_page, signup_page, home_page, grant_details_page, chat_page]) demo.load(fn=lambda: "landing", inputs=None, outputs=page_state) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, debug=True, share=True)