Arjun Agarwal commited on
Commit
d58e2f4
·
1 Parent(s): f1a2033

entire app

Browse files
Files changed (2) hide show
  1. app.py +302 -61
  2. schema.sql +172 -0
app.py CHANGED
@@ -4,14 +4,16 @@ import os
4
  import time
5
  from supabase import create_client
6
  import pandas as pd
 
7
  from dotenv import load_dotenv
 
8
 
9
  # Load environment variables
10
  load_dotenv()
11
 
12
  # Initialize OpenAI client
13
  client = OpenAI(
14
- api_key=os.environ.get("API_TOKEN"),
15
  )
16
 
17
  # Initialize Supabase client
@@ -20,83 +22,322 @@ supabase = create_client(
20
  os.getenv("SUPABASE_KEY", "")
21
  )
22
 
23
- def predict(message, history, system_prompt, model, max_tokens, temperature, top_p):
24
- messages = [{"role": "system", "content": system_prompt}]
25
- messages.extend(history if history else [])
26
- messages.append({"role": "user", "content": message})
27
-
28
- start_time = time.time()
29
- response = client.chat.completions.create(
30
- model=model,
31
- messages=messages,
32
- max_tokens=max_tokens,
33
- temperature=temperature,
34
- top_p=top_p,
35
- stop=None,
36
- stream=True
37
  )
 
38
 
39
- full_message = ""
40
- first_chunk_time = None
41
- last_yield_time = None
42
 
43
- for chunk in response:
44
- if chunk.choices and chunk.choices[0].delta.content:
45
- if first_chunk_time is None:
46
- first_chunk_time = time.time() - start_time
 
 
 
 
 
 
47
 
48
- full_message += chunk.choices[0].delta.content
49
- current_time = time.time()
50
- chunk_time = current_time - start_time
51
- print(f"Message received {chunk_time:.2f} seconds after request: {chunk.choices[0].delta.content}")
 
 
 
 
 
 
 
 
52
 
53
- if last_yield_time is None or (current_time - last_yield_time >= 0.25):
54
- yield full_message
55
- last_yield_time = current_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- if full_message:
58
- total_time = time.time() - start_time
59
- full_message += f" (First Chunk: {first_chunk_time:.2f}s, Total: {total_time:.2f}s)"
60
- yield full_message
 
 
 
 
 
61
 
62
- def fetch_table_data(table_name):
63
  try:
64
- response = supabase.table(table_name).select("*").execute()
65
- df = pd.DataFrame(response.data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  return df
67
  except Exception as e:
68
  return f"Error: {str(e)}"
69
 
70
- with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  with gr.Tabs():
72
- with gr.Tab("Chatbot"):
73
- chat_interface = gr.ChatInterface(
74
- fn=predict,
75
- additional_inputs=[
76
- gr.Textbox("You are a helpful AI assistant.", label="System Prompt"),
77
- gr.Dropdown(["gpt-4o", "gpt-4o-mini"], label="Model"),
78
- gr.Slider(800, 4000, value=2000, label="Max Token"),
79
- gr.Slider(0, 1, value=0.7, label="Temperature"),
80
- gr.Slider(0, 1, value=0.95, label="Top P"),
81
- ],
82
- css="footer{display:none !important}"
 
 
 
 
 
 
 
 
 
83
  )
84
 
85
- with gr.Tab("Table Viewer"):
86
- gr.Markdown("# Supabase Table Viewer")
87
- with gr.Row():
88
- table_input = gr.Textbox(
89
- label="Table Name",
90
- placeholder="Enter the table name",
91
- value=""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  )
93
- view_btn = gr.Button("View Table")
94
- output_table = gr.Dataframe()
95
 
96
- view_btn.click(
97
- fn=fetch_table_data,
98
- inputs=[table_input],
99
- outputs=[output_table]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  )
101
 
102
  if __name__ == "__main__":
 
4
  import time
5
  from supabase import create_client
6
  import pandas as pd
7
+ import numpy as np
8
  from dotenv import load_dotenv
9
+ from typing import List, Dict
10
 
11
  # Load environment variables
12
  load_dotenv()
13
 
14
  # Initialize OpenAI client
15
  client = OpenAI(
16
+ api_key=os.environ.get("OPENAI_API_KEY"),
17
  )
18
 
19
  # Initialize Supabase client
 
22
  os.getenv("SUPABASE_KEY", "")
23
  )
