bluenevus commited on
Commit
17fb27b
·
1 Parent(s): 34f1f65

Update app.py via AI Editor

Browse files
Files changed (1) hide show
  1. app.py +67 -167
app.py CHANGED
@@ -33,7 +33,6 @@ uploaded_documents = {}
33
  uploaded_documents_fileid = {}
34
  uploaded_proposals = {}
35
  uploaded_proposals_fileid = {}
36
- generated_documents = {}
37
  shredded_documents = {}
38
  shredded_document = None
39
  generated_response = None
@@ -146,6 +145,16 @@ def save_proposal_as_docx(proposal_text, base_filename):
146
  memf.seek(0)
147
  return memf.read()
148
 
 
 
 
 
 
 
 
 
 
 
149
  def process_document(action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None, cancel_event=None, stream=True, selected_generated_filename=None):
150
  global shredded_document, generated_response, stream_buffer
151
 
@@ -153,7 +162,7 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
153
 
154
  doc_content = None
155
  doc_fileid = None
156
- if action in ["shred", "proposal"]:
157
  if selected_filename and selected_filename in uploaded_documents:
158
  doc_content = uploaded_documents[selected_filename]
159
  doc_fileid = uploaded_documents_fileid.get(selected_filename)
@@ -258,7 +267,8 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
258
  if not cancel_event or not cancel_event.is_set():
259
  docx_bytes = save_proposal_as_docx(response, rfp_filename)
260
  generated_docx_name = f"{os.path.splitext(rfp_filename)[0]}_proposal.docx"
261
- generated_documents[generated_docx_name] = docx_bytes
 
262
  result_holder["text"] = response
263
  result_holder["docx_name"] = generated_docx_name
264
  else:
@@ -277,29 +287,15 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
277
  return result_holder["text"], None, result_holder["docx_name"], result_holder["text"]
278
 
279
  elif action == 'compliance':
280
- # --- COMPLIANCE FUNCTIONALITY ADDED HERE ---
281
- # Get selected generated doc (proposal) and selected RFP doc
282
- if not selected_generated_filename or selected_generated_filename not in generated_documents:
283
  return "No generated document selected for compliance.", None, None, None
284
  if not selected_filename or selected_filename not in uploaded_documents:
285
  return "No RFP/SOW/PWS/RFI document selected for compliance.", None, None, None
286
 
287
- proposal_docx_bytes = generated_documents[selected_generated_filename]
288
  rfp_text = uploaded_documents[selected_filename]
 
289
  logging.info(f"Compliance check: comparing proposal [{selected_generated_filename}] to RFP [{selected_filename}]")
290
 
