GodsDevProject commited on
Commit
eb5e858
·
verified ·
1 Parent(s): 2e91748

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +230 -149
app.py CHANGED
@@ -1,15 +1,15 @@
1
  import gradio as gr
2
  import time
3
  import hashlib
4
- import zipfile
5
  import io
6
- import uuid
 
 
7
  from datetime import datetime
8
- from urllib.parse import quote_plus, urlparse
9
- from collections import Counter
10
  import requests
 
11
 
12
- import plotly.graph_objects as go
13
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
14
  from reportlab.lib.styles import getSampleStyleSheet
15
 
@@ -17,27 +17,124 @@ from citations import bluebook_exhibit, table_of_authorities
17
  from foia_requests import generate_foia_request_text
18
 
19
  # ======================================================
20
- # OPTIONAL PDF TEXT EXTRACTION (STRICTLY OPT-IN)
 
 
 
 
 
 
 
 
21
  # ======================================================
22
 
23
  PDF_TEXT_AVAILABLE = False
 
 
24
  try:
25
  from pdfminer.high_level import extract_text
26
  PDF_TEXT_AVAILABLE = True
27
  except Exception:
28
- PDF_TEXT_AVAILABLE = False
 
 
 
 
 
 
29
 
30
  # ======================================================
31
- # FEATURE GATES (HF SAFE)
32
  # ======================================================
33
 
34
- ENABLE_AI = True
35
- ENABLE_PDF_EXTRACTION = True
36
- ENABLE_LITIGATION_PDF = True
37
- ENABLE_COVERAGE_HEATMAP = True
38
 
39
  # ======================================================
40
- # BASE ADAPTER (LINK-OUT ONLY)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  # ======================================================
42
 
43
  class FOIAAdapter:
@@ -56,10 +153,6 @@ class FOIAAdapter:
56
  "latency_ms": latency
57
  }]
58
 
59
- # ======================================================
60
- # LIVE AGENCIES (PUBLIC READING ROOMS)
61
- # ======================================================
62
-
63
  class CIA(FOIAAdapter):
64
  agency = "CIA"
65
  search_url = "https://www.cia.gov/readingroom/search/site/{q}"
@@ -88,234 +181,222 @@ class NSA(FOIAAdapter):
88
  agency = "NSA"
89
  search_url = "https://www.nsa.gov/resources/everyone/foia/reading-room/?q={q}"
90
 
91
- LIVE_ADAPTERS = [CIA(), FBI(), DOJ(), DHS(), STATE(), GSA(), NSA()]
92
-
93
- # ======================================================
94
- # GLOBAL STATE (SESSION MEMORY ONLY)
95
- # ======================================================
96
-
97
- LAST_RESULTS = []
98
- SELECTED_INDEX = None
99
-
100
- # ======================================================
101
- # UTILITIES
102
- # ======================================================
103
-
104
- def citation_hash(r):
105
- return hashlib.sha256(
106
- f"{r['agency']}|{r['url']}|{r['timestamp']}".encode()
107
- ).hexdigest()[:16]
108
-
109
- def ai_disclosure():
110
- return (
111
- "\n\n---\n"
112
- "AI DISCLOSURE\n"
113
- "• User-initiated analysis only\n"
114
- "• Public FOIA materials only\n"
115
- "• AI output is not evidence or legal advice\n"
116
- "• Verify against original sources\n"
117
- )
118
-
119
- def hash_ai_output(text):
120
- return hashlib.sha256(text.encode()).hexdigest()
121
 
122
  # ======================================================
123
  # SEARCH
124
  # ======================================================
125
 
126
- def run_search(query):
127
- global LAST_RESULTS
 
128
  LAST_RESULTS = []
129
  rows = []
130
 
131
- for adapter in LIVE_ADAPTERS:
 
132
  for r in adapter.search(query):
133
  r["hash"] = citation_hash(r)
 
 
 
 
 
134
  LAST_RESULTS.append(r)
135
  rows.append([
136
  r["agency"],
137
  r["title"],
138
- r["url"],
139
  r["hash"],
140
  f"{r['latency_ms']} ms"
141
  ])
142
 
143
- return rows, render_cards()
144
 
145
  # ======================================================
146
- # RESULTS CARDS (POLISHED)
147
  # ======================================================