24
 
25
+ def get_embedding(text: str) -> List[float]:
26
+ """Get OpenAI embedding for text."""
27
+ response = client.embeddings.create(
28
+ model="text-embedding-3-small",
29
+ input=text
 
 
 
 
 
 
 
 
 
30
  )
31
+ return response.data[0].embedding
32
 
33
+ def cosine_similarity(a: List[float], b: List[float]) -> float:
34
+ """Calculate cosine similarity between two vectors."""
35
+ return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
36
 
37
+ def sign_up_school(username, password, name, address_line1, city, state, zip_code,
38
+ school_type, educational_level, enrollment, title_i_status,
39
+ free_reduced_lunch_percentage, contact_email, additional_info):
40
+ try:
41
+ # First, create auth user
42
+ auth_response = supabase.auth.sign_up({
43
+ "email": username,
44
+ "password": password
45
+ })
46
+ user_id = auth_response.user.id
47
 
48
+ # Create school profile text for embedding
49
+ profile_text = f"""
50
+ School: {name}
51
+ Type: {school_type}
52
+ Level: {educational_level}
53
+ Title I: {title_i_status}
54
+ Free/Reduced Lunch: {free_reduced_lunch_percentage}%
55
+ Additional Info: {additional_info}
56
+ """
57
+
58
+ # Get embedding for school profile
59
+ profile_embedding = get_embedding(profile_text)
60
 
61
+ # Then, insert school details
62
+ school_data = {
63
+ "user_id": user_id,
64
+ "name": name,
65
+ "address_line1": address_line1,
66
+ "city": city,
67
+ "state": state,
68
+ "zip_code": zip_code,
69
+ "school_type": school_type,
70
+ "educational_level": educational_level,
71
+ "enrollment": enrollment,
72
+ "title_i_status": title_i_status,
73
+ "free_reduced_lunch_percentage": free_reduced_lunch_percentage,
74
+ "contact_email": contact_email,
75
+ "additional_info": additional_info,
76
+ "embedding": profile_embedding
77
+ }
78
+
79
+ response = supabase.table('schools').insert(school_data).execute()
80
+ return "School profile created successfully!"
81
+ except Exception as e:
82
+ return f"Error: {str(e)}"
83
 
84
+ def login(username, password):
85
+ try:
86
+ response = supabase.auth.sign_in_with_password({
87
+ "email": username,
88
+ "password": password
89
+ })
90
+ return "Login successful!", response.user.id
91
+ except Exception as e:
92
+ return f"Error: {str(e)}", None
93
 
94
+ def search_grants(query, school_id=None):
95
  try:
96
+ # Get query embedding
97
+ query_embedding = get_embedding(query)
98
+
99
+ # Get school profile data if available
100
+ school_data = None
101
+ if school_id:
102
+ school_response = supabase.table('schools').select("*").eq('id', school_id).execute()
103
+ if school_response.data:
104
+ school_data = school_response.data[0]
105
+
106
+ # Get all grants
107
+ grants_response = supabase.table('grants').select("*").execute()
108
+ grants = grants_response.data
109
+
110
+ # Calculate relevance scores using embeddings
111
+ scored_grants = []
112
+ for grant in grants:
113
+ if grant.get('embedding'):
114
+ # Calculate similarity between query and grant
115
+ query_similarity = cosine_similarity(query_embedding, grant['embedding'])
116
+
117
+ # If we have school data, also consider school-grant similarity
118
+ school_similarity = 0
119
+ if school_data and school_data.get('embedding'):
120
+ school_similarity = cosine_similarity(school_data['embedding'], grant['embedding'])
121
+
122
+ # Combined score (70% query relevance, 30% school profile match)
123
+ total_score = (0.7 * query_similarity) + (0.3 * school_similarity)
124
+
125
+ scored_grants.append({
126
+ 'Title': grant['title'],
127
+ 'Provider': grant['provider'],
128
+ 'Due_date': grant['due_date'],
129
+ 'Description': grant['description'],
130
+ 'Funding_amount': grant['funding_amount'],
131
+ 'Score': total_score
132
+ })
133
+
134
+ # Sort by score and convert to DataFrame
135
+ scored_grants.sort(key=lambda x: x['Score'], reverse=True)
136
+ df = pd.DataFrame(scored_grants)
137
  return df
