bluenevus commited on
Commit
f5b637d
·
1 Parent(s): 71a7339

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +281 -151
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import base64
2
  import io
3
  import os
@@ -6,7 +7,7 @@ from docx import Document
6
  from io import BytesIO, StringIO
7
  import dash # Version 3.0.3 (or compatible)
8
  import dash_bootstrap_components as dbc # Version 2.0.2 (or compatible)
9
- from dash import html, dcc, Input, Output, State, callback_context, ALL
10
  from dash.exceptions import PreventUpdate # <-- Import PreventUpdate from here
11
  import google.generativeai as genai
12
  from docx.shared import Pt
@@ -14,6 +15,7 @@ from docx.enum.style import WD_STYLE_TYPE
14
  from PyPDF2 import PdfReader
15
  import logging
16
  import uuid # For unique IDs if needed with pattern matching
 
17
 
18
  # --- Logging Configuration ---
19
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -39,9 +41,9 @@ try:
39
  else:
40
  genai.configure(api_key=api_key)
41
  # Specify a model compatible with function calling or more advanced generation if needed.
42
- # Using 'gemini-pro' as a generally available and capable model.
43
- model = genai.GenerativeModel('gemini-2.5-pro-preview-03-25')
44
- logging.info("Gemini AI configured successfully using 'gemini-pro'.")
45
  except Exception as e:
46
  logging.error(f"Error configuring Gemini AI: {e}", exc_info=True)
47
  model = None
@@ -182,7 +184,7 @@ app.layout = dbc.Container(fluid=True, className="dbc", children=[
182
  )
183
  ]), className="mb-3"
184
  ),
185
- dbc.Button("Download Output", id="btn-download", color="success", className="mt-3", style={'display': 'none'}), # Hidden initially
186
  dcc.Download(id="download-document"),
187
 
188
  html.Hr(),