148
 
149
  def render_cards():
150
  cards = []
151
  for idx, r in enumerate(LAST_RESULTS):
152
- preview = (
153
- f"<iframe src='{r['url']}' height='220' width='100%'></iframe>"
154
- if r["url"].lower().endswith(".pdf")
155
- else f"<a href='{r['url']}' target='_blank'>Open FOIA Page</a>"
156
  )
157
-
158
  cards.append(f"""
159
  <div class="card">
160
  <div class="card-header">
161
  <b>{r['agency']}</b>
162
- <span class="badge">⏱ {r['latency_ms']} ms</span>
163
  </div>
164
  <div class="card-title">{r['title']}</div>
165
  {preview}
166
  <div class="actions">
167
- <button onclick="selectDoc({idx})">Ask AI</button>
168
- <a href="{r['url']}" target="_blank">View</a>
169
- <a href="{r['url']}" download>Download</a>
170
  </div>
171
  </div>
172
  """)
173
-
174
- return "".join(cards) if cards else "<i>No results</i>"
175
 
176
  # ======================================================
177
- # PDF EXTRACTION (OPT-IN)
178
  # ======================================================
179
 
180
- def extract_pdf_text(url):
181
- if not (PDF_TEXT_AVAILABLE and ENABLE_PDF_EXTRACTION):
182
- return ""
183
- try:
184
- r = requests.get(url, timeout=15)
185
- with open("/tmp/doc.pdf", "wb") as f:
186
- f.write(r.content)
187
- return extract_text("/tmp/doc.pdf")[:6000]
188
- except Exception:
189
- return ""
190
 
191
  # ======================================================
192
- # AI ASK + CITATION CROSS-CHECK
193
  # ======================================================
194
 
195
  def ask_ai(opt_in, pdf_opt_in, question):
196
  if not opt_in:
197
- return " AI requires explicit opt-in."
198
 
199
  if SELECTED_INDEX is None:
200
- return "Select a document first."
201
 
202
  r = LAST_RESULTS[SELECTED_INDEX]
203
- context = ""
204
 
205
- if pdf_opt_in and r["url"].lower().endswith(".pdf"):
206
- context = extract_pdf_text(r["url"])
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  analysis = (
209
- f"{bluebook_exhibit(r, SELECTED_INDEX + 1)}\n\n"
210
- f"User Question:\n{question}\n\n"
211
- f"Extracted Context:\n{context[:1500]}\n\n"
212
- f"AI Summary:\nThis is a public FOIA document. "
213
- f"Assertions should be verified against the cited exhibit."
214
  )
215
 
216
  final = analysis + ai_disclosure()
217
  return final + f"\n\nIntegrity Hash: {hash_ai_output(final)}"
218
 
219
  # ======================================================
220
- # LITIGATION APPENDIX (WITH TOA)
221
  # ======================================================
222
 
223
- def litigation_appendix():
224
  buf = io.BytesIO()
225
- doc = SimpleDocTemplate(buf)
226
  styles = getSampleStyleSheet()
 
227
  story = []
228
 
229
- story.append(Paragraph("Litigation Appendix", styles["Title"]))
230
  story.append(Spacer(1, 12))
231
 
232
- story.append(Paragraph("Table of Authorities", styles["Heading1"]))
233
- for line in table_of_authorities(LAST_RESULTS):
234
- story.append(Paragraph(line, styles["Normal"]))
235
-
236
- story.append(PageBreak())
237
-
238
  for i, r in enumerate(LAST_RESULTS, start=1):
239
- story.append(Paragraph(f"Exhibit A-{i}", styles["Heading2"]))
240
- story.append(Paragraph(bluebook_exhibit(r, i), styles["Normal"]))
241
- story.append(Spacer(1, 8))
 
 
 
242
 
243
  doc.build(story)
244
  buf.seek(0)
245
  return buf
246
 
247
  # ======================================================
248
- # COVERAGE HEATMAP
249
  # ======================================================
250
 
251
- def coverage_heatmap():
252
- counts = Counter(r["agency"] for r in LAST_RESULTS)
253
- return go.Figure(
254
- data=go.Heatmap(
255
- z=[[counts.get(a.agency, 0)] for a in LIVE_ADAPTERS],
256
- x=["Results"],
257
- y=[a.agency for a in LIVE_ADAPTERS],
258
- colorscale="Blues"
259
- ),
260
- layout=go.Layout(title="Agency Coverage Heatmap")
261
- )
262
 