138
  except Exception as e:
139
  return f"Error: {str(e)}"
140
 
141
+ def chat_with_ai(message, history, grant_id=None, school_id=None):
142
+ try:
143
+ # Get grant and school data
144
+ grant_data = supabase.table('grants').select("*").eq('id', grant_id).execute()
145
+ school_data = supabase.table('schools').select("*").eq('id', school_id).execute()
146
+
147
+ # Add context to the system prompt
148
+ system_prompt = f"""You are an AI assistant helping with grant applications.
149
+ Grant Details: {grant_data.data[0] if grant_data.data else 'Not specified'}
150
+ School Details: {school_data.data[0] if school_data.data else 'Not specified'}
151
+
152
+ Your role is to help schools complete grant applications effectively by:
153
+ 1. Understanding the grant requirements and eligibility criteria
154
+ 2. Crafting responses that align with the school's profile and needs
155
+ 3. Ensuring all responses are specific, detailed, and compelling
156
+ 4. Highlighting the school's strengths and unique qualities
157
+ 5. Providing guidance on required documentation and attachments
158
+
159
+ Please be specific, professional, and thorough in your responses.
160
+ """
161
+
162
+ messages = [{"role": "system", "content": system_prompt}]
163
+ messages.extend(history if history else [])
164
+ messages.append({"role": "user", "content": message})
165
+
166
+ response = client.chat.completions.create(
167
+ model="gpt-4",
168
+ messages=messages,
169
+ max_tokens=2000,
170
+ temperature=0.7,
171
+ stream=True
172
+ )
173
+
174
+ full_message = ""
175
+ for chunk in response:
176
+ if chunk.choices and chunk.choices[0].delta.content:
177
+ full_message += chunk.choices[0].delta.content
178
+ yield full_message
179
+
180
+ # Store chat history in database
181
+ if grant_id and school_id:
182
+ # Get or create application record
183
+ app_response = supabase.table('applications').select("id").eq('grant_id', grant_id).eq('school_id', school_id).execute()
184
+ if not app_response.data:
185
+ app_response = supabase.table('applications').insert({
186
+ 'grant_id': grant_id,
187
+ 'school_id': school_id,
188
+ 'status': 'Draft'
189
+ }).execute()
190
+
191
+ application_id = app_response.data[0]['id']
192
+
193
+ # Store messages
194
+ supabase.table('application_chat_history').insert([
195
+ {
196
+ 'application_id': application_id,
197
+ 'message_type': 'user',
198
+ 'content': message
199
+ },
200
+ {
201
+ 'application_id': application_id,
202
+ 'message_type': 'assistant',
203
+ 'content': full_message
204
+ }
205
+ ]).execute()
206
+
207
+ except Exception as e:
208
+ yield f"Error: {str(e)}"
209
+
210
+ # Custom CSS for better UI
211
+ custom_css = """
212
+ .gradio-container {
213
+ max-width: 1200px !important;
214
+ margin: auto;
215
+ padding-top: 20px;
216
+ }
217
+ .main-header {
218
+ background-color: #2c3e50;
219
+ color: white;
220
+ padding: 1rem;
221
+ margin: -20px -20px 20px -20px;
222
+ text-align: center;
223
+ }
224
+ .grant-box {
225
+ border: 1px solid #ddd;
226
+ padding: 15px;
227
+ margin: 10px 0;
228
+ border-radius: 5px;
229
+ background-color: white;
230
+ }
231
+ .grant-box:hover {
232
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
233
+ }
234
+ """
235
+
236
+ with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
237
+ # Session state for user
238
+ current_user_id = gr.State(None)
239
+ current_school_id = gr.State(None)
240
+
241
  with gr.Tabs():