@@ -197,7 +199,11 @@ app.layout = dbc.Container(fluid=True, className="dbc", children=[
197
  type="circle",
198
  children=[
199
  dbc.Textarea(id="chat-input", placeholder="Enter instructions to refine the document shown above...", className="mb-2", style={'whiteSpace': 'normal', 'wordWrap': 'break-word'}), # Ensure word wrap
200
- dbc.Button("Send Chat", id="btn-send-chat", color="secondary", className="mb-3"), # Use secondary style
 
 
 
 
201
  html.Div(id="chat-output", style={'whiteSpace': 'pre-wrap', 'marginTop': '10px', 'border': '1px solid #eee', 'padding': '10px', 'borderRadius': '5px', 'minHeight': '50px'}) # Add border/padding
202
  ]
203
  )
@@ -271,8 +277,8 @@ def generate_ai_document(doc_type, input_docs, context_docs=None):
271
  if not model:
272
  logging.error("Gemini AI model not initialized.")
273
  return "Error: AI Model not configured. Please check API Key."
274
- if not input_docs or not any(input_docs): # Check if list exists and has content
275
- logging.warning(f"generate_ai_document called for {doc_type} with no input documents.")
276
  return f"Error: Missing required input document(s) for {doc_type} generation."
277
 
278
  # Combine input documents into a single string
@@ -287,7 +293,7 @@ def generate_ai_document(doc_type, input_docs, context_docs=None):
287
  **Core Instructions:**
288
  1. **Adhere Strictly to the Task:** Generate *only* the content for the '{doc_type}'. Do not add introductions, summaries, or conversational filler unless it's part of the requested document format itself.
289
  2. **Follow Format Guidelines:**
290
- * **Spreadsheet Types (Shred, Reviews, LOE, Board):** Structure output clearly. Use Markdown tables or a delimited format (like CSV) suitable for parsing. Define clear columns (e.g., `PWS_Section | Requirement | Finding | Recommendation` for reviews; `Section | Task | Estimated_Hours | Resource_Type` for LOE).
291
  * **Proposal Sections (Pink, Red, Gold):** Write professional, compelling prose. Use active voice ("MicroHealth will..."). Directly address requirements from context (Shredded PWS). Detail the 'how' (technical approach, methodology, workflow, tools). Incorporate innovation and benefits (efficiency, quality, outcomes). Substantiate claims (e.g., cite Gartner, Forrester if applicable). Clearly state roles/responsibilities (labor categories). Ensure compliance with Section L/M (Evaluation Criteria) from context. Avoid vague terms ('might', 'could', 'potentially'); be assertive and confident. Use paragraphs primarily; limit bullet points to lists where essential.
292
  3. **Utilize Provided Documents:**
293
  * **Context Document(s):** Use these as the primary reference or baseline (e.g., Shredded Requirements are the basis for compliance).
@@ -308,7 +314,7 @@ def generate_ai_document(doc_type, input_docs, context_docs=None):
308
  **Detailed Instructions for '{doc_type}':**
309
  {document_types.get(doc_type, "Generate the requested document based on the inputs and context.")}
310
 
311
- **Begin '{doc_type}' Output:**
312
  """
313
 
314
  logging.info(f"Generating AI document for: {doc_type}")
@@ -319,11 +325,29 @@ def generate_ai_document(doc_type, input_docs, context_docs=None):
319
  response = model.generate_content(prompt) # Consider adding request_options={'timeout': 300} if needed
320
 
321
  # Handle potential safety blocks or empty responses
322
- if not response.parts:
323
- logging.warning(f"Gemini AI returned no parts for {doc_type}. Potential safety block or empty response.")
324
- generated_text = f"Error: AI returned no content for {doc_type}. This might be due to safety filters or an issue with the prompt/input."
325
- else:
326
- generated_text = response.text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
  logging.info(f"Successfully generated document for: {doc_type}")
329
  # Update global state for download/chat *only if successful*
@@ -331,7 +355,7 @@ def generate_ai_document(doc_type, input_docs, context_docs=None):
331
  current_display_document = generated_text
332
  current_display_type = doc_type
333
  else:
334
- # Ensure error message is displayed if AI returns an error internally
335
  current_display_document = generated_text
336
  current_display_type = doc_type # Still set type so user knows what failed
337
 
@@ -458,7 +482,8 @@ def handle_file_remove(n_clicks, current_file_list_display):
458
  raise PreventUpdate
459
 
460
  # Check if any click count is > 0 (or just check the specific one that triggered)
461
- if not any(nc > 0 for nc in n_clicks if nc is not None): # Ensure click happened
 
462
  raise PreventUpdate
463
 
464
 
@@ -507,7 +532,8 @@ def handle_file_remove(n_clicks, current_file_list_display):
507
  else:
508
  status_message += "Ready for 'Shred' or other actions."
509
 
510
- return updated_file_list_display, status_message
 
511
 
512
 
513
  # 3. Handle Action Button Clicks (Show Controls or Trigger Generation)
@@ -526,13 +552,14 @@ def handle_action_button(n_clicks):
526
  global shredded_document, pink_document, red_document, gold_document, pink_review_document, red_review_document, gold_review_document, loe_document, virtual_board_document
527
  # Reset potentially uploaded review files when a *new* main action is selected from the left nav
528
  global uploaded_pink_content, uploaded_red_content, uploaded_gold_content
 
529
 
530
  triggered_id_dict = callback_context.triggered_id
531
  if not triggered_id_dict or not isinstance(triggered_id_dict, dict) or 'index' not in triggered_id_dict:
532
  raise PreventUpdate
533
 
534
  # Check if any click count is > 0
535
- if not any(nc > 0 for nc in n_clicks if nc is not None):
536
  raise PreventUpdate
537
 
538
  action_type = triggered_id_dict['index']
@@ -542,8 +569,8 @@ def handle_action_button(n_clicks):
542
  review_controls_style = {'display': 'none'} # Hide review controls by default
543
  review_controls_children = []
544
  status_message = f"Selected action: {action_type}"
545
- doc_preview_children = dash.no_update # Avoid clearing preview unless needed
546
- loading_style = {'visibility':'hidden'} # Hide loading dots by default
547
  download_style = {'display': 'none'} # Hide download button by default
548
 
549
 
@@ -568,6 +595,8 @@ def handle_action_button(n_clicks):
568
  if not shredded_document:
569
  status_message = "Error: Please 'Shred' the source documents first."
570
  doc_preview_children = html.Div(status_message, className="text-danger")
 
 
571
  return review_controls_style, [], status_message, doc_preview_children, loading_style, download_style
572
 
573
  # Check specific prerequisites and get the document to review
@@ -588,6 +617,8 @@ def handle_action_button(n_clicks):
588
  if prereq_doc is None and action_type != "Pink Review": # Shred is checked above
589
  status_message = f"Error: Please complete '{prereq_doc_name.replace(' Document','')}' first."
590
  doc_preview_children = html.Div(status_message, className="text-danger")
 
 
591
  return review_controls_style, [], status_message, doc_preview_children, loading_style, download_style
592
 
593
 
@@ -632,15 +663,19 @@ def handle_action_button(n_clicks):
632
  # Clear preview when showing controls, provide instruction
633
  doc_preview_children = html.Div(f"Configure input source for {base_doc_type} document and click 'Generate {action_type}'.", style={'padding':'10px'})
634
  status_message = f"Ready to configure input for {action_type}."
 
 
 
635
 
636
 
637
  # --- Actions Triggering Direct Generation (Shred, Pink, Red, Gold, LOE, Virtual Board) ---
638
  else:
639
  review_controls_style = {'display': 'none'} # Hide review controls
640
  review_controls_children = []
641
- loading_style = {'visibility':'visible'} # Show loading dots
642
  doc_preview_children = "" # Clear preview while loading/generating
643
  status_message = f"Generating {action_type}..."
 
644
 
645
  # Determine inputs based on action type
646
  input_docs = []
@@ -693,8 +728,11 @@ def handle_action_button(n_clicks):
693
  if generation_possible:
694
  result_doc = generate_ai_document(action_type, input_docs, context_docs)
695
 
696
- # Store result in the correct global variable
697
  if result_doc and not result_doc.startswith("Error:"):
 
 
 
698
  if action_type == "Shred": shredded_document = result_doc
699
  elif action_type == "Pink": pink_document = result_doc
700
  elif action_type == "Red": red_document = result_doc
@@ -703,20 +741,25 @@ def handle_action_button(n_clicks):
703
  elif action_type == "Virtual Board": virtual_board_document = result_doc
704
  # Reviews are handled separately
705
 
706
- doc_preview_children = dcc.Markdown(result_doc, style={'wordWrap': 'break-word'})
707
  status_message = f"{action_type} generated successfully."
708
- download_style = {'display': 'inline-block'} # Show download button on success
709
  else:
710
  # If generation failed, result_doc contains the error message from generate_ai_document
711
  doc_preview_children = html.Div(result_doc, className="text-danger") # Display error in preview
712
  status_message = f"Error generating {action_type}. See preview for details."
 
 
713
  download_style = {'display': 'none'} # Hide download button on error
714
 
715
  else:
716
  # Generation not possible due to prerequisites
717
  doc_preview_children = html.Div(status_message, className="text-danger")
 
 
 
718
 
719
- loading_style = {'visibility':'hidden'} # Hide loading dots when finished/failed
720
 
721
 
722
  return review_controls_style, review_controls_children, status_message, doc_preview_children, loading_style, download_style
@@ -733,17 +776,21 @@ def toggle_review_upload_visibility(radio_value, current_style):
733
  # Preserves existing style attributes while toggling 'display'
734
  new_style = current_style.copy() if current_style else {}
735
  should_display = (radio_value == 'upload')
736
-
737
- if should_display:
738
- new_style['display'] = 'block'
739
- else:
740
- new_style['display'] = 'none'
741
 
742
  # Prevent update if display style is already correct
743
- if ('display' in current_style and current_style['display'] == new_style['display']):
744
  raise PreventUpdate
745
  else:
746
- logging.debug(f"Toggling review upload visibility. Radio: {radio_value}, New Style: {new_style}")
 
 
 
 
 
 
 
 
747
  return new_style
748
 
749
 
@@ -754,30 +801,37 @@ def toggle_review_upload_visibility(radio_value, current_style):
754
  Input('upload-review-doc', 'contents'),
755
  State('upload-review-doc', 'filename'),
756
  # Get the current review type from the button ID that generated the controls
757
- State({'type': 'generate-review-button', 'index': ALL}, 'id'),
 
 
758
  prevent_initial_call=True
759
  )
760
- def handle_review_upload(contents, filename, button_ids):
761
  global uploaded_pink_content, uploaded_red_content, uploaded_gold_content
762
 
763
- if contents is None or filename is None or not button_ids:
764
- # No file uploaded or controls not fully rendered yet
765
  raise PreventUpdate
766
 
767
- # Determine which review type this upload is for based on the visible button ID
768
- # Assumes only one set of review controls is visible, thus only one button ID in the list
769
- if not isinstance(button_ids, list) or len(button_ids) == 0:
770
- logging.warning("handle_review_upload: Could not determine review type from button ID state.")
771
- raise PreventUpdate
 
 
 
 
 
 
 
 
 
772
 
773
- # Extract the first ID dictionary from the list
774
- button_id_dict = button_ids[0]
775
- if not isinstance(button_id_dict, dict) or 'index' not in button_id_dict:
776
- logging.warning("handle_review_upload: Invalid button ID format.")
777
- raise PreventUpdate
778
 
779
- review_type = button_id_dict['index'] # e.g., "Pink Review"
780
- base_type = review_type.split(" ")[0] # e.g., "Pink"
781
 
782
  logging.info(f"Handling upload of file '{filename}' for {review_type} input.")
783
 
@@ -794,6 +848,9 @@ def handle_review_upload(contents, filename, button_ids):
794
  if error:
795
  status_bar_message = f"Error processing uploaded {base_type} file: {error}"
796
  upload_status_display = html.Div(f"Failed to load {filename}: {error}", className="text-danger small")
 
 
 
797
  else:
798
  status_bar_message = f"Uploaded '{filename}' successfully for {review_type} input."
799
  upload_status_display = html.Div(f"Using uploaded file: {filename}", className="text-success small")
@@ -823,13 +880,14 @@ def generate_review_document(n_clicks, source_option, button_ids):
823
  global shredded_document, pink_document, red_document, gold_document
824
  global pink_review_document, red_review_document, gold_review_document
825
  global uploaded_pink_content, uploaded_red_content, uploaded_gold_content
 
826
 
827
  triggered_id_dict = callback_context.triggered_id
828
  if not triggered_id_dict or not isinstance(triggered_id_dict, dict) or 'index' not in triggered_id_dict:
829
  raise PreventUpdate
830
 
831
  # Check if any click count is > 0
832
- if not any(nc > 0 for nc in n_clicks if nc is not None):
833
  raise PreventUpdate
834
 
835
  review_type = triggered_id_dict['index'] # e.g., "Pink Review"
@@ -838,14 +896,16 @@ def generate_review_document(n_clicks, source_option, button_ids):
838
 
839
  doc_preview_children = "" # Clear preview
840
  status_message = f"Generating {review_type}..."
841
- loading_style = {'visibility':'visible'} # Show loading dots
842
  download_style = {'display': 'none'} # Hide download initially
 
 
843
 
844
 
845
  # --- Prerequisite Check ---
846
  if not shredded_document:
847
  status_message = "Error: 'Shredded' document is missing. Please perform 'Shred' first."
848
- loading_style = {'visibility':'hidden'}
849
  doc_preview_children = html.Div(status_message, className="text-danger")
850
  return doc_preview_children, status_message, loading_style, download_style
851
 
@@ -860,8 +920,8 @@ def generate_review_document(n_clicks, source_option, button_ids):
860
  elif base_type == "Gold": input_document_content = gold_document
861
 
862
  if not input_document_content:
863
- status_message = f"Error: Cannot use 'generated' option. The {input_doc_source_name} was not found (was it generated successfully?)."
864
- loading_style = {'visibility':'hidden'}
865
  doc_preview_children = html.Div(status_message, className="text-danger")
866
  return doc_preview_children, status_message, loading_style, download_style
867
 
@@ -872,14 +932,13 @@ def generate_review_document(n_clicks, source_option, button_ids):
872
  elif base_type == "Gold": input_document_content = uploaded_gold_content
873
 
874
  if not input_document_content:
875
- # Check if the upload component has children (file name displayed)
876
- status_message = f"Error: Cannot use 'upload' option. No {base_type} document was successfully uploaded and processed for this review step."
877
- loading_style = {'visibility':'hidden'}
878
  doc_preview_children = html.Div(status_message, className="text-danger")
879
  return doc_preview_children, status_message, loading_style, download_style
880
  else:
881
  status_message = f"Error: Invalid source option '{source_option}' selected."
882
- loading_style = {'visibility':'hidden'}
883
  doc_preview_children = html.Div(status_message, className="text-danger")
884
  return doc_preview_children, status_message, loading_style, download_style
885
 
@@ -892,56 +951,76 @@ def generate_review_document(n_clicks, source_option, button_ids):
892
  review_result = generate_ai_document(review_type, [input_document_content], context_docs=[shredded_document])
893
 
894
  if review_result and not review_result.startswith("Error:"):
895
- doc_preview_children = dcc.Markdown(review_result, style={'wordWrap': 'break-word'})
896
  status_message = f"{review_type} generated successfully using {input_doc_source_name}."
897
  # Store the result in the correct global variable
898
  if review_type == "Pink Review": pink_review_document = review_result
899
  elif review_type == "Red Review": red_review_document = review_result
900
  elif review_type == "Gold Review": gold_review_document = review_result
901
- download_style = {'display': 'inline-block'} # Show download button
 
 
 
902
  else:
903
  # review_result contains the error message
904
  doc_preview_children = html.Div(f"Error generating {review_type}: {review_result}", className="text-danger")
905
  status_message = f"Failed to generate {review_type}. See preview for details."
 
 
906
  download_style = {'display': 'none'}
907
 
908
- loading_style = {'visibility':'hidden'} # Hide loading dots
909
  return doc_preview_children, status_message, loading_style, download_style
910
 
911
 
912
- # 7. Handle Chat Interaction to Refine Displayed Document
913
  @app.callback(
914
- Output('chat-output', 'children'), # Display chat confirmation/error
915
- Output('document-preview', 'children', allow_duplicate=True), # Update the preview
916
  Output('status-bar', 'children', allow_duplicate=True), # Update main status
 
917
  Input('btn-send-chat', 'n_clicks'),
 
918
  State('chat-input', 'value'), # Get the chat instruction
919
  prevent_initial_call=True
920
  )
921
- def handle_chat(n_clicks, chat_input):
922
  global current_display_document, current_display_type
923
  # Also need to update the specific underlying document variable (e.g., pink_document)
924
  # so the chat changes persist if that document is used later.
925
  global shredded_document, pink_document, red_document, gold_document, pink_review_document, red_review_document, gold_review_document, loe_document, virtual_board_document
926
 
927
- if not n_clicks or not chat_input or not chat_input.strip():
928
- # No click or empty input
 
929
  raise PreventUpdate
930
- if not current_display_document or not current_display_type:
931
- # No document currently loaded in the preview to refine
932
- return html.Div("Error: No document is currently displayed to refine.", className="text-warning"), dash.no_update, "Cannot refine: No document loaded in preview."
 
 
 
933
 
934
- logging.info(f"Chat refinement requested for displayed document type: {current_display_type}. Instruction: '{chat_input[:100]}...'")
 
 
 
 
 
 
 
935
 
936
- # Construct prompt for refinement
937
- prompt = f"""**Objective:** Refine the following '{current_display_type}' document based *only* on the user's instruction below.
 
 
938
 
939
  **Your Role:** Act as an editor making precise changes.
940
 
941
  **Core Instructions:**
942
  1. **Apply Instruction:** Modify the 'Original Document' solely based on the 'User Instruction'.
943
  2. **Maintain Context:** Preserve the overall structure, tone, and format of the original document unless the instruction explicitly directs otherwise.
944
- 3. **Output Only Updated Document:** Provide *only* the complete, updated '{current_display_type}' document. Do not add any conversational text, preamble, or explanation of changes.
945
 
946
  **Original Document:**
947
  ```text
@@ -956,26 +1035,46 @@ def handle_chat(n_clicks, chat_input):
956
  **Begin Updated '{current_display_type}' Output:**
957
  """
958
 
959
- try:
960
- # Show loading indicator for chat refinement? (Optional, maybe use inner loading)
961
- status_message = f"Refining {current_display_type} based on chat instruction..."
962
- # Note: Consider adding a loading state specifically for the chat output area.
 
963
 
964
- response = model.generate_content(prompt)
965
 
966
- # Handle potential safety blocks or empty responses
967
- if not response.parts:
968
- logging.warning(f"Gemini AI returned no parts for chat refinement of {current_display_type}.")
969
- updated_document = f"Error: AI returned no content during refinement. This might be due to safety filters or an issue with the instruction."
970
- chat_response_display = html.Div(updated_document, className="text-danger")
971
- status_message = f"Error refining {current_display_type} via chat."
972
- return chat_response_display, dash.no_update, status_message # Don't update preview if refinement failed
973
- else:
974
- updated_document = response.text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
975
  logging.info(f"Successfully refined {current_display_type} via chat.")
976
 
977
- # --- CRITICAL: Update the correct underlying global variable ---
978
- # This ensures the refined document is used in subsequent steps if needed.
979
  original_doc_updated = False
980
  if current_display_type == "Shred": shredded_document = updated_document; original_doc_updated = True
981
  elif current_display_type == "Pink": pink_document = updated_document; original_doc_updated = True
@@ -999,22 +1098,24 @@ def handle_chat(n_clicks, chat_input):
999
  # Display confirmation in chat output area
1000
  chat_response_display = html.Div([
1001
  html.Strong("Refinement applied successfully."),
1002
- #html.P(f'Instruction: "{chat_input}"'), # Optional: Echo instruction
1003
  html.Hr(),
1004
  html.Em("Preview above has been updated. The changes will be used if this document is input for subsequent steps.")
1005
  ])
1006
  status_message = f"{current_display_type} updated via chat instruction."
1007
  # Update the document preview itself
1008
- doc_preview_update = dcc.Markdown(updated_document, style={'wordWrap': 'break-word'})
1009
 
1010
- return chat_response_display, doc_preview_update, status_message
1011
 
1012
- except Exception as e:
1013
- logging.error(f"Error during chat refinement call for {current_display_type}: {e}", exc_info=True)
1014
- chat_response_display = html.Div(f"Error refining document via chat: {str(e)}", className="text-danger")
1015
- status_message = f"Error refining {current_display_type} via chat."
1016
- # Do not update the main document preview if chat refinement fails
1017
- return chat_response_display, dash.no_update, status_message
 
 
 
1018
 
1019
 
1020
  # 8. Handle Download Button Click
@@ -1027,8 +1128,8 @@ def download_generated_document(n_clicks):
1027
  """Prepares the currently displayed document for download."""
1028
  global current_display_document, current_display_type
1029
 
1030
- if not n_clicks or current_display_document is None or current_display_type is None:
1031
- # No clicks or nothing to download
1032
  raise PreventUpdate
1033
 
1034
  logging.info(f"Download requested for displayed document: {current_display_type}")
@@ -1047,57 +1148,85 @@ def download_generated_document(n_clicks):
1047
  data_io = StringIO(current_display_document)
1048
  df = None
1049
 
1050
- # Attempt 1: Try parsing as CSV (or detect delimiter)
1051
- try:
1052
- # Read a sample to sniff delimiter
1053
- sniffer_sample = data_io.read(2048) # Read more data for better sniffing
1054
- data_io.seek(0) # Reset pointer after reading sample
1055
- dialect = pd.io.parsers.readers.csv.Sniffer().sniff(sniffer_sample, delimiters=',|\t') # Sniff common delimiters
1056
- df = pd.read_csv(data_io, sep=dialect.delimiter)
1057
- logging.info(f"Successfully parsed {current_display_type} using detected delimiter '{dialect.delimiter}'.")
1058
- except Exception as e_csv:
1059
- logging.warning(f"Could not parse {current_display_type} as standard CSV/TSV ({e_csv}). Trying Markdown Table parsing.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1060
  data_io.seek(0) # Reset pointer
1061
-
1062
- # Attempt 2: Try parsing as a Markdown table (simple version)
1063
- lines = [line.strip() for line in data_io.readlines() if line.strip()]
1064
- header = []
1065
- data = []
1066
- header_found = False
1067
- separator_found = False
1068
-
1069
- for line in lines:
1070
- if line.startswith('|') and line.endswith('|'):
1071
- parts = [p.strip() for p in line.strip('|').split('|')]
1072
- if not header_found:
1073
- header = parts
1074
- header_found = True
1075
- elif '---' in line: # Detect separator line
1076
- separator_found = True
1077
- # Optional: Check if separator alignment matches header count
1078
- if len(line.strip('|').split('|')) != len(header):
1079
- logging.warning("Markdown table header/separator mismatch detected.")
1080
- # Decide whether to proceed or fail parsing
1081
- elif header_found and separator_found: # Only add data lines after header and separator
1082
- if len(parts) == len(header):
1083
- data.append(parts)
1084
- else:
1085
- logging.warning(f"Markdown table row data mismatch (expected {len(header)}, got {len(parts)}): {line}")
1086
-
1087
-
1088
- if header and data:
1089
- df = pd.DataFrame(data, columns=header)
1090
- logging.info(f"Successfully parsed {current_display_type} as Markdown Table.")
1091
- else:
1092
- logging.warning(f"Could not parse {current_display_type} as Markdown Table after CSV attempt failed.")
1093
- # Fallback: If no DataFrame could be created, send as text
1094
- return dict(content=current_display_document, filename=f"{safe_filename_base}.txt")
1095
 
1096
  # If DataFrame was created, save to Excel
1097
  output = BytesIO()
1098
- # Use xlsxwriter engine for better compatibility/features if needed
1099
  with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
1100
- df.to_excel(writer, sheet_name=current_display_type[:31], index=False) # Sheet name limit is 31 chars
 
 
 
 
 
 
 
 
 
 
1101
  logging.info(f"Sending {filename} (Excel format)")
1102
  return dcc.send_bytes(output.getvalue(), filename)
1103
 
@@ -1117,8 +1246,8 @@ def download_generated_document(n_clicks):
1117
  if paragraph_text.strip():
1118
  doc.add_paragraph(paragraph_text)
1119
  else:
1120
- # Optionally add an empty paragraph to represent blank lines, or skip
1121
- doc.add_paragraph() # Add blank lines if they were intentional
1122
 
1123
  # Save the document to an in-memory BytesIO object
1124
  output = BytesIO()
@@ -1138,7 +1267,8 @@ if __name__ == '__main__':
1138
  # Set debug=False for production/deployment environments like Hugging Face Spaces
1139
  # Set host='0.0.0.0' to make the app accessible on the network (required for Docker/Spaces)
1140
  # Default port 8050, using 7860 as often used for ML demos/Spaces
1141
- # Use server=app.server for Gunicorn compatibility (multi-threading via workers)
1142
  # Multi-threading for multiple simultaneous user support is handled by the deployment server (e.g., Gunicorn with workers), not directly in app.run for production.
 
1143
  app.run(debug=False, host='0.0.0.0', port=7860)
1144
- print("Dash application has finished running.")
 
 
1
+ ```python
2
  import base64
3
  import io
4
  import os
 
7
  from io import BytesIO, StringIO
8
  import dash # Version 3.0.3 (or compatible)
9
  import dash_bootstrap_components as dbc # Version 2.0.2 (or compatible)
10
+ from dash import html, dcc, Input, Output, State, callback_context, ALL, no_update # Import no_update
11
  from dash.exceptions import PreventUpdate # <-- Import PreventUpdate from here
12
  import google.generativeai as genai
13
  from docx.shared import Pt
 
15
  from PyPDF2 import PdfReader
16
  import logging
17
  import uuid # For unique IDs if needed with pattern matching
18
+ import xlsxwriter # Needed for Excel export engine
19
 
20
  # --- Logging Configuration ---
21
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
41
  else:
42
  genai.configure(api_key=api_key)
43
  # Specify a model compatible with function calling or more advanced generation if needed.
44
+ # Using 'gemini-1.5-pro-latest' for potential better performance, check compatibility if issues arise.
45
+ model = genai.GenerativeModel('gemini-2.5-pro-preview-03-25'') # Updated model
46
+ logging.info("Gemini AI configured successfully using 'gemini-1.5-pro-latest'.")
47
  except Exception as e:
48
  logging.error(f"Error configuring Gemini AI: {e}", exc_info=True)
49
  model = None
 
184
  )
185
  ]), className="mb-3"