263
- # ======================================================
264
- # FOIA REQUEST GENERATOR
265
- # ======================================================
 
 
 
266
 
267
- def foia_request(agency, subject, requester):
268
- return generate_foia_request_text(agency, subject, requester)
 
269
 
270
  # ======================================================
271
  # UI
272
  # ======================================================
273
 
274
  CSS = """
275
- .search textarea {font-size:18px;padding:14px}
276
  .card {border:1px solid #ddd;border-radius:14px;padding:16px;margin-bottom:18px}
277
  .card-header {display:flex;justify-content:space-between}
278
- .card-title {margin:8px 0 12px}
279
- .actions button, .actions a {margin-right:10px}
280
- .badge {background:#eef;padding:4px 8px;border-radius:8px;font-size:12px}
281
  """
282
 
283
  with gr.Blocks(css=CSS, title="Federal FOIA Intelligence Search") as app:
284
- gr.Markdown("## 🏛️ Federal FOIA Intelligence Search\nPublic Reading Rooms Only")
285
-
286
- with gr.Tab("🔍 Search"):
287
- query = gr.Textbox(
288
- label="Search FOIA Reading Rooms",
289
- elem_classes=["search"],
290
- placeholder="e.g. procurement, AATIP, surveillance"
 
 
 
 
 
 
291
  )
292
- search_btn = gr.Button("Search", variant="primary")
293
- table = gr.Dataframe(headers=["Agency","Title","URL","Hash","Latency"])
294
  gallery = gr.HTML()
295
- search_btn.click(run_search, query, [table, gallery])
 
296
 
297
- with gr.Tab("🧠 Ask AI"):
298
- ai_opt = gr.Checkbox(label="Enable AI (Explicit Opt-In)")
299
  pdf_opt = gr.Checkbox(label="Allow PDF Text Extraction")
300
- question = gr.Textbox(lines=4)
301
- answer = gr.Textbox(lines=18)
302
- gr.Button("Ask AI").click(ask_ai, [ai_opt, pdf_opt, question], answer)
303
-
304
- with gr.Tab("📊 Analysis"):
305
- gr.Button("Coverage Heatmap").click(coverage_heatmap, outputs=gr.Plot())
306
-
307
- with gr.Tab("⚖️ Court Tools"):
308
- gr.Button("Generate Litigation Appendix PDF").click(
309
- litigation_appendix, outputs=gr.File()
 
310
  )
311
 
312
- with gr.Tab("📝 FOIA Request"):
313
- agency = gr.Textbox(label="Agency")
314
- subject = gr.Textbox(label="Records Requested")
315
- requester = gr.Textbox(label="Requester Name")
316
- output = gr.Textbox(lines=14)
317
- gr.Button("Generate FOIA Request").click(
318
- foia_request, [agency, subject, requester], output
 
 
319
  )
320
 
321
  app.launch()
 
1
  import gradio as gr
2
  import time
3
  import hashlib
 
4
  import io
5
+ import json
6
+ import zipfile
7
+ import base64
8
  from datetime import datetime
9
+ from urllib.parse import quote_plus
 
10
  import requests
11
+ import os
12
 
 
13
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
14
  from reportlab.lib.styles import getSampleStyleSheet
15
 
 
17
  from foia_requests import generate_foia_request_text
18
 
19
  # ======================================================
20
+ # HARD FEATURE FLAGS (GOVERNANCE ENFORCED)
21
+ # ======================================================
22
+
23
+ ENABLE_FAISS_PHASE_4 = False # MUST remain False unless formal approval
24
+ ENABLE_AI = True
25
+ ENABLE_PDF_EXTRACTION = True
26
+
27
+ # ======================================================
28
+ # OPTIONAL PDF SUPPORT
29
  # ======================================================
30
 
31
  PDF_TEXT_AVAILABLE = False
32
+ PDF_THUMBNAIL_AVAILABLE = False
33
+
34
  try:
35
  from pdfminer.high_level import extract_text
36
  PDF_TEXT_AVAILABLE = True
37
  except Exception:
38
+ pass
39
+
40
+ try:
41
+ from pdf2image import convert_from_bytes
42
+ PDF_THUMBNAIL_AVAILABLE = True
43
+ except Exception:
44
+ pass
45
 
46
  # ======================================================
47
+ # SESSION STATE
48
  # ======================================================
49
 
50
+ LAST_RESULTS = []
51
+ SELECTED_INDEX = None
 
 
52
 
53
  # ======================================================
54
+ # HELPERS
55
+ # ======================================================
56
+
57
+ def citation_hash(r):
58
+ return hashlib.sha256(
59
+ f"{r['agency']}|{r['url']}|{r['timestamp']}".encode()
60
+ ).hexdigest()[:16]
61
+
62
+ def signed_permalink_manifest(results):
63
+ """
64
+ Deterministic, hash-anchored manifest suitable for citation or audit.
65
+ """
66
+ payload = {
67
+ "generated_utc": datetime.utcnow().isoformat(),
68
+ "tool": "Federal FOIA Intelligence Search",
69
+ "documents": [
70
+ {
71
+ "exhibit": i + 1,
72
+ "agency": r["agency"],
73
+ "title": r["title"],
74
+ "resolved_url": r["resolved_url"],
75
+ "hash": r["hash"]
76
+ }
77
+ for i, r in enumerate(results)
78
+ ]
79
+ }
80
+ payload["manifest_hash"] = hashlib.sha256(
81
+ json.dumps(payload, sort_keys=True).encode()
82
+ ).hexdigest()
83
+ return payload
84
+
85
+ def fre_callout():
86
+ return (
87
+ "FRE Reference (Educational):\n"
88
+ "• Rule 901 – Authentication\n"
89
+ "• Rule 803(8) – Public Records Exception\n"
90
+ "• Rule 1005 – Copies of Public Records\n"
91
+ "Not legal advice."
92
+ )
93
+
94
+ def ai_disclosure():
95
+ return (
96
+ "\n\n---\n"
97
+ "AI DISCLOSURE\n"
98
+ "• User-initiated only\n"
99
+ "• Public FOIA documents only\n"
100
+ "• No legal advice\n"
101
+ "• Verify against cited exhibit\n"
102
+ )
103
+
104
+ def hash_ai_output(text):
105
+ return hashlib.sha256(text.encode()).hexdigest()
106
+
107
+ def resolve_pdf_url(url):
108
+ try:
109
+ r = requests.get(
110
+ url,
111
+ timeout=15,
112
+ allow_redirects=True,
113
+ headers={"User-Agent": "FOIA-Research-Tool"}
114
+ )
115
+ ct = r.headers.get("content-type", "").lower()
116
+ is_pdf = r.url.lower().endswith(".pdf") or "application/pdf" in ct
117
+ return is_pdf, r.url
118
+ except Exception:
119
+ return False, url
120
+
121
+ def generate_pdf_thumbnails(url, max_pages=3):
122
+ if not PDF_THUMBNAIL_AVAILABLE:
123
+ return []
124
+ try:
125
+ r = requests.get(url, timeout=15)
126
+ images = convert_from_bytes(r.content, first_page=1, last_page=max_pages)
127
+ thumbs = []
128
+ for img in images:
129
+ buf = io.BytesIO()
130
+ img.save(buf, format="PNG")
131
+ thumbs.append(base64.b64encode(buf.getvalue()).decode())
132
+ return thumbs
133
+ except Exception:
134
+ return []
135
+
136
+ # ======================================================
137
+ # FOIA ADAPTERS
138
  # ======================================================
139
 
140
  class FOIAAdapter:
 
153
  "latency_ms": latency
154
  }]
155
 
 
 
 
 
156
  class CIA(FOIAAdapter):
157
  agency = "CIA"
158
  search_url = "https://www.cia.gov/readingroom/search/site/{q}"
 
181
  agency = "NSA"
182
  search_url = "https://www.nsa.gov/resources/everyone/foia/reading-room/?q={q}"
183
 