242
+ # Entry Page
243
+ with gr.Tab("Entry"):
244
+ with gr.Box(elem_classes="main-header"):
245
+ gr.Markdown("# Welcome to GrantRight")
246
+ gr.Markdown("### Connecting Schools with Grant Opportunities")
247
+
248
+ with gr.Row():
249
+ with gr.Column():
250
+ create_profile_btn = gr.Button("Create School Profile", size="large")
251
+ with gr.Column():
252
+ with gr.Group():
253
+ login_username = gr.Textbox(label="Username")
254
+ login_password = gr.Textbox(label="Password", type="password")
255
+ login_btn = gr.Button("Log In", size="large")
256
+ login_status = gr.Textbox(label="Status", interactive=False)
257
+
258
+ login_btn.click(
259
+ fn=login,
260
+ inputs=[login_username, login_password],
261
+ outputs=[login_status, current_user_id]
262
  )
263
 
264
+ # Create School Profile
265
+ with gr.Tab("Create Profile"):
266
+ gr.Markdown("# Create School Profile")
267
+ with gr.Group():
268
+ username = gr.Textbox(label="Username (Email)")
269
+ password = gr.Textbox(label="Password", type="password")
270
+ name = gr.Textbox(label="School Name")
271
+ with gr.Row():
272
+ address = gr.Textbox(label="Address Line 1")
273
+ city = gr.Textbox(label="City")
274
+ state = gr.Dropdown(choices=["CA", "NY", "TX"], label="State")
275
+ zip_code = gr.Textbox(label="ZIP Code")
276
+ with gr.Row():
277
+ school_type = gr.Dropdown(
278
+ choices=["Public", "Private", "Charter", "Nonprofit", "Other"],
279
+ label="School Type"
280
+ )
281
+ educational_level = gr.Dropdown(
282
+ choices=["K-12", "Higher Education", "Both"],
283
+ label="Educational Level"
284
+ )
285
+ with gr.Row():
286
+ enrollment = gr.Number(label="Enrollment")
287
+ title_i = gr.Checkbox(label="Title I Status")
288
+ lunch_percentage = gr.Number(label="Free/Reduced Lunch Percentage")
289
+ email = gr.Textbox(label="Contact Email")
290
+ additional_info = gr.Textbox(label="Additional Information", lines=3)
291
+ submit_profile = gr.Button("Create Profile", size="large")
292
+ status = gr.Textbox(label="Status", interactive=False)
293
+
294
+ submit_profile.click(
295
+ fn=sign_up_school,
296
+ inputs=[username, password, name, address, city, state, zip_code,
297
+ school_type, educational_level, enrollment, title_i,
298
+ lunch_percentage, email, additional_info],
299
+ outputs=[status]
300
+ )
301
+
302
+ # Home Page (Grant Search)
303
+ with gr.Tab("Home"):
304
+ with gr.Box(elem_classes="main-header"):
305
+ gr.Markdown("# Find Your Perfect Grant Match")
306
+
307
+ with gr.Group():
308
+ search_input = gr.Textbox(
309
+ label="Enter information about school goals and grants you're interested in",
310
+ lines=3,
311
+ placeholder="Example: We're looking for STEM education grants to establish a robotics program..."
312
+ )
313
+ search_btn = gr.Button("Search Grants", size="large")
314
+ grants_output = gr.Dataframe(
315
+ headers=["Title", "Provider", "Due_date", "Description", "Funding_amount", "Score"],
316
+ label="Matching Grants",
317
+ elem_classes="grant-box"
318
  )
 
 
319
 
