diff --git a/main.py b/main.py index 554ba01..300bc5f 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ from PySide6.QtWidgets import ( QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QTextEdit, QLineEdit, QLabel, QSplitter, QMessageBox, QPushButton, - QFileDialog, QSizePolicy, QListWidget, QListWidgetItem + QFileDialog, QSizePolicy, QListWidget, QListWidgetItem, QCheckBox ) from PySide6.QtCore import Qt, Slot, Signal, QObject, QBuffer, QIODevice, QRunnable, QThreadPool, QTimer, QRect from PySide6.QtGui import QPixmap, QImage, QIcon, QTextCursor @@ -33,33 +33,36 @@ class WorkerSignals(QObject): error = Signal(str, list, str, int, str) class GeminiRunnable(QRunnable): - def __init__(self, model_to_use, query_content_parts, source, iteration_count): + def __init__(self, model_to_use, query_content_parts, source, iteration_count, thinking_step=0): # Added thinking_step super().__init__() self.model_to_use = model_to_use self.query_content_parts = query_content_parts self.source = source self.iteration_count = iteration_count + self.thinking_step = thinking_step # Store thinking_step self.signals = WorkerSignals() self.is_cancelled = False @Slot() def run(self): if self.is_cancelled: - self.signals.error.emit("Request cancelled before start", self.query_content_parts, self.source, self.iteration_count, self.model_to_use.model_name) + self.signals.error.emit("Request cancelled before start", self.query_content_parts, self.source, self.iteration_count, self.model_to_use.model_name, self.thinking_step) return try: + # Note: generate_content takes 'contents', not 'query_content_parts' directly in some SDK versions. + # Assuming self.query_content_parts is already in the correct format for model_to_use.generate_content() response = self.model_to_use.generate_content(self.query_content_parts) if self.is_cancelled: - self.signals.error.emit("Request cancelled during execution", self.query_content_parts, self.source, self.iteration_count, self.model_to_use.model_name) + self.signals.error.emit("Request cancelled during execution", self.query_content_parts, self.source, self.iteration_count, self.model_to_use.model_name, self.thinking_step) return - self.signals.finished.emit(response, None, self.query_content_parts, self.source, self.iteration_count, self.model_to_use.model_name) + self.signals.finished.emit(response, None, self.query_content_parts, self.source, self.iteration_count, self.model_to_use.model_name, self.thinking_step) except Exception as e: print(f"[GeminiRunnable Error] Exception during API call: {e}") if not self.is_cancelled: - self.signals.error.emit(str(e), self.query_content_parts, self.source, self.iteration_count, self.model_to_use.model_name) + self.signals.error.emit(str(e), self.query_content_parts, self.source, self.iteration_count, self.model_to_use.model_name, self.thinking_step) def cancel(self): - print(f"[GeminiRunnable] Attempting to cancel task for source {self.source}, iter {self.iteration_count}") + print(f"[GeminiRunnable] Attempting to cancel task for source {self.source}, iter {self.iteration_count}, step {self.thinking_step}") self.is_cancelled = True class HotkeySignal(QObject): @@ -110,6 +113,15 @@ def _setup_ui(self, main_layout): left_panel_layout.setContentsMargins(0,0,0,0) self.new_chat_button = QPushButton("➕ New Chat"); self.new_chat_button.clicked.connect(self.handle_new_chat_button) left_panel_layout.addWidget(self.new_chat_button) + + self.toggle_deep_thinking_button = QPushButton("Toggle Deep Thinking Panel") + self.toggle_deep_thinking_button.clicked.connect(self.toggle_deep_thinking_panel_visibility) + left_panel_layout.addWidget(self.toggle_deep_thinking_button) + + self.multi_step_deep_think_enabled_checkbox = QCheckBox("Enable Multi-Step Thinking") + self.multi_step_deep_think_enabled_checkbox.setToolTip("When enabled, Gemini will show its step-by-step reasoning in the Deep Thinking Panel.") + left_panel_layout.addWidget(self.multi_step_deep_think_enabled_checkbox) + self.history_list_widget = QListWidget(); self.history_list_widget.setObjectName("historyPanel") self.history_list_widget.itemClicked.connect(self._load_session_from_history_item) left_panel_layout.addWidget(self.history_list_widget); splitter.addWidget(left_panel_widget) @@ -132,7 +144,9 @@ def _setup_ui(self, main_layout): self.input_field.setEnabled(False); self.attach_image_button.setEnabled(False); self.screenshot_button.setEnabled(False) chat_layout.addWidget(self.attached_image_preview_label); chat_layout.addLayout(input_area_layout); splitter.addWidget(chat_panel_widget) self.context_panel = QTextEdit(readOnly=True, placeholderText="Contextual Info"); splitter.addWidget(self.context_panel) - splitter.setSizes([200, 750, 250]); + self.deep_thinking_panel = QTextEdit(readOnly=True, placeholderText="Deep Thoughts...") # New panel + splitter.addWidget(self.deep_thinking_panel) # Add to splitter + splitter.setSizes([200, 600, 250, 150]); # Adjusted sizes for the new panel main_layout.addWidget(splitter) self.statusBar().showMessage("Waiting for API Key...") @@ -415,14 +429,16 @@ def _start_gemini_request(self): iter_data['last_thinking_message'] = msg QApplication.processEvents() if self.active_gemini_runnable: self.active_gemini_runnable.cancel() - runnable = GeminiRunnable(iter_data['model_to_use'], iter_data['current_content_parts'], iter_data['source'], iter_data['iteration_count']) + # Pass the current thinking_step from iter_data + current_thinking_step = iter_data.get('thinking_step', 0) + runnable = GeminiRunnable(iter_data['model_to_use'], iter_data['current_content_parts'], iter_data['source'], iter_data['iteration_count'], current_thinking_step) runnable.signals.finished.connect(self.handle_gemini_finished) runnable.signals.error.connect(self.handle_gemini_error) self.active_gemini_runnable = runnable; self.thread_pool.start(runnable) return True - def process_gemini_query(self, parts, source, model, img_path=None, is_cont=False): - if not is_cont: + def process_gemini_query(self, parts, source, model, img_path=None, is_cont=False, thinking_step=0, accumulated_thoughts=""): + if not is_cont: # This block is for the initial query or start of a new multi-step query if self.active_gemini_runnable and (self.active_gemini_runnable.source == source or source == "main_chat"): self.active_gemini_runnable.cancel(); self.active_gemini_runnable = None if not model: @@ -430,89 +446,242 @@ def process_gemini_query(self, parts, source, model, img_path=None, is_cont=Fals if source == "insight_overlay_chat": self.insight_overlay.receive_gemini_response(err_msg) else: self._add_message_to_current_session("system", err_msg) self.enable_input_areas(source); return False + initial_txt = next((p['text'] for p in parts if 'text' in p), "") - self.current_iteration_data = {'source': source, 'iteration_count': 0, 'max_iterations': 1 if source == "insight_overlay_chat" else 3, - 'model_to_use': model, 'original_image_path': img_path, 'initial_user_prompt_text': initial_txt, - 'last_thinking_message': None, 'original_query_content': list(parts), 'current_content_parts': list(parts)} - else: + + # Determine if multi-step deep thinking is enabled (placeholder for actual UI element) + multi_step_deep_think_enabled = getattr(self, 'multi_step_deep_think_enabled_checkbox', None) and self.multi_step_deep_think_enabled_checkbox.isChecked() + + max_thinking_steps = 1 + if multi_step_deep_think_enabled and source == "main_chat": # Only for main chat for now + max_thinking_steps = 2 # E.g., Step 0: Initial thought, Step 1: Refinement + + current_parts = list(parts) + if multi_step_deep_think_enabled and thinking_step == 0 and source == "main_chat": + # Modify prompt for the first step + original_prompt = initial_txt + current_parts = [{'text': f"Think step-by-step to construct a comprehensive answer for the following query: \"{original_prompt}\". Provide your initial thoughts and reasoning."}] + # Preserve image if present + img_part = next((p for p in parts if "inline_data" in p), None) + if img_part: current_parts.append(img_part) + + + self.current_iteration_data = { + 'source': source, 'iteration_count': 0, + 'max_iterations': 1 if source == "insight_overlay_chat" else 3, # Plugin iterations + 'model_to_use': model, 'original_image_path': img_path, + 'initial_user_prompt_text': initial_txt, + 'last_thinking_message': None, 'original_query_content': list(parts), + 'current_content_parts': current_parts, + 'thinking_step': thinking_step, # Current phase of multi-step thought + 'max_thinking_steps': max_thinking_steps, # Total steps for multi-step thought + 'accumulated_thoughts': accumulated_thoughts # Store thoughts from previous steps + } + else: # This block is for plugin continuations or subsequent steps in multi-step thinking if not self.current_iteration_data or self.current_iteration_data['source'] != source: - self._add_message_to_current_session("system", f"Err: Iter mismatch {source}.") + self._add_message_to_current_session("system", f"Err: Iter/Thinking mismatch {source}.") self.enable_input_areas(source); return False - self.current_iteration_data['current_content_parts'] = list(parts) + + self.current_iteration_data['current_content_parts'] = list(parts) # parts here would be constructed by handle_gemini_finished for the next step self.current_iteration_data['model_to_use'] = model if model else self.gemini_model - if source == "main_chat" and not is_cont: self.input_field.setEnabled(False); self.attach_image_button.setEnabled(False); self.screenshot_button.setEnabled(False) + self.current_iteration_data['thinking_step'] = thinking_step + self.current_iteration_data['accumulated_thoughts'] = accumulated_thoughts + # iteration_count for plugins is handled within handle_gemini_finished + + if source == "main_chat" and not is_cont: # Disable input only on the very first call of a sequence + self.input_field.setEnabled(False); self.attach_image_button.setEnabled(False); self.screenshot_button.setEnabled(False) + return self._start_gemini_request() - @Slot(object, str, list, str, int, str) - def handle_gemini_finished(self, resp_obj, _, __, src_worker, iter_worker): + @Slot(object, str, list, str, int, str, int) # Added thinking_step_worker + def handle_gemini_finished(self, resp_obj, _, __, src_worker, iter_worker, model_name_worker, thinking_step_worker): iter_data = self.current_iteration_data - if not iter_data or iter_data['source'] != src_worker or iter_data['iteration_count'] != iter_worker: - print(f"Info: Stale/mismatched response for {src_worker}. Ignoring."); return + if not iter_data or iter_data['source'] != src_worker or iter_data['iteration_count'] != iter_worker or iter_data['thinking_step'] != thinking_step_worker: + print(f"Info: Stale/mismatched response for {src_worker} iter {iter_worker} think_step {thinking_step_worker}. Current is iter {iter_data.get('iteration_count', -1)} think_step {iter_data.get('thinking_step', -1)}. Ignoring."); return if self.active_gemini_runnable and self.active_gemini_runnable.is_cancelled: self.active_gemini_runnable = None; print(f"Info: Response for cancelled {src_worker}. Ignoring."); return self.active_gemini_runnable = None resp_txt = resp_obj.text if resp_obj.text else "" + # --- Insight Overlay Handling (remains largely the same, but uses full response for deep thinking) --- if src_worker == "insight_overlay_chat": self.insight_overlay.remove_last_message_from_display() self.insight_overlay.receive_gemini_response(resp_txt if resp_txt else "Sorry, I couldn't process that.") if not resp_txt and resp_obj.candidates: print(f"Gemini API (Insight) details: {resp_obj.candidates}") + self.deep_thinking_panel.setText(f"Insight Overlay Response (Final):\n{resp_txt}\n\nFull Response Object:\n{resp_obj}") self.current_iteration_data = None; return - if iter_data['source'] == "main_chat" and iter_data.get('last_thinking_message'): # Check source and if key exists + # --- Main Chat - Multi-Step and Plugin Handling --- + if iter_data['source'] == "main_chat" and iter_data.get('last_thinking_message'): self.remove_last_chat_message(iter_data['last_thinking_message']) iter_data['last_thinking_message'] = None - if iter_data['source'] == "main_chat": self._add_message_to_current_session("gemini", resp_txt) + multi_step_deep_think_enabled = getattr(self, 'multi_step_deep_think_enabled_checkbox', None) and self.multi_step_deep_think_enabled_checkbox.isChecked() + + # If in multi-step thinking and not yet at the final thinking step + if multi_step_deep_think_enabled and iter_data['thinking_step'] < iter_data['max_thinking_steps'] - 1: + current_thoughts = iter_data.get('accumulated_thoughts', "") + new_thoughts = f"Step {iter_data['thinking_step'] + 1} Thoughts:\n{resp_txt}\n\n" + updated_accumulated_thoughts = current_thoughts + new_thoughts + + self.deep_thinking_panel.setText(updated_accumulated_thoughts + f"Full Response (Step {iter_data['thinking_step'] + 1}):\n{resp_obj}") + + next_thinking_step = iter_data['thinking_step'] + 1 + + # Prepare prompt for the next step (e.g., refinement) + original_prompt = iter_data['initial_user_prompt_text'] + next_prompt_text = f"Previous thoughts: \"{resp_txt}\". Now, refine these thoughts to provide a concise and direct answer to the original query: \"{original_prompt}\"." + + next_parts = [{'text': next_prompt_text}] + if iter_data.get('original_image_path'): # Carry over image if it exists + try: + _, ext = os.path.splitext(iter_data['original_image_path']); mime = f"image/{ext[1:].lower().replace('jpg','jpeg')}" + with open(iter_data['original_image_path'],"rb") as f: next_parts.append({"inline_data":{"mime_type":mime,"data":f.read()}}) + except Exception as e: + self._add_message_to_current_session("system", f"Error re-attaching image for multi-step: {e}") + self.deep_thinking_panel.append(f"\nError re-attaching image for multi-step: {e}") + + + # Call process_gemini_query for the next thinking step + # Note: is_cont=True here refers to continuing the overall query, not plugin continuation. + # Plugin iterations are reset for each thinking step if plugins are involved *within* a thinking step. + self.process_gemini_query(next_parts, + source=iter_data['source'], + model=iter_data['model_to_use'], # Or decide if model can change + img_path=iter_data['original_image_path'], + is_cont=True, # Important: this signals continuation of the current query session + thinking_step=next_thinking_step, + accumulated_thoughts=updated_accumulated_thoughts) + return # End here, wait for the next step's response + + # --- Final Response Processing (either single step, or final step of multi-step) --- + final_display_response = resp_txt + if multi_step_deep_think_enabled: + iter_data['accumulated_thoughts'] += f"Step {iter_data['thinking_step'] + 1} (Final Answer):\n{resp_txt}\n\n" + self.deep_thinking_panel.setText(iter_data['accumulated_thoughts'] + f"Full Response (Final):\n{resp_obj}") + else: # Single step response + self.deep_thinking_panel.setText(f"Main Chat Response:\n{resp_txt}\n\nFull Response Object:\n{resp_obj}") + + # Add final response to chat area + self._add_message_to_current_session("gemini", final_display_response) + + # --- Plugin Handling (can occur after any thinking step, or in single-step mode) --- + iter_data['iteration_count'] += 1 # This is for plugin iterations + plugin_match = re.search(r"\[PLUGIN_CALL:\s*(\w+)(.*?)\]", final_display_response) - iter_data['iteration_count'] += 1 - plugin_match = re.search(r"\[PLUGIN_CALL:\s*(\w+)(.*?)\]", resp_txt) if plugin_match and iter_data['iteration_count'] < iter_data['max_iterations']: name, p_str = plugin_match.groups(); name=name.strip(); p_str=p_str.strip() - before = resp_txt[:plugin_match.start()].strip(); after = resp_txt[plugin_match.end():].strip() - if before: self._add_message_to_current_session("gemini", before) + # ... (rest of plugin handling logic remains similar) + before = final_display_response[:plugin_match.start()].strip(); after = final_display_response[plugin_match.end():].strip() + if before: self._add_message_to_current_session("gemini", before) # Display text before plugin call self._add_message_to_current_session("system", f"Gemini: Using plugin '{name}'...") + self.deep_thinking_panel.append(f"\nPlugin Call: {name} with params {p_str}") QApplication.processEvents() + params = self._parse_plugin_params(p_str); fn = self.plugin_manager.get_plugin_function(name) - next_p, next_m = [], self.gemini_model + next_p_plugin, next_m_plugin = [], self.gemini_model # Default model for plugin continuation + if fn: try: - res = fn(**params); self._add_message_to_current_session("plugin_result", f"Plugin '{name}' Result:\n{res}") - next_p = [{'text': f"Original: '{iter_data['initial_user_prompt_text']}'. Plugin '{name}'({params})->{res}. Final answer or continue. {after}"}] - except Exception as e: self._add_message_to_current_session("system", f"Plugin Error: {e}"); next_p = [{'text': f"Plugin '{name}' fail: {e}. Skip."}] - else: self._add_message_to_current_session("system", f"Plugin '{name}' not found."); next_p = [{'text': f"Plugin '{name}' not found. Skip."}] - self.process_gemini_query(next_p, iter_data['source'], next_m, is_cont=True) - else: - if plugin_match: self._add_message_to_current_session("gemini", "Max plugin iterations reached.") - if not plugin_match and resp_txt: pass - elif not resp_txt and not plugin_match : self._add_message_to_current_session("gemini", "No text response.") - if not resp_txt and resp_obj.candidates: print(f"Gemini API (Main) details: {resp_obj.candidates}") - self.enable_input_areas(iter_data['source']); self.current_iteration_data = None - - @Slot(str, list, str, int, str) - def handle_gemini_error(self, err_msg, _, src_worker, iter_worker, model_name): + res = fn(**params); + plugin_result_msg = f"Plugin '{name}' Result:\n{res}" + self._add_message_to_current_session("plugin_result", plugin_result_msg) + self.deep_thinking_panel.append(f"\nPlugin Result: {res}") + # Construct prompt for Gemini to process plugin result + # If in multi-step, this plugin call is part of the *current* thinking_step's processing. + # The prompt should guide Gemini to integrate the plugin result into its current line of thought for this step. + prompt_after_plugin = f"The user's original query was: \"{iter_data['initial_user_prompt_text']}\". " + if multi_step_deep_think_enabled: + prompt_after_plugin += f"You are currently in thinking step {iter_data['thinking_step'] + 1}. Your thoughts so far in this step (before this plugin) were: \"{final_display_response[:plugin_match.start()]}\". " + prompt_after_plugin += f"You called plugin '{name}' with parameters '{params}' which returned: '{res}'. Now, continue processing based on this result. {after}" + next_p_plugin = [{'text': prompt_after_plugin}] + + except Exception as e: + error_msg = f"Plugin Error: {e}" + self._add_message_to_current_session("system", error_msg) + self.deep_thinking_panel.append(f"\n{error_msg}") + next_p_plugin = [{'text': f"There was an error with plugin '{name}': {e}. Please try to answer without it or inform the user."}] + else: + not_found_msg = f"Plugin '{name}' not found." + self._add_message_to_current_session("system", not_found_msg) + self.deep_thinking_panel.append(f"\n{not_found_msg}") + next_p_plugin = [{'text': f"Plugin '{name}' was not found. Please proceed without it."}] + + # Continue processing with plugin result. This is still part of the current thinking_step. + # iteration_count is for plugins, thinking_step is for the broader multi-step thought. + self.process_gemini_query(next_p_plugin, + source=iter_data['source'], + model=next_m_plugin, + img_path=iter_data['original_image_path'], # Carry over image if relevant for plugin context + is_cont=True, # Signal continuation + thinking_step=iter_data['thinking_step'], # Remain in the same thinking step + accumulated_thoughts=iter_data.get('accumulated_thoughts', "")) # Pass current accumulated thoughts + return # End here, wait for Gemini's response to plugin processing + + # --- No more plugins or max plugin iterations reached --- + if plugin_match: # Max plugin iterations reached + max_iter_msg = "Max plugin iterations reached." + self._add_message_to_current_session("gemini", max_iter_msg) # Or system, depending on desired UX + self.deep_thinking_panel.append(f"\n{max_iter_msg}") + + if not final_display_response and not plugin_match : # No text and no plugin means empty response + self._add_message_to_current_session("gemini", "No text response received.") + self.deep_thinking_panel.append("\nNo text response received from Gemini.") + + if not final_display_response and resp_obj.candidates: # Log if no text but candidates exist + print(f"Gemini API (Main Chat - Final) details: {resp_obj.candidates}") + self.deep_thinking_panel.append(f"\nGemini API Details (No Text): {resp_obj.candidates}") + + self.enable_input_areas(iter_data['source']) + self.current_iteration_data = None # Clear data after final processing (or if no more steps/plugins) + + + @Slot(str, list, str, int, str, int) # Added thinking_step_worker argument + def handle_gemini_error(self, err_msg, _, src_worker, iter_worker, model_name, thinking_step_worker): # Added thinking_step_worker iter_data = self.current_iteration_data - if not iter_data or iter_data['source'] != src_worker or iter_data['iteration_count'] != iter_worker: - print(f"Info: Stale/mismatched Gemini ERROR for {src_worker}. Ignoring."); return + # Check thinking_step_worker as well + if not iter_data or iter_data['source'] != src_worker or iter_data['iteration_count'] != iter_worker or iter_data['thinking_step'] != thinking_step_worker: # Compare all relevant fields + print(f"Info: Stale/mismatched Gemini ERROR for {src_worker} iter {iter_worker} think_step {thinking_step_worker}. Current iter_data: {iter_data}. Ignoring."); return if self.active_gemini_runnable and self.active_gemini_runnable.is_cancelled: - self.active_gemini_runnable = None; print(f"Info: Error for cancelled {src_worker}. Err: {err_msg}. Ignoring."); return + self.active_gemini_runnable = None; print(f"Info: Error for cancelled {src_worker} iter {iter_worker} think_step {thinking_step_worker}. Err: {err_msg}. Ignoring."); return self.active_gemini_runnable = None + # --- Insight Overlay Error Handling --- if src_worker == "insight_overlay_chat": self.insight_overlay.remove_last_message_from_display() self.insight_overlay.receive_gemini_response(f"Error: {err_msg}") - elif iter_data.get('last_thinking_message'): - self.remove_last_chat_message(iter_data['last_thinking_message']) + self.deep_thinking_panel.setText(f"Insight Overlay Error:\n{err_msg}\nModel: {model_name}") + self.current_iteration_data = None # Clear data for insight overlay errors + self.enable_input_areas(src_worker) # Re-enable input for insight overlay + return # Stop further processing for insight errors + + # --- Main Chat Error Handling (covers multi-step and plugins) --- + if iter_data: # Should always be true if not insight_overlay + if iter_data.get('last_thinking_message'): + self.remove_last_chat_message(iter_data['last_thinking_message']) + iter_data['last_thinking_message'] = None - if iter_data : iter_data['last_thinking_message'] = None + error_source_info = f"Source: {src_worker}, Plugin Iteration: {iter_worker + 1}" + if getattr(self, 'multi_step_deep_think_enabled_checkbox', None) and self.multi_step_deep_think_enabled_checkbox.isChecked(): + error_source_info += f", Thinking Step: {thinking_step_worker + 1}" - error_display_msg = f"Gemini Error (Iter {iter_data['iteration_count'] + 1 if iter_data else 'N/A'}): {err_msg}" - if src_worker == "main_chat": - self._add_message_to_current_session("system_error", error_display_msg) + self.deep_thinking_panel.append(f"\n\n--- ERROR ENCOUNTERED ---\n{error_source_info}\nModel: {model_name}\nError: {err_msg}\n--------------------------") - if iter_data and iter_data.get('original_image_path') and not self.attached_image_path: self.attached_image_path = iter_data['original_image_path'] - self.enable_input_areas(src_worker if iter_data else None); - if iter_data: self.current_iteration_data = None + # Add a user-facing error message to the main chat + user_error_message = f"Gemini Error during processing ({error_source_info}): {err_msg}" + self._add_message_to_current_session("system_error", user_error_message) + + # Restore original image if it was part of the query that failed + if iter_data.get('original_image_path') and not self.attached_image_path: + self.attached_image_path = iter_data['original_image_path'] + # Potentially update UI to show the image is re-attached, if applicable, though clear_attached_image_preview does more + else: # Should not happen if not insight_overlay, but as a fallback + self.deep_thinking_panel.setText(f"General Error (No IterData):\n{err_msg}\nModel: {model_name}\nSource: {src_worker}\nPlugin Iter: {iter_worker}, Thinking Step: {thinking_step_worker}") + self._add_message_to_current_session("system_error", f"A general Gemini error occurred: {err_msg}") + + + self.enable_input_areas(src_worker if iter_data else None) # Re-enable main chat inputs + if iter_data: self.current_iteration_data = None # Clear current iteration data as the process is halted def _parse_plugin_params(self, params_str: str) -> dict: params = {}; @@ -573,6 +742,15 @@ def toggle_insight_overlay(self): self.clear_attached_image_preview() self.insight_overlay.show_overlay() + @Slot() + def toggle_deep_thinking_panel_visibility(self): + if self.deep_thinking_panel.isVisible(): + self.deep_thinking_panel.hide() + self.toggle_deep_thinking_button.setText("Show Deep Thinking Panel") + else: + self.deep_thinking_panel.show() + self.toggle_deep_thinking_button.setText("Hide Deep Thinking Panel") + def closeEvent(self, event): if self.active_gemini_runnable: self.active_gemini_runnable.cancel() self.thread_pool.clear(); self.thread_pool.waitForDone(1000)