brainsqueeze commited on
Commit
96a926d
·
verified ·
1 Parent(s): 8ef0c0e

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +604 -0
app.py ADDED
@@ -0,0 +1,604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Tuple, TypedDict
2
+ import os
3
+
4
+ import gradio as gr
5
+ from docx import Document
6
+ from docx.shared import Pt
7
+ from fpdf import FPDF
8
+
9
+ from .editor import create_editable_section, update_highlighted_text
10
+ from .services import (
11
+ organization_pair_autofill,
12
+ cost_estimator,
13
+ draft_letter,
14
+ help_mission_statement,
15
+ send_feedback
16
+ )
17
+ from .constants import JS, CATEGORIES
18
+
19
+
20
+ class LoggedComponents(TypedDict):
21
+ context: List[gr.components.Component]
22
+ letter: List[gr.components.Component]
23
+ relevance: gr.components.Component
24
+ coherence: gr.components.Component
25
+ fluency: gr.components.Component
26
+ overall_quality: gr.components.Component
27
+ comments: gr.components.Component
28
+ email: gr.components.Component
29
+
30
+
31
+ def update_links(selected_categories: List[str]) -> str:
32
+ """Generates links of verified data sources of selected categories
33
+
34
+ Parameters
35
+ ----------
36
+ selected_categories : List[str]
37
+ One or more categories of interest
38
+
39
+ Returns
40
+ -------
41
+ str
42
+ Formatted links for display
43
+ """
44
+ all_links = ""
45
+ for category in selected_categories:
46
+ links = CATEGORIES.get(category, f"No resources available for {category}.")
47
+ all_links += f"### {category}\n" + links + "\n\n"
48
+ return all_links
49
+
50
+
51
+ def generate_pdf(*sections: str) -> str:
52
+ """Generates a downloadable PDF of LOI
53
+
54
+ Returns
55
+ -------
56
+ str
57
+ Path to the saved PDF
58
+ """
59
+ pdf = FPDF()
60
+ # pdf.set_auto_page_break(0)
61
+ pdf.add_page()
62
+
63
+ root = os.path.dirname(os.path.abspath(__file__))
64
+ pdf.add_font(family="Arial", fname=os.path.join(root, "ARIAL.TTF"))
65
+ pdf.set_font("Arial", size=12)
66
+ line_height = pdf.font_size * 1.2
67
+
68
+ pdf.multi_cell(w=0, h=line_height, text="[Your name]\n")
69
+ pdf.multi_cell(w=0, h=line_height, text="[Organization name]\n")
70
+ pdf.multi_cell(w=0, h=line_height, text="[Address]\n")
71
+ pdf.ln(line_height)
72
+ pdf.multi_cell(w=0, h=line_height, text="[Today's date]\n")
73
+ pdf.ln(line_height)
74
+ pdf.multi_cell(w=0, h=line_height, text="[Funder contact name]\n")
75
+ pdf.multi_cell(w=0, h=line_height, text="[Funder organization name]\n")
76
+ pdf.multi_cell(w=0, h=line_height, text="[Address]\n")
77
+ pdf.ln(line_height)
78
+ pdf.multi_cell(w=0, h=line_height, text="Re: [Project title]\n")
79
+ pdf.ln(line_height)
80
+
81
+ for section in sections:
82
+ # debug
83
+ print("Adding section:", section)
84
+ pdf.multi_cell(w=0, h=line_height, text=section)
85
+ pdf.ln(line_height) # Single spacing
86
+
87
+ pdf_output = "/tmp/LOI_by_Candid_AI.pdf"
88
+ pdf.output(pdf_output)
89
+ return pdf_output
90
+
91
+
92
+ def generate_docx(*sections: str) -> str:
93
+ """Generates a downloadable Word document of LOI
94
+
95
+ Returns
96
+ -------
97
+ str
98
+ Path to the saved Word document
99
+ """
100
+ doc = Document()
101
+ style = doc.styles['Normal']
102
+ style.font.name = 'Arial' # Arial font
103
+ style.font.size = Pt(12) # 12 point
104
+ style.paragraph_format.line_spacing = 1.0 # Single spacing
105
+
106
+ doc.add_paragraph("[Your name]")
107
+ doc.add_paragraph("[Organization name]")
108
+ doc.add_paragraph("[Address]")
109
+ doc.add_paragraph("")
110
+ doc.add_paragraph("[Today's date]")
111
+ doc.add_paragraph("")
112
+ doc.add_paragraph("[Funder contact name]")
113
+ doc.add_paragraph("[Funder organization name]")
114
+ doc.add_paragraph("[Address]")
115
+ doc.add_paragraph("")
116
+ doc.add_paragraph("Re: [Project title]")
117
+ doc.add_paragraph("")
118
+
119
+ for section in sections:
120
+ doc.add_paragraph(section)
121
+
122
+ docx_output = "/tmp/LOI_by_Candid_AI.docx"
123
+ doc.save(docx_output)
124
+ return docx_output
125
+
126
+
127
+ def build_drafter() -> Tuple[LoggedComponents, gr.Blocks]:
128
+ with gr.Blocks(theme=gr.themes.Soft(), title="LOI writer") as demo:
129
+ gr.Markdown("<h1>Demo: LOI writer</h1>")
130
+
131
+ with gr.Row():
132
+ with gr.Column():
133
+ gr.Markdown(
134
+ """<h2>
135
+ Step 1: Enter the name/EIN of your nonprofit and the name/EIN of the intended funder.
136
+ Use <a href='https://beta.candid.org/' target="_blank" rel="noopener noreferrer">Candid</a>
137
+ to find this information, if not known.
138
+ </h2>"""
139
+ )
140
+ org_name_text = gr.Textbox(label="*Name of your organization", lines=1)
141
+ org_ein_text = gr.Textbox(label="EIN of your organization", lines=1)
142
+ foundation_name_text = gr.Textbox(label="*To which foundation you are writing an LOI to", lines=1)
143
+ foundation_ein_text = gr.Textbox(label="EIN of the foundation", lines=1)
144
+
145
+ gr.HTML("<div style='margin-bottom: 20px;'></div>")
146
+
147
+ with gr.Row():
148
+ with gr.Column(scale=7):
149
+ gr.Markdown(
150
+ """<h2>
151
+ Step 2: Autofill with data you have
152
+ <a
153
+ href='https://candid.org/share-your-impact'
154
+ target="_blank"
155
+ rel="noopener noreferrer"
156
+ > shared with Candid</a>.
157
+ Contents can be reviewed and edited before used for LOI generation
158
+ </h2>"""
159
+ )
160
+ with gr.Column(scale=1):
161
+ autofill_btn = gr.Button("AutoFill (Optional)")
162
+
163
+ recip_json = gr.State()
164
+ # recip_candid_id = gr.State()
165
+ recip_candid_id = gr.Text(visible=False, interactive=False)
166
+ funder_json = gr.State()
167
+ # funder_candid_id = gr.State()
168
+ funder_candid_id = gr.Text(visible=False, interactive=False)
169
+
170
+ gr.HTML("<div style='margin-bottom: 20px;'></div>")
171
+ gr.Markdown('<h2>Step 3: Provide the basic information to make your "ask" clear</h2>')
172
+
173
+ projects_text = gr.Textbox(
174
+ label="*Main projects",
175
+ info="Select from the following projects or enter your own project",
176
+ interactive=True,
177
+ lines=2
178
+ )
179
+
180
+ project_name_text = gr.Textbox(
181
+ label="Project name / title",
182
+ info="Something pithy a reader remembers",
183
+ interactive=True,
184
+ lines=1
185
+ )
186
+ project_purpose_text = gr.Textbox(label="Purpose of the project", lines=1)
187
+ amount_text = gr.Textbox(label="Amount needed or requested", lines=1)
188
+ gr.Markdown(
189
+ """
190
+ <strong>Need help with estimating amount?</strong>
191
+ Run our cost estimator to get important details to consider.
192
+ Read the recommendations, decide on a number, and fill in the field above.
193
+ """
194
+ )
195
+ with gr.Row(variant='panel'):
196
+ amount_estimate_text = gr.Textbox(
197
+ label="Guide on How Much to Request",
198
+ value="Click on \"Estimate amount\" to get results",
199
+ lines=3,
200
+ scale=5
201
+ )
202
+ cost_calc_button = gr.Button(value="Estimate amount")
203
+
204
+ org_mission_statement_text = gr.Textbox(
205
+ label="Mission statement of your organization",
206
+ interactive=True, lines=2
207
+ )
208
+
209
+ with gr.Accordion("Need Help with writing mission statement? Expand to see details", open=False):
210
+ gr.Markdown(
211
+ """
212
+ <strong>Need help with writing mission statement?</strong>
213
+ Enter info to include in mission statement, and click "Get help" to generate.
214
+ """
215
+ )
216
+ textbox_info = gr.Textbox(label="Enter info")
217
+ button_help = gr.Button("Get help")
218
+
219
+ # pylint: disable=no-member
220
+ button_help.click(
221
+ help_mission_statement,
222
+ inputs=[org_name_text, textbox_info],
223
+ outputs=[org_mission_statement_text]
224
+ )
225
+
226
+ foundation_mission_statement_text = gr.Textbox(
227
+ label="Mission statement of the foundation",
228
+ interactive=True, lines=2
229
+ )
230
+ prior_contact_text = gr.Textbox(
231
+ label="Prior contact with or prior funding from the foundation (if applicable)",
232
+ interactive=True, lines=1
233
+ )
234
+ connection_text = gr.Textbox(label="Connection to the foundation's guidelines", lines=1)
235
+
236
+ with gr.Accordion():
237
+ gr.Markdown("""
238
+ <h2>Step 4: Consider answering the following questions to make your LOI sections more detailed and compelling</h2>
239
+ """)
240
+ gr.Markdown("<h3>Organization Description</h3>")
241
+ capacity_text = gr.Textbox(
242
+ label="What is capacity of the organization to meet the stated need?",
243
+ interactive=True, lines=3
244
+ )
245
+ history_text = gr.Textbox(
246
+ label=(
247
+ "Share a very brief history/description of current programs that showcase credibility "
248
+ "and commitment."
249
+ ),
250
+ interactive=True, lines=3
251
+ )
252
+ path_text = gr.Textbox(
253
+ label=(
254
+ "Demonstrate direct connection between what is currently being done and what you wish to "
255
+ "accomplish with the requested funding."
256
+ ),
257
+ interactive=True, lines=3
258
+ )
259
+ accomplishment_text = gr.Textbox(
260
+ label="What recent organizational accomplishment is most aligned with this project?",
261
+ interactive=True, lines=3
262
+ )
263
+
264
+ gr.Markdown("<h3>Need Statement</h3>")
265
+ target_text = gr.Textbox(
266
+ label=(
267
+ "A description of the target population and geographical area. Who will benefit from this "
268
+ "project?"
269
+ ),
270
+ interactive=True, lines=2
271
+ )
272
+ data_text = gr.Textbox(
273
+ label=(
274
+ "Appropriate statistical data in abbreviated form."
275
+ ),
276
+ info="Please cite reliable sources: current media, research studies / reports",
277
+ interactive=True, lines=3
278
+ )
279
+
280
+ gr.Markdown(
281
+ """<strong>Need help with finding data sources?</strong>
282
+ See below for selected statistics sources recommended by Candid Learning.
283
+ """
284
+ )
285
+
286
+ with gr.Row():
287
+ category_dropdown = gr.Dropdown(
288
+ label="Select One or More Categories",
289
+ choices=list(CATEGORIES.keys()),
290
+ multiselect=True
291
+ )
292
+ data_source_button = gr.Button(value="Get Resource Links by Category")
293
+ link_markdown = gr.Markdown()
294
+
295
+ gr.Markdown("<h3>Project Description</h3>")
296
+ desired_objectives_text = gr.Textbox(
297
+ label="Describe the desired objectives for this project.",
298
+ interactive=True, lines=2
299
+ )
300
+ major_activities_text = gr.Textbox(
301
+ label="Describe the major activities of this project.",
302
+ interactive=True, lines=2
303
+ )
304
+ key_staff_text = gr.Textbox(
305
+ label="Names and titles of key project staff.",
306
+ interactive=True, lines=2
307
+ )
308
+ stand_out_text = gr.Textbox(
309
+ label="What is going to make your project/organization stand out?",
310
+ interactive=True, lines=2
311
+ )
312
+ success_text = gr.Textbox(label="How will you know if the project is successful?", lines=2)
313
+
314
+ gr.Markdown("<h3>Funding Request</h3>")
315
+ funding_history_text = gr.Textbox(label="What is your funding history?", interactive=True, lines=2)
316
+ other_funding_text = gr.Textbox(
317
+ label=(
318
+ "Other funding sources being approached for support of this project should be listed in a "
319
+ "brief sentence/paragraph."
320
+ ),
321
+ interactive=True, lines=3
322
+ )
323
+ follow_up_text = gr.Textbox(
324
+ label=(
325
+ "Affirm your readiness to answer further questions and suggest how the funder can learn "
326
+ "more. Be clear about how to follow up."
327
+ ),
328
+ interactive=True, lines=2
329
+ )
330
+
331
+ gr.Markdown(
332
+ '<h2>Step 5: Review your input and submit for generation</h2>'
333
+ )
334
+ write_btn = gr.Button("Write LOI")
335
+
336
+ with gr.Column():
337
+ gr.Markdown(
338
+ """<h2>
339
+ Step 6: Review the generated sections.
340
+ Click on "Edit Section" to run the editorial AI for rewrite suggestions, or make your own edits.
341
+ </h2>"""
342
+ )
343
+ opening = gr.Textbox(label="Opening", interactive=True, lines=3)
344
+ create_editable_section("Opening", JS, update_highlighted_text, opening)
345
+
346
+
347
+ org_desc = gr.Textbox(label="Organization Description", interactive=True, lines=10)
348
+ create_editable_section("Organization Description", JS, update_highlighted_text, org_desc)
349
+
350
+ need = gr.Textbox(label="Need", interactive=True, lines=10)
351
+ create_editable_section("Need", JS, update_highlighted_text, need)
352
+
353
+ project_desc = gr.Textbox(label="Project Description", interactive=True, lines=10)
354
+ create_editable_section("Project Description", JS, update_highlighted_text, project_desc)
355
+
356
+ fund_request = gr.Textbox(label="Funding Request", interactive=True, lines=5)
357
+ create_editable_section("Funding Request", JS, update_highlighted_text, fund_request)
358
+
359
+ conclusion = gr.Textbox(label="Conclusion", interactive=True, lines=5)
360
+ create_editable_section("Conclusion", JS, update_highlighted_text, conclusion)
361
+
362
+ gr.Markdown(
363
+ """<h2>
364
+ Step 7: Select your preferred document type
365
+ and scroll to the bottom of the page to download the file.
366
+ </h2>"""
367
+ )
368
+
369
+ with gr.Row():
370
+ download_pdf_button = gr.Button("Download as PDF")
371
+ download_word_button = gr.Button("Download as docx")
372
+
373
+ # pylint: disable=no-member
374
+ download_pdf_button.click(
375
+ generate_pdf,
376
+ inputs=[opening, org_desc, need, project_desc, fund_request, conclusion],
377
+ outputs=gr.File(label="Download as PDF")
378
+ )
379
+
380
+ # pylint: disable=no-member
381
+ download_word_button.click(
382
+ generate_docx,
383
+ inputs=[opening, org_desc, need, project_desc, fund_request, conclusion],
384
+ outputs=gr.File(label="Download as docx")
385
+ )
386
+
387
+ # pylint: disable=no-member
388
+ autofill_btn.click(
389
+ fn=organization_pair_autofill,
390
+ inputs=[org_name_text, org_ein_text, foundation_name_text, foundation_ein_text],
391
+ outputs=[
392
+ projects_text,
393
+ capacity_text,
394
+ follow_up_text,
395
+ accomplishment_text,
396
+ org_mission_statement_text,
397
+ foundation_mission_statement_text,
398
+ funding_history_text,
399
+ recip_json,
400
+ recip_candid_id,
401
+ funder_json,
402
+ funder_candid_id
403
+ ],
404
+ api_name=False,
405
+ queue=True,
406
+ )
407
+
408
+ cost_calc_button.click(
409
+ fn=cost_estimator,
410
+ inputs=[
411
+ recip_candid_id,
412
+ recip_json,
413
+ funder_json,
414
+ projects_text
415
+ ],
416
+ outputs=[amount_estimate_text],
417
+ api_name=False,
418
+ queue=True
419
+ )
420
+
421
+ data_source_button.click(fn=update_links, inputs=category_dropdown, outputs=link_markdown)
422
+
423
+ write_btn.click(
424
+ fn=draft_letter,
425
+ inputs=[
426
+ org_name_text,
427
+ foundation_name_text,
428
+ projects_text,
429
+ amount_text,
430
+ org_mission_statement_text,
431
+ foundation_mission_statement_text,
432
+ project_name_text,
433
+ project_purpose_text,
434
+ prior_contact_text,
435
+ connection_text,
436
+ capacity_text,
437
+ path_text,
438
+ accomplishment_text,
439
+ history_text,
440
+ funding_history_text,
441
+ target_text,
442
+ data_text,
443
+ desired_objectives_text,
444
+ major_activities_text,
445
+ key_staff_text,
446
+ stand_out_text,
447
+ success_text,
448
+ other_funding_text,
449
+ follow_up_text
450
+ ],
451
+ outputs=[opening, org_desc, need, project_desc, fund_request, conclusion],
452
+ api_name=False,
453
+ queue=True,
454
+ )
455
+
456
+ logged = LoggedComponents(
457
+ context=[
458
+ recip_candid_id,
459
+ funder_candid_id,
460
+ org_mission_statement_text,
461
+ foundation_mission_statement_text,
462
+ project_name_text,
463
+ project_purpose_text,
464
+ amount_text,
465
+ prior_contact_text,
466
+ connection_text,
467
+ capacity_text,
468
+ history_text,
469
+ accomplishment_text,
470
+ target_text,
471
+ data_text,
472
+ desired_objectives_text,
473
+ funding_history_text,
474
+ other_funding_text
475
+ ],
476
+ letter=[opening, org_desc, need, project_desc, fund_request, conclusion]
477
+ )
478
+ return logged, demo
479
+
480
+
481
+ def build_feedback(components: LoggedComponents) -> gr.Blocks:
482
+ def handle_feedback(*args):
483
+ try:
484
+ context = {
485
+ "recipient_candid_entity_id": args[0],
486
+ "funder_candid_entity_id": args[1],
487
+ "recipient_mission": args[2],
488
+ "funder_mission": args[3],
489
+ "project_name": args[4],
490
+ "project_purpose": args[5],
491
+ "amount": args[6],
492
+ "prior_contact": args[7],
493
+ "connection": args[8],
494
+ "capacity": args[9],
495
+ "history": args[10],
496
+ "accomplishment": args[11],
497
+ "target": args[12],
498
+ "data": args[13],
499
+ "desired_objectives": args[14],
500
+ "funding_history": args[15],
501
+ "other_funding": args[16],
502
+ }
503
+
504
+ letter_fields = (
505
+ "opening", "org_description", "need",
506
+ "project_description", "funding_request", "conclusion"
507
+ )
508
+ letter = dict(zip(letter_fields, args[17: (17 + 6)]))
509
+ send_feedback(
510
+ context=context,
511
+ letter=letter,
512
+ relevance=args[-6],
513
+ coherence=args[-5],
514
+ fluency=args[-4],
515
+ overall_quality=args[-3],
516
+ comments=args[-2],
517
+ email=args[-1]
518
+ )
519
+ gr.Info("Thank you for providing feedback!")
520
+ except Exception as ex:
521
+ raise gr.Error(f"Failed to submit feedback: {ex}")
522
+
523
+ with gr.Blocks(theme=gr.themes.Soft(), title="LOI writer") as demo:
524
+ gr.Markdown("<h1 align='center'>Help us improve this tool with your valuable feedback</h1>")
525
+ gr.Markdown(
526
+ "<h3> Please rate each of the following aspects on a rating scale of 1-5, "
527
+ "where 1 is 'very poor' and 5 is 'very good'.</h3>"
528
+ )
529
+
530
+ with gr.Row():
531
+ with gr.Column():
532
+ relevance = gr.Radio(
533
+ [1, 2, 3, 4, 5],
534
+ label="Relevance",
535
+ info=(
536
+ "Relevance measures the extent to which the model's generated responses are pertinent "
537
+ "and directly related to the given questions."
538
+ )
539
+ )
540
+
541
+ coherence = gr.Radio(
542
+ [1, 2, 3, 4, 5],
543
+ label="Coherence",
544
+ info=(
545
+ "Coherence evaluates how well the language model can produce output that flows smoothly, "
546
+ "reads naturally, and resembles human-like language."
547
+ )
548
+ )
549
+
550
+ fluency = gr.Radio(
551
+ [1, 2, 3, 4, 5],
552
+ label="Fluency",
553
+ info=(
554
+ "Fluency evaluates the language proficiency of a generative AI's predicted answer. It assesses "
555
+ "how well the generated text adheres to grammatical rules, syntactic structures, and "
556
+ "appropriate usage of vocabulary, resulting in linguistically correct and "
557
+ "natural-sounding responses."
558
+ )
559
+ )
560
+
561
+ overall_quality = gr.Radio([1, 2, 3, 4, 5], label="Overall Quality")
562
+ comment = gr.Textbox(label="Additional comments (optional)", lines=4)
563
+ email = gr.Textbox(label="Your email (optional)", lines=1)
564
+
565
+ components["relevance"] = relevance
566
+ components["coherence"] = coherence
567
+ components["fluency"] = fluency
568
+ components["overall_quality"] = overall_quality
569
+ components["comments"] = comment
570
+ components["email"] = email
571
+
572
+ submit = gr.Button("Submit Feedback")
573
+
574
+ # pylint: disable=no-member
575
+ submit.click(
576
+ fn=handle_feedback,
577
+ inputs=[comp for k, cl in components.items() for comp in (cl if isinstance(cl, list) else [cl])],
578
+ outputs=None,
579
+ preprocess=False,
580
+ )
581
+ return demo
582
+
583
+
584
+ def build_demo():
585
+ logger, drafter = build_drafter()
586
+ feedback = build_feedback(logger)
587
+
588
+ return gr.TabbedInterface(
589
+ interface_list=[drafter, feedback],
590
+ tab_names=["LOI writer", "Feedback"],
591
+ title="Candid's letter of intent (LOI) writer",
592
+ theme=gr.themes.Soft()
593
+ )
594
+
595
+
596
+ if __name__ == '__main__':
597
+ app = build_demo()
598
+ app.queue(max_size=5).launch(
599
+ show_api=False,
600
+ auth=[
601
+ (os.getenv("APP_USERNAME"), os.getenv("APP_PASSWORD")),
602
+ ],
603
+ auth_message="Login to Candid's letter of intent demo"
604
+ )