320
+ search_btn.click(
321
+ fn=search_grants,
322
+ inputs=[search_input, current_school_id],
323
+ outputs=[grants_output]
324
+ )
325
+
326
+ # Grant Application Chat
327
+ with gr.Tab("Grant Chat"):
328
+ with gr.Box(elem_classes="main-header"):
329
+ gr.Markdown("# Grant Application Assistant")
330
+
331
+ chat_interface = gr.ChatInterface(
332
+ fn=chat_with_ai,
333
+ title="Ask me anything about the grant application",
334
+ description="I can help you craft responses, understand requirements, and highlight your school's strengths.",
335
+ examples=[
336
+ "What information should I include about our school's demographics?",
337
+ "How should I describe our STEM program achievements?",
338
+ "What supporting documents are typically required?",
339
+ "How can I make our budget proposal more compelling?"
340
+ ]
341
  )
342
 
343
  if __name__ == "__main__":
schema.sql ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Enable the vector extension for embeddings
2
+ CREATE EXTENSION IF NOT EXISTS vector;
3
+
4
+ -- Create schools table
5
+ CREATE TABLE schools (
6
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
7
+ user_id UUID REFERENCES auth.users(id),
8
+ name TEXT NOT NULL,
9
+ address_line1 TEXT,
10
+ city TEXT,
11
+ state TEXT,
12
+ zip_code TEXT,
13
+ school_type TEXT CHECK (school_type IN ('Public', 'Private', 'Charter', 'Nonprofit', 'Other')),
14
+ educational_level TEXT CHECK (educational_level IN ('K-12', 'Higher Education', 'Both')),
15
+ enrollment INTEGER,
16
+ title_i_status BOOLEAN DEFAULT FALSE,
17
+ free_reduced_lunch_percentage FLOAT,
18
+ contact_email TEXT,
19
+ additional_info TEXT,
20
+ embedding vector(1536), -- For OpenAI embeddings
21
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()),
22
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW())
23
+ );
24
+
25
+ -- Create grants table
26
+ CREATE TABLE grants (
27
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
28
+ title TEXT NOT NULL,
29
+ url TEXT,
30
+ provider TEXT,
31
+ created_date TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()),
32
+ due_date TIMESTAMP WITH TIME ZONE,
33
+ funding_amount DECIMAL,
34
+ contact_info TEXT,
35
+ description TEXT,
36
+ embedding vector(1536), -- For OpenAI embeddings
37
+ requirements TEXT, -- Specific requirements for the grant
38
+ eligibility TEXT, -- Eligibility criteria
39
+ category TEXT[] -- Array of categories (e.g., ['STEM', 'Education', 'Technology'])
40
+ );
41
+
42
+ -- Create applications table to track grant applications
43
+ CREATE TABLE applications (
44
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
45
+ school_id UUID REFERENCES schools(id),
46
+ grant_id UUID REFERENCES grants(id),
47
+ status TEXT CHECK (status IN ('Draft', 'Submitted', 'Under Review', 'Approved', 'Rejected')),
48
+ submission_date TIMESTAMP WITH TIME ZONE,
49
+ last_updated TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()),
50
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()),
51
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()),
52
+ UNIQUE(school_id, grant_id)
53
+ );
54
+
55
+ -- Create application_chat_history table to store chat interactions
56
+ CREATE TABLE application_chat_history (
57
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
58
+ application_id UUID REFERENCES applications(id),
59
+ message_type TEXT CHECK (message_type IN ('user', 'assistant')),
60
+ content TEXT,
61
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW())
62
+ );
63
+
64
+ -- Create saved_responses table to store drafted responses
65
+ CREATE TABLE saved_responses (
66
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
67
+ application_id UUID REFERENCES applications(id),
68
+ question_key TEXT, -- Identifier for the question being answered
69
+ response_text TEXT, -- The drafted response
70
+ last_edited TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()),
71
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW())
72
+ );
73
+
74
+ -- Create application_documents table for uploaded files
75
+ CREATE TABLE application_documents (
76
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
77
+ application_id UUID REFERENCES applications(id),
78
+ document_type TEXT, -- e.g., 'Budget', 'Timeline', 'Letter of Support'
79
+ file_name TEXT,
80
+ file_url TEXT,
81
+ uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW())
82
+ );
83
+
84
+ -- Add RLS (Row Level Security) policies
85
+ ALTER TABLE schools ENABLE ROW LEVEL SECURITY;
86
+ ALTER TABLE applications ENABLE ROW LEVEL SECURITY;
87
+ ALTER TABLE application_chat_history ENABLE ROW LEVEL SECURITY;
88
+ ALTER TABLE saved_responses ENABLE ROW LEVEL SECURITY;
89
+ ALTER TABLE application_documents ENABLE ROW LEVEL SECURITY;
90
+
91
+ -- Create policies
92
+ CREATE POLICY "Users can view their own school profile"
93
+ ON schools FOR SELECT
94
+ USING (auth.uid() = user_id);
95
+
96
+ CREATE POLICY "Users can update their own school profile"
97
+ ON schools FOR UPDATE
98
+ USING (auth.uid() = user_id);
99
+
100
+ CREATE POLICY "Users can insert their own school profile"
101
+ ON schools FOR INSERT
102
+ WITH CHECK (auth.uid() = user_id);
103
+
104
+ CREATE POLICY "Users can view their own applications"
105
+ ON applications FOR SELECT
106
+ USING (EXISTS (
107
+ SELECT 1 FROM schools
108
+ WHERE schools.id = applications.school_id
109
+ AND schools.user_id = auth.uid()
110
+ ));
111
+
112
+ CREATE POLICY "Users can create their own applications"
113
+ ON applications FOR INSERT
114
+ WITH CHECK (EXISTS (
115
+ SELECT 1 FROM schools
116
+ WHERE schools.id = applications.school_id
117
+ AND schools.user_id = auth.uid()
118
+ ));
119
+
120
+ CREATE POLICY "Users can view their own chat history"
121
+ ON application_chat_history FOR SELECT
122
+ USING (EXISTS (
123
+ SELECT 1 FROM applications
124
+ JOIN schools ON schools.id = applications.school_id
125
+ WHERE application_chat_history.application_id = applications.id
126
+ AND schools.user_id = auth.uid()
127
+ ));
128
+
129
+ CREATE POLICY "Users can insert chat messages"
130
+ ON application_chat_history FOR INSERT
131
+ WITH CHECK (EXISTS (
132
+ SELECT 1 FROM applications
133
+ JOIN schools ON schools.id = applications.school_id
134
+ WHERE application_chat_history.application_id = applications.id
135
+ AND schools.user_id = auth.uid()
136
+ ));
137
+
138
+ CREATE POLICY "Users can view their saved responses"
139
+ ON saved_responses FOR SELECT
140
+ USING (EXISTS (
141
+ SELECT 1 FROM applications
142
+ JOIN schools ON schools.id = applications.school_id
143
+ WHERE saved_responses.application_id = applications.id
144
+ AND schools.user_id = auth.uid()
145
+ ));
146
+
147
+ CREATE POLICY "Users can manage their saved responses"
148
+ ON saved_responses FOR ALL
149
+ USING (EXISTS (
150
+ SELECT 1 FROM applications
151
+ JOIN schools ON schools.id = applications.school_id
152
+ WHERE saved_responses.application_id = applications.id
153
+ AND schools.user_id = auth.uid()
154
+ ));
155
+
156
+ CREATE POLICY "Users can view their documents"
157
+ ON application_documents FOR SELECT
158
+ USING (EXISTS (
159
+ SELECT 1 FROM applications
160
+ JOIN schools ON schools.id = applications.school_id
161
+ WHERE application_documents.application_id = applications.id
162
+ AND schools.user_id = auth.uid()
163
+ ));
164
+
165
+ CREATE POLICY "Users can manage their documents"
166
+ ON application_documents FOR ALL
167
+ USING (EXISTS (
168
+ SELECT 1 FROM applications
169
+ JOIN schools ON schools.id = applications.school_id
170
+ WHERE application_documents.application_id = applications.id
171
+ AND schools.user_id = auth.uid()
172
+ ));