186
  ),
187
+ dbc.Button("Download Output", id="btn-download", color="success", className="mt-3 me-2", style={'display': 'none'}), # Hidden initially, add margin
188
  dcc.Download(id="download-document"),
189
 
190
  html.Hr(),
 
199
  type="circle",
200
  children=[
201
  dbc.Textarea(id="chat-input", placeholder="Enter instructions to refine the document shown above...", className="mb-2", style={'whiteSpace': 'normal', 'wordWrap': 'break-word'}), # Ensure word wrap
202
+ # Button Group for Send and Clear Chat
203
+ dbc.ButtonGroup([
204
+ dbc.Button("Send Chat", id="btn-send-chat", color="secondary"), # Use secondary style
205
+ dbc.Button("Clear Chat", id="btn-clear-chat", color="tertiary") # Use tertiary style
206
+ ], className="mb-3"),
207
  html.Div(id="chat-output", style={'whiteSpace': 'pre-wrap', 'marginTop': '10px', 'border': '1px solid #eee', 'padding': '10px', 'borderRadius': '5px', 'minHeight': '50px'}) # Add border/padding
208
  ]
209
  )
 
277
  if not model:
278
  logging.error("Gemini AI model not initialized.")
279
  return "Error: AI Model not configured. Please check API Key."
