bluenevus commited on
Commit
67360ee
·
1 Parent(s): 43e6676

Update app.py via AI Editor

Browse files
Files changed (1) hide show
  1. app.py +303 -308
app.py CHANGED
@@ -3,344 +3,339 @@ import io
3
  import os
4
  import pandas as pd
5
  from docx import Document
6
- from io import BytesIO, StringIO
7
- import dash # Version 3.0.3
8
- import dash_bootstrap_components as dbc # Version 2.0.2
9
- from dash import html, dcc, Input, Output, State, callback_context, ALL, no_update
10
- from dash.exceptions import PreventUpdate
11
  import google.generativeai as genai
12
  from docx.shared import Pt
13
  from docx.enum.style import WD_STYLE_TYPE
14
  from PyPDF2 import PdfReader
15
- import logging
16
- import uuid
17
- import xlsxwriter # Needed for Excel export engine
18
- import threading # For multi-threading
19
- import time # For progress indicator
20
 
21
- # --- Logging Configuration ---
22
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
23
 
24
- # --- Initialize Dash app ---
25
- # dash==3.0.3
26
- # dash-bootstrap-components==2.0.2
27
- app = dash.Dash(__name__,
28
- external_stylesheets=[dbc.themes.BOOTSTRAP],
29
- suppress_callback_exceptions=True,
30
- meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}])
31
- server = app.server # Expose server for deployment
32
 
33
- # --- Configure Gemini AI ---
34
- # IMPORTANT: Set the GEMINI_API_KEY environment variable.
35
- try:
36
- # Prefer direct CUDA GPU configuration in app.py - Note: Not directly applicable for cloud APIs like Gemini.
37
- # Configuration happens via environment variable or direct API key setting.
38
- api_key = os.environ.get("GEMINI_API_KEY")
39
- if not api_key:
40
- logging.warning("GEMINI_API_KEY environment variable not found. AI features will be disabled.")
41
- model = None
42
- else:
43
- genai.configure(api_key=api_key)
44
- # Using 'gemini-1.5-pro-latest' or similar advanced model is recommended.
45
- # Using the user-specified model: gemini-2.5-pro-preview-03-25
46
- model = genai.GenerativeModel('gemini-2.5-pro-preview-03-25')
47
- logging.info("Gemini AI configured successfully using 'gemini-2.5-pro-preview-03-25'.")
48
- except Exception as e:
49
- logging.error(f"Error configuring Gemini AI: {e}", exc_info=True)
50
- model = None
51
-
52
- # --- Global Variables ---
53
- # Using dcc.Store for more robust state management is recommended for production,
54
- # but for simplicity and current scope, using global variables with locks for threading.
55
- # A lock for thread-safe access to shared global variables
56
- data_lock = threading.Lock()
57
-
58
- # {session_id: {filename: content_text}} - Store uploaded files per session
59
  uploaded_files = {}
 
 
 
 
60
 
61
- # {session_id: document_content} - Stores the *results* of generation/review steps per session
62
- shredded_document = {}
63
- pink_review_document = {}
64
- red_review_document = {}
65
- gold_review_document = {}
66
- loe_document = {}
67
- virtual_board_document = {}
68
-
69
- # {session_id: document_content} - Stores the *generated* proposal drafts per session
70
- pink_document = {}
71
- red_document = {}
72
- gold_document = {}
73
-
74
- # {session_id: content_text} - Store uploaded content specifically for review inputs per session
75
- uploaded_pink_content = {}
76
- uploaded_red_content = {}
77
- uploaded_gold_content = {}
78
-
79
- # {session_id: {'doc': content, 'type': doc_type, 'format': format}} - Store the currently displayed document, its type, and format for download/chat per session
80
- current_display_document = {}
81
-
82
- # --- Document Types ---
83
  document_types = {
84
- "Shred": "Generate a requirements spreadsheet from the PWS/Source Docs, identifying action words (shall, will, perform, etc.) by section.",
85
- "Pink": "Create a compliant and compelling Pink Team proposal draft based on the Shredded requirements.",
86
- "Pink Review": "Evaluate a Pink Team draft against Shredded requirements. Output findings (compliance, gaps, recommendations) in a spreadsheet.",
87
- "Red": "Create a Red Team proposal draft, addressing feedback from the Pink Review and enhancing compliance/compellingness.",
88
- "Red Review": "Evaluate a Red Team draft against Shredded requirements and Pink Review findings. Output findings in a spreadsheet.",
89
- "Gold": "Create a Gold Team proposal draft, addressing feedback from the Red Review for final compliance and polish.",
90
- "Gold Review": "Perform a final compliance review of the Gold Team draft against Shredded requirements and Red Review findings. Output findings.",
91
- "Virtual Board": "Simulate a source selection board evaluation of the final proposal against PWS/Shred requirements and evaluation criteria (Sec L/M). Output evaluation.",
92
- "LOE": "Generate a Level of Effort (LOE) estimate spreadsheet based on the Shredded requirements."
93
  }
