Spaces:
Paused
Paused
Update app.py
Browse files
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'
|
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 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
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 |
-
|
|
|
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
|
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 =
|
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'] ==
|
744 |
raise PreventUpdate
|
745 |
else:
|
746 |
-
logging.debug(f"Toggling review upload visibility. Radio: {radio_value}, New
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
758 |
prevent_initial_call=True
|
759 |
)
|
760 |
-
def handle_review_upload(contents, filename,
|
761 |
global uploaded_pink_content, uploaded_red_content, uploaded_gold_content
|
762 |
|
763 |
-
if contents is None or filename is None or not
|
764 |
-
# No file uploaded or controls not
|
765 |
raise PreventUpdate
|
766 |
|
767 |
-
# Determine which review type this upload is for
|
768 |
-
|
769 |
-
|
770 |
-
|
771 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
772 |
|
773 |
-
|
774 |
-
|
775 |
-
|
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
|
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 |
-
|
876 |
-
|
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 |
-
|
|
|
|
|
|
|
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
|
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(
|
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 |
-
|
928 |
-
|
|
|
929 |
raise PreventUpdate
|
930 |
-
|
931 |
-
|
932 |
-
|
|
|
|
|
|
|
933 |
|
934 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
935 |
|
936 |
-
|
937 |
-
|
|
|
|
|
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 |
-
|
960 |
-
|
961 |
-
|
962 |
-
|
|
|
963 |
|
964 |
-
|
965 |
|
966 |
-
|
967 |
-
|
968 |
-
|
969 |
-
|
970 |
-
|
971 |
-
|
972 |
-
|
973 |
-
|
974 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
975 |
logging.info(f"Successfully refined {current_display_type} via chat.")
|
976 |
|
977 |
-
#
|
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 |
-
|
1013 |
-
|
1014 |
-
|
1015 |
-
|
1016 |
-
|
1017 |
-
|
|
|
|
|
|
|
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
|
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 |
-
#
|
1051 |
-
|
1052 |
-
|
1053 |
-
|
1054 |
-
|
1055 |
-
|
1056 |
-
|
1057 |
-
|
1058 |
-
|
1059 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1060 |
data_io.seek(0) # Reset pointer
|
1061 |
-
|
1062 |
-
|
1063 |
-
|
1064 |
-
|
1065 |
-
|
1066 |
-
|
1067 |
-
|
1068 |
-
|
1069 |
-
|
1070 |
-
|
1071 |
-
|
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
|
1099 |
with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
|
1100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
#
|
1121 |
-
doc.add_paragraph()
|
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 |
+
```
|