280
+ if not input_docs or not any(doc.strip() for doc in input_docs if doc): # Check if list exists and has non-empty content
281
+ logging.warning(f"generate_ai_document called for {doc_type} with no valid input documents.")
282
  return f"Error: Missing required input document(s) for {doc_type} generation."
283
 
284
  # Combine input documents into a single string
 
293
  **Core Instructions:**
294
  1. **Adhere Strictly to the Task:** Generate *only* the content for the '{doc_type}'. Do not add introductions, summaries, or conversational filler unless it's part of the requested document format itself.
295
  2. **Follow Format Guidelines:**
296
+ * **Spreadsheet Types (Shred, Reviews, LOE, Board):** Structure output clearly. Use Markdown tables or a delimited format (like CSV) suitable for parsing. Define clear columns (e.g., `PWS_Section | Requirement | Finding | Recommendation` for reviews; `Section | Task | Estimated_Hours | Resource_Type` for LOE). Use '|' as the primary delimiter for tables.
297
  * **Proposal Sections (Pink, Red, Gold):** Write professional, compelling prose. Use active voice ("MicroHealth will..."). Directly address requirements from context (Shredded PWS). Detail the 'how' (technical approach, methodology, workflow, tools). Incorporate innovation and benefits (efficiency, quality, outcomes). Substantiate claims (e.g., cite Gartner, Forrester if applicable). Clearly state roles/responsibilities (labor categories). Ensure compliance with Section L/M (Evaluation Criteria) from context. Avoid vague terms ('might', 'could', 'potentially'); be assertive and confident. Use paragraphs primarily; limit bullet points to lists where essential.
