CheboluGayatri/Code_Genai_explainer
0
1import streamlit as st
2import uuid, datetime, json, os, subprocess, socket
3from PIL import Image, ImageOps, ImageFilter
4import io
5import pandas as pd
6import time
7
8# --- Dependency Imports ---
9# Ensure these are installed: pip install pytesseract pdfplumber python-docx ollama pandas Pillow
10try:
11 import pytesseract
12 # CRITICAL: Windows users may need to set Tesseract path:
13 # pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
14except Exception:
15 pytesseract = None
16
17try:
18 import pdfplumber
19except Exception:
20 pdfplumber = None
21
22try:
23 from docx import Document
24except Exception:
25 Document = None
26
27try:
28 import ollama
29 OLLAMA_AVAILABLE = True
30except Exception:
31 ollama = None
32 OLLAMA_AVAILABLE = False
33
34# Page config
35st.set_page_config(page_title="CodeGenesis Explainer", layout="wide")
36
37# --- Paths / storage ---
38DATA_FILE = "chats.json"
39UPLOAD_DIR = "uploads"
40os.makedirs(UPLOAD_DIR, exist_ok=True)
41
42# ----------------------------------------------------------------------------------
43# SYSTEM INSTRUCTION
44# ----------------------------------------------------------------------------------
45SYSTEM_INSTRUCTION = (
46 "You are CodeGenesis, an expert AI assistant specialized in generating, explaining, and correcting code. "
47 "You are also a highly knowledgeable and comprehensive general assistant, like a top-tier LLM (e.g., ChatGPT or Gemini). "
48 "Your response should directly and completely address the user's request, whether it's a general question or a file/code analysis request. "
49 "For Code Analysis, Fixing, or Generation requests, provide the necessary explanation and corrections, and always include the complete and correct code block using proper language syntax highlighting (e.e.g., python...). "
50 "For General Questions, provide a comprehensive, accurate, and detailed answer directly. Your tone must be professional, helpful, and friendly."
51)
52
53# ---------- Helper Functions ----------
54def now_iso(): return datetime.datetime.now().isoformat()
55def make_chat(title=None):
56 return {"id": uuid.uuid4().hex, "title": title or "New Chat", "messages": [], "created_at": now_iso()}
57def load_chats():
58 if os.path.exists(DATA_FILE):
59 try: return json.load(open(DATA_FILE, "r", encoding="utf-8"))
60 except Exception: return []
61 return []
62def save_chats(chats):
63 open(DATA_FILE, "w", encoding="utf-8").write(json.dumps(chats, indent=2, ensure_ascii=False))
64def ensure_ollama_running():
65 if not OLLAMA_AVAILABLE: return False
66 try:
67 with socket.create_connection(("127.0.0.1", 11434), timeout=0.2): return True
68 except Exception: return False
69def get_installed_models():
70 if not OLLAMA_AVAILABLE or not ensure_ollama_running(): return ["(Ollama not available)"]
71 try:
72 res = subprocess.run(["ollama", "list"], capture_output=True, text=True, check=False, timeout=5)
73 lines = [l.strip().split()[0] for l in res.stdout.splitlines()[1:] if l.strip()]
74 return lines or ["gemma:2b"]
75 except Exception:
76 return ["(Error listing models)"]
77def preprocess_image_for_ocr(pil_img):
78 try:
79 img = pil_img.convert("L")
80 w, h = img.size
81 if img.getpixel((0,0)) + img.getpixel((w-1, h-1)) < 50: img = ImageOps.invert(img)
82 img = ImageOps.autocontrast(img)
83 if max(w, h) < 1000:
84 scale = max(1, int(1200 / max(w, h)))
85 img = img.resize((w * scale, h * scale), Image.Resampling.LANCZOS)
86 img = img.filter(ImageFilter.SHARPEN)
87 img = img.point(lambda p: 255 if p > 150 else 0)
88 return img
89 except Exception:
90 return pil_img
91def extract_text(path):
92 ext = path.split(".")[-1].lower()
93 try:
94 if ext in ["jpg", "jpeg", "png", "bmp", "tiff"]:
95 if not pytesseract: return "(ERROR: pytesseract not installed.)"
96 try:
97 with Image.open(path) as im:
98 text = pytesseract.image_to_string(im, config='--psm 3')
99 if not text.strip() or len(text.strip().split()) < 10:
100 im_processed = preprocess_image_for_ocr(im)
101 text = pytesseract.image_to_string(im_processed, config='--psm 6')
102 return text.strip() or "(No significant text detected in image.)"
103 except pytesseract.TesseractNotFoundError: return "(ERROR: Tesseract executable not found.)"
104 except Exception as e: return f"(OCR failed: {type(e)._name_}: {e}.)"
105 if ext == "pdf":
106 # PDF support is here
107 if not pdfplumber: return "(ERROR: pdfplumber not installed.)"
108 txt = ""
109 with pdfplumber.open(path) as pdf:
110 for p in pdf.pages: txt += (p.extract_text() or "") + "\n"
111 return txt.strip() or "(No text detected in PDF.)"
112 if ext == "docx":
113 # DOCX support is here
114 if not Document: return "(ERROR: python-docx not installed.)"
115 doc = Document(path)
116 txt = "\n".join(p.text for p in doc.paragraphs)
117 return txt.strip() or "(Empty DOCX file.)"
118 if ext == "txt":
119 with open(path, "r", encoding="utf-8", errors="ignore") as f: return f.read()
120 if ext == "csv": return pd.read_csv(path, nrows=200).to_string()
121 return "(Unsupported file type for extraction.)"
122 except Exception as e: return f"(Extraction error: {type(e)._name_}: {e})"
123
124# ---------- Session State Initialization ----------
125if "chats" not in st.session_state: st.session_state["chats"] = load_chats()
126if not st.session_state["chats"]:
127 new_chat = make_chat("New Chat")
128 st.session_state["chats"].append(new_chat)
129 save_chats(st.session_state["chats"])
130elif st.session_state["chats"][0]["title"] == "Welcome":
131 st.session_state["chats"][0]["title"] = "New Chat"
132 st.session_state["chats"][0]["messages"] = []
133 save_chats(st.session_state["chats"])
134if "current_chat" not in st.session_state or st.session_state["current_chat"] not in [c["id"] for c in st.session_state["chats"]]: st.session_state["current_chat"] = st.session_state["chats"][0]["id"]
135if "show_upload" not in st.session_state: st.session_state["show_upload"] = False
136if "llm_running" not in st.session_state: st.session_state["llm_running"] = False
137if "new_message_to_process" not in st.session_state: st.session_state["new_message_to_process"] = False
138if "message_content" not in st.session_state: st.session_state["message_content"] = ""
139if "sidebar_search" not in st.session_state: st.session_state["sidebar_search"] = ""
140if "staged_file_context" not in st.session_state: st.session_state["staged_file_context"] = None
141
142chats = st.session_state["chats"]
143chat = next((c for c in chats if c["id"] == st.session_state["current_chat"]), None)
144if chat is None:
145 chat = make_chat()
146 st.session_state["chats"].insert(0, chat)
147 st.session_state["current_chat"] = chat["id"]
148 save_chats(st.session_state["chats"])
149
150# ---------- Core Logic Functions ----------
151def build_file_prompt(file_info, action_text):
152 """Generates the comprehensive LLM prompt for file analysis."""
153 extracted_content_chunk = file_info['extracted_text'][:12000]
154
155 extraction_status = "Extraction successful."
156 if file_info['extracted_text'].startswith("(ERROR"):
157 extraction_status = f"Extraction failed with error: {file_info['extracted_text']}. Warn the user."
158 elif "No significant text detected" in file_info['extracted_text']:
159 extraction_status = "Extraction warning: Little to no relevant text/code was detected. Advise the user to verify the content."
160
161 return f"""
162--- FILE ANALYSIS CONTEXT ---
163File Name: {file_info['name']}
164File Type: {file_info['type']}
165Extraction Status: {extraction_status}
166CRITICAL INSTRUCTION: This is a combined file and action request. Process the user's action against the content below.
167
168--- EXTRACTED CONTENT (12000 characters max) ---
169{extracted_content_chunk}
170
171--- USER ACTION ---
172User's requested action: {action_text}
173"""
174
175def handle_send_click():
176 """
177 Handles text message submission. Combines staged file content (if any) with the action text.
178 Modified to separate file info/path from user text for display order.
179 """
180 msg_key = st.session_state.get("message_content_input", "").strip()
181
182 if msg_key:
183 if chat["title"] == "New Chat":
184 chat["title"] = msg_key[:30] + ("..." if len(msg_key) > 30 else "")
185
186 # Check for staged file
187 if st.session_state["staged_file_context"]:
188 file_info = st.session_state["staged_file_context"]
189
190 # 1. Prepare visible message structure
191 user_action_content = msg_key
192 file_icon = "๐ท" if file_info.get("type").startswith("image") else "๐"
193
194 # The file summary is now a separate element, not part of 'content'
195 file_summary_content = f"{file_icon} File *{file_info['name']}* uploaded."
196
197 # Create the message object
198 msg_obj = {
199 "role": "user",
200 "content": user_action_content, # This is the user's question, displayed LAST
201 "file_summary": file_summary_content # Displayed FIRST
202 }
203
204 if file_info.get("type").startswith("image"):
205 msg_obj["image_path"] = file_info['path'] # Image displayed in the middle
206
207 chat["messages"].append(msg_obj)
208
209 # 2. Build the hidden combined prompt for the LLM
210 llm_prompt = build_file_prompt(file_info, msg_key)
211 st.session_state["staged_file_context"] = None # Clear the staged file context
212
213 else:
214 # Regular chat submission
215 chat["messages"].append({"role": "user", "content": msg_key})
216 llm_prompt = f"USER QUERY (GENERAL/CONVERSATIONAL/NEW CODE REQUEST): {msg_key}\n\n[End of query. Provide a direct and complete answer, using code blocks if requested.]"
217
218 # 3. Add hidden prompt for LLM
219 chat["messages"].append({"role": "user", "content": llm_prompt, "hidden": True})
220
221 st.session_state.message_content_input = ""
222 st.session_state.new_message_to_process = True
223 save_chats(st.session_state["chats"])
224
225def handle_file_upload_only():
226 """
227 Processes the uploaded file, extracts text, and stages it in session state.
228 """
229 uploaded_file = st.session_state.get("file_upload_widget")
230
231 if uploaded_file is None or uploaded_file.size == 0:
232 st.session_state["file_upload_widget"] = None
233 return
234
235 st.session_state["show_upload"] = False # Close the upload expander
236
237 filename = f"{uuid.uuid4().hex}_{uploaded_file.name}"
238 path = os.path.join(UPLOAD_DIR, filename)
239
240 with st.spinner(f"Processing '{uploaded_file.name}' (OCR / extract)..."):
241 with open(path, "wb") as f:
242 f.write(uploaded_file.getvalue())
243 extracted = extract_text(path)
244
245 if chat["title"] == "New Chat":
246 chat["title"] = uploaded_file.name.replace(".", "_")[:30]
247
248 # Stage the file context.
249 st.session_state["staged_file_context"] = {
250 "name": uploaded_file.name,
251 "type": uploaded_file.type,
252 "path": path,
253 "extracted_text": extracted
254 }
255
256 st.session_state["file_upload_widget"] = None
257 # st.rerun() removed here to prevent the "no-op" error.
258
259# ---------------- SIDEBAR ----------------
260with st.sidebar:
261 st.title("CodeGenesis Explainer")
262 st.markdown("---")
263
264 if OLLAMA_AVAILABLE:
265 if not ensure_ollama_running():
266 st.error("โ Ollama server not reachable. Run ollama serve in your terminal.")
267 else:
268 st.info("โน Ollama library not installed. Install with pip install ollama.")
269
270 st.subheader("Model Selection")
271 models = get_installed_models()
272 default_model_index = 0
273 is_model_selection_disabled = False
274
275 preferred_models = ["llama3:8b", "codellama:7b", "gemma:2b"]
276 for i, model_name in enumerate(preferred_models):
277 if model_name in models:
278 try:
279 default_model_index = models.index(model_name)
280 break
281 except ValueError:
282 pass
283
284 if not models or "(Ollama not available)" in models[0]:
285 is_model_selection_disabled = True
286
287 model = st.selectbox("Model", models, index=default_model_index, key="selected_model", disabled=is_model_selection_disabled)
288 st.markdown("---")
289
290 if st.button("โ New Chat", use_container_width=True):
291 nc = make_chat()
292 st.session_state["chats"].insert(0, nc)
293 st.session_state["current_chat"] = nc["id"]
294 save_chats(st.session_state["chats"])
295 st.rerun()
296
297 st.subheader("Chat History")
298 st.text_input("๐ Search", key="sidebar_search", placeholder="Search chat titles...")
299
300 search_term = st.session_state.sidebar_search.lower()
301 chats_list_filtered = [c for c in st.session_state["chats"] if search_term in c["title"].lower()] if search_term else st.session_state["chats"]
302
303 for c in chats_list_filtered:
304 cols = st.columns([0.8, 0.2])
305 with cols[0]:
306 button_style = "primary" if c["id"] == st.session_state["current_chat"] else "secondary"
307 if st.button(c["title"], key=f"chat-{c['id']}", use_container_width=True, type=button_style):
308 st.session_state["current_chat"] = c["id"]
309 st.rerun()
310 with cols[1]:
311 if st.button("๐", key=f"del-{c['id']}", use_container_width=True, help="Delete chat", type="secondary"):
312 st.session_state["chats"] = [x for x in st.session_state["chats"] if x["id"] != c["id"]]
313 save_chats(st.session_state["chats"])
314 if not st.session_state["chats"]:
315 new_chat_after_delete = make_chat("New Chat")
316 st.session_state["chats"].append(new_chat_after_delete)
317 st.session_state["current_chat"] = new_chat_after_delete["id"]
318 elif st.session_state["current_chat"] == c["id"]:
319 st.session_state["current_chat"] = st.session_state["chats"][0]["id"]
320 st.rerun()
321
322
323# ---------------- MAIN CHAT DISPLAY (Image size fixed to 300px) ----------------
324chat_container = st.container()
325with chat_container:
326 for msg in chat["messages"]:
327 if msg.get("hidden"):
328 continue
329 with st.chat_message(msg["role"]):
330
331 # 1. Display File Summary FIRST
332 if msg.get("file_summary"):
333 st.markdown(msg["file_summary"])
334
335 # 2. Display Image SECOND (Fixed small width)
336 if msg.get("image_path"):
337 try:
338 # Image size fixed to 300px width for smaller display
339 st.image(msg["image_path"], width=300)
340 except Exception:
341 st.write("(Could not display image)")
342
343 # 3. Display User's Question/Content LAST
344 st.markdown(msg["content"])
345
346 # Custom CSS for fixed input bar and styling
347 st.markdown("""
348 <style>
349 /* Hide the Streamlit header */
350 .stApp > header { display: none; }
351 /* Add padding to the main content so it doesn't overlap the fixed input bar */
352 .main { padding-bottom: 160px; }
353
354 /* Style for the fixed input container */
355 .fixed-input-container {
356 position: fixed;
357 bottom: 0;
358 z-index: 1000;
359 background: #f0f2f6; /* Match Streamlit's background color */
360 border-top: 1px solid #ccc;
361 padding: 12px 12px 12px 18px;
362 left: 0;
363 width: 100%;
364 box-sizing: border-box;
365 }
366
367 /* Adjust the fixed input container width for non-mobile views (with sidebar) */
368 @media (min-width: 768px) {
369 .fixed-input-container {
370 left: 300px; /* Assumes sidebar is 300px wide */
371 width: calc(100% - 300px);
372 }
373 }
374
375 /* General sidebar button styling for aesthetics */
376 section[data-testid="stSidebar"] button[data-testid="baseButton-primary"] {
377 background-color: #FFFFFF !important;
378 border-color: #DDDDDD !important;
379 color: #1A1A1A !important;
380 box-shadow: 0 0 2px 0 rgba(0,0,0,0.1);
381 }
382
383 section[data-testid="stSidebar"] button[data-testid="baseButton-secondary"] {
384 background-color: transparent !important;
385 border-color: transparent !important;
386 color: #333333 !important;
387 }
388
389 </style>
390 """, unsafe_allow_html=True)
391
392
393# ---------------- LLM GENERATION LOGIC ----------------
394if st.session_state["new_message_to_process"] and not st.session_state["llm_running"]:
395 st.session_state["llm_running"] = True
396 st.session_state["new_message_to_process"] = False
397
398 # Scroll to bottom before starting generation
399 st.markdown('<script>window.scrollTo(0, document.body.scrollHeight);</script>', unsafe_allow_html=True)
400
401 with st.chat_message("assistant"):
402 placeholder = st.empty()
403 partial_response_content = ""
404
405 final_prompt = next((m["content"] for m in reversed(chat["messages"]) if m.get("hidden") and m["role"] == "user"), None)
406 if final_prompt is None:
407 final_prompt = chat["messages"][-1]["content"] if chat["messages"] else "Please provide a valid query."
408
409 if not OLLAMA_AVAILABLE or not ensure_ollama_running() or is_model_selection_disabled:
410 err_msg = (
411 "โ Local LLM (Ollama) not available or server is down. "
412 "Action: Please ensure you have Ollama installed, run ollama serve in your terminal, and pull the desired model."
413 )
414 placeholder.markdown(err_msg)
415 chat["messages"].append({"role": "assistant", "content": err_msg})
416 save_chats(chats)
417 else:
418 messages_for_ollama = [
419 {"role": "system", "content": SYSTEM_INSTRUCTION},
420 {"role": "user", "content": final_prompt}
421 ]
422
423 try:
424 model_to_use = model if model not in ["(Ollama not available)"] else "gemma:2b"
425
426 for chunk in ollama.chat(model=model_to_use, messages=messages_for_ollama, stream=True):
427 content = chunk.get("message", {}).get("content", "")
428 if content:
429 partial_response_content += content
430 placeholder.markdown(partial_response_content + "โ")
431
432 placeholder.markdown(partial_response_content)
433
434 chat["messages"].append({"role": "assistant", "content": partial_response_content})
435 save_chats(chats)
436
437 except Exception as e:
438 err_msg = f"โ Error during LLM generation: {type(e)._name_}: {e}. Ensure model *{model}* is pulled."
439 placeholder.markdown(err_msg)
440 chat["messages"].append({"role": "assistant", "content": err_msg})
441 save_chats(chats)
442
443 st.session_state["llm_running"] = False
444 st.rerun()
445
446# ---------------- FIXED INPUT BAR / UPLOAD ----------------
447st.markdown('<div class="fixed-input-container">', unsafe_allow_html=True)
448
449if st.session_state["show_upload"]:
450 with st.expander("Upload code/document for analysis (Image, PDF, DOCX, TXT, CSV)", expanded=True):
451 st.file_uploader(
452 "Upload file (max size is determined by Streamlit settings)",
453 type=["jpg", "jpeg", "png", "pdf", "docx", "txt", "csv"], # <--- Includes PDF and DOCX
454 key="file_upload_widget",
455 label_visibility="collapsed",
456 disabled=st.session_state["llm_running"],
457 on_change=handle_file_upload_only
458 )
459
460col_plus, col_text = st.columns([0.07, 0.93], gap="small")
461with col_plus:
462 if st.button("โ", key="upload_toggle_btn", disabled=st.session_state["llm_running"], help="Toggle file upload panel", use_container_width=True):
463 st.session_state["show_upload"] = not st.session_state["show_upload"]
464
465with col_text:
466 placeholder_text = "Ask any question...."
467 if st.session_state["staged_file_context"]:
468 # CRITICAL: This guides the user when a file is ready.
469 placeholder_text = f"File '{st.session_state['staged_file_context']['name']}' is ready. Enter your question (e.g., 'Fix the code', 'Summarize this')."
470
471 st.text_input(
472 "Type message here...",
473 key="message_content_input",
474 label_visibility="collapsed",
475 disabled=st.session_state["llm_running"],
476 on_change=handle_send_click, # This triggers the full LLM generation
477 placeholder=placeholder_text
478 )
479
480st.markdown('</div>', unsafe_allow_html=True)
481
482
483
484
485
486
487
488
489
490
491
492
493 