94
 
95
- # --- Layout Definition ---
96
- app.layout = dbc.Container(fluid=True, className="dbc", children=[
97
- dcc.Store(id='session-id', storage_type='session'), # Store for unique session ID
98
- # Title Row
99
- dbc.Row(
100
- dbc.Col(html.H1("Proposal AI Assistant", className="text-center my-4"), width=12)
101
- ),
102
-
103
- # Progress Indicator Row
104
- dbc.Row(
105
- dbc.Col(
106
- dcc.Loading(
107
- id="loading-indicator",
108
- type="dots", # Changed to dots
109
- children=[html.Div(id="loading-output", style={'height': '10px'})], # Simplified children
110
- overlay_style={"visibility":"hidden", "opacity": 0}, # Hide default overlay
111
- style={'visibility':'hidden'}, # Initially hidden
112
- parent_style={'minHeight': '30px'}, # Ensure space is allocated
113
- fullscreen=False,
114
- ),
115
- width=12,
116
- className="text-center mb-3"
117
- )
118
- ),
119
-
120
- # Main Content Row
121
  dbc.Row([
122
- # Left Column (Nav/Upload) - lg=4 (approx 33%)
123
- dbc.Col(
124
- dbc.Card(
125
- dbc.CardBody([
126
- html.H4("1. Upload Source Documents", className="card-title"),
127
- dcc.Upload(
128
- id='upload-document',
129
- children=html.Div(['Drag and Drop or ', html.A('Select PWS/Source Files')]),
130
- style={ # Basic styling, colors/backgrounds handled by CSS
131
- 'width': '100%', 'height': '60px', 'lineHeight': '60px',
132
- 'borderWidth': '1px', 'borderStyle': 'dashed', 'borderRadius': '5px',
133
- 'textAlign': 'center', 'margin': '10px 0'
134
- },
135
- multiple=True
136
- ),
137
- dbc.Card( # Inner card for file list
138
- dbc.CardBody(
139
- html.Div(id='file-list', style={'maxHeight': '150px', 'overflowY': 'auto', 'fontSize': '0.9em'})
140
- ), className="mb-3" # Removed inline style
141
- ),
142
- html.Hr(),
143
- html.H4("2. Select Action", className="card-title mt-3"),
144
- dbc.Card( # Inner card for buttons
145
- dbc.CardBody([
146
- *[dbc.Button(
147
- doc_type,
148
- id={'type': 'action-button', 'index': doc_type},
149
- color="primary", # Use bootstrap classes
150
- className="mb-2 w-100 d-block",
151
- style={'textAlign': 'left', 'whiteSpace': 'normal', 'height': 'auto', 'wordWrap': 'break-word'} # Style for word wrap
152
- ) for doc_type in document_types.keys()]
153
- ])
154
- )
155
  ]),
156
- # color="light", # Let CSS handle background
157
- className="h-100 left-nav-card", # Add custom class for CSS targeting
 
 
 
 
 
 
 
 
 
158
  ),
159
- width=12, lg=4, # Full width on small, 4/12 on large
160
- className="mb-3 mb-lg-0",
161
- style={'paddingRight': '15px'} # Add padding between columns
162
- ),
163
-
164
- # Right Column (Status/Preview/Controls/Chat) - lg=8 (approx 67%)
165
- dbc.Col(
166
- dbc.Card(
167
- dbc.CardBody([
168
- dbc.Alert(id='status-bar', children="Upload source documents and select an action.", color="info"),
169
- dbc.Card(id='review-controls-card', children=[dbc.CardBody(id='review-controls')], className="mb-3", style={'display': 'none'}), # Initially hidden review controls
170
- dbc.Card( # Card for preview
171
- dbc.CardBody([
172
- html.H5("Document Preview / Output", className="card-title"),
173
- dcc.Loading(
174
- id="loading-preview",
175
- type="circle",
176
- children=[html.Div(id='document-preview', style={'whiteSpace': 'pre-wrap', 'wordWrap': 'break-word', 'maxHeight': '400px', 'overflowY': 'auto', 'border': '1px solid #ccc', 'padding': '10px', 'borderRadius': '5px'})] # Added wordWrap
177
- )
178
- ]), className="mb-3"
179
- ),
180
- dbc.Button("Download Output", id="btn-download", color="success", className="mt-3 me-2", style={'display': 'none'}), # Initially hidden download
181
- dcc.Download(id="download-document"),
182
- html.Hr(),
183
- dbc.Card( # Card for chat
184
- dbc.CardBody([
185
- html.H5("Refine Output (Chat)", className="card-title"),
186
- dcc.Loading(
187
- id="chat-loading",
188
- type="circle",
189
- children=[
190
- 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
191
- dbc.ButtonGroup([
192
- dbc.Button("Send Chat", id="btn-send-chat", color="secondary"),
193
- dbc.Button("Clear Chat", id="btn-clear-chat", color="tertiary")
194
- ], className="mb-3"),
195
- html.Div(id="chat-output", style={'whiteSpace': 'pre-wrap', 'wordWrap': 'break-word', 'marginTop': '10px', 'border': '1px solid #eee', 'padding': '10px', 'borderRadius': '5px', 'minHeight': '50px'}) # Added wordWrap
196
- ]
197
- )
198
- ]), className="mb-3"
199
- )
200
- ]),
201
- # color="white", # Let CSS handle background
202
- className="h-100 right-nav-card", # Add custom class for CSS targeting
203
  ),
204
- width=12, lg=8, # Full width on small, 8/12 on large
205
- style={'paddingLeft': '15px'} # Add padding between columns
206
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  ])
208
- ], style={'padding': '0 15px'}) # Add padding around the container
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
- # --- Helper Functions ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
- def get_session_id(session_id_value=None):
214
- """Gets the current session ID or generates a new one."""
215
- if session_id_value:
216
- return session_id_value
217
- # Fallback for initial load or if session ID is missing
218
- new_id = str(uuid.uuid4())
219
- logging.info(f"Generated new session ID: {new_id}")
220
- return new_id
 
 
 
 
 
 
 
 
221
 
222
- def parse_generated_content(content_text):
223
- """Attempts to parse AI-generated content into a DataFrame if it looks like a table."""
224
- try:
225
- # Simple check: does it contain multiple lines and pipe characters?
226
- if content_text and '\n' in content_text and '|' in content_text:
227
- # Try parsing as Markdown-like table (skip lines that don't fit)
228
- lines = [line.strip() for line in content_text.strip().split('\n')]
229
- # Remove separator lines like |---|---|
230
- lines = [line for line in lines if not all(c in '-| ' for c in line)]
231
- if len(lines) > 1:
232
- # Use the first line as header, split by '|'
233
- header = [h.strip() for h in lines[0].strip('|').split('|')]
234
- data_rows = []
235
- for line in lines[1:]:
236
- values = [v.strip() for v in line.strip('|').split('|')]
237
- if len(values) == len(header): # Ensure matching column count
238
- data_rows.append(values)
239
- else:
240
- logging.warning(f"Skipping row due to mismatched columns: {line}")
 
 
 
 
 
 
 
 
 
 
 
241
 
242
- if data_rows:
243
- df = pd.DataFrame(data_rows, columns=header)
244
- logging.info("Successfully parsed generated content as DataFrame.")
245
- return df
246
- except Exception as e:
247
- logging.warning(f"Could not parse content into DataFrame: {e}. Treating as plain text.")
248
- # If parsing fails or it doesn't look like a table, return None
249
- logging.info("Content does not appear to be a table or parsing failed. Treating as plain text.")
250
- return None
251
 
252
- def process_document(contents, filename):
253
- """Processes uploaded file content (PDF or DOCX) and returns text, or None and error message."""
254
- if contents is None:
255
- logging.warning(f"process_document called with None contents for {filename}")
256
- return None, f"Error: No content provided for {filename}."
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
- try:
259
- content_type, content_string = contents.split(',')
260
- decoded = base64.b64decode(content_string)
261
- logging.info(f"Processing file: {filename}")
262
- text = None
263
- error_message = None
264
 
265
- if filename.lower().endswith('.docx'):
266
- doc = Document(io.BytesIO(decoded))
267
- text = "\n".join([para.text for para in doc.paragraphs if para.text.strip()])
268
- logging.info(f"Successfully processed DOCX: {filename}")
269
- elif filename.lower().endswith('.pdf'):
270
- pdf = PdfReader(io.BytesIO(decoded))
271
- extracted_pages = []
272
- for i, page in enumerate(pdf.pages):
273
- try:
274
- page_text = page.extract_text()
275
- if page_text:
276
- extracted_pages.append(page_text)
277
- except Exception as page_e:
278
- logging.warning(f"Could not extract text from page {i+1} of {filename}: {page_e}")
279
- text = "\n\n".join(extracted_pages)
280
- if not text:
281
- logging.warning(f"No text extracted from PDF: {filename}. It might be image-based or corrupted.")
282
- error_message = f"Error: No text could be extracted from PDF {filename}. It might be image-based or require OCR."
283
- else:
284
- logging.info(f"Successfully processed PDF: {filename}")
285
- else:
286
- logging.warning(f"Unsupported file format: {filename}")
287
- error_message = f"Unsupported file format: {filename}. Please upload PDF or DOCX."
288
 
289
- return text, error_message
290
- except Exception as e:
291
- logging.error(f"Error processing document {filename}: {e}", exc_info=True)
292
- return None, f"Error processing file {filename}: {str(e)}"
293
 
294
- def get_combined_uploaded_text(session_id, file_dict):
295
- """Combines text content of files in the provided dictionary for a session."""
296
- with data_lock:
297
- session_files = file_dict.get(session_id, {})
298
- if not session_files:
299
- return ""
300
- # Combine content, adding filenames for context if multiple files
301
- if len(session_files) > 1:
302
- return "\n\n--- FILE BREAK ---\n\n".join(
303
- f"**File: {fname}**\n\n{content}" for fname, content in session_files.items()
304
- )
305
  else:
306
- return next(iter(session_files.values()), "")
 
 
 
307
 
 
 
 
 
308
 
309
- def generate_ai_document(session_id, doc_type, input_docs, context_docs=None):
310
- """Generates document using Gemini AI. Returns generated content and format ('text' or 'dataframe')."""
311
- if not model:
312
- logging.error(f"[{session_id}] Gemini AI model not initialized.")
313
- return "Error: AI Model not configured. Please check API Key.", 'text'
314
- if not input_docs or not any(doc.strip() for doc in input_docs if doc):
315
- logging.warning(f"[{session_id}] generate_ai_document called for {doc_type} with no valid input documents.")
316
- return f"Error: Missing required input document(s) for {doc_type} generation.", 'text'
 
317
 
318
- combined_input = "\n\n---\n\n".join(filter(None, input_docs))
319
- combined_context = "\n\n---\n\n".join(filter(None, context_docs)) if context_docs else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
- # Define expected output format based on doc_type
322
- is_spreadsheet_type = doc_type in ["Shred", "Pink Review", "Red Review", "Gold Review", "LOE", "Virtual Board"]
323
- output_format_instruction = """**Output Format:** Structure the output as a clear, parseable Markdown table. Use '|' as the column delimiter. Define meaningful column headers relevant to the task (e.g., PWS_Section, Requirement, Action_Verb for Shred; Section, Requirement, Compliance_Status, Finding, Recommendation for Reviews; Section, Task, Estimated_Hours, Resource_Type for LOE). Ensure each row corresponds to a distinct item (e.g., requirement, finding, task).""" if is_spreadsheet_type else """**Output Format:** Write professional, compelling proposal prose. Use clear paragraphs and standard formatting. Address all requirements logically. Avoid tables unless explicitly part of the proposal structure."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
- prompt = f"""**Objective:** Generate the '{doc_type}' document.
326
- **Your Role:** Act as an expert proposal writer/analyst specialized in government contracting.
327
- **Core Instructions:**
328
- 1. **Adhere Strictly to the Task:** Generate *only* the content for the '{doc_type}'. Do not add introductions, summaries, explanations, or conversational filler unless it's part of the requested document format itself (e.g., an executive summary within a proposal draft).
329
- 2. **Follow Format Guidelines:** {output_format_instruction}
330
- 3. **Content Requirements:**
331
- * **Shred:** Identify requirements (explicit and implied), action verbs (shall, will, must, provide, perform, etc.), and PWS section references.
332
- * **Proposal Sections (Pink, Red, Gold):** Write compliant and compelling content. Directly address requirements from the Context Document(s). Detail the 'how' (approach, methodology, tools). Incorporate win themes, strengths, and discriminators. Substantiate claims. Use active voice ("Our team will..."). Ensure compliance with evaluation criteria (e.g., Section L/M). Clearly map responses back to PWS requirements.
333
- * **Reviews (Pink, Red, Gold):** Evaluate the submitted draft against the requirements (Shred/PWS) and previous review findings (if applicable). Identify compliance issues, gaps, weaknesses, and areas for improvement. Provide actionable recommendations. Be specific and reference relevant sections.
334
- * **LOE:** Estimate the Level of Effort (hours, resource types) required to fulfill each major task or requirement identified in the Shred/PWS. Justify estimates briefly if necessary.
335
- * **Virtual Board:** Simulate a source selection evaluation. Assess the final proposal against the PWS/Shred and evaluation criteria (Sec L/M). Assign strengths, weaknesses, deficiencies, risks. Provide a summary evaluation.
336
- 4. **Utilize Provided Documents:**
337
- * **Context Document(s):** These provide the baseline or reference material (e.g., Shredded Requirements, PWS Section L/M, Previous Review Findings). Refer to them diligently.
338
- * **Primary Input Document(s):** This is the main subject of the task (e.g., the PWS text to be Shredded, the Pink draft to be Reviewed, the Red Review findings to incorporate into the Gold draft). Analyze and process this document according to the task.
339
- **Provided Documents:**
340
- **Context Document(s):**
341
- ```text
342
- {combined_context if combined_context else "N/A"}
343
- ```
344
- **Primary Input Document(s):**
345
- ```text
346
- {combined_input}
 
3
  import os
4
  import pandas as pd
5
  from docx import Document
6
+ from io import BytesIO
7
+ import dash
8
+ import dash_bootstrap_components as dbc
9
+ from dash import html, dcc, Input, Output, State, callback_context
 
10
  import google.generativeai as genai
11
  from docx.shared import Pt
12
  from docx.enum.style import WD_STYLE_TYPE
13
  from PyPDF2 import PdfReader
14
+ from io import StringIO
 
 
 
 
15
 
16
+ # Initialize Dash app
17
+ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
18
 
19
+ # Configure Gemini AI
20
+ genai.configure(api_key=os.environ["GEMINI_API_KEY"])
21
+ model = genai.GenerativeModel('gemini-2.5-pro-preview-03-25')
 
 
 
 
 
22
 
23
+ # Global variables
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  uploaded_files = {}
25
+ current_document = None
26
+ document_type = None
27
+ shredded_document = None
28
+ pink_review_document = None
29
 
30
+ # Document types and their descriptions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  document_types = {
32
+ "Shred": "Generate a requirements spreadsheet of the Project Work Statement (PWS) identified by action words like shall, will, perform etc. by pws section, requirement. Do not write as if you're responding to the proposal. Its a spreadsheet to distill the requirements, not microhealth's approach",
33
+ "Pink": "Create a Pink Team document based on the PWS outline. Your goal is to be compliant and compelling.",
34
+ "Pink Review": "Evaluate compliance of the Pink Team document against the requirements and output a spreadsheet of non compliant findings by pws number, the goal of that pws section, what made it non compliant and your recommendations for recovery",
35
+ "Red": "Produce a Red Team document based on the Pink Review by pws sections. Your goal is to be compliant and compelling by recovering all the findings in Pink Review",
36
+ "Red Review": "Evaluate compliance of the Red Team document against the requirements and output a spreadsheet of non compliant findings by pws number, the goal of that pws section, what made it non compliant and your recommendations for recovery",
37
+ "Gold": "Create a Pink Team document based on the PWS response by pws sections. Your goal is to be compliant and compelling by recovering all the findings in Red Review",
38
+ "Gold Review": "Perform a final compliance review against the requirements and output a spreadsheet of non compliant findings by pws number, the goal of that pws section, what made it non compliant and your recommendations for recovery",
39
+ "Virtual Board": "Based on the requirements and in particular the evaulation criteria, you will evaluate the proposal as if you were a contracting office and provide section by section evaluation as unsatisfactory, satisfactory, good, very good, excellent and why in a spreadsheet",
40
+ "LOE": "Generate a Level of Effort (LOE) breakdown as a spreadsheet"
41
  }
42
 
43
+ app.layout = dbc.Container([
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  dbc.Row([
45
+ dbc.Col([
46
+ html.H4("Proposal Documents", className="mt-3 mb-4"),
47
+ dcc.Upload(
48
+ id='upload-document',
49
+ children=html.Div([
50
+ 'Drag and Drop or ',
51
+ html.A('Select Files')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  ]),
53
+ style={
54
+ 'width': '100%',
55
+ 'height': '60px',
56
+ 'lineHeight': '60px',
57
+ 'borderWidth': '1px',
58
+ 'borderStyle': 'dashed',
59
+ 'borderRadius': '5px',
60
+ 'textAlign': 'center',
61
+ 'margin': '10px 0'
62
+ },
63
+ multiple=True
64
  ),
65
+ html.Div(id='file-list'),
66
+ html.Hr(),
67
+ html.Div([
68
+ dbc.Button(
69
+ doc_type,
70
+ id=f'btn-{doc_type.lower().replace("_", "-")}',
71
+ color="link",
72
+ className="mb-2 w-100 text-left custom-button",
73
+ style={'overflow': 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap'}
74
+ ) for doc_type in document_types.keys()
75
+ ])
76
+ ], width=3),
77
+ dbc.Col([
78
+ html.Div(id='status-bar', className="alert alert-info", style={'marginBottom': '20px'}),
79
+ dcc.Loading(
80
+ id="loading-indicator",
81
+ type="dot",
82
+ children=[html.Div(id="loading-output")]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  ),
84
+ html.Div(id='document-preview', className="border p-3 mb-3"),
85
+ dbc.Button("Download Document", id="btn-download", color="success", className="mt-3"),
86
+ dcc.Download(id="download-document"),
87
+ html.Hr(),
88
+ html.Div(id='pink-review-upload', style={'display': 'none'}, children=[
89
+ dcc.Upload(
90
+ id='upload-pink-review',
91
+ children=html.Div(['Drag and Drop or ', html.A('Select Pink Review File')]),
92
+ style={
93
+ 'width': '100%',
94
+ 'height': '60px',
95
+ 'lineHeight': '60px',
96
+ 'borderWidth': '1px',
97
+ 'borderStyle': 'dashed',
98
+ 'borderRadius': '5px',
99
+ 'textAlign': 'center',
100
+ 'margin': '10px 0'
101
+ },
102
+ multiple=False
103
+ ),
104
+ html.Div(id='pink-review-file-name')
105
+ ]),
106
+ dcc.Loading(
107
+ id="chat-loading",
108
+ type="dot",
109
+ children=[
110
+ dbc.Input(id="chat-input", type="text", placeholder="Chat with AI to update document...", className="mb-2"),
111
+ dbc.Button("Send", id="btn-send-chat", color="primary", className="mb-3"),
112
+ html.Div(id="chat-output")
113
+ ]
114
+ )
115
+ ], width=9)
116
  ])
117
+ ], fluid=True)
118
 
119
+ def process_document(contents, filename):
120
+ content_type, content_string = contents.split(',')
121
+ decoded = base64.b64decode(content_string)
122
+ try:
123
+ if filename.lower().endswith('.docx'):
124
+ doc = Document(BytesIO(decoded))
125
+ text = "\n".join([para.text for para in doc.paragraphs])
126
+ return text
127
+ elif filename.lower().endswith('.pdf'):
128
+ pdf = PdfReader(BytesIO(decoded))
129
+ text = ""
130
+ for page in pdf.pages:
131
+ text += page.extract_text()
132
+ return text
133
+ else:
134
+ return f"Unsupported file format: {filename}. Please upload a PDF or DOCX file."
135
+ except Exception as e:
136
+ return f"Error processing document: {str(e)}"
137
 
138
+ @app.callback(
139
+ Output('file-list', 'children'),
140
+ Output('status-bar', 'children'),
141
+ Input('upload-document', 'contents'),
142
+ State('upload-document', 'filename'),
143
+ State('file-list', 'children')
144
+ )
145
+ def update_output(list_of_contents, list_of_names, existing_files):
146
+ global uploaded_files, shredded_document
147
+ if list_of_contents is not None:
148
+ new_files = []
149
+ for i, (content, name) in enumerate(zip(list_of_contents, list_of_names)):
150
+ file_content = process_document(content, name)
151
+ uploaded_files[name] = file_content
152
+ new_files.append(html.Div([
153
+ html.Button('×', id={'type': 'remove-file', 'index': name}, style={'marginRight': '5px', 'fontSize': '10px'}),
154
+ html.Span(name)
155
+ ]))
156
+ if existing_files is None:
157
+ existing_files = []
158
+ shredded_document = None # Reset shredded document when new files are uploaded
159
+ return existing_files + new_files, "Document uploaded. Please click 'Shred' to proceed."
160
+ return existing_files, "Please upload a document and click 'Shred' to begin."
161
 
162
+ @app.callback(
163
+ Output('file-list', 'children', allow_duplicate=True),
164
+ Output('status-bar', 'children', allow_duplicate=True),
165
+ Input({'type': 'remove-file', 'index': dash.ALL}, 'n_clicks'),
166
+ State('file-list', 'children'),
167
+ prevent_initial_call=True
168
+ )
169
+ def remove_file(n_clicks, existing_files):
170
+ global uploaded_files, shredded_document
171
+ ctx = dash.callback_context
172
+ if not ctx.triggered:
173
+ raise dash.exceptions.PreventUpdate
174
+ removed_file = ctx.triggered[0]['prop_id'].split(',')[0].split(':')[-1].strip('}')
175
+ uploaded_files.pop(removed_file, None)
176
+ shredded_document = None # Reset shredded document when a file is removed
177
+ return [file for file in existing_files if file['props']['children'][1]['props']['children'] != removed_file], "Document removed. Please upload a document and click 'Shred' to begin."
178
 
179
+ def generate_document(document_type, file_contents):
180
+ prompt = f"""Generate a {document_type} based on the following project artifacts:
181
+ {' '.join(file_contents)}
182
+ Instructions:
183
+ 1. Create the {document_type} as a detailed document.
184
+ 2. Use proper formatting and structure.
185
+ 3. Include all necessary sections and details.
186
+ 4. Start the output immediately with the document content.
187
+ 5. IMPORTANT: If the document type is Pink, Red, Gold and not review type, loe or board
188
+ then your goal is to be compliant and compelling based on the
189
+ requrements, write in paragraph in active voice as
190
+ MicroHealth, limit bullets, answer the
191
+ requirement with what MicroHealth will do
192
+ to satisfy the requirement, the technical
193
+ approach with innovation for efficiency,
194
+ productivity, quality and measurable
195
+ outcomes, the industry standard that
196
+ methodology is based on if applicable,
197
+ detail the workflow or steps to accomplish
198
+ the requirement with labor categories that
199
+ will do those tasks in that workflow,
200
+ reference reputable research like gartner,
201
+ forrester, IDC, Deloitte, Accenture etc
202
+ with measures of success and substantiation
203
+ of MicroHealth's approach. Never use soft words
204
+ like maybe, could be, should, possible be definitive in your language and confident.
205
+ 6. you must also take into account section L&M of the document which is the evaluation criteria
206
+ to be sure we address them.
207
+ Now, generate the {document_type}:
208
+ """
209
 
210
+ response = model.generate_content(prompt)
211
+ return response.text
 
 
 
 
 
 
 
212
 
213
+ @app.callback(
214
+ Output('document-preview', 'children'),
215
+ Output('loading-output', 'children'),
216
+ Output('status-bar', 'children', allow_duplicate=True),
217
+ Output('pink-review-upload', 'style'),
218
+ [Input(f'btn-{doc_type.lower().replace("_", "-")}', 'n_clicks') for doc_type in document_types.keys()],
219
+ State('pink-review-file-name', 'children'),
220
+ prevent_initial_call=True
221
+ )
222
+ def generate_document_preview(*args):
223
+ global current_document, document_type, shredded_document, pink_review_document
224
+ ctx = dash.callback_context
225
+ if not ctx.triggered:
226
+ raise dash.exceptions.PreventUpdate
227
+ button_id = ctx.triggered[0]['prop_id'].split('.')[0]
228
+ document_type = button_id.replace('btn-', '').replace('-', '_').title()
229
+ pink_review_file = args[-1]
230
 
231
+ if not uploaded_files and document_type != "Shred":
232
+ return html.Div("Please upload and shred a document first."), "", "Please upload and shred a document first.", {'display': 'none'}
 
 
 
 
233
 
234
+ if document_type == "Shred":
235
+ if not uploaded_files:
236
+ return html.Div("Please upload a document before shredding."), "", "Please upload a document before shredding.", {'display': 'none'}
237
+ file_contents = list(uploaded_files.values())
238
+ try:
239
+ shredded_document = generate_document(document_type, file_contents)
240
+ return dcc.Markdown(shredded_document), f"{document_type} generated", "Document shredded. You can now proceed with other operations.", {'display': 'none'}
241
+ except Exception as e:
242
+ print(f"Error generating document: {str(e)}")
243
+ return html.Div(f"Error generating document: {str(e)}"), "Error", "An error occurred while shredding the document.", {'display': 'none'}
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
+ if shredded_document is None:
246
+ return html.Div("Please shred a document first."), "", "Please shred a document first.", {'display': 'none'}
 
 
247
 
248
+ if document_type == "Pink Review":
249
+ return html.Div("Please upload a Pink Team document or use the generated one."), "", "Please upload a Pink Team document or use the generated one.", {'display': 'block'}
250
+
251
+ if document_type in ["Red", "Red Review"] and pink_review_document is None:
252
+ return html.Div("Please complete Pink Review first."), "", "Please complete Pink Review first.", {'display': 'none'}
253
+
254
+ try:
255
+ if document_type == "Pink Review" and pink_review_file:
256
+ current_document = generate_document(document_type, [pink_review_file, shredded_document])
257
+ elif document_type in ["Red", "Red Review"]:
258
+ current_document = generate_document(document_type, [pink_review_document, shredded_document])
259
  else:
260
+ current_document = generate_document(document_type, [shredded_document])
261
+
262
+ if document_type == "Pink Review":
263
+ pink_review_document = current_document
264
 
265
+ return dcc.Markdown(current_document), f"{document_type} generated", f"{document_type} document generated successfully.", {'display': 'none'}
266
+ except Exception as e:
267
+ print(f"Error generating document: {str(e)}")
268
+ return html.Div(f"Error generating document: {str(e)}"), "Error", "An error occurred while generating the document.", {'display': 'none'}
269
 
270
+ @app.callback(
271
+ Output('pink-review-file-name', 'children'),
272
+ Input('upload-pink-review', 'contents'),
273
+ State('upload-pink-review', 'filename')
274
+ )
275
+ def update_pink_review_filename(contents, filename):
276
+ if contents is not None:
277
+ return filename
278
+ return ""
279
 
280
+ @app.callback(
281
+ Output('chat-output', 'children'),
282
+ Output('document-preview', 'children', allow_duplicate=True),
283
+ Input('btn-send-chat', 'n_clicks'),
284
+ State('chat-input', 'value'),
285
+ prevent_initial_call=True
286
+ )
287
+ def update_document_via_chat(n_clicks, chat_input):
288
+ global current_document, document_type
289
+ if not chat_input or current_document is None:
290
+ raise dash.exceptions.PreventUpdate
291
+
292
+ prompt = f"""Update the following {document_type} based on this instruction: {chat_input}
293
+ Current document:
294
+ {current_document}
295
+ Instructions:
296
+ 1. Provide the updated document content.
297
+ 2. Maintain proper formatting and structure.
298
+ 3. Incorporate the requested changes seamlessly.
299
+ Now, provide the updated {document_type}:
300
+ """
301
+
302
+ response = model.generate_content(prompt)
303
+ current_document = response.text
304
+
305
+ return f"Document updated based on: {chat_input}", dcc.Markdown(current_document)
306
 
307
+ @app.callback(
308
+ Output("download-document", "data"),
309
+ Input("btn-download", "n_clicks"),
310
+ prevent_initial_call=True
311
+ )
312
+ def download_document(n_clicks):
313
+ global current_document, document_type
314
+ if current_document is None:
315
+ raise dash.exceptions.PreventUpdate
316
+
317
+ if document_type == "LOE":
318
+ # Create a pandas DataFrame for LOE
319
+ df = pd.read_csv(StringIO(current_document))
320
+
321
+ # Save the DataFrame to an Excel file
322
+ output = BytesIO()
323
+ with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
324
+ df.to_excel(writer, sheet_name='LOE', index=False)
325
+
326
+ return dcc.send_bytes(output.getvalue(), f"{document_type}.xlsx")
327
+ else:
328
+ # Create an in-memory Word document
329
+ doc = Document()
330
+ doc.add_paragraph(current_document)
331
+
332
+ # Save the document to a BytesIO object
333
+ output = BytesIO()
334
+ doc.save(output)
335
+
336
+ return dcc.send_bytes(output.getvalue(), f"{document_type}.docx")
337
 
338
+ if __name__ == '__main__':
339
+ print("Starting the Dash application...")
340
+ app.run(debug=False, host='0.0.0.0', port=7860)
341
+ print("Dash application has finished running.")