298
  3. **Utilize Provided Documents:**
299
  * **Context Document(s):** Use these as the primary reference or baseline (e.g., Shredded Requirements are the basis for compliance).
 
314
  **Detailed Instructions for '{doc_type}':**
315
  {document_types.get(doc_type, "Generate the requested document based on the inputs and context.")}
316
 
317
+ **Begin '{doc_type}' Output (Use Markdown table format for spreadsheet types):**
318
  """
319
 
320
  logging.info(f"Generating AI document for: {doc_type}")
 
325
  response = model.generate_content(prompt) # Consider adding request_options={'timeout': 300} if needed
326
 
327
  # Handle potential safety blocks or empty responses
328
+ # Accessing response text might differ slightly based on API version/model behavior
329
+ generated_text = ""
330
+ try:
331
+ if hasattr(response, 'text'):
332
+ generated_text = response.text
333
+ elif hasattr(response, 'parts') and response.parts:
334
+ # Concatenate text from parts if necessary
335
+ generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
336
+ else:
337
+ # Check for finish_reason if available
338
+ finish_reason = getattr(response, 'prompt_feedback', {}).get('block_reason') or getattr(response, 'candidates', [{}])[0].get('finish_reason')
339
+ logging.warning(f"Gemini AI response for {doc_type} has no text/parts. Finish Reason: {finish_reason}. Response: {response}")
340
+ generated_text = f"Error: AI returned no content for {doc_type}. Possible reason: {finish_reason}. Check Gemini safety settings or prompt complexity."
341
+
342
+ except Exception as resp_err:
343
+ logging.error(f"Error extracting text from Gemini response for {doc_type}: {resp_err}", exc_info=True)
344
+ generated_text = f"Error: Could not parse AI response for {doc_type}."
345
+
346
+
347
+ if not generated_text.strip() and not generated_text.startswith("Error:"):
348
+ logging.warning(f"Gemini AI returned empty text for {doc_type}.")
349
+ generated_text = f"Error: AI returned empty content for {doc_type}. Please try again or adjust the input documents."
350
+
351
 
352
  logging.info(f"Successfully generated document for: {doc_type}")
353
  # Update global state for download/chat *only if successful*
 
355
  current_display_document = generated_text
356
  current_display_type = doc_type
357
  else:
358
+ # Ensure error message is displayed if AI returns an error internally or extraction failed
359
  current_display_document = generated_text
360
  current_display_type = doc_type # Still set type so user knows what failed
361
 
 
482
  raise PreventUpdate
483
 
484
  # Check if any click count is > 0 (or just check the specific one that triggered)
485
+ # Ensure n_clicks is a list before using any()
486
+ if not n_clicks or not any(nc for nc in n_clicks if nc is not None): # Check if any click occurred
487
  raise PreventUpdate
488
 
489
 
 
532
  else:
533
  status_message += "Ready for 'Shred' or other actions."
534
 
535
+ # If the list is now empty, return an empty list instead of None
536
+ return updated_file_list_display if updated_file_list_display else [], status_message
537
 
538
 
539
  # 3. Handle Action Button Clicks (Show Controls or Trigger Generation)
 
552
  global shredded_document, pink_document, red_document, gold_document, pink_review_document, red_review_document, gold_review_document, loe_document, virtual_board_document
553
  # Reset potentially uploaded review files when a *new* main action is selected from the left nav
554
  global uploaded_pink_content, uploaded_red_content, uploaded_gold_content
555
+ global current_display_document, current_display_type # Need to update preview/download state
556
 
557
  triggered_id_dict = callback_context.triggered_id
558
  if not triggered_id_dict or not isinstance(triggered_id_dict, dict) or 'index' not in triggered_id_dict:
559
  raise PreventUpdate
560
 
561
  # Check if any click count is > 0
562
+ if not n_clicks or not any(nc for nc in n_clicks if nc is not None):
563
  raise PreventUpdate
564
 
565
  action_type = triggered_id_dict['index']
 
569
  review_controls_style = {'display': 'none'} # Hide review controls by default
570
  review_controls_children = []
571
  status_message = f"Selected action: {action_type}"
572
+ doc_preview_children = no_update # Avoid clearing preview unless needed
573
+ loading_style = {'visibility':'hidden', 'height': '30px'} # Hide loading dots by default
574
  download_style = {'display': 'none'} # Hide download button by default
575
 
576
 
 
595
  if not shredded_document:
596
  status_message = "Error: Please 'Shred' the source documents first."
597
  doc_preview_children = html.Div(status_message, className="text-danger")
598
+ current_display_document = None # Clear potentially stale preview
599
+ current_display_type = None
600
  return review_controls_style, [], status_message, doc_preview_children, loading_style, download_style
601
 
602
  # Check specific prerequisites and get the document to review
 
617
  if prereq_doc is None and action_type != "Pink Review": # Shred is checked above
618
  status_message = f"Error: Please complete '{prereq_doc_name.replace(' Document','')}' first."
619
  doc_preview_children = html.Div(status_message, className="text-danger")
620
+ current_display_document = None
621
+ current_display_type = None
622
  return review_controls_style, [], status_message, doc_preview_children, loading_style, download_style
623
 
624
 
 
663
  # Clear preview when showing controls, provide instruction
664
  doc_preview_children = html.Div(f"Configure input source for {base_doc_type} document and click 'Generate {action_type}'.", style={'padding':'10px'})
665
  status_message = f"Ready to configure input for {action_type}."
666
+ current_display_document = None # Clear internal state for preview/download too
667
+ current_display_type = None
668
+ download_style = {'display': 'none'} # Ensure download button is hidden
669
 
670
 
671
  # --- Actions Triggering Direct Generation (Shred, Pink, Red, Gold, LOE, Virtual Board) ---
672
  else:
673
  review_controls_style = {'display': 'none'} # Hide review controls
674
  review_controls_children = []
675
+ loading_style = {'visibility':'visible', 'height': '30px'} # Show loading dots
676
  doc_preview_children = "" # Clear preview while loading/generating
677
  status_message = f"Generating {action_type}..."
678
+ download_style = {'display': 'none'} # Hide download during generation
679
 
680
  # Determine inputs based on action type
681
  input_docs = []
 
728
  if generation_possible:
729
  result_doc = generate_ai_document(action_type, input_docs, context_docs)
730
 
731
+ # Store result in the correct global variable AND current_display vars
732
  if result_doc and not result_doc.startswith("Error:"):
733
+ current_display_document = result_doc # Set for preview/download/chat
734
+ current_display_type = action_type
735
+
736
  if action_type == "Shred": shredded_document = result_doc
737
  elif action_type == "Pink": pink_document = result_doc
738
  elif action_type == "Red": red_document = result_doc
 
741
  elif action_type == "Virtual Board": virtual_board_document = result_doc
742
  # Reviews are handled separately
743
 
744
+ doc_preview_children = dcc.Markdown(result_doc, style={'wordWrap': 'break-word', 'overflowX': 'auto'}) # Allow horizontal scroll for wide tables
745
  status_message = f"{action_type} generated successfully."
746
+ download_style = {'display': 'inline-block', 'marginRight': '10px'} # Show download button on success
747
  else:
748
  # If generation failed, result_doc contains the error message from generate_ai_document
749
  doc_preview_children = html.Div(result_doc, className="text-danger") # Display error in preview
750
  status_message = f"Error generating {action_type}. See preview for details."
751
+ current_display_document = result_doc # Show error in preview but prevent download/chat
752
+ current_display_type = action_type
753
  download_style = {'display': 'none'} # Hide download button on error
754
 
755
  else:
756
  # Generation not possible due to prerequisites
757
  doc_preview_children = html.Div(status_message, className="text-danger")
758
+ current_display_document = None # No doc generated
759
+ current_display_type = None
760
+ download_style = {'display': 'none'}
761
 
762
+ loading_style = {'visibility':'hidden', 'height': '30px'} # Hide loading dots when finished/failed
763
 
764
 
765
  return review_controls_style, review_controls_children, status_message, doc_preview_children, loading_style, download_style
 
776
  # Preserves existing style attributes while toggling 'display'
777
  new_style = current_style.copy() if current_style else {}
778
  should_display = (radio_value == 'upload')
779
+ new_display_value = 'block' if should_display else 'none'
 
 
 
 
780
 
781
  # Prevent update if display style is already correct
782
+ if current_style and ('display' in current_style and current_style['display'] == new_display_value):
783
  raise PreventUpdate
784
  else:
785
+ logging.debug(f"Toggling review upload visibility. Radio: {radio_value}, New display: {new_display_value}")
786
+ new_style['display'] = new_display_value
787
+ # Make sure other style defaults are present if creating new style dict
788
+ if not current_style:
789
+ new_style.update({
790
+ 'width': '100%', 'height': '60px', 'lineHeight': '60px', 'borderWidth': '1px',
791
+ 'borderStyle': 'dashed', 'borderRadius': '5px', 'textAlign': 'center', 'margin': '10px 0',
792
+ 'backgroundColor': '#f8f9fa'
793
+ })
794
  return new_style
795
 
796
 
 
801
  Input('upload-review-doc', 'contents'),
802
  State('upload-review-doc', 'filename'),
803
  # Get the current review type from the button ID that generated the controls
804
+ # We need the ID that triggered the controls to be shown, not necessarily the state of the generate button itself
805
+ # Let's get the action_type from the state of the review controls (which are generated by action buttons)
806
+ State('review-controls', 'children'),
807
  prevent_initial_call=True
808
  )
809
+ def handle_review_upload(contents, filename, review_controls_children):
810
  global uploaded_pink_content, uploaded_red_content, uploaded_gold_content
811
 
812
+ if contents is None or filename is None or not review_controls_children:
813
+ # No file uploaded or controls not populated yet (e.g., user selected action but hasn't uploaded)
814
  raise PreventUpdate
815
 
816
+ # Determine which review type this upload is for. Find the H5 title.
817
+ review_type = None
818
+ base_type = None
819
+ try:
820
+ # Assuming the first child is the H5 title like "Configure Input for Pink Review"
821
+ if isinstance(review_controls_children, list) and len(review_controls_children) > 0 and isinstance(review_controls_children[0], html.H5):
822
+ title_text = review_controls_children[0].children
823
+ # Extract "Pink Review", "Red Review", etc.
824
+ parts = title_text.split(" for ")
825
+ if len(parts) > 1:
826
+ review_type = parts[1].strip()
827
+ base_type = review_type.split(" ")[0] # Pink, Red, Gold
828
+ except Exception as e:
829
+ logging.warning(f"Could not reliably determine review type from review controls children: {e}")
830
 
831
+ if not review_type or not base_type:
832
+ logging.warning("handle_review_upload: Could not determine review type from controls.")
833
+ return html.Div("Error determining review type.", className="text-danger small"), "Internal error: Could not determine review type."
 
 
834
 
 
 
835
 
836
  logging.info(f"Handling upload of file '{filename}' for {review_type} input.")
837
 
 
848
  if error:
849
  status_bar_message = f"Error processing uploaded {base_type} file: {error}"
850
  upload_status_display = html.Div(f"Failed to load {filename}: {error}", className="text-danger small")
851
+ elif file_content_text is None: # Handle case where process_document returns (None, None)
852
+ status_bar_message = f"Error processing uploaded {base_type} file: No content could be extracted."
853
+ upload_status_display = html.Div(f"Failed to load {filename}: No text extracted.", className="text-danger small")
854
  else:
855
  status_bar_message = f"Uploaded '{filename}' successfully for {review_type} input."
856
  upload_status_display = html.Div(f"Using uploaded file: {filename}", className="text-success small")
 
880
  global shredded_document, pink_document, red_document, gold_document
881
  global pink_review_document, red_review_document, gold_review_document
882
  global uploaded_pink_content, uploaded_red_content, uploaded_gold_content
883
+ global current_display_document, current_display_type # Update preview state
884
 
885
  triggered_id_dict = callback_context.triggered_id
886
  if not triggered_id_dict or not isinstance(triggered_id_dict, dict) or 'index' not in triggered_id_dict:
887
  raise PreventUpdate
888
 
889
  # Check if any click count is > 0
890
+ if not n_clicks or not any(nc for nc in n_clicks if nc is not None):
891
  raise PreventUpdate
892
 
893
  review_type = triggered_id_dict['index'] # e.g., "Pink Review"
 
896
 
897
  doc_preview_children = "" # Clear preview
898
  status_message = f"Generating {review_type}..."
899
+ loading_style = {'visibility':'visible', 'height': '30px'} # Show loading dots
900
  download_style = {'display': 'none'} # Hide download initially
901
+ current_display_document = None # Clear display state
902
+ current_display_type = None
903
 
904
 
905
  # --- Prerequisite Check ---
906
  if not shredded_document:
907
  status_message = "Error: 'Shredded' document is missing. Please perform 'Shred' first."
908
+ loading_style = {'visibility':'hidden', 'height': '30px'}
909
  doc_preview_children = html.Div(status_message, className="text-danger")
910
  return doc_preview_children, status_message, loading_style, download_style
911
 
 
920
  elif base_type == "Gold": input_document_content = gold_document
921
 
922
  if not input_document_content:
923
+ status_message = f"Error: Cannot use 'generated' option. The {input_doc_source_name} was not found (was it generated successfully?). Please generate or upload it."
924
+ loading_style = {'visibility':'hidden', 'height': '30px'}
925
  doc_preview_children = html.Div(status_message, className="text-danger")
926
  return doc_preview_children, status_message, loading_style, download_style
927
 
 
932
  elif base_type == "Gold": input_document_content = uploaded_gold_content
933
 
934
  if not input_document_content:
935
+ status_message = f"Error: Cannot use 'upload' option. No {base_type} document was successfully uploaded and processed for this review step. Please upload a valid file."
936
+ loading_style = {'visibility':'hidden', 'height': '30px'}
 
937
  doc_preview_children = html.Div(status_message, className="text-danger")
938
  return doc_preview_children, status_message, loading_style, download_style
939
  else:
940
  status_message = f"Error: Invalid source option '{source_option}' selected."
941
+ loading_style = {'visibility':'hidden', 'height': '30px'}
942
  doc_preview_children = html.Div(status_message, className="text-danger")
943
  return doc_preview_children, status_message, loading_style, download_style
944
 
 
951
  review_result = generate_ai_document(review_type, [input_document_content], context_docs=[shredded_document])
952
 
953
  if review_result and not review_result.startswith("Error:"):
954
+ doc_preview_children = dcc.Markdown(review_result, style={'wordWrap': 'break-word', 'overflowX': 'auto'}) # Allow horizontal scroll
955
  status_message = f"{review_type} generated successfully using {input_doc_source_name}."
956
  # Store the result in the correct global variable
957
  if review_type == "Pink Review": pink_review_document = review_result
958
  elif review_type == "Red Review": red_review_document = review_result
959
  elif review_type == "Gold Review": gold_review_document = review_result
960
+ # Update display state
961
+ current_display_document = review_result
962
+ current_display_type = review_type
963
+ download_style = {'display': 'inline-block', 'marginRight': '10px'} # Show download button
964
  else:
965
  # review_result contains the error message
966
  doc_preview_children = html.Div(f"Error generating {review_type}: {review_result}", className="text-danger")
967
  status_message = f"Failed to generate {review_type}. See preview for details."
968
+ current_display_document = review_result # Show error, but don't allow download/chat
969
+ current_display_type = review_type
970
  download_style = {'display': 'none'}
971
 
972
+ loading_style = {'visibility':'hidden', 'height': '30px'} # Hide loading dots
973
  return doc_preview_children, status_message, loading_style, download_style
974
 
975
 
976
+ # 7. Handle Chat Interaction (Send and Clear)
977
  @app.callback(
978
+ Output('chat-output', 'children', allow_duplicate=True), # Display chat confirmation/error or clear
979
+ Output('document-preview', 'children', allow_duplicate=True), # Update the preview on successful refinement
980
  Output('status-bar', 'children', allow_duplicate=True), # Update main status
981
+ Output('chat-input', 'value'), # Clear input field after send/clear
982
  Input('btn-send-chat', 'n_clicks'),
983
+ Input('btn-clear-chat', 'n_clicks'),
984
  State('chat-input', 'value'), # Get the chat instruction
985
  prevent_initial_call=True
986
  )
987
+ def handle_chat(send_clicks, clear_clicks, chat_input):
988
  global current_display_document, current_display_type
989
  # Also need to update the specific underlying document variable (e.g., pink_document)
990
  # so the chat changes persist if that document is used later.
991
  global shredded_document, pink_document, red_document, gold_document, pink_review_document, red_review_document, gold_review_document, loe_document, virtual_board_document
992
 
993
+ # Determine which button was pressed
994
+ ctx = callback_context
995
+ if not ctx.triggered:
996
  raise PreventUpdate
997
+ button_id = ctx.triggered[0]['prop_id'].split('.')[0]
998
+
999
+ # --- Handle Clear Chat ---
1000
+ if button_id == 'btn-clear-chat' and clear_clicks > 0:
1001
+ logging.info("Clear chat button clicked.")
1002
+ return html.Div("Chat cleared.", className="text-muted fst-italic"), no_update, "Chat cleared.", "" # Clear output, no preview change, update status, clear input
1003
 
1004
+ # --- Handle Send Chat ---
1005
+ if button_id == 'btn-send-chat' and send_clicks > 0:
1006
+ if not chat_input or not chat_input.strip():
1007
+ # No input to send
1008
+ return html.Div("Please enter an instruction.", className="text-warning small"), no_update, "Chat instruction empty.", chat_input # Keep input
1009
+ if not current_display_document or not current_display_type:
1010
+ # No document currently loaded in the preview to refine
1011
+ return html.Div("Error: No document is currently displayed to refine.", className="text-warning"), no_update, "Cannot refine: No document loaded in preview.", "" # Clear input
1012
 
1013
+ logging.info(f"Chat refinement requested for displayed document type: {current_display_type}. Instruction: '{chat_input[:100]}...'")
1014
+
1015
+ # Construct prompt for refinement
1016
+ prompt = f"""**Objective:** Refine the following '{current_display_type}' document based *only* on the user's instruction below.
1017
 
