CoolFace
Apppublic

CheboluGayatri/Code_Genai_explainer

sourceHugging Facemitupdated 3mo agoView on Hugging Face
0likes
chatapp.py493 linesDownload Raw Back to root
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