184
+ ALL_ADAPTERS = {
185
+ "CIA": CIA(),
186
+ "FBI": FBI(),
187
+ "DOJ": DOJ(),
188
+ "DHS": DHS(),
189
+ "State": STATE(),
190
+ "GSA": GSA(),
191
+ "NSA": NSA()
192
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
  # ======================================================
195
  # SEARCH
196
  # ======================================================
197
 
198
+ def run_search(query, agencies):
199
+ global LAST_RESULTS, SELECTED_INDEX
200
+ SELECTED_INDEX = None
201
  LAST_RESULTS = []
202
  rows = []
203
 
204
+ for name in agencies:
205
+ adapter = ALL_ADAPTERS[name]
206
  for r in adapter.search(query):
207
  r["hash"] = citation_hash(r)
208
+ r["resolved_pdf"], r["resolved_url"] = resolve_pdf_url(r["url"])
209
+ r["thumbnails"] = (
210
+ generate_pdf_thumbnails(r["resolved_url"])
211
+ if r["resolved_pdf"] else []
212
+ )
213
  LAST_RESULTS.append(r)
214
  rows.append([
215
  r["agency"],
216
  r["title"],
217
+ r["resolved_url"],
218
  r["hash"],
219
  f"{r['latency_ms']} ms"
220
  ])
221
 
222
+ return rows, render_cards(), "No document selected"
223
 
224
  # ======================================================
225
+ # RENDER CARDS
226
  # ======================================================
227
 
228
  def render_cards():
229
  cards = []
230
  for idx, r in enumerate(LAST_RESULTS):
231
+ thumbs = "".join(
232
+ f'<img src="data:image/png;base64,{t}" style="width:32%;margin:2px;border:1px solid #ccc" />'
233
+ for t in r["thumbnails"]
 
234
  )
235
+ preview = thumbs or f'<a href="{r["resolved_url"]}" target="_blank">Open FOIA Reading Room</a>'
236
  cards.append(f"""
237
  <div class="card">
238
  <div class="card-header">
239
  <b>{r['agency']}</b>
240
+ <span class="badge">{r['latency_ms']} ms</span>
241
  </div>
242
  <div class="card-title">{r['title']}</div>
243
  {preview}
244
  <div class="actions">
245
+ <button onclick="selectDoc({idx})">Select</button>
246
+ <a href="{r['resolved_url']}" target="_blank">View</a>
 
247
  </div>
248
  </div>
249
  """)
250
+ return "".join(cards) or "<i>No results</i>"
 
251
 
252
  # ======================================================
253
+ # DOC SELECTION
254
  # ======================================================
255
 
256
+ def select_doc(idx):
257
+ global SELECTED_INDEX
258
+ SELECTED_INDEX = idx
259
+ return f"Selected document #{idx + 1}"
 
 
 
 
 
 
260
 
261
  # ======================================================
262
+ # AI ASK
263
  # ======================================================
264
 
265
  def ask_ai(opt_in, pdf_opt_in, question):
266
  if not opt_in:
267
+ return "Explicit AI opt-in required."
268
 
269
  if SELECTED_INDEX is None:
270
+ return "Select a document first."
271
 
272
  r = LAST_RESULTS[SELECTED_INDEX]
 
273
 
274
+ if not r["resolved_pdf"]:
275
+ return "AI available only for public PDFs."
276
+
277
+ context = ""
278
+ pin_cite = "n.p."
279
+
280
+ if pdf_opt_in and PDF_TEXT_AVAILABLE:
281
+ try:
282
+ raw = extract_text(io.BytesIO(
283
+ requests.get(r["resolved_url"], timeout=15).content
284
+ ))
285
+ context = raw[:4000]
286
+ pin_cite = "p. 1"
287
+ except Exception:
288
+ pass
289
 
290
  analysis = (
291
+ f"{bluebook_exhibit(r, SELECTED_INDEX + 1, pin=pin_cite)}\n\n"
292
+ f"{fre_callout()}\n\n"
293
+ f"Question:\n{question}\n\n"
294
+ f"Context:\n{context}"
 
295
  )
296
 
297
  final = analysis + ai_disclosure()
298
  return final + f"\n\nIntegrity Hash: {hash_ai_output(final)}"
299
 
300
  # ======================================================
301
+ # CLERK EXHIBIT PACKET (PDF)
302
  # ======================================================
303
 
304
+ def generate_exhibit_packet():
305
  buf = io.BytesIO()
 
306
  styles = getSampleStyleSheet()
307
+ doc = SimpleDocTemplate(buf)
308
  story = []
309
 
310
+ story.append(Paragraph("Exhibit Packet (Clerk Format)", styles["Title"]))
311
  story.append(Spacer(1, 12))
312
 
 
 
 
 
 
 
313
  for i, r in enumerate(LAST_RESULTS, start=1):
314
+ story.append(Paragraph(
315
+ f"Exhibit {i}: {r['agency']} — {r['title']}", styles["Heading2"]
316
+ ))
317
+ story.append(Paragraph(r["resolved_url"], styles["Normal"]))
318
+ story.append(Paragraph(f"Hash: {r['hash']}", styles["Code"]))
319
+ story.append(Spacer(1, 12))
320
 
321
  doc.build(story)
322
  buf.seek(0)
323
  return buf
324
 
325
  # ======================================================
326
+ # PACER-READY BUNDLE (ZIP)
327
  # ======================================================
328
 
329
+ def generate_pacer_bundle():
330
+ buf = io.BytesIO()
331
+ z = zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED)
 
 
 
 
 
 
 
 
332
 