1018
  **Your Role:** Act as an editor making precise changes.
1019
 
1020
  **Core Instructions:**
1021
  1. **Apply Instruction:** Modify the 'Original Document' solely based on the 'User Instruction'.
1022
  2. **Maintain Context:** Preserve the overall structure, tone, and format of the original document unless the instruction explicitly directs otherwise.
1023
+ 3. **Output Only Updated Document:** Provide *only* the complete, updated '{current_display_type}' document. Do not add any conversational text, preamble, or explanation of changes. Ensure the output format matches the original (e.g., Markdown table if original was a table).
1024
 
1025
  **Original Document:**
1026
  ```text
 
1035
  **Begin Updated '{current_display_type}' Output:**
1036
  """
1037
 
1038
+ try:
1039
+ # Show loading indicator for chat refinement? (Optional, maybe use inner loading)
1040
+ status_message = f"Refining {current_display_type} based on chat instruction..."
1041
+ # Note: Using a separate loading indicator for chat output is possible but adds complexity.
1042
+ # For now, rely on the main status bar.
1043
 
1044
+ response = model.generate_content(prompt)
1045
 
1046
+ # Handle potential safety blocks or empty responses (refined logic)
1047
+ updated_document = ""
1048
+ try:
1049
+ if hasattr(response, 'text'):
1050
+ updated_document = response.text
1051
+ elif hasattr(response, 'parts') and response.parts:
1052
+ updated_document = "".join(part.text for part in response.parts if hasattr(part, 'text'))
1053
+ else:
1054
+ finish_reason = getattr(response, 'prompt_feedback', {}).get('block_reason') or getattr(response, 'candidates', [{}])[0].get('finish_reason')
1055
+ logging.warning(f"Gemini AI response for chat refinement of {current_display_type} has no text/parts. Finish Reason: {finish_reason}. Response: {response}")
1056
+ updated_document = f"Error: AI returned no content during refinement. Possible reason: {finish_reason}. Check Gemini safety settings or instruction complexity."
1057
+
1058
+ except Exception as resp_err:
1059
+ logging.error(f"Error extracting text from Gemini refinement response for {current_display_type}: {resp_err}", exc_info=True)
1060
+ updated_document = f"Error: Could not parse AI response during refinement for {current_display_type}."
1061
+
1062
+ if not updated_document.strip() and not updated_document.startswith("Error:"):
1063
+ logging.warning(f"Gemini AI returned empty text for chat refinement of {current_display_type}.")
1064
+ updated_document = f"Error: AI returned empty content during refinement for {current_display_type}. Please try a different instruction."
1065
+
1066
+
1067
+ # If refinement failed, show error in chat output, don't update preview
1068
+ if updated_document.startswith("Error:"):
1069
+ chat_response_display = html.Div(updated_document, className="text-danger")
1070
+ status_message = f"Error refining {current_display_type} via chat."
1071
+ return chat_response_display, no_update, status_message, "" # Clear input
1072
+
1073
+
1074
+ # --- Successful Refinement ---
1075
  logging.info(f"Successfully refined {current_display_type} via chat.")
1076
 
1077
+ # CRITICAL: Update the correct underlying global variable
 
1078
  original_doc_updated = False
1079
  if current_display_type == "Shred": shredded_document = updated_document; original_doc_updated = True
1080
  elif current_display_type == "Pink": pink_document = updated_document; original_doc_updated = True
 
1098
  # Display confirmation in chat output area
1099
  chat_response_display = html.Div([
1100
  html.Strong("Refinement applied successfully."),
 
1101
  html.Hr(),
1102
  html.Em("Preview above has been updated. The changes will be used if this document is input for subsequent steps.")
1103
  ])
1104
  status_message = f"{current_display_type} updated via chat instruction."
1105
  # Update the document preview itself
1106
+ doc_preview_update = dcc.Markdown(updated_document, style={'wordWrap': 'break-word', 'overflowX': 'auto'})
1107
 
1108
+ return chat_response_display, doc_preview_update, status_message, "" # Clear input field
1109
 
1110
+ except Exception as e:
1111
+ logging.error(f"Error during chat refinement call for {current_display_type}: {e}", exc_info=True)
1112
+ chat_response_display = html.Div(f"Error refining document via chat: {str(e)}", className="text-danger")
1113
+ status_message = f"Error refining {current_display_type} via chat."
1114
+ # Do not update the main document preview if chat refinement fails
1115
+ return chat_response_display, no_update, status_message, "" # Clear input
1116
+
1117
+ # If no button was triggered (shouldn't normally happen with prevent_initial_call)
1118
+ raise PreventUpdate
1119
 
1120
 
1121
  # 8. Handle Download Button Click
 
1128
  """Prepares the currently displayed document for download."""
1129
  global current_display_document, current_display_type
1130
 
1131
+ if not n_clicks or current_display_document is None or current_display_type is None or current_display_document.startswith("Error:"):
1132
+ # No clicks, nothing to download, or current display is an error message
1133
  raise PreventUpdate
1134
 
1135
  logging.info(f"Download requested for displayed document: {current_display_type}")
 
1148
  data_io = StringIO(current_display_document)
1149
  df = None
1150
 
1151
+ # Refined parsing logic: prioritize Markdown tables, then CSV/TSV
1152
+ lines = [line.strip() for line in data_io.readlines() if line.strip()]
1153
+ data_io.seek(0) # Reset pointer
1154
+
1155
+ # Attempt 1: Parse as Markdown table
1156
+ header = []
1157
+ data = []
1158
+ header_found = False
1159
+ separator_found = False
1160
+ potential_header_line = -1
1161
+ potential_sep_line = -1
1162
+
1163
+ for i, line in enumerate(lines):
1164
+ if line.startswith('|') and line.endswith('|'):
1165
+ parts = [p.strip() for p in line.strip('|').split('|')]
1166
+ if not header_found and '---' not in line: # Potential header
1167
+ header = parts
1168
+ header_found = True
1169
+ potential_header_line = i
1170
+ elif header_found and '---' in line and potential_header_line == i - 1: # Separator line immediately follows header
1171
+ separator_found = True
1172
+ potential_sep_line = i
1173
+ # Optional: Check column count match
1174
+ if len(line.strip('|').split('|')) != len(header):
1175
+ logging.warning("Markdown table header/separator column count mismatch detected.")
1176
+ # Reset, might not be a valid table
1177
+ header_found = False
1178
+ separator_found = False
1179
+ header = []
1180
+ continue
1181
+ elif header_found and separator_found and potential_sep_line == i - 1: # Data line follows separator
1182
+ if len(parts) == len(header):
1183
+ data.append(parts)
1184
+ else:
1185
+ logging.warning(f"Markdown table row data mismatch (expected {len(header)}, got {len(parts)}): {line}")
1186
+ # Decide whether to continue or break parsing for this row
1187
+ else: # Doesn't fit the pattern, reset if we were in the middle of parsing a potential table
1188
+ if header_found or separator_found:
1189
+ logging.debug(f"Resetting Markdown parse state at line: {line}")
1190
+ header_found = False
1191
+ separator_found = False
1192
+ header = []
1193
+ data = []
1194
+
1195
+
1196
+ if header and data:
1197
+ df = pd.DataFrame(data, columns=header)
1198
+ logging.info(f"Successfully parsed {current_display_type} as Markdown Table.")
1199
+ else:
1200
+ # Attempt 2: Try parsing as CSV/TSV
1201
+ logging.warning(f"Could not parse {current_display_type} as Markdown Table. Attempting CSV/TSV parsing.")
1202
  data_io.seek(0) # Reset pointer
1203
+ try:
1204
+ # Read a sample to sniff delimiter
1205
+ sniffer_sample = data_io.read(4096) # Read more data for better sniffing
1206
+ data_io.seek(0) # Reset pointer after reading sample
1207
+ dialect = pd.io.parsers.readers.csv.Sniffer().sniff(sniffer_sample, delimiters=',|\t') # Sniff common delimiters
1208
+ df = pd.read_csv(data_io, sep=dialect.delimiter)
1209
+ logging.info(f"Successfully parsed {current_display_type} using detected delimiter '{dialect.delimiter}'.")
1210
+ except Exception as e_csv:
1211
+ logging.warning(f"Could not parse {current_display_type} as standard CSV/TSV after Markdown attempt failed ({e_csv}). Sending as text.")
1212
+ # Fallback: If no DataFrame could be created, send as text
1213
+ return dict(content=current_display_document, filename=f"{safe_filename_base}.txt")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1214
 
1215
  # If DataFrame was created, save to Excel
1216
  output = BytesIO()
1217
+ # Use xlsxwriter engine for better compatibility/features
1218
  with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
1219
+ sheet_name = safe_filename_base[:31] # Use sanitized name, limit 31 chars
1220
+ df.to_excel(writer, sheet_name=sheet_name, index=False)
1221
+ # Auto-adjust column widths (optional, can be slow for large files)
1222
+ worksheet = writer.sheets[sheet_name]
1223
+ for idx, col in enumerate(df): # loop through columns
1224
+ series = df[col]
1225
+ max_len = max((
1226
+ series.astype(str).map(len).max(), # len of largest item
1227
+ len(str(series.name)) # len of column name/header
1228
+ )) + 1 # adding a little extra space
1229
+ worksheet.set_column(idx, idx, max_len) # set column width
1230
  logging.info(f"Sending {filename} (Excel format)")
1231
  return dcc.send_bytes(output.getvalue(), filename)
1232
 
 
1246
  if paragraph_text.strip():
1247
  doc.add_paragraph(paragraph_text)
1248
  else:
1249
+ # Add an empty paragraph to represent blank lines
1250
+ doc.add_paragraph()
1251
 
1252
  # Save the document to an in-memory BytesIO object
1253
  output = BytesIO()
 
1267
  # Set debug=False for production/deployment environments like Hugging Face Spaces
1268
  # Set host='0.0.0.0' to make the app accessible on the network (required for Docker/Spaces)
1269
  # Default port 8050, using 7860 as often used for ML demos/Spaces
 
1270
  # Multi-threading for multiple simultaneous user support is handled by the deployment server (e.g., Gunicorn with workers), not directly in app.run for production.
1271
+ # Set debug=True locally for development, False for HF Spaces
1272
  app.run(debug=False, host='0.0.0.0', port=7860)
1273
+ print("Dash application has finished running.")
1274
+ ```