291
- # Decode the proposal docx to text (best-effort, using python-docx)
292
- proposal_text = ""
293
- try:
294
- file_stream = io.BytesIO(proposal_docx_bytes)
295
- doc = Document(file_stream)
296
- proposal_text = "\n".join([para.text for para in doc.paragraphs])
297
- if not proposal_text.strip():
298
- proposal_text = "[Unable to extract text from proposal docx]"
299
- except Exception as e:
300
- proposal_text = "[Error decoding proposal docx: {}]".format(str(e))
301
- logging.error("Error extracting text from proposal docx: %s", e)
302
-
303
  prompt = (
304
  "You are a proposal compliance expert. Use the following RFP/SOW/PWS/RFI and the generated proposal response. "
305
  "Compare the proposal to the RFP requirements and win themes. "
@@ -325,6 +321,14 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
325
  break
326
  result = stream_buffer["preview"]
327
  result_holder["text"] = result
 
 
 
 
 
 
 
 
328
  logging.info("Compliance check completed.")
329
  except Exception as e:
330
  logging.error("Error during compliance check: %s", e)
@@ -334,7 +338,7 @@ def process_document(action, selected_filename=None, chat_input=None, rfp_decode
334
  t = Thread(target=thread_compliance)
335
  t.start()
336
  t.join()
337
- return result_holder["text"], None, None, result_holder["text"]
338
 
339
  elif action == 'recover':
340
  return "Recovery not implemented yet.", None, None, None
@@ -350,16 +354,14 @@ def get_uploaded_doc_list(docdict):
350
  doc_list = []
351
  for filename in docdict:
352
  file_content = docdict[filename]
353
- if filename.lower().endswith('.docx'):
354
- mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
355
- b64 = None
356
- try:
357
- docx_bytes = save_shredded_as_docx(file_content, filename)
358
  b64 = base64.b64encode(docx_bytes).decode('utf-8')
359
- except Exception:
360
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
361
- else:
362
- mime = "text/plain"
363
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
364
  download_link = html.A(
365
  filename,
@@ -403,16 +405,11 @@ def get_uploaded_proposal_list(docdict):
403
  doc_list = []
404
  for filename in docdict:
405
  file_content = docdict[filename]
406
- if filename.lower().endswith('.docx'):
407
- mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
408
- b64 = None
409
- try:
410
- docx_bytes = save_proposal_as_docx(file_content, filename)
411
- b64 = base64.b64encode(docx_bytes).decode('utf-8')
412
- except Exception:
413
- b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
414
- else:
415
- mime = "text/plain"
416
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
417
  download_link = html.A(
418
  filename,
@@ -429,46 +426,6 @@ def get_uploaded_proposal_list(docdict):
429
  )
430
  return dbc.ListGroup(doc_list, flush=True)
431
 
432
- def get_generated_doc_list(docdict):
433
- if not docdict:
434
- return html.Div("No generated documents yet.", style={"wordWrap": "break-word"})
435
- doc_list = []
436
- for filename in docdict:
437
- docx_bytes = docdict[filename]
438
- b64 = base64.b64encode(docx_bytes).decode('utf-8')
439
- download_link = html.A(
440
- filename,
441
- href=f"data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,{b64}",
442
- download=filename,
443
- target="_blank",
444
- style={"wordWrap": "break-word", "marginRight": "10px", "textDecoration": "underline"}
445
- )
446
- doc_list.append(
447
- dbc.ListGroupItem([
448
- download_link,
449
- dbc.Button("Delete", id={'type': 'delete-generated-btn', 'index': filename, 'group': 'generated'}, size="sm", color="danger", className="float-end ms-2")
450
- ], className="d-flex justify-content-between align-items-center")
451
- )
452
- return dbc.ListGroup(doc_list, flush=True)
453
-
454
- def get_generated_rfp_download_section(selected_value):
455
- if not selected_value or selected_value not in generated_documents:
456
- return html.Div("No generated document selected.", style={"wordWrap": "break-word"})
457
- docx_bytes = generated_documents[selected_value]
458
- b64 = base64.b64encode(docx_bytes).decode('utf-8')
459
- download_link = html.A(
460
- f"Download {selected_value}",
461
- href=f"data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,{b64}",
462
- download=selected_value,
463
- target="_blank",
464
- style={"wordWrap": "break-word"}
465
- )
466
- return html.Div([
467
- html.H6("RFP/SOW/PWS/RFI", style={"marginTop": "10px"}),
468
- html.Div(download_link, style={"marginBottom": "15px"}),
469
- html.Div("Preview not available for docx. Download to view.", style={"wordWrap": "break-word"})
470
- ])
471
-
472
  app.layout = dbc.Container([
473
  dcc.Store(id='shred-store', data={'text': None, 'docx_name': None}),
474
  dcc.Store(id='proposal-store', data={'text': None, 'docx_name': None}),
@@ -507,15 +464,14 @@ app.layout = dbc.Container([
507
  multiple=False
508
  ),
509
  html.Hr(style={"marginTop": "20px", "marginBottom": "10px"}),
510
- html.H6("Shredded Requirements"),
511
  html.Div(get_shredded_doc_list(shredded_documents), id='shredded-doc-list')
512
  ])
513
  ], className="mb-3"),
514
-
515
  dbc.Card([
516
  dbc.CardHeader(html.H5("Proposal")),
517
  dbc.CardBody([
518
- html.H6("Uploaded Documents"),
519
  html.Div(get_uploaded_proposal_list(uploaded_proposals), id='uploaded-proposal-list'),
520
  dcc.Dropdown(
521
  id='select-proposal-dropdown',
@@ -544,22 +500,6 @@ app.layout = dbc.Container([
544
  )
545
  ])
546
  ], className="mb-3"),
547
-
548
- dbc.Card([
549
- dbc.CardHeader(html.H5("Generated Documents")),
550
- dbc.CardBody([
551
- dcc.Dropdown(
552
- id='select-generated-dropdown',
553
- options=[{'label': fn, 'value': fn} for fn in generated_documents.keys()],
554
- placeholder="Select a generated document",
555
- value=next(iter(generated_documents), None),
556
- style={"marginBottom": "10px"}
557
- ),
558
- html.Div(id='generated-rfp-section'),
559
- html.Hr(style={"marginTop": "20px", "marginBottom": "10px"}),
560
- html.Div(get_generated_doc_list(generated_documents), id='generated-doc-list'),
561
- ])
562
- ], className="mb-3")
563
  ], style={'minWidth': '260px', 'width':'30vw','maxWidth':'30vw'}, width=3),
564
 
565
  dbc.Col([
@@ -598,10 +538,9 @@ app.layout = dbc.Container([
598
  Output('select-document-dropdown', 'options'),
599
  Output('select-document-dropdown', 'value'),
600
  Output('shredded-doc-list', 'children'),
601
- Output('generated-doc-list', 'children'),
602
- Output('select-generated-dropdown', 'options'),
603
- Output('select-generated-dropdown', 'value'),
604
- Output('generated-rfp-section', 'children'),
605
  Output('stream-status', 'data'),
606
  Output('stream-interval', 'disabled'),
607
  [
@@ -616,15 +555,12 @@ app.layout = dbc.Container([
616
  State('upload-proposal', 'filename'),
617
  Input({'type': 'delete-proposal-btn', 'index': ALL, 'group': 'proposal'}, 'n_clicks'),
618
  State('select-proposal-dropdown', 'value'),
619
- Input({'type': 'delete-generated-btn', 'index': ALL, 'group': 'generated'}, 'n_clicks'),
620
- State('select-generated-dropdown', 'value'),
621
  Input({'type': 'delete-shredded-btn', 'index': ALL, 'group': 'shredded'}, 'n_clicks'),
622
  State('shredded-doc-list', 'children'),
623
- Input('select-generated-dropdown', 'value'),
624
  State('chat-input', 'value'),
625
  State('select-document-dropdown', 'value'),
626
  State('select-proposal-dropdown', 'value'),
627
- State('select-generated-dropdown', 'value'),
628
  Input('cancel-action-btn', 'n_clicks'),
629
  Input('stream-interval', 'n_intervals'),
630
  State('stream-status', 'data')
@@ -635,10 +571,9 @@ def master_callback(
635
  shred_clicks, proposal_clicks, compliance_clicks,
636
  rfp_content, rfp_filename, rfp_delete_clicks, selected_doc,
637
  proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
638
- generated_delete_clicks, selected_generated,
639
  shredded_delete_clicks, shredded_doc_children,
640
- select_generated_value,
641
- chat_input, selected_filename, selected_proposal_dropdown, selected_generated_dropdown_state,
642
  cancel_clicks,
643
  stream_n_intervals, stream_status
644
  ):
@@ -659,8 +594,7 @@ def master_callback(
659
 
660
  rfp_delete_clicks = safe_get_n_clicks(ctx, 5)
661
  proposal_delete_clicks = safe_get_n_clicks(ctx, 9)
662
- generated_delete_clicks = safe_get_n_clicks(ctx, 11)
663
- shredded_delete_clicks = safe_get_n_clicks(ctx, 13)
664
 
665
  uploaded_rfp_decoded_bytes = None
666
 
@@ -668,7 +602,7 @@ def master_callback(
668
 
669
  if triggered_id == 'stream-interval':
670
  if not stream_status or not stream_status.get('streaming'):
671
- return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, stream_status, True
672
  preview = stream_buffer.get("preview", "")
673
  still_streaming = gemini_lock.locked()
674
  if not still_streaming:
@@ -678,20 +612,18 @@ def master_callback(
678
  doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
679
  shredded_doc_list_items = get_shredded_doc_list(shredded_documents)
680
  uploaded_doc_list = get_uploaded_doc_list(uploaded_documents)
681
- generated_doc_list = get_generated_doc_list(generated_documents)
682
- generated_doc_options = [{'label': fn, 'value': fn} for fn in generated_documents.keys()]
683
- generated_doc_value = select_generated_value if select_generated_value in generated_documents else (next(iter(generated_documents), None) if generated_documents else None)
684
- generated_rfp_section = get_generated_rfp_download_section(generated_doc_value)
685
  return (
686
  shred_store, proposal_store, output_data_upload,
687
  uploaded_doc_list, doc_options, doc_value,
688
  shredded_doc_list_items,
689
- generated_doc_list, generated_doc_options, generated_doc_value,
690
- generated_rfp_section,
691
  stream_status,
692
  True
693
  )
694
- return dash.no_update, dash.no_update, dcc.Markdown(preview, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"}), dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, stream_status, False
695
 
696
  if triggered_id == 'cancel-action-btn':
697
  stream_event.set()
@@ -701,16 +633,14 @@ def master_callback(
701
  doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
702
  shredded_doc_list_items = get_shredded_doc_list(shredded_documents)
703
  uploaded_doc_list = get_uploaded_doc_list(uploaded_documents)
704
- generated_doc_list = get_generated_doc_list(generated_documents)
705
- generated_doc_options = [{'label': fn, 'value': fn} for fn in generated_documents.keys()]
706
- generated_doc_value = select_generated_value if select_generated_value in generated_documents else (next(iter(generated_documents), None) if generated_documents else None)
707
- generated_rfp_section = get_generated_rfp_download_section(generated_doc_value)
708
  return (
709
  shred_store, proposal_store, output_data_upload,
710
  uploaded_doc_list, doc_options, doc_value,
711
  shredded_doc_list_items,
712
- generated_doc_list, generated_doc_options, generated_doc_value,
713
- generated_rfp_section,
714
  {'streaming': False},
715
  True
716
  )
@@ -786,23 +716,10 @@ def master_callback(
786
  upload_triggered = True
787
  break
788
 
789
- if triggered_id and isinstance(generated_delete_clicks, list):
790
- for i, n_click in enumerate(generated_delete_clicks):
791
- if n_click:
792
- btn_id = ctx.inputs_list[11][i]['id']
793
- del_filename = btn_id['index']
794
- if del_filename in generated_documents:
795
- del generated_documents[del_filename]
796
- logging.info(f"Generated doc deleted: {del_filename}")
797
- if selected_generated == del_filename:
798
- selected_generated = None
799
- upload_triggered = True
800
- break
801
-
802
  if triggered_id and isinstance(shredded_delete_clicks, list):
803
  for i, n_click in enumerate(shredded_delete_clicks):
804
  if n_click:
805
- btn_id = ctx.inputs_list[13][i]['id']
806
  del_filename = btn_id['index']
807
  if del_filename in shredded_documents:
808
  del shredded_documents[del_filename]
@@ -814,10 +731,9 @@ def master_callback(
814
  doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
815
  shredded_doc_list_items = get_shredded_doc_list(shredded_documents)
816
  uploaded_doc_list = get_uploaded_doc_list(uploaded_documents)
817
- generated_doc_list = get_generated_doc_list(generated_documents)
818
- generated_doc_options = [{'label': fn, 'value': fn} for fn in generated_documents.keys()]
819
- generated_doc_value = select_generated_value if select_generated_value in generated_documents else (next(iter(generated_documents), None) if generated_documents else None)
820
- generated_rfp_section = get_generated_rfp_download_section(generated_doc_value)
821
 
822
  output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
823
 
@@ -829,8 +745,7 @@ def master_callback(
829
  shred_store, proposal_store, output_data_upload,
830
  uploaded_doc_list, doc_options, doc_value,
831
  shredded_doc_list_items,
832
- generated_doc_list, generated_doc_options, generated_doc_value,
833
- generated_rfp_section,
834
  {'streaming': False},
835
  True
836
  )
@@ -843,56 +758,41 @@ def master_callback(
843
  finally:
844
  gemini_lock.release()
845
  action_name = "shred" if triggered_id=="shred-action-btn" else ("proposal" if triggered_id=="proposal-action-btn" else "compliance")
846
- t = Thread(target=stream_gemini_thread, args=(action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, generated_doc_value))
 
847
  t.daemon = True
848
  t.start()
849
  output_data_upload = dcc.Markdown("Starting Gemini operation...", style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
 
850
  return (
851
  shred_store, proposal_store, output_data_upload,
852
  uploaded_doc_list, doc_options, doc_value,
853
  shredded_doc_list_items,
854
- generated_doc_list, generated_doc_options, generated_doc_value,
855
- generated_rfp_section,
856
  {'streaming': True},
857
  False
858
  )
859
 
860
- if triggered_id == 'select-generated-dropdown':
861
- generated_rfp_section = get_generated_rfp_download_section(select_generated_value)
862
- return (
863
- shred_store, proposal_store, output_data_upload,
864
- uploaded_doc_list, doc_options, doc_value,
865
- shredded_doc_list_items,
866
- generated_doc_list, generated_doc_options, generated_doc_value,
867
- generated_rfp_section,
868
- {'streaming': False},
869
- True
870
- )
871
-
872
  if upload_triggered:
873
  doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
874
- generated_doc_value = generated_doc_value if generated_doc_value in generated_documents else (next(iter(generated_documents), None) if generated_documents else None)
875
- generated_rfp_section = get_generated_rfp_download_section(generated_doc_value)
876
  output_data_upload = html.Div("Upload/Delete completed.", style={"wordWrap": "break-word"})
877
  return (
878
  shred_store, proposal_store, output_data_upload,
879
  uploaded_doc_list, doc_options, doc_value,
880
  shredded_doc_list_items,
881
- generated_doc_list, generated_doc_options, generated_doc_value,
882
- generated_rfp_section,
883
  {'streaming': False},
884
  True
885
  )
886
 
887
  doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
888
- generated_doc_value = generated_doc_value if generated_doc_value in generated_documents else (next(iter(generated_documents), None) if generated_documents else None)
889
- generated_rfp_section = get_generated_rfp_download_section(generated_doc_value)
890
  return (
891
  shred_store, proposal_store, output_data_upload,
892
  uploaded_doc_list, doc_options, doc_value,
893
  shredded_doc_list_items,
894
- generated_doc_list, generated_doc_options, generated_doc_value,
895
- generated_rfp_section,
896
  {'streaming': False},
897
  True
898
  )
 
33
  uploaded_documents_fileid = {}
34
  uploaded_proposals = {}
35
  uploaded_proposals_fileid = {}
 
36
  shredded_documents = {}
37
  shredded_document = None
38
  generated_response = None
 
145
  memf.seek(0)
146
  return memf.read()
147
 
148
+ def save_compliance_as_docx(compliance_text, rfp_filename):
149
+ doc = Document()
150
+ doc.add_heading(f"Compliance Check for {rfp_filename}", 0)
151
+ for line in compliance_text.split('\n'):
152
+ doc.add_paragraph(line)
153
+ memf = io.BytesIO()
154
+ doc.save(memf)
155
+ memf.seek(0)
156
+ return memf.read()
157
+
158
  def process_document(action, selected_filename=None, chat_input=None, rfp_decoded_bytes=None, cancel_event=None, stream=True, selected_generated_filename=None):
159
  global shredded_document, generated_response, stream_buffer
160
 
 
162
 
163
  doc_content = None
164
  doc_fileid = None
165
+ if action in ["shred", "proposal", "compliance"]:
166
  if selected_filename and selected_filename in uploaded_documents:
167
  doc_content = uploaded_documents[selected_filename]
168
  doc_fileid = uploaded_documents_fileid.get(selected_filename)
 
267
  if not cancel_event or not cancel_event.is_set():
268
  docx_bytes = save_proposal_as_docx(response, rfp_filename)
269
  generated_docx_name = f"{os.path.splitext(rfp_filename)[0]}_proposal.docx"
270
+ uploaded_proposals[generated_docx_name] = response
271
+ uploaded_proposals_fileid[generated_docx_name] = None
272
  result_holder["text"] = response
273
  result_holder["docx_name"] = generated_docx_name
274
  else:
 
287
  return result_holder["text"], None, result_holder["docx_name"], result_holder["text"]
288
 
289
  elif action == 'compliance':
290
+ if not selected_generated_filename or selected_generated_filename not in uploaded_proposals:
 
 
291
  return "No generated document selected for compliance.", None, None, None
292
  if not selected_filename or selected_filename not in uploaded_documents:
293
  return "No RFP/SOW/PWS/RFI document selected for compliance.", None, None, None
294
 
 
295
  rfp_text = uploaded_documents[selected_filename]
296
+ proposal_text = uploaded_proposals[selected_generated_filename]
297
  logging.info(f"Compliance check: comparing proposal [{selected_generated_filename}] to RFP [{selected_filename}]")
298
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  prompt = (
300
  "You are a proposal compliance expert. Use the following RFP/SOW/PWS/RFI and the generated proposal response. "
301
  "Compare the proposal to the RFP requirements and win themes. "
 
321
  break
322
  result = stream_buffer["preview"]
323
  result_holder["text"] = result
324
+ if not cancel_event or not cancel_event.is_set():
325
+ docx_bytes = save_compliance_as_docx(result, selected_filename)
326
+ compliance_docx_name = f"{os.path.splitext(selected_filename)[0]}_compliance_check.docx"
327
+ uploaded_documents[compliance_docx_name] = result
328
+ shredded_documents[compliance_docx_name] = docx_bytes
329
+ result_holder["docx_name"] = compliance_docx_name
330
+ else:
331
+ result_holder["docx_name"] = None
332
  logging.info("Compliance check completed.")
333
  except Exception as e:
334
  logging.error("Error during compliance check: %s", e)
 
338
  t = Thread(target=thread_compliance)
339
  t.start()
340
  t.join()
341
+ return result_holder["text"], None, result_holder["docx_name"], result_holder["text"]
342
 
343
  elif action == 'recover':
344
  return "Recovery not implemented yet.", None, None, None
 
354
  doc_list = []
355
  for filename in docdict:
356
  file_content = docdict[filename]
357
+ mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" if filename.lower().endswith('.docx') else "text/plain"
358
+ try:
359
+ if filename.lower().endswith('.docx') and filename in shredded_documents:
360
+ docx_bytes = shredded_documents[filename]
 
361
  b64 = base64.b64encode(docx_bytes).decode('utf-8')
362
+ else:
363
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
364
+ except Exception:
 
365
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
366
  download_link = html.A(
367
  filename,
 
405
  doc_list = []
406
  for filename in docdict:
407
  file_content = docdict[filename]
408
+ mime = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
409
+ try:
410
+ docx_bytes = save_proposal_as_docx(file_content, filename)
411
+ b64 = base64.b64encode(docx_bytes).decode('utf-8')
412
+ except Exception:
 
 
 
 
 
413
  b64 = base64.b64encode(file_content.encode('utf-8')).decode('utf-8')
414
  download_link = html.A(
415
  filename,
 
426
  )
427
  return dbc.ListGroup(doc_list, flush=True)
428
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  app.layout = dbc.Container([
430
  dcc.Store(id='shred-store', data={'text': None, 'docx_name': None}),
431
  dcc.Store(id='proposal-store', data={'text': None, 'docx_name': None}),
 
464
  multiple=False
465
  ),
466
  html.Hr(style={"marginTop": "20px", "marginBottom": "10px"}),
467
+ html.H6("Shredded/Compliance Documents"),
468
  html.Div(get_shredded_doc_list(shredded_documents), id='shredded-doc-list')
469
  ])
470
  ], className="mb-3"),
 
471
  dbc.Card([
472
  dbc.CardHeader(html.H5("Proposal")),
473
  dbc.CardBody([
474
+ html.H6("Generated Proposals"),
475
  html.Div(get_uploaded_proposal_list(uploaded_proposals), id='uploaded-proposal-list'),
476
  dcc.Dropdown(
477
  id='select-proposal-dropdown',
 
500
  )
501
  ])
502
  ], className="mb-3"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  ], style={'minWidth': '260px', 'width':'30vw','maxWidth':'30vw'}, width=3),
504
 
505
  dbc.Col([
 
538
  Output('select-document-dropdown', 'options'),
539
  Output('select-document-dropdown', 'value'),
540
  Output('shredded-doc-list', 'children'),
541
+ Output('uploaded-proposal-list', 'children'),
542
+ Output('select-proposal-dropdown', 'options'),
543
+ Output('select-proposal-dropdown', 'value'),
 
544
  Output('stream-status', 'data'),
545
  Output('stream-interval', 'disabled'),
546
  [
 
555
  State('upload-proposal', 'filename'),
556
  Input({'type': 'delete-proposal-btn', 'index': ALL, 'group': 'proposal'}, 'n_clicks'),
557
  State('select-proposal-dropdown', 'value'),
 
 
558
  Input({'type': 'delete-shredded-btn', 'index': ALL, 'group': 'shredded'}, 'n_clicks'),
559
  State('shredded-doc-list', 'children'),
560
+ State('select-proposal-dropdown', 'value'),
561
  State('chat-input', 'value'),
562
  State('select-document-dropdown', 'value'),
563
  State('select-proposal-dropdown', 'value'),
 
564
  Input('cancel-action-btn', 'n_clicks'),
565
  Input('stream-interval', 'n_intervals'),
566
  State('stream-status', 'data')
 
571
  shred_clicks, proposal_clicks, compliance_clicks,
572
  rfp_content, rfp_filename, rfp_delete_clicks, selected_doc,
573
  proposal_content, proposal_filename, proposal_delete_clicks, selected_proposal,
 
574
  shredded_delete_clicks, shredded_doc_children,
575
+ select_proposal_dropdown_value,
576
+ chat_input, selected_filename, selected_proposal_dropdown, selected_proposal_dropdown_state,
577
  cancel_clicks,
578
  stream_n_intervals, stream_status
579
  ):
 
594
 
595
  rfp_delete_clicks = safe_get_n_clicks(ctx, 5)
596
  proposal_delete_clicks = safe_get_n_clicks(ctx, 9)
597
+ shredded_delete_clicks = safe_get_n_clicks(ctx, 11)
 
598
 
599
  uploaded_rfp_decoded_bytes = None
600
 
 
602
 
603
  if triggered_id == 'stream-interval':
604
  if not stream_status or not stream_status.get('streaming'):
605
+ return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, stream_status, True
606
  preview = stream_buffer.get("preview", "")
607
  still_streaming = gemini_lock.locked()
608
  if not still_streaming:
 
612
  doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
613
  shredded_doc_list_items = get_shredded_doc_list(shredded_documents)
614
  uploaded_doc_list = get_uploaded_doc_list(uploaded_documents)
615
+ uploaded_proposal_list = get_uploaded_proposal_list(uploaded_proposals)
616
+ proposal_options = [{'label': fn, 'value': fn} for fn in uploaded_proposals.keys()]
617
+ proposal_value = select_proposal_dropdown_value if select_proposal_dropdown_value in uploaded_proposals else (next(iter(uploaded_proposals), None) if uploaded_proposals else None)
 
618
  return (
619
  shred_store, proposal_store, output_data_upload,
620
  uploaded_doc_list, doc_options, doc_value,
621
  shredded_doc_list_items,
622
+ uploaded_proposal_list, proposal_options, proposal_value,
 
623
  stream_status,
624
  True
625
  )
626
+ return dash.no_update, dash.no_update, dcc.Markdown(preview, style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"}), dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, stream_status, False
627
 
628
  if triggered_id == 'cancel-action-btn':
629
  stream_event.set()
 
633
  doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
634
  shredded_doc_list_items = get_shredded_doc_list(shredded_documents)
635
  uploaded_doc_list = get_uploaded_doc_list(uploaded_documents)
636
+ uploaded_proposal_list = get_uploaded_proposal_list(uploaded_proposals)
637
+ proposal_options = [{'label': fn, 'value': fn} for fn in uploaded_proposals.keys()]
638
+ proposal_value = select_proposal_dropdown_value if select_proposal_dropdown_value in uploaded_proposals else (next(iter(uploaded_proposals), None) if uploaded_proposals else None)
 
639
  return (
640
  shred_store, proposal_store, output_data_upload,
641
  uploaded_doc_list, doc_options, doc_value,
642
  shredded_doc_list_items,
643
+ uploaded_proposal_list, proposal_options, proposal_value,
 
644
  {'streaming': False},
645
  True
646
  )
 
716
  upload_triggered = True
717
  break
718
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719
  if triggered_id and isinstance(shredded_delete_clicks, list):
720
  for i, n_click in enumerate(shredded_delete_clicks):
721
  if n_click:
722
+ btn_id = ctx.inputs_list[11][i]['id']
723
  del_filename = btn_id['index']
724
  if del_filename in shredded_documents:
725
  del shredded_documents[del_filename]
 
731
  doc_value = selected_doc if selected_doc in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
732
  shredded_doc_list_items = get_shredded_doc_list(shredded_documents)
733
  uploaded_doc_list = get_uploaded_doc_list(uploaded_documents)
734
+ uploaded_proposal_list = get_uploaded_proposal_list(uploaded_proposals)
735
+ proposal_options = [{'label': fn, 'value': fn} for fn in uploaded_proposals.keys()]
736
+ proposal_value = select_proposal_dropdown_value if select_proposal_dropdown_value in uploaded_proposals else (next(iter(uploaded_proposals), None) if uploaded_proposals else None)
 
737
 
738
  output_data_upload = html.Div("No action taken yet.", style={"wordWrap": "break-word"})
739
 
 
745
  shred_store, proposal_store, output_data_upload,
746
  uploaded_doc_list, doc_options, doc_value,
747
  shredded_doc_list_items,
748
+ uploaded_proposal_list, proposal_options, proposal_value,
 
749
  {'streaming': False},
750
  True
751
  )
 
758
  finally:
759
  gemini_lock.release()
760
  action_name = "shred" if triggered_id=="shred-action-btn" else ("proposal" if triggered_id=="proposal-action-btn" else "compliance")
761
+ # For compliance, select_proposal_dropdown_value is the proposal doc
762
+ t = Thread(target=stream_gemini_thread, args=(action_name, doc_value, chat_input, uploaded_rfp_decoded_bytes, select_proposal_dropdown_value))
763
  t.daemon = True
764
  t.start()
765
  output_data_upload = dcc.Markdown("Starting Gemini operation...", style={"whiteSpace": "pre-wrap", "wordWrap": "break-word"})
766
+ # After action is started, future stream-interval will update the lists
767
  return (
768
  shred_store, proposal_store, output_data_upload,
769
  uploaded_doc_list, doc_options, doc_value,
770
  shredded_doc_list_items,
771
+ uploaded_proposal_list, proposal_options, proposal_value,
 
772
  {'streaming': True},
773
  False
774
  )
775
 
 
 
 
 
 
 
 
 
 
 
 
 
776
  if upload_triggered:
777
  doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
778
+ proposal_value = proposal_value if proposal_value in uploaded_proposals else (next(iter(uploaded_proposals), None) if uploaded_proposals else None)
 
779
  output_data_upload = html.Div("Upload/Delete completed.", style={"wordWrap": "break-word"})
780
  return (
781
  shred_store, proposal_store, output_data_upload,
782
  uploaded_doc_list, doc_options, doc_value,
783
  shredded_doc_list_items,
784
+ uploaded_proposal_list, proposal_options, proposal_value,
 
785
  {'streaming': False},
786
  True
787
  )
788
 
789
  doc_value = doc_value if doc_value in uploaded_documents else (next(iter(uploaded_documents), None) if uploaded_documents else None)
790
+ proposal_value = proposal_value if proposal_value in uploaded_proposals else (next(iter(uploaded_proposals), None) if uploaded_proposals else None)
 
791
  return (
792
  shred_store, proposal_store, output_data_upload,
793
  uploaded_doc_list, doc_options, doc_value,
794
  shredded_doc_list_items,
795
+ uploaded_proposal_list, proposal_options, proposal_value,
 
796
  {'streaming': False},
797
  True
798
  )