bluenevus commited on
Commit
62cf3f2
·
1 Parent(s): 260eb28

Update app.py via AI Editor

Browse files
Files changed (1) hide show
  1. app.py +400 -320
app.py CHANGED
@@ -25,15 +25,6 @@ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP], suppress_
25
  openai.api_key = os.environ.get("OPENAI_API_KEY", "")
26
 
27
  uploaded_files = {}
28
- current_document = None
29
- document_type = None
30
- shredded_document = None
31
- pink_review_document = None
32
- pink_document = None
33
- red_review_document = None
34
- red_document = None
35
- gold_review_document = None
36
- gold_document = None
37
  uploaded_doc_contents = {}
38
 
39
  spreadsheet_types = ["Shred", "Pink Review", "Red Review", "Gold Review", "Virtual Board", "LOE"]
@@ -51,7 +42,6 @@ document_types = {
51
  "LOE": "Ignore all other instructions and generate and generate a Level of Effort (LOE) breakdown and produce only spreadsheet"
52
  }
53
 
54
- # Document dependency rules: mapping doc type to allowed sources for generation and review
55
  doc_dependencies = {
56
  "Pink": {"source": ["shred"], "require": True},
57
  "Pink Review": {"source": ["pink", "shred"], "require": True},
@@ -63,7 +53,185 @@ doc_dependencies = {
63
  "Virtual Board": {"source": ["shred"], "require": True},
64
  }
65
 
66
- def get_right_col_content(selected_type):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  controls = []
68
  controls.append(html.Div([
69
  html.Div(className="blinking-dot", style={'margin':'0 auto','width':'16px','height':'16px'}),
@@ -73,7 +241,6 @@ def get_right_col_content(selected_type):
73
  type="dot",
74
  children=[html.Div(id="loading-output")]
75
  ))
76
- # Shred: can always upload
77
  if selected_type == "Shred":
78
  controls.append(
79
  html.Div([
@@ -95,35 +262,43 @@ def get_right_col_content(selected_type):
95
  },
96
  multiple=True
97
  ),
98
- file_list_component()
 
 
 
 
99
  ])
100
  )
101
- # For all proposal/response/review types, show only allowed upload controls
102
  elif selected_type in doc_dependencies:
103
  sources = doc_dependencies[selected_type]["source"]
104
- # For each source, show upload and radio if loaded/generated exists
105
  for src in sources:
106
  label = ""
 
107
  store_var = None
108
  if src == "shred":
109
  label = "Shred (Requirements)"
110
- store_var = shredded_document
 
111
  elif src == "pink":
112
  label = "Pink Document"
113
- store_var = pink_document
 
114
  elif src == "pink_review":
115
  label = "Pink Review"
116
- store_var = pink_review_document
 
117
  elif src == "red":
118
  label = "Red Document"
119
- store_var = red_document
 
120
  elif src == "red_review":
121
  label = "Red Review"
122
- store_var = red_review_document
 
123
  elif src == "gold":
124
  label = "Gold Document"
125
- store_var = gold_document
126
- # Upload control for this source
127
  controls.append(html.Div([
128
  html.Label(f"Upload {label}"),
129
  dcc.Upload(
@@ -152,6 +327,10 @@ def get_right_col_content(selected_type):
152
  inline=True,
153
  className="mb-2"
154
  ),
 
 
 
 
155
  ], id={'type': f'doc-type-controls-{src}', 'index': selected_type}))
