# ./app.py """ The Interface Skeleton - The code sets up the navigation panel and the multimodal chat interface The .then() chain: Previously, the save happened "in the background." Now, handle_save explicitly returns the new load_history() results to the history_list component, causing it to "re-render" with the new chat visible. The chat_id_state: By passing this back and forth, the app knows if it should update an existing file in the HF Dataset or create a new one. history_list.click: This is the bridge that makes the sidebar interactive. Without this event, clicking the "Recent Conversations" wouldn't do anything. """ import gradio as gr from core_logic import chat_function from storage import save_chat, load_history, get_chat_content from git_agent import manage_github_repo # Theme and css parameters not included under gr.Blocks constructor for Gradio 6 with gr.Blocks() as demo: # This state keeps track of the filename for the current session chat_id_state = gr.State("") # Hidden state tracking variable to maintain files across the chat session staged_files_state = gr.State([]) with gr.Row(): # --- Left Panel: Sidebar History --- with gr.Column(scale=1, variant="secondary"): gr.Markdown("### 🛠️ Silicon Architect") new_btn = gr.Button("➕ New Chat", variant="primary") # The sidebar component history_list = gr.Dataset( components=[gr.Textbox(visible=False)], label="Recent Conversations", samples=load_history(), type="values", samples_per_page=20 ) # --- Center Panel: Main Core Multimodal Chat --- with gr.Column(scale=3): # type="messages" not included as it is now default/implicit in Gradio 6 chatbot = gr.Chatbot(show_label=False, height=700) """ chat_input = gr.MultimodalTextbox( interactive=True, placeholder="Discuss architecture or ask CoderG to produce course documentation...", show_label=False, # FIX 1: Allow users to stack multiple files before sending file_count="multiple", # FIX 2: Stop rich text clipboard objects from hijacking the paste buffer file_types=[".png", ".jpg", ".jpeg", ".bmp", ".pdf", ".xlsx", ".xls", ".docx", ".md", ".py", ".html", ".cs", ".js", ".json", ".csv", ".zip", ".tar.gz", ".log", ".txt"] # Expanded file type support ) """ # 1. Use a standard Textbox. It treats all paste inputs strictly as raw text strings, # completely preventing the automated file-attachment generation bug. chat_input = gr.Textbox( interactive=True, placeholder="Discuss architecture, paste code blocks, or ask CoderG to produce course documentation...", show_label=False, lines=1, # <--- Crucial change: Reverts default Enter behavior back to sending max_lines=10, # <--- Keeps the box flexible so it expands when pasting large text such as code bases scale=8, submit_btn=False # <--- Removes the forced sidebar submit button, allowing 'Enter' to natively act as the submission key ) # 2. Wire Up the Component Handlers - 2. Provide a distinct, clear upload node that connects to your perception agent pipeline # Triggered immediately when a user finishes choosing files in the file browser window upload_btn = gr.UploadButton( "📎 Attach Documents/Images", file_count="multiple", file_types=[".png", ".jpg", ".jpeg", ".bmp", ".pdf", ".xlsx", ".xls", ".docx", ".md", ".py", ".html", ".cs", ".js", ".json", ".csv", ".zip", ".tar.gz", ".log", ".txt"], scale=2 ) # Visual text tracker showing exactly what files have successfully staged upload_status = gr.Markdown("") # --- Right Panel: Agentic Control Tower --- with gr.Column(scale=1, variant="secondary"): gr.Markdown("### 🚀 CoderG Authorization Core") gr.Markdown("_Authorize code outputs to be compiled into dedicated remote repositories._") target_repo = gr.Textbox( label="Target Repository Name", placeholder="e.g., advanced-python-course", value="dynamic-course-repo" ) commit_txt = gr.Textbox( label="Commit Message", value="Automated generation via CoderG Agent" ) staged_files = gr.Textbox( label="Staged Files (Comma-separated)", value="COURSE_README.md" ) approve_btn = gr.Button("Approve & Push to GitHub", variant="primary") gr.Markdown("#### 📊 Deployment Telemetry Logs") output_log = gr.Markdown("_Awaiting local environment staging completion..._") # --- LOGIC FUNCTIONS --- # --- 1: Create a staging function to process files when uploaded --- def handle_file_upload(uploaded_files, current_staged_files): """Processes files when selected via browser and appends to staging session state.""" if not current_staged_files: current_staged_files = [] # Gradio returns a list of file objects when file_count="multiple" for file_obj in uploaded_files: # Check the property format of incoming file assets from Gradio 6 file_path = file_obj.name if hasattr(file_obj, 'name') else file_obj if file_path and file_path not in current_staged_files: current_staged_files.append(file_path) # Return a visual message showing how many files are safely staged status_msg = f"🟢 **{len(current_staged_files)} file(s) staged successfully and attached to next prompt.**" return current_staged_files, status_msg def bot_response(message, history, chat_id): user_content = message["text"] # 1. Pass a CLEAN copy of the historical conversation *before* appending new turns # This ensures core_logic gets a proper history trail without duplication. clean_history_snapshot = list(history) # 2. Now prepare the live local UI array for streaming feedback history.append({"role": "user", "content": user_content}) history.append({"role": "assistant", "content": ""}) # 3. Run the generator using your clean background state snapshot for partial_resp in chat_function(message, clean_history_snapshot): history[-1]["content"] = partial_resp yield history def handle_save(history, chat_id): # 1. Save the actual data new_id = save_chat(chat_id, history) # 2. Get the latest from hub current_list = load_history() # 3. Ensure the current one is definitely at the top if [new_id] not in current_list: current_list.insert(0, [new_id]) return new_id, gr.update(samples=current_list) def load_past_chat(selected_list): chat_id = selected_list[0] content = get_chat_content(chat_id) return content, chat_id def push_authorized(repo_name, commit_msg, files_list): """Triggers git_agent to build the target repository, clean up local folders, and log tasks.""" # Cleanly split comma separated lists of files files = [f.strip() for f in files_list.split(",") if f.strip()] if not repo_name.strip(): yield "❌ **Deployment Aborted:** Repository name cannot be empty." return yield "◌ _Connecting to GitHub REST API Engine..._" result = manage_github_repo(repo_name.strip(), commit_msg, files) yield f"{result}" # --- INTERACTION ARCHITECTURE / EVENT HANDLERS --- # Hook the UploadButton event listener loop to stage chosen files right away upload_btn.upload( fn=handle_file_upload, inputs=[upload_btn, staged_files_state], outputs=[staged_files_state, upload_status] ) # Submission wrapper to package parameters together for your multi-format engine # When submitting, we pass BOTH the text and the hidden staged files state array! def process_submission(message_text, current_staged_files, history, chat_id): if not message_text.strip() and not current_staged_files: return history, "", current_staged_files, "" # Packaging matching the identical format of core_logic expectations payload = { "text": message_text, "files": current_staged_files } # Stream responses through bot loops and clear inputs when complete for updated_history in bot_response(payload, history, chat_id): # Continuously yield state update frames, wiping values upon initial loop entry yield updated_history, "", [], "" # 1. Bind Enter/Submit behavior for the chat input text box chat_input.submit( fn=process_submission, inputs=[chat_input, staged_files_state, chatbot, chat_id_state], outputs=[chatbot, chat_input, staged_files_state, upload_status] ).then( fn=handle_save, inputs=[chatbot, chat_id_state], outputs=[chat_id_state, history_list] ) # 2. Click Sidebar Item -> Load Content history_list.click( fn=load_past_chat, inputs=[history_list], outputs=[chatbot, chat_id_state] ) # 3. New Chat Button Initialization new_btn.click( fn=lambda: ([], "", [], load_history(), "", "_Awaiting local environment staging completion..._"), inputs=None, outputs=[chatbot, chat_id_state, staged_files_state, history_list, upload_status, output_log] ) # 4. Bind the Control Tower Approve Action button approve_btn.click( fn=push_authorized, inputs=[target_repo, commit_txt, staged_files], outputs=[output_log] ) """ # 1. Submit Chat -> Stream Response -> Save -> Refresh Sidebar chat_input.submit( bot_response, [chat_input, chatbot, chat_id_state], [chatbot] ).then( handle_save, [chatbot, chat_id_state], [chat_id_state, history_list] ) # 2. Click Sidebar Item -> Load Content history_list.click( load_past_chat, [history_list], [chatbot, chat_id_state] ) # 3. New Chat Button Initialization new_btn.click( lambda: ([], "", load_history(), "_Awaiting local environment staging completion..._"), None, [chatbot, chat_id_state, history_list, output_log] ) # 4. Bind the Control Tower Approve Action button approve_btn.click( push_authorized, [target_repo, commit_txt, staged_files], [output_log] ) """ # Fully consolidated theme and styling injections down into launch() parameter fields demo.launch(theme=gr.themes.Soft(), css="styles.css")