333
+ manifest = signed_permalink_manifest(LAST_RESULTS)
334
+ z.writestr("manifest.json", json.dumps(manifest, indent=2))
335
+ z.writestr("README.txt",
336
+ "PACER-Ready Educational Bundle\n"
337
+ "No filing performed. User responsible for review.\n"
338
+ )
339
 
340
+ z.close()
341
+ buf.seek(0)
342
+ return buf
343
 
344
  # ======================================================
345
  # UI
346
  # ======================================================
347
 
348
  CSS = """
 
349
  .card {border:1px solid #ddd;border-radius:14px;padding:16px;margin-bottom:18px}
350
  .card-header {display:flex;justify-content:space-between}
351
+ .badge {background:#eef;padding:4px 8px;border-radius:8px}
 
 
352
  """
353
 
354
  with gr.Blocks(css=CSS, title="Federal FOIA Intelligence Search") as app:
355
+ gr.Markdown("## Federal FOIA Intelligence Search\nPublic Reading Rooms Only")
356
+
357
+ gr.HTML("""
358
+ <button onclick="window.open('/governance-site/index.html','_blank')">
359
+ Governance & Trust Documentation
360
+ </button>
361
+ """)
362
+
363
+ with gr.Tab("Search"):
364
+ agencies = gr.CheckboxGroup(
365
+ choices=list(ALL_ADAPTERS.keys()),
366
+ value=list(ALL_ADAPTERS.keys()),
367
+ label="Agencies"
368
  )
369
+ query = gr.Textbox(placeholder="e.g. AATIP, surveillance")
370
+ table = gr.Dataframe(headers=["Agency","Title","Resolved URL","Hash","Latency"])
371
  gallery = gr.HTML()
372
+ status = gr.Textbox(label="Selection Status")
373
+ gr.Button("Search").click(run_search, [query, agencies], [table, gallery, status])
374
 
375
+ with gr.Tab("Ask AI"):
376
+ ai_opt = gr.Checkbox(label="Enable AI")
377
  pdf_opt = gr.Checkbox(label="Allow PDF Text Extraction")
378
+ q = gr.Textbox(lines=4)
379
+ a = gr.Textbox(lines=18)
380
+ gr.Button("Ask AI").click(ask_ai, [ai_opt, pdf_opt, q], a)
381
+
382
+ with gr.Tab("Exports"):
383
+ gr.Markdown("### Signed / Clerk / PACER Outputs")
384
+ gr.File(label="Clerk Exhibit Packet (PDF)").upload(
385
+ lambda: generate_exhibit_packet(), outputs=None
386
+ )
387
+ gr.File(label="PACER-Ready Bundle (ZIP)").upload(
388
+ lambda: generate_pacer_bundle(), outputs=None
389
  )
390
 
391
+ with gr.Tab("FOIA Request"):
392
+ agency = gr.Textbox()
393
+ subject = gr.Textbox()
394
+ requester = gr.Textbox()
395
+ out = gr.Textbox(lines=14)
396
+ gr.Button("Generate").click(
397
+ lambda a,s,r: generate_foia_request_text(a,s,r),
398
+ [agency, subject, requester],
399
+ out
400
  )
401
 
402
  app.launch()