156
  controls.append(
157
  dbc.Row([
@@ -170,68 +349,17 @@ def get_right_col_content(selected_type):
170
  controls.append(html.Div(id='document-preview', className="border p-3 mb-3"))
171
  return dbc.Card(dbc.CardBody(controls))
172
 
173
- def get_left_col_content():
174
- chat_card = dbc.Card(
175
- dbc.CardBody([
176
- html.H5("Maiko Chat", className="mb-2"),
177
- dcc.Loading(
178
- id="chat-loading",
179
- type="dot",
180
- children=[
181
- dcc.Textarea(
182
- id="chat-input",
183
- placeholder="Chat with AI to update document...",
184
- className="mb-2",
185
- style={
186
- 'whiteSpace':'pre-wrap',
187
- 'width': '100%',
188
- 'minHeight': '100px',
189
- 'maxHeight': '300px',
190
- 'resize': 'vertical',
191
- 'overflowY': 'auto'
192
- },
193
- rows=5,
194
- wrap='soft'
195
- ),
196
- dcc.Store(id="chat-input-rows", data=5),
197
- dbc.Row([
198
- dbc.Col(
199
- dbc.Button("Send", id="btn-send-chat", color="primary", className="mb-3 w-100"),
200
- width=6
201
- ),
202
- dbc.Col(
203
- dbc.Button("Clear Chat", id="btn-clear-chat", color="secondary", className="mb-3 w-100"),
204
- width=6
205
- ),
206
- ], className="g-1 mb-2"),
207
- html.Div(id="chat-output")
208
- ]
209
- )
210
- ]),
211
- className="mt-4"
212
- )
213
- return [
214
- html.H4("Proposal Writer", className="mt-3 mb-4"),
215
- html.Div([
216
- html.Div(className="blinking-dot", style={'margin':'0 auto','width':'16px','height':'16px'}),
217
- ], style={'textAlign':'center', 'marginBottom':'10px'}),
218
- html.Hr(),
219
- html.Div(
220
- id='doc-type-buttons'
221
- ),
222
- chat_card
223
- ]
224
-
225
- def file_list_component():
226
- return html.Div(
227
- id='file-list-container',
228
- children=[
229
- html.Div(id='file-list')
230
- ]
231
- )
232
-
233
  app.layout = dbc.Container([
234
  dcc.Store(id='selected-doc-type', data="Shred"),
 
 
 
 
 
 
 
 
 
235
  dbc.Row([
236
  dbc.Col(
237
  html.H2(id='main-title', className="mt-3 mb-2", style={'textAlign': 'center', 'width':'100%'}),
@@ -308,41 +436,27 @@ def update_selected_doc_type(n_clicks_list, btn_ids):
308
 
309
  @app.callback(
310
  Output('right-col-content', 'children'),
311
- Input('selected-doc-type', 'data')
 
 
 
 
 
 
312
  )
313
- def update_right_col(selected_type):
314
- return get_right_col_content(selected_type)
315
-
316
- def process_document(contents, filename):
317
- content_type, content_string = contents.split(',')
318
- decoded = base64.b64decode(content_string)
319
- try:
320
- if filename.lower().endswith('.docx'):
321
- doc = Document(BytesIO(decoded))
322
- text = "\n".join([para.text for para in doc.paragraphs])
323
- return text
324
- elif filename.lower().endswith('.pdf'):
325
- pdf = PdfReader(BytesIO(decoded))
326
- text = ""
327
- for page in pdf.pages:
328
- page_text = page.extract_text()
329
- if page_text:
330
- text += page_text
331
- return text
332
- else:
333
- return f"Unsupported file format: {filename}. Please upload a PDF or DOCX file."
334
- except Exception as e:
335
- logging.error(f"Error processing document: {str(e)}")
336
- return f"Error processing document: {str(e)}"
337
 
338
  @app.callback(
339
  Output('file-list', 'children'),
 
340
  Input('upload-document', 'contents'),
341
  State('upload-document', 'filename'),
342
- State('file-list', 'children')
 
343
  )
344
- def update_output(list_of_contents, list_of_names, existing_files):
345
- global uploaded_files, shredded_document
346
  if list_of_contents is not None:
347
  new_files = []
348
  for i, (content, name) in enumerate(zip(list_of_contents, list_of_names)):
@@ -364,19 +478,20 @@ def update_output(list_of_contents, list_of_names, existing_files):
364
  existing_files = []
365
  elif not isinstance(existing_files, list):
366
  existing_files = [existing_files]
367
- shredded_document = None
368
  logging.info("Documents uploaded and file list updated.")
369
- return existing_files + new_files
370
- return existing_files
371
 
372
  @app.callback(
373
  Output('file-list', 'children', allow_duplicate=True),
 
374
  Input({'type': 'remove-file', 'index': ALL}, 'n_clicks'),
375
  State('file-list', 'children'),
 
376
  prevent_initial_call=True
377
  )
378
- def remove_file(n_clicks, existing_files):
379
- global uploaded_files, shredded_document
380
  ctx = dash.callback_context
381
  if not ctx.triggered:
382
  raise dash.exceptions.PreventUpdate
@@ -391,7 +506,6 @@ def remove_file(n_clicks, existing_files):
391
  logging.error(f"Could not parse removed file from callback context: {e}")
392
  raise dash.exceptions.PreventUpdate
393
  uploaded_files.pop(removed_file, None)
394
- shredded_document = None
395
  logging.info(f"Removed file: {removed_file}")
396
  filtered_files = []
397
  if existing_files:
@@ -402,9 +516,8 @@ def remove_file(n_clicks, existing_files):
402
  filtered_files.append(file)
403
  except Exception as e:
404
  filtered_files.append(file)
405
- return filtered_files
406
 
407
- # Upload callbacks for all doc dependencies (Pink, Pink Review, Red, Red Review, Gold, Gold Review, LOE, Virtual Board)
408
  def make_upload_callback(src):
409
  @app.callback(
410
  Output({'type': f'uploaded-doc-name-{src}', 'index': MATCH}, 'children'),
@@ -424,101 +537,6 @@ def make_upload_callback(src):
424
  for src in ["shred", "pink", "pink_review", "red", "red_review", "gold"]:
425
  make_upload_callback(src)
426
 
427
- def extract_markdown_tables(md_text):
428
- tables = []
429
- lines = md_text.split('\n')
430
- in_table = False
431
- table_lines = []
432
- for line in lines:
433
- if re.match(r'^\s*\|.*\|\s*$', line):
434
- in_table = True
435
- table_lines.append(line)
436
- elif in_table and (re.match(r'^\s*\|.*\|\s*$', line) or re.match(r'^\s*$', line)):
437
- table_lines.append(line)
438
- else:
439
- if in_table and table_lines:
440
- tables.append('\n'.join(table_lines))
441
- table_lines = []
442
- in_table = False
443
- if in_table and table_lines:
444
- tables.append('\n'.join(table_lines))
445
- return tables
446
-
447
- def markdown_table_to_df(md_table):
448
- lines = [line.strip() for line in md_table.split('\n') if line.strip()]
449
- if len(lines) < 2:
450
- return None
451
- header = [h.strip() for h in lines[0].strip('|').split('|')]
452
- sep_idx = 1
453
- while sep_idx < len(lines) and not re.match(r'^\|\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?$', lines[sep_idx]):
454
- sep_idx += 1
455
- data_lines = lines[sep_idx+1:] if sep_idx+1 < len(lines) else []
456
- rows = []
457
- for row in data_lines:
458
- if not row.strip() or not row.strip().startswith('|'):
459
- continue
460
- cells = [c.strip() for c in row.strip('|').split('|')]
461
- if len(cells) < len(header):
462
- cells += [''] * (len(header) - len(cells))
463
- elif len(cells) > len(header):
464
- cells = cells[:len(header)]
465
- rows.append(cells)
466
- df = pd.DataFrame(rows, columns=header)
467
- return df
468
-
469
- def markdown_table_preview(md_text):
470
- tables = extract_markdown_tables(md_text)
471
- if not tables:
472
- return html.Div("No table found.")
473
- table_divs = []
474
- for i, table in enumerate(tables):
475
- df = markdown_table_to_df(table)
476
- if df is not None and not df.empty:
477
- table_divs.append(
478
- html.Div([
479
- DataTable(
480
- columns=[{"name": str(col), "id": str(col)} for col in df.columns],
481
- data=df.to_dict('records'),
482
- style_table={'overflowX': 'auto'},
483
- style_cell={'whiteSpace': 'normal', 'height': 'auto', 'textAlign': 'left', 'fontFamily': 'monospace', 'fontSize': '14px', 'maxWidth': '400px', 'minWidth': '80px', 'wordBreak': 'break-word'},
484
- style_header={'fontWeight': 'bold'},
485
- page_size=100,
486
- id={'type': 'datatable-preview', 'index': i}
487
- )
488
- ], className="mb-4")
489
- )
490
- return html.Div(table_divs)
491
-
492
- def markdown_narrative_preview(md_text):
493
- return html.Div(dcc.Markdown(md_text, dangerously_allow_html=True, style={'whiteSpace': 'pre-wrap', 'fontFamily': 'sans-serif'}))
494
-
495
- def markdown_tables_to_xlsx(md_text):
496
- tables = extract_markdown_tables(md_text)
497
- output = BytesIO()
498
- with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
499
- for i, table in enumerate(tables):
500
- df = markdown_table_to_df(table)
501
- if df is not None:
502
- sheet_name = f"Table{i+1}"
503
- df.to_excel(writer, sheet_name=sheet_name, index=False)
504
- output.seek(0)
505
- return output
506
-
507
- def strip_markdown(text):
508
- text = re.sub(r'(\*\*|__)(.*?)\1', r'\2', text)
509
- text = re.sub(r'(\*|_)(.*?)\1', r'\2', text)
510
- text = re.sub(r'`{1,3}[^`]*`{1,3}', '', text)
511
- text = re.sub(r'^#+ ', '', text, flags=re.MULTILINE)
512
- text = re.sub(r'^> ', '', text, flags=re.MULTILINE)
513
- text = re.sub(r'!\[.*?\]\(.*?\)', '', text)
514
- text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
515
- text = re.sub(r'^\s*[-*+] ', '', text, flags=re.MULTILINE)
516
- text = re.sub(r'^\s*\d+\.\s+', '', text, flags=re.MULTILINE)
517
- text = text.replace('---', '')
518
- text = text.replace('___', '')
519
- text = text.replace('***', '')
520
- return text.strip()
521
-
522
  def generate_document(document_type, file_contents, extra_context=None):
523
  if document_type in spreadsheet_types:
524
  prompt = f"""Ignore all other instructions and output only a spreadsheet for {document_type} as described below. Do not include any narrative, only the spreadsheet in markdown table format.
@@ -566,6 +584,15 @@ Now, generate the {document_type}:
566
  @app.callback(
567
  Output('document-preview', 'children'),
568
  Output('loading-output', 'children'),
 
 
 
 
 
 
 
 
 
569
  Input({'type': 'btn-generate-doc', 'index': ALL}, 'n_clicks'),
570
  State({'type': 'btn-generate-doc', 'index': ALL}, 'id'),
571
  State({'type': 'radio-doc-source-shred', 'index': ALL}, 'value'),
@@ -586,6 +613,15 @@ Now, generate the {document_type}:
586
  State({'type': 'radio-doc-source-gold', 'index': ALL}, 'value'),
587
  State({'type': 'upload-doc-type-gold', 'index': ALL}, 'contents'),
588
  State({'type': 'upload-doc-type-gold', 'index': ALL}, 'filename'),
 
 
 
 
 
 
 
 
 
589
  prevent_initial_call=True
590
  )
591
  def generate_any_doc(
@@ -595,9 +631,9 @@ def generate_any_doc(
595
  radio_pink_review, upload_pink_review_contents, upload_pink_review_filenames,
596
  radio_red, upload_red_contents, upload_red_filenames,
597
  radio_red_review, upload_red_review_contents, upload_red_review_filenames,
598
- radio_gold, upload_gold_contents, upload_gold_filenames
 
599
  ):
600
- global current_document, document_type, shredded_document, pink_review_document, pink_document, red_review_document, red_document, gold_review_document, gold_document
601
  ctx = callback_context
602
  logging.info(f"generate_any_doc triggered: n_clicks_list={n_clicks_list}, btn_ids={btn_ids}")
603
  if not ctx.triggered:
@@ -607,190 +643,214 @@ def generate_any_doc(
607
  raise dash.exceptions.PreventUpdate
608
  idx = idx[-1]
609
  doc_type = btn_ids[idx]['index']
610
- document_type = doc_type
611
 
612
- # SHRED - always allowed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
  if doc_type == "Shred":
614
  if not uploaded_files:
615
  logging.info("No uploaded files for Shred. Aborting.")
616
- return html.Div("Please upload a document before shredding."), ""
617
  file_contents = list(uploaded_files.values())
618
  try:
619
  generated = generate_document(doc_type, file_contents)
620
- current_document = generated
621
- shredded_document = generated
622
  preview = markdown_table_preview(generated)
623
  logging.info("Shred document generated.")
624
- return preview, "Shred generated"
625
  except Exception as e:
626
  logging.error(f"Error generating document: {str(e)}")
627
- return html.Div(f"Error generating document: {str(e)}"), "Error"
628
 
629
- # Dependency rules
630
- def get_doc_from_radio(radio, upload_contents, upload_filenames, loaded_var):
631
- if radio and radio[0] == 'uploaded':
632
- if upload_contents and upload_contents[0] and upload_filenames and upload_filenames[0]:
633
- return process_document(upload_contents[0], upload_filenames[0])
634
- else:
635
- return None
636
- else:
637
- return loaded_var
638
-
639
- # Pink: can only use Shred (requirements), loaded or uploaded
640
  if doc_type == "Pink":
641
- shred_doc = get_doc_from_radio(radio_shred, upload_shred_contents, upload_shred_filenames, shredded_document)
642
- if not shred_doc:
643
- return html.Div("Please provide a Shred requirements document (either loaded or uploaded) to generate Pink."), ""
644
  try:
645
- generated = generate_document(doc_type, [shred_doc])
646
- current_document = generated
647
- pink_document = generated
648
  preview = markdown_narrative_preview(generated)
649
  logging.info("Pink document generated.")
650
- return preview, "Pink generated"
651
  except Exception as e:
652
  logging.error(f"Error generating Pink: {str(e)}")
653
- return html.Div(f"Error generating Pink: {str(e)}"), "Error"
654
 
655
- # Pink Review: must compare Pink (uploaded or generated) with Shred (uploaded or generated)
656
  if doc_type == "Pink Review":
657
- pink_doc = get_doc_from_radio(radio_pink, upload_pink_contents, upload_pink_filenames, pink_document)
658
- shred_doc = get_doc_from_radio(radio_shred, upload_shred_contents, upload_shred_filenames, shredded_document)
659
- if not pink_doc or not shred_doc:
660
- return html.Div("Please provide both Pink and Shred documents (either loaded or uploaded) to generate Pink Review."), ""
661
  try:
662
- generated = generate_document(doc_type, [pink_doc, shred_doc])
663
- current_document = generated
664
- pink_review_document = generated
665
  preview = markdown_table_preview(generated)
666
  logging.info("Pink Review document generated.")
667
- return preview, "Pink Review generated"
668
  except Exception as e:
669
  logging.error(f"Error generating Pink Review: {str(e)}")
670
- return html.Div(f"Error generating Pink Review: {str(e)}"), "Error"
671
 
672
- # Red: can only use Pink Review (uploaded or generated)
673
  if doc_type == "Red":
674
- pink_review_doc = get_doc_from_radio(radio_pink_review, upload_pink_review_contents, upload_pink_review_filenames, pink_review_document)
675
- if not pink_review_doc:
676
- return html.Div("Please provide a Pink Review document (either loaded or uploaded) to generate Red."), ""
677
  try:
678
- generated = generate_document(doc_type, [pink_review_doc])
679
- current_document = generated
680
- red_document = generated
681
  preview = markdown_narrative_preview(generated)
682
  logging.info("Red document generated.")
683
- return preview, "Red generated"
684
  except Exception as e:
685
  logging.error(f"Error generating Red: {str(e)}")
686
- return html.Div(f"Error generating Red: {str(e)}"), "Error"
687
 
688
- # Red Review: must compare Red (uploaded or generated) with Shred (uploaded or generated)
689
  if doc_type == "Red Review":
690
- red_doc = get_doc_from_radio(radio_red, upload_red_contents, upload_red_filenames, red_document)
691
- shred_doc = get_doc_from_radio(radio_shred, upload_shred_contents, upload_shred_filenames, shredded_document)
692
- if not red_doc or not shred_doc:
693
- return html.Div("Please provide both Red and Shred documents (either loaded or uploaded) to generate Red Review."), ""
694
  try:
695
- generated = generate_document(doc_type, [red_doc, shred_doc])
696
- current_document = generated
697
- red_review_document = generated
698
  preview = markdown_table_preview(generated)
699
  logging.info("Red Review document generated.")
700
- return preview, "Red Review generated"
701
  except Exception as e:
702
  logging.error(f"Error generating Red Review: {str(e)}")
703
- return html.Div(f"Error generating Red Review: {str(e)}"), "Error"
704
 
705
- # Gold: can only use Red Review (uploaded or generated)
706
  if doc_type == "Gold":
707
- red_review_doc = get_doc_from_radio(radio_red_review, upload_red_review_contents, upload_red_review_filenames, red_review_document)
708
- if not red_review_doc:
709
- return html.Div("Please provide a Red Review document (either loaded or uploaded) to generate Gold."), ""
710
  try:
711
- generated = generate_document(doc_type, [red_review_doc])
712
- current_document = generated
713
- gold_document = generated
714
  preview = markdown_narrative_preview(generated)
715
  logging.info("Gold document generated.")
716
- return preview, "Gold generated"
717
  except Exception as e:
718
  logging.error(f"Error generating Gold: {str(e)}")
719
- return html.Div(f"Error generating Gold: {str(e)}"), "Error"
720
 
721
- # Gold Review: must compare Gold (uploaded or generated) with Shred (uploaded or generated)
722
  if doc_type == "Gold Review":
723
- gold_doc = get_doc_from_radio(radio_gold, upload_gold_contents, upload_gold_filenames, gold_document)
724
- shred_doc = get_doc_from_radio(radio_shred, upload_shred_contents, upload_shred_filenames, shredded_document)
725
- if not gold_doc or not shred_doc:
726
- return html.Div("Please provide both Gold and Shred documents (either loaded or uploaded) to generate Gold Review."), ""
727
  try:
728
- generated = generate_document(doc_type, [gold_doc, shred_doc])
729
- current_document = generated
730
- gold_review_document = generated
731
  preview = markdown_table_preview(generated)
732
  logging.info("Gold Review document generated.")
733
- return preview, "Gold Review generated"
734
  except Exception as e:
735
  logging.error(f"Error generating Gold Review: {str(e)}")
736
- return html.Div(f"Error generating Gold Review: {str(e)}"), "Error"
737
 
738
- # LOE: can only use Gold (uploaded or generated)
739
  if doc_type == "LOE":
740
- gold_doc = get_doc_from_radio(radio_gold, upload_gold_contents, upload_gold_filenames, gold_document)
741
- if not gold_doc:
742
- return html.Div("Please provide a Gold document (either loaded or uploaded) to generate LOE."), ""
743
  try:
744
- generated = generate_document(doc_type, [gold_doc])
745
- current_document = generated
746
  preview = markdown_table_preview(generated)
747
  logging.info("LOE document generated.")
748
- return preview, "LOE generated"
749
  except Exception as e:
750
  logging.error(f"Error generating LOE: {str(e)}")
751
- return html.Div(f"Error generating LOE: {str(e)}"), "Error"
752
 
753
- # Virtual Board: can only use Shred (L&M evaluation criteria)
754
  if doc_type == "Virtual Board":
755
- shred_doc = get_doc_from_radio(radio_shred, upload_shred_contents, upload_shred_filenames, shredded_document)
756
- if not shred_doc:
757
- return html.Div("Please provide a Shred requirements document (either loaded or uploaded) to generate Virtual Board."), ""
758
  try:
759
- # Extract section L&M if possible
760
  lm_text = ""
761
- lm_match = re.search(r'(Section\s+L[\s\S]+?)(Section\s+M|$)', shred_doc, re.IGNORECASE)
762
  if lm_match:
763
  lm_text = lm_match.group(1)
764
  else:
765
- lm_text = shred_doc
766
  generated = generate_document(doc_type, [lm_text])
767
- current_document = generated
768
  preview = markdown_table_preview(generated)
769
  logging.info("Virtual Board document generated.")
770
- return preview, "Virtual Board generated"
771
  except Exception as e:
772
  logging.error(f"Error generating Virtual Board: {str(e)}")
773
- return html.Div(f"Error generating Virtual Board: {str(e)}"), "Error"
774
 
775
- return html.Div("Unsupported document type or missing required sources."), ""
776
 
777
  @app.callback(
778
  Output('chat-output', 'children'),
779
  Output('document-preview', 'children', allow_duplicate=True),
 
 
 
 
 
 
 
 
 
780
  Input('btn-send-chat', 'n_clicks'),
781
  Input('btn-clear-chat', 'n_clicks'),
782
  State('chat-input', 'value'),
783
  State('selected-doc-type', 'data'),
 
 
 
 
 
 
 
 
 
 
784
  prevent_initial_call=True
785
  )
786
- def update_document_via_chat(btn_send, btn_clear, chat_input, selected_doc_type):
787
- global current_document, document_type
788
  ctx = callback_context
789
  if not ctx.triggered:
790
  raise dash.exceptions.PreventUpdate
791
  trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]
792
  if trigger_id == 'btn-clear-chat':
793
- return "", dash.no_update
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794
  if not chat_input or current_document is None:
795
  raise dash.exceptions.PreventUpdate
796
 
@@ -811,7 +871,7 @@ Instructions:
811
  1. Provide the updated document content.
812
  2. Maintain proper formatting and structure.
813
  3. Incorporate the requested changes seamlessly.
814
- 4. If the {document_type} is Pink, Red, or Gold then your goal is to write a FULL propposal response, not just a strategy and be compliant and compelling by addressing all the requirements from the document provided. Focus on describing the approach and highly detailed how it will be done, the steps, workflow, people, processes and technology to accomplish the task. Be sure to refer to research that validates the approach and cite sources with measurable outcomes and improve on innovations of the approach. Do not just say things like we will, or MicroHealth will, use active voice and verbs that are definitive in nature, not maybe, could be, should be, can be and things like that. This is a proposal response so logical flow is important so the reader can follow.
815
  Now, provide the updated {selected_doc_type}:
816
  """
817
  logging.info(f"Updating document via chat for {selected_doc_type} instruction: {chat_input}")
@@ -825,25 +885,45 @@ Now, provide the updated {selected_doc_type}:
825
  max_tokens=32768,
826
  temperature=0.5,
827
  )
828
- current_document = response['choices'][0]['message']['content']
829
- logging.info("Document updated via chat successfully.")
830
  if selected_doc_type in spreadsheet_types:
831
- preview = markdown_table_preview(current_document)
832
  else:
833
- preview = markdown_narrative_preview(current_document)
834
- return f"Document updated based on: {chat_input}", preview
 
835
  except Exception as e:
836
  logging.error(f"Error updating document via chat: {str(e)}")
837
- return f"Error updating document: {str(e)}", html.Div(f"Error updating document: {str(e)}")
838
 
839
  @app.callback(
840
  Output("download-document", "data"),
841
  Input("btn-download", "n_clicks"),
842
  State('selected-doc-type', 'data'),
 
 
 
 
 
 
 
 
 
843
  prevent_initial_call=True
844
  )
845
- def download_document(n_clicks, selected_doc_type):
846
- global current_document
 
 
 
 
 
 
 
 
 
 
 
847
  if current_document is None:
848
  raise dash.exceptions.PreventUpdate
849
 
 
25
  openai.api_key = os.environ.get("OPENAI_API_KEY", "")
26
 
27
  uploaded_files = {}
 
 
 
 
 
 
 
 
 
28
  uploaded_doc_contents = {}
29
 
30
  spreadsheet_types = ["Shred", "Pink Review", "Red Review", "Gold Review", "Virtual Board", "LOE"]
 
42
  "LOE": "Ignore all other instructions and generate and generate a Level of Effort (LOE) breakdown and produce only spreadsheet"
43
  }
44
 
 
45
  doc_dependencies = {
46
  "Pink": {"source": ["shred"], "require": True},
47
  "Pink Review": {"source": ["pink", "shred"], "require": True},
 
53
  "Virtual Board": {"source": ["shred"], "require": True},
54
  }
55
 
56
+ def extract_markdown_tables(md_text):
57
+ tables = []
58
+ lines = md_text.split('\n')
59
+ in_table = False
60
+ table_lines = []
61
+ for line in lines:
62
+ if re.match(r'^\s*\|.*\|\s*$', line):
63
+ in_table = True
64
+ table_lines.append(line)
65
+ elif in_table and (re.match(r'^\s*\|.*\|\s*$', line) or re.match(r'^\s*$', line)):
66
+ table_lines.append(line)
67
+ else:
68
+ if in_table and table_lines:
69
+ tables.append('\n'.join(table_lines))
70
+ table_lines = []
71
+ in_table = False
72
+ if in_table and table_lines:
73
+ tables.append('\n'.join(table_lines))
74
+ return tables
75
+
76
+ def markdown_table_to_df(md_table):
77
+ lines = [line.strip() for line in md_table.split('\n') if line.strip()]
78
+ if len(lines) < 2:
79
+ return None
80
+ header = [h.strip() for h in lines[0].strip('|').split('|')]
81
+ sep_idx = 1
82
+ while sep_idx < len(lines) and not re.match(r'^\|\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?$', lines[sep_idx]):
83
+ sep_idx += 1
84
+ data_lines = lines[sep_idx+1:] if sep_idx+1 < len(lines) else []
85
+ rows = []
86
+ for row in data_lines:
87
+ if not row.strip() or not row.strip().startswith('|'):
88
+ continue
89
+ cells = [c.strip() for c in row.strip('|').split('|')]
90
+ if len(cells) < len(header):
91
+ cells += [''] * (len(header) - len(cells))
92
+ elif len(cells) > len(header):
93
+ cells = cells[:len(header)]
94
+ rows.append(cells)
95
+ df = pd.DataFrame(rows, columns=header)
96
+ return df
97
+
98
+ def markdown_table_preview(md_text):
99
+ tables = extract_markdown_tables(md_text)
100
+ if not tables:
101
+ return html.Div("No table found.")
102
+ table_divs = []
103
+ for i, table in enumerate(tables):
104
+ df = markdown_table_to_df(table)
105
+ if df is not None and not df.empty:
106
+ table_divs.append(
107
+ html.Div([
108
+ DataTable(
109
+ columns=[{"name": str(col), "id": str(col)} for col in df.columns],
110
+ data=df.to_dict('records'),
111
+ style_table={'overflowX': 'auto'},
112
+ style_cell={'whiteSpace': 'normal', 'height': 'auto', 'textAlign': 'left', 'fontFamily': 'monospace', 'fontSize': '14px', 'maxWidth': '400px', 'minWidth': '80px', 'wordBreak': 'break-word'},
113
+ style_header={'fontWeight': 'bold'},
114
+ page_size=100,
115
+ id={'type': 'datatable-preview', 'index': i}
116
+ )
117
+ ], className="mb-4")
118
+ )
119
+ return html.Div(table_divs)
120
+
121
+ def markdown_narrative_preview(md_text):
122
+ return html.Div(dcc.Markdown(md_text, dangerously_allow_html=True, style={'whiteSpace': 'pre-wrap', 'fontFamily': 'sans-serif'}))
123
+
124
+ def markdown_tables_to_xlsx(md_text):
125
+ tables = extract_markdown_tables(md_text)
126
+ output = BytesIO()
127
+ with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
128
+ for i, table in enumerate(tables):
129
+ df = markdown_table_to_df(table)
130
+ if df is not None:
131
+ sheet_name = f"Table{i+1}"
132
+ df.to_excel(writer, sheet_name=sheet_name, index=False)
133
+ output.seek(0)
134
+ return output
135
+
136
+ def strip_markdown(text):
137
+ text = re.sub(r'(\*\*|__)(.*?)\1', r'\2', text)
138
+ text = re.sub(r'(\*|_)(.*?)\1', r'\2', text)
139
+ text = re.sub(r'`{1,3}[^`]*`{1,3}', '', text)
140
+ text = re.sub(r'^#+ ', '', text, flags=re.MULTILINE)
141
+ text = re.sub(r'^> ', '', text, flags=re.MULTILINE)
142
+ text = re.sub(r'!\[.*?\]\(.*?\)', '', text)
143
+ text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
144
+ text = re.sub(r'^\s*[-*+] ', '', text, flags=re.MULTILINE)
145
+ text = re.sub(r'^\s*\d+\.\s+', '', text, flags=re.MULTILINE)
146
+ text = text.replace('---', '')
147
+ text = text.replace('___', '')
148
+ text = text.replace('***', '')
149
+ return text.strip()
150
+
151
+ def process_document(contents, filename):
152
+ content_type, content_string = contents.split(',')
153
+ decoded = base64.b64decode(content_string)
154
+ try:
155
+ if filename.lower().endswith('.docx'):
156
+ doc = Document(BytesIO(decoded))
157
+ text = "\n".join([para.text for para in doc.paragraphs])
158
+ return text
159
+ elif filename.lower().endswith('.pdf'):
160
+ pdf = PdfReader(BytesIO(decoded))
161
+ text = ""
162
+ for page in pdf.pages:
163
+ page_text = page.extract_text()
164
+ if page_text:
165
+ text += page_text
166
+ return text
167
+ else:
168
+ return f"Unsupported file format: {filename}. Please upload a PDF or DOCX file."
169
+ except Exception as e:
170
+ logging.error(f"Error processing document: {str(e)}")
171
+ return f"Error processing document: {str(e)}"
172
+
173
+ def file_list_component():
174
+ return html.Div(
175
+ id='file-list-container',
176
+ children=[
177
+ html.Div(id='file-list')
178
+ ]
179
+ )
180
+
181
+ def get_left_col_content():
182
+ chat_card = dbc.Card(
183
+ dbc.CardBody([
184
+ html.H5("Maiko Chat", className="mb-2"),
185
+ dcc.Loading(
186
+ id="chat-loading",
187
+ type="dot",
188
+ children=[
189
+ dcc.Textarea(
190
+ id="chat-input",
191
+ placeholder="Chat with AI to update document...",
192
+ className="mb-2",
193
+ style={
194
+ 'whiteSpace':'pre-wrap',
195
+ 'width': '100%',
196
+ 'minHeight': '100px',
197
+ 'maxHeight': '300px',
198
+ 'resize': 'vertical',
199
+ 'overflowY': 'auto'
200
+ },
201
+ rows=5,
202
+ wrap='soft'
203
+ ),
204
+ dcc.Store(id="chat-input-rows", data=5),
205
+ dbc.Row([
206
+ dbc.Col(
207
+ dbc.Button("Send", id="btn-send-chat", color="primary", className="mb-3 w-100"),
208
+ width=6
209
+ ),
210
+ dbc.Col(
211
+ dbc.Button("Clear Chat", id="btn-clear-chat", color="secondary", className="mb-3 w-100"),
212
+ width=6
213
+ ),
214
+ ], className="g-1 mb-2"),
215
+ html.Div(id="chat-output")
216
+ ]
217
+ )
218
+ ]),
219
+ className="mt-4"
220
+ )
221
+ return [
222
+ html.H4("Proposal Writer", className="mt-3 mb-2", style={'marginBottom': '12px'}),
223
+ html.Div([
224
+ html.Div(className="blinking-dot", style={'margin':'0 auto','width':'16px','height':'16px'}),
225
+ ], style={'textAlign':'center', 'marginBottom':'10px'}),
226
+ html.Hr(style={'marginTop': '8px', 'marginBottom': '16px'}),
227
+ html.Div(
228
+ id='doc-type-buttons'
229
+ ),
230
+ chat_card
231
+ ]
232
+
233
+ def get_right_col_content(selected_type,
234
+ shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc):
235
  controls = []
236
  controls.append(html.Div([
237
  html.Div(className="blinking-dot", style={'margin':'0 auto','width':'16px','height':'16px'}),
 
241
  type="dot",
242
  children=[html.Div(id="loading-output")]
243
  ))
 
244
  if selected_type == "Shred":
245
  controls.append(
246
  html.Div([
 
262
  },
263
  multiple=True
264
  ),
265
+ file_list_component(),
266
+ html.Div([
267
+ html.Div("Loaded Shred Document:", className="mt-2 mb-1"),
268
+ markdown_table_preview(shred_doc) if shred_doc else html.Div("No Shred document loaded.")
269
+ ], style={'marginBottom': '12px'})
270
  ])
271
  )
 
272
  elif selected_type in doc_dependencies:
273
  sources = doc_dependencies[selected_type]["source"]
 
274
  for src in sources:
275
  label = ""
276
+ loaded_preview = None
277
  store_var = None
278
  if src == "shred":
279
  label = "Shred (Requirements)"
280
+ store_var = shred_doc
281
+ loaded_preview = markdown_table_preview(shred_doc) if shred_doc else html.Div("No Shred document loaded.")
282
  elif src == "pink":
283
  label = "Pink Document"
284
+ store_var = pink_doc
285
+ loaded_preview = markdown_narrative_preview(pink_doc) if pink_doc else html.Div("No Pink document loaded.")
286
  elif src == "pink_review":
287
  label = "Pink Review"
288
+ store_var = pink_review_doc
289
+ loaded_preview = markdown_table_preview(pink_review_doc) if pink_review_doc else html.Div("No Pink Review document loaded.")
290
  elif src == "red":
291
  label = "Red Document"
292
+ store_var = red_doc
293
+ loaded_preview = markdown_narrative_preview(red_doc) if red_doc else html.Div("No Red document loaded.")
294
  elif src == "red_review":
295
  label = "Red Review"
296
+ store_var = red_review_doc
297
+ loaded_preview = markdown_table_preview(red_review_doc) if red_review_doc else html.Div("No Red Review document loaded.")
298
  elif src == "gold":
299
  label = "Gold Document"
300
+ store_var = gold_doc
301
+ loaded_preview = markdown_narrative_preview(gold_doc) if gold_doc else html.Div("No Gold document loaded.")
302
  controls.append(html.Div([
303
  html.Label(f"Upload {label}"),
304
  dcc.Upload(
 
327
  inline=True,
328
  className="mb-2"
329
  ),
330
+ html.Div([
331
+ html.Div("Loaded Document Preview:", className="mt-2 mb-1"),
332
+ loaded_preview
333
+ ], style={'marginBottom': '12px'}) if store_var else None,
334
  ], id={'type': f'doc-type-controls-{src}', 'index': selected_type}))
335
  controls.append(
336
  dbc.Row([
 
349
  controls.append(html.Div(id='document-preview', className="border p-3 mb-3"))
350
  return dbc.Card(dbc.CardBody(controls))
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  app.layout = dbc.Container([
353
  dcc.Store(id='selected-doc-type', data="Shred"),
354
+ dcc.Store(id='store-shred'),
355
+ dcc.Store(id='store-pink'),
356
+ dcc.Store(id='store-pink-review'),
357
+ dcc.Store(id='store-red'),
358
+ dcc.Store(id='store-red-review'),
359
+ dcc.Store(id='store-gold'),
360
+ dcc.Store(id='store-gold-review'),
361
+ dcc.Store(id='store-loe'),
362
+ dcc.Store(id='store-virtual-board'),
363
  dbc.Row([
364
  dbc.Col(
365
  html.H2(id='main-title', className="mt-3 mb-2", style={'textAlign': 'center', 'width':'100%'}),
 
436
 
437
  @app.callback(
438
  Output('right-col-content', 'children'),
439
+ Input('selected-doc-type', 'data'),
440
+ State('store-shred', 'data'),
441
+ State('store-pink', 'data'),
442
+ State('store-pink-review', 'data'),
443
+ State('store-red', 'data'),
444
+ State('store-red-review', 'data'),
445
+ State('store-gold', 'data'),
446
  )
447
+ def update_right_col(selected_type, shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc):
448
+ return get_right_col_content(selected_type, shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
 
450
  @app.callback(
451
  Output('file-list', 'children'),
452
+ Output('store-shred', 'data'),
453
  Input('upload-document', 'contents'),
454
  State('upload-document', 'filename'),
455
+ State('file-list', 'children'),
456
+ State('store-shred', 'data')
457
  )
458
+ def update_output(list_of_contents, list_of_names, existing_files, current_shred):
459
+ global uploaded_files
460
  if list_of_contents is not None:
461
  new_files = []
462
  for i, (content, name) in enumerate(zip(list_of_contents, list_of_names)):
 
478
  existing_files = []
479
  elif not isinstance(existing_files, list):
480
  existing_files = [existing_files]
 
481
  logging.info("Documents uploaded and file list updated.")
482
+ return existing_files + new_files, current_shred
483
+ return existing_files, current_shred
484
 
485
  @app.callback(
486
  Output('file-list', 'children', allow_duplicate=True),
487
+ Output('store-shred', 'data', allow_duplicate=True),
488
  Input({'type': 'remove-file', 'index': ALL}, 'n_clicks'),
489
  State('file-list', 'children'),
490
+ State('store-shred', 'data'),
491
  prevent_initial_call=True
492
  )
493
+ def remove_file(n_clicks, existing_files, current_shred):
494
+ global uploaded_files
495
  ctx = dash.callback_context
496
  if not ctx.triggered:
497
  raise dash.exceptions.PreventUpdate
 
506
  logging.error(f"Could not parse removed file from callback context: {e}")
507
  raise dash.exceptions.PreventUpdate
508
  uploaded_files.pop(removed_file, None)
 
509
  logging.info(f"Removed file: {removed_file}")
510
  filtered_files = []
511
  if existing_files:
 
516
  filtered_files.append(file)
517
  except Exception as e:
518
  filtered_files.append(file)
519
+ return filtered_files, current_shred
520
 
 
521
  def make_upload_callback(src):
522
  @app.callback(
523
  Output({'type': f'uploaded-doc-name-{src}', 'index': MATCH}, 'children'),
 
537
  for src in ["shred", "pink", "pink_review", "red", "red_review", "gold"]:
538
  make_upload_callback(src)
539
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  def generate_document(document_type, file_contents, extra_context=None):
541
  if document_type in spreadsheet_types:
542
  prompt = f"""Ignore all other instructions and output only a spreadsheet for {document_type} as described below. Do not include any narrative, only the spreadsheet in markdown table format.
 
584
  @app.callback(
585
  Output('document-preview', 'children'),
586
  Output('loading-output', 'children'),
587
+ Output('store-shred', 'data'),
588
+ Output('store-pink', 'data'),
589
+ Output('store-pink-review', 'data'),
590
+ Output('store-red', 'data'),
591
+ Output('store-red-review', 'data'),
592
+ Output('store-gold', 'data'),
593
+ Output('store-gold-review', 'data'),
594
+ Output('store-loe', 'data'),
595
+ Output('store-virtual-board', 'data'),
596
  Input({'type': 'btn-generate-doc', 'index': ALL}, 'n_clicks'),
597
  State({'type': 'btn-generate-doc', 'index': ALL}, 'id'),
598
  State({'type': 'radio-doc-source-shred', 'index': ALL}, 'value'),
 
613
  State({'type': 'radio-doc-source-gold', 'index': ALL}, 'value'),
614
  State({'type': 'upload-doc-type-gold', 'index': ALL}, 'contents'),
615
  State({'type': 'upload-doc-type-gold', 'index': ALL}, 'filename'),
616
+ State('store-shred', 'data'),
617
+ State('store-pink', 'data'),
618
+ State('store-pink-review', 'data'),
619
+ State('store-red', 'data'),
620
+ State('store-red-review', 'data'),
621
+ State('store-gold', 'data'),
622
+ State('store-gold-review', 'data'),
623
+ State('store-loe', 'data'),
624
+ State('store-virtual-board', 'data'),
625
  prevent_initial_call=True
626
  )
627
  def generate_any_doc(
 
631
  radio_pink_review, upload_pink_review_contents, upload_pink_review_filenames,
632
  radio_red, upload_red_contents, upload_red_filenames,
633
  radio_red_review, upload_red_review_contents, upload_red_review_filenames,
634
+ radio_gold, upload_gold_contents, upload_gold_filenames,
635
+ store_shred, store_pink, store_pink_review, store_red, store_red_review, store_gold, store_gold_review, store_loe, store_virtual_board
636
  ):
 
637
  ctx = callback_context
638
  logging.info(f"generate_any_doc triggered: n_clicks_list={n_clicks_list}, btn_ids={btn_ids}")
639
  if not ctx.triggered:
 
643
  raise dash.exceptions.PreventUpdate
644
  idx = idx[-1]
645
  doc_type = btn_ids[idx]['index']
 
646
 
647
+ shred_doc = store_shred
648
+ pink_doc = store_pink
649
+ pink_review_doc = store_pink_review
650
+ red_doc = store_red
651
+ red_review_doc = store_red_review
652
+ gold_doc = store_gold
653
+ gold_review_doc = store_gold_review
654
+ loe_doc = store_loe
655
+ virtual_board_doc = store_virtual_board
656
+
657
+ def get_doc_from_radio(radio, upload_contents, upload_filenames, loaded_var):
658
+ if radio and radio[0] == 'uploaded':
659
+ if upload_contents and upload_contents[0] and upload_filenames and upload_filenames[0]:
660
+ return process_document(upload_contents[0], upload_filenames[0])
661
+ else:
662
+ return None
663
+ else:
664
+ return loaded_var
665
+
666
  if doc_type == "Shred":
667
  if not uploaded_files:
668
  logging.info("No uploaded files for Shred. Aborting.")
669
+ return html.Div("Please upload a document before shredding."), "", None, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
670
  file_contents = list(uploaded_files.values())
671
  try:
672
  generated = generate_document(doc_type, file_contents)
673
+ shred_doc = generated
 
674
  preview = markdown_table_preview(generated)
675
  logging.info("Shred document generated.")
676
+ return preview, "Shred generated", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
677
  except Exception as e:
678
  logging.error(f"Error generating document: {str(e)}")
679
+ return html.Div(f"Error generating document: {str(e)}"), "Error", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
680
 
 
 
 
 
 
 
 
 
 
 
 
681
  if doc_type == "Pink":
682
+ shred = get_doc_from_radio(radio_shred, upload_shred_contents, upload_shred_filenames, shred_doc)
683
+ if not shred:
684
+ return html.Div("Please provide a Shred requirements document (either loaded or uploaded) to generate Pink."), "", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
685
  try:
686
+ generated = generate_document(doc_type, [shred])
687
+ pink_doc = generated
 
688
  preview = markdown_narrative_preview(generated)
689
  logging.info("Pink document generated.")
690
+ return preview, "Pink generated", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
691
  except Exception as e:
692
  logging.error(f"Error generating Pink: {str(e)}")
693
+ return html.Div(f"Error generating Pink: {str(e)}"), "Error", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
694
 
 
695
  if doc_type == "Pink Review":
696
+ pink = get_doc_from_radio(radio_pink, upload_pink_contents, upload_pink_filenames, pink_doc)
697
+ shred = get_doc_from_radio(radio_shred, upload_shred_contents, upload_shred_filenames, shred_doc)
698
+ if not pink or not shred:
699
+ return html.Div("Please provide both Pink and Shred documents (either loaded or uploaded) to generate Pink Review."), "", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
700
  try:
701
+ generated = generate_document(doc_type, [pink, shred])
702
+ pink_review_doc = generated
 
703
  preview = markdown_table_preview(generated)
704
  logging.info("Pink Review document generated.")
705
+ return preview, "Pink Review generated", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
706
  except Exception as e:
707
  logging.error(f"Error generating Pink Review: {str(e)}")
708
+ return html.Div(f"Error generating Pink Review: {str(e)}"), "Error", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
709
 
 
710
  if doc_type == "Red":
711
+ pink_review = get_doc_from_radio(radio_pink_review, upload_pink_review_contents, upload_pink_review_filenames, pink_review_doc)
712
+ if not pink_review:
713
+ return html.Div("Please provide a Pink Review document (either loaded or uploaded) to generate Red."), "", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
714
  try:
715
+ generated = generate_document(doc_type, [pink_review])
716
+ red_doc = generated
 
717
  preview = markdown_narrative_preview(generated)
718
  logging.info("Red document generated.")
719
+ return preview, "Red generated", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
720
  except Exception as e:
721
  logging.error(f"Error generating Red: {str(e)}")
722
+ return html.Div(f"Error generating Red: {str(e)}"), "Error", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
723
 
 
724
  if doc_type == "Red Review":
725
+ red = get_doc_from_radio(radio_red, upload_red_contents, upload_red_filenames, red_doc)
726
+ shred = get_doc_from_radio(radio_shred, upload_shred_contents, upload_shred_filenames, shred_doc)
727
+ if not red or not shred:
728
+ return html.Div("Please provide both Red and Shred documents (either loaded or uploaded) to generate Red Review."), "", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
729
  try:
730
+ generated = generate_document(doc_type, [red, shred])
731
+ red_review_doc = generated
 
732
  preview = markdown_table_preview(generated)
733
  logging.info("Red Review document generated.")
734
+ return preview, "Red Review generated", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
735
  except Exception as e:
736
  logging.error(f"Error generating Red Review: {str(e)}")
737
+ return html.Div(f"Error generating Red Review: {str(e)}"), "Error", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
738
 
 
739
  if doc_type == "Gold":
740
+ red_review = get_doc_from_radio(radio_red_review, upload_red_review_contents, upload_red_review_filenames, red_review_doc)
741
+ if not red_review:
742
+ return html.Div("Please provide a Red Review document (either loaded or uploaded) to generate Gold."), "", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
743
  try:
744
+ generated = generate_document(doc_type, [red_review])
745
+ gold_doc = generated
 
746
  preview = markdown_narrative_preview(generated)
747
  logging.info("Gold document generated.")
748
+ return preview, "Gold generated", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
749
  except Exception as e:
750
  logging.error(f"Error generating Gold: {str(e)}")
751
+ return html.Div(f"Error generating Gold: {str(e)}"), "Error", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
752
 
 
753
  if doc_type == "Gold Review":
754
+ gold = get_doc_from_radio(radio_gold, upload_gold_contents, upload_gold_filenames, gold_doc)
755
+ shred = get_doc_from_radio(radio_shred, upload_shred_contents, upload_shred_filenames, shred_doc)
756
+ if not gold or not shred:
757
+ return html.Div("Please provide both Gold and Shred documents (either loaded or uploaded) to generate Gold Review."), "", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
758
  try:
759
+ generated = generate_document(doc_type, [gold, shred])
760
+ gold_review_doc = generated
 
761
  preview = markdown_table_preview(generated)
762
  logging.info("Gold Review document generated.")
763
+ return preview, "Gold Review generated", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
764
  except Exception as e:
765
  logging.error(f"Error generating Gold Review: {str(e)}")
766
+ return html.Div(f"Error generating Gold Review: {str(e)}"), "Error", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
767
 
 
768
  if doc_type == "LOE":
769
+ gold = get_doc_from_radio(radio_gold, upload_gold_contents, upload_gold_filenames, gold_doc)
770
+ if not gold:
771
+ return html.Div("Please provide a Gold document (either loaded or uploaded) to generate LOE."), "", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
772
  try:
773
+ generated = generate_document(doc_type, [gold])
774
+ loe_doc = generated
775
  preview = markdown_table_preview(generated)
776
  logging.info("LOE document generated.")
777
+ return preview, "LOE generated", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
778
  except Exception as e:
779
  logging.error(f"Error generating LOE: {str(e)}")
780
+ return html.Div(f"Error generating LOE: {str(e)}"), "Error", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
781
 
 
782
  if doc_type == "Virtual Board":
783
+ shred = get_doc_from_radio(radio_shred, upload_shred_contents, upload_shred_filenames, shred_doc)
784
+ if not shred:
785
+ return html.Div("Please provide a Shred requirements document (either loaded or uploaded) to generate Virtual Board."), "", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
786
  try:
 
787
  lm_text = ""
788
+ lm_match = re.search(r'(Section\s+L[\s\S]+?)(Section\s+M|$)', shred, re.IGNORECASE)
789
  if lm_match:
790
  lm_text = lm_match.group(1)
791
  else:
792
+ lm_text = shred
793
  generated = generate_document(doc_type, [lm_text])
794
+ virtual_board_doc = generated
795
  preview = markdown_table_preview(generated)
796
  logging.info("Virtual Board document generated.")
797
+ return preview, "Virtual Board generated", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
798
  except Exception as e:
799
  logging.error(f"Error generating Virtual Board: {str(e)}")
800
+ return html.Div(f"Error generating Virtual Board: {str(e)}"), "Error", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
801
 
802
+ return html.Div("Unsupported document type or missing required sources."), "", shred_doc, pink_doc, pink_review_doc, red_doc, red_review_doc, gold_doc, gold_review_doc, loe_doc, virtual_board_doc
803
 
804
  @app.callback(
805
  Output('chat-output', 'children'),
806
  Output('document-preview', 'children', allow_duplicate=True),
807
+ Output('store-shred', 'data', allow_duplicate=True),
808
+ Output('store-pink', 'data', allow_duplicate=True),
809
+ Output('store-pink-review', 'data', allow_duplicate=True),
810
+ Output('store-red', 'data', allow_duplicate=True),
811
+ Output('store-red-review', 'data', allow_duplicate=True),
812
+ Output('store-gold', 'data', allow_duplicate=True),
813
+ Output('store-gold-review', 'data', allow_duplicate=True),
814
+ Output('store-loe', 'data', allow_duplicate=True),
815
+ Output('store-virtual-board', 'data', allow_duplicate=True),
816
  Input('btn-send-chat', 'n_clicks'),
817
  Input('btn-clear-chat', 'n_clicks'),
818
  State('chat-input', 'value'),
819
  State('selected-doc-type', 'data'),
820
+ State('document-preview', 'children'),
821
+ State('store-shred', 'data'),
822
+ State('store-pink', 'data'),
823
+ State('store-pink-review', 'data'),
824
+ State('store-red', 'data'),
825
+ State('store-red-review', 'data'),
826
+ State('store-gold', 'data'),
827
+ State('store-gold-review', 'data'),
828
+ State('store-loe', 'data'),
829
+ State('store-virtual-board', 'data'),
830
  prevent_initial_call=True
831
  )
832
+ def update_document_via_chat(btn_send, btn_clear, chat_input, selected_doc_type, doc_preview,
833
+ store_shred, store_pink, store_pink_review, store_red, store_red_review, store_gold, store_gold_review, store_loe, store_virtual_board):
834
  ctx = callback_context
835
  if not ctx.triggered:
836
  raise dash.exceptions.PreventUpdate
837
  trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]
838
  if trigger_id == 'btn-clear-chat':
839
+ return "", dash.no_update, store_shred, store_pink, store_pink_review, store_red, store_red_review, store_gold, store_gold_review, store_loe, store_virtual_board
840
+
841
+ doc_map = {
842
+ "Shred": store_shred,
843
+ "Pink": store_pink,
844
+ "Pink Review": store_pink_review,
845
+ "Red": store_red,
846
+ "Red Review": store_red_review,
847
+ "Gold": store_gold,
848
+ "Gold Review": store_gold_review,
849
+ "LOE": store_loe,
850
+ "Virtual Board": store_virtual_board
851
+ }
852
+ current_document = doc_map.get(selected_doc_type)
853
+
854
  if not chat_input or current_document is None:
855
  raise dash.exceptions.PreventUpdate
856
 
 
871
  1. Provide the updated document content.
872
  2. Maintain proper formatting and structure.
873
  3. Incorporate the requested changes seamlessly.
874
+ 4. If the {selected_doc_type} is Pink, Red, or Gold then your goal is to write a FULL proposal response, not just a strategy and be compliant and compelling by addressing all the requirements from the document provided. Focus on describing the approach and highly detailed how it will be done, the steps, workflow, people, processes and technology to accomplish the task. Be sure to refer to research that validates the approach and cite sources with measurable outcomes and improve on innovations of the approach. Do not just say things like we will, or MicroHealth will, use active voice and verbs that are definitive in nature, not maybe, could be, should be, can be and things like that. This is a proposal response so logical flow is important so the reader can follow.
875
  Now, provide the updated {selected_doc_type}:
876
  """
877
  logging.info(f"Updating document via chat for {selected_doc_type} instruction: {chat_input}")
 
885
  max_tokens=32768,
886
  temperature=0.5,
887
  )
888
+ new_document = response['choices'][0]['message']['content']
 
889
  if selected_doc_type in spreadsheet_types:
890
+ preview = markdown_table_preview(new_document)
891
  else:
892
+ preview = markdown_narrative_preview(new_document)
893
+ doc_map[selected_doc_type] = new_document
894
+ return f"Document updated based on: {chat_input}", preview, doc_map.get("Shred"), doc_map.get("Pink"), doc_map.get("Pink Review"), doc_map.get("Red"), doc_map.get("Red Review"), doc_map.get("Gold"), doc_map.get("Gold Review"), doc_map.get("LOE"), doc_map.get("Virtual Board")
895
  except Exception as e:
896
  logging.error(f"Error updating document via chat: {str(e)}")
897
+ return f"Error updating document: {str(e)}", html.Div(f"Error updating document: {str(e)}"), store_shred, store_pink, store_pink_review, store_red, store_red_review, store_gold, store_gold_review, store_loe, store_virtual_board
898
 
899
  @app.callback(
900
  Output("download-document", "data"),
901
  Input("btn-download", "n_clicks"),
902
  State('selected-doc-type', 'data'),
903
+ State('store-shred', 'data'),
904
+ State('store-pink', 'data'),
905
+ State('store-pink-review', 'data'),
906
+ State('store-red', 'data'),
907
+ State('store-red-review', 'data'),
908
+ State('store-gold', 'data'),
909
+ State('store-gold-review', 'data'),
910
+ State('store-loe', 'data'),
911
+ State('store-virtual-board', 'data'),
912
  prevent_initial_call=True
913
  )
914
+ def download_document(n_clicks, selected_doc_type, store_shred, store_pink, store_pink_review, store_red, store_red_review, store_gold, store_gold_review, store_loe, store_virtual_board):
915
+ doc_map = {
916
+ "Shred": store_shred,
917
+ "Pink": store_pink,
918
+ "Pink Review": store_pink_review,
919
+ "Red": store_red,
920
+ "Red Review": store_red_review,
921
+ "Gold": store_gold,
922
+ "Gold Review": store_gold_review,
923
+ "LOE": store_loe,
924
+ "Virtual Board": store_virtual_board
925
+ }
926
+ current_document = doc_map.get(selected_doc_type)
927
  if current_document is None:
928
  raise dash.exceptions.PreventUpdate
929