From deae4302d52507fa9222581124af7b136f7a93bc Mon Sep 17 00:00:00 2001 From: FURK4NGG <105324908+FURK4NGG@users.noreply.github.com> Date: Sat, 16 May 2026 09:56:22 +0300 Subject: [PATCH] Add files via upload --- ai.py | 137 +++++++++++++++++++++------------- ui.py | 232 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 204 insertions(+), 165 deletions(-) diff --git a/ai.py b/ai.py index 12aa2ec..adbbcad 100644 --- a/ai.py +++ b/ai.py @@ -160,11 +160,19 @@ def get_ui_text(cfg: dict, key: str, **kwargs) -> str: def _tavily_search(cfg: dict, query: str, max_results: int = 5) -> str: api_key = str(cfg.get("tavily_api_key") or "").strip() if not api_key: - return "[WEB_SEARCH_ERROR]\nTavily API key is missing.\n[/WEB_SEARCH_ERROR]" + return ( + "[WEB_SEARCH_ERROR]\n" + f"{get_ui_text(cfg, 'o_Tavily_API_Key_Missing')}\n" + "[/WEB_SEARCH_ERROR]" + ) query = str(query or "").strip() if not query: - return "[WEB_SEARCH_ERROR]\nEmpty search query.\n[/WEB_SEARCH_ERROR]" + return ( + "[WEB_SEARCH_ERROR]\n" + f"{get_ui_text(cfg, 'o_Empty_Search_Query')}\n" + "[/WEB_SEARCH_ERROR]" + ) try: r = requests.post( @@ -271,7 +279,7 @@ def _safe_read_text(path: Path, max_bytes: int = 250_000) -> str: data = data[:max_bytes] return data.decode("utf-8", errors="replace") except Exception as e: - return f"[Dosya okunamadı: {e}]" + return f"[File could not be read: {e}]" def _safe_read_pdf(path: Path, max_chars: int = 50000) -> str: try: @@ -296,39 +304,35 @@ def _looks_like_empty_pdf_text(text: str) -> bool: return len(t) < 80 def _user_wants_image_edit(text: str) -> bool: - t = str(text or "").lower() + """ + Legacy auto-detection helper. - image_words = [ - "image", "picture", "photo", "graphic", "diagram", "visual", - "resim", "görsel", "gorsel", "fotoğraf", "fotograf", - "şekil", "sekil", "grafik", "diyagram" - ] + Şu an bilinçli olarak kullanılmıyor. + PDF kararları Prompt Chooser üzerinden veriliyor: + - pdf_text + - pdf_image + - pdf_text_image - edit_words = [ - "change", "replace", "modify", "edit", "remove", "add", - "değiştir", "degistir", "düzenle", "duzenle", - "kaldır", "kaldir", "ekle", "yenile" - ] + PNG/JPG/WebP gibi normal görsellerde de model zaten görseli ve kullanıcı isteğini + birlikte gördüğü için ayrıca keyword tabanlı yönlendirme yapılmıyor. - return any(w in t for w in image_words) and any(w in t for w in edit_words) + İleride ayrı bir "Auto image edit mode" eklenirse bu fonksiyon yeniden + aktif kullanılabilir. + """ + return False def _user_wants_text_edit(text: str) -> bool: - t = str(text or "").lower() + """ + Legacy auto-detection helper. - text_words = [ - "text", "writing", "font", "title", "heading", "paragraph", - "yazı", "yazi", "metin", "font", "başlık", "baslik", - "paragraf", "içerik", "icerik" - ] + Şu an bilinçli olarak kullanılmıyor. + PDF text/image ayrımı artık Prompt Chooser tarafından belirleniyor. - edit_words = [ - "shorten", "minimal", "rewrite", "summarize", "increase", "make bigger", - "kısalt", "kisalt", "minimal", "özetle", "ozetle", - "büyüt", "buyut", "düzenle", "duzenle" - ] - - return any(w in t for w in text_words) and any(w in t for w in edit_words) + İleride ayrı bir "Auto text edit mode" eklenirse bu fonksiyon yeniden + aktif kullanılabilir. + """ + return False def _analyze_pdf_kind(path: Path) -> dict: """ @@ -696,7 +700,7 @@ def _safe_read_docx(path: Path, max_chars: int = 50000) -> str: return out[:max_chars] if out else "[DOCX file is empty or text could not be extracted.]" except Exception as e: - return f"[DOCX okunamadı: {e}]" + return f"[DOCX could not be read: {e}]" def _safe_read_xlsx(path: Path, max_rows: int = 200, max_chars: int = 50000) -> str: try: @@ -722,7 +726,7 @@ def _safe_read_xlsx(path: Path, max_rows: int = 200, max_chars: int = 50000) -> return "\n".join(parts).strip()[:max_chars] except Exception as e: - return f"[XLSX okunamadı: {e}]" + return f"[XLSX could not be read: {e}]" def _is_text_file(path: Path) -> bool: ext = path.suffix.lower() @@ -1803,7 +1807,7 @@ def _build_blocks_and_cache_info(msg: dict, cache_images_dir: Path | None, pdf_m f"mime: {mime}\n" f"editable: {'true' if editable else 'false'}\n" "type: unsupported\n" - "note: Bu dosya türü şu an metin olarak okunamıyor.\n" + "note: This file type cannot currently be read as text.\n" "[/FILE]\n" ) }) @@ -2160,7 +2164,7 @@ def _get_models_list(cfg: dict) -> list[dict]: def _get_chat_model_entry(cfg: dict, chat_file: Path) -> dict: models = _get_models_list(cfg) if not models: - _die("ai_models in config is empty. At least one model must be defined.", 1) + _die(get_ui_text(cfg, "o_AI_Models_Empty"), 1) model_ids = {m["id"] for m in models} @@ -2575,17 +2579,15 @@ def main(): pdf_mode_text_image = "pdf_text_image" in selected_prompt_blocks_set if is_pdf_mixed_request: - wants_image = _user_wants_image_edit(last_text) - wants_text = _user_wants_text_edit(last_text) - - # Kullanıcı text istiyor ama resim aynı kalsın diyorsa: - # AI'ye image edit yaptırma. - if wants_text and not wants_image: - pdf_mode_text = True - pdf_mode_image = False - pdf_mode_text_image = False - - is_pdf_mixed_image_only_request = wants_image and not wants_text + # Mixed PDF kararını keyword tahminiyle vermiyoruz. + # Kullanıcının açık seçimi Prompt Chooser üzerinden gelir: + # - pdf_text -> sadece metin blokları + # - pdf_image -> sadece görsel blokları / sayfa görseli + # - pdf_text_image -> metin + görsel layout birlikte + # + # Bu yüzden _user_wants_image_edit() / _user_wants_text_edit() + # burada kullanılmaz. + is_pdf_mixed_image_only_request = bool(pdf_mode_image and not pdf_mode_text_image and not pdf_mode_text) has_editable_pdf_request = ( is_pdf_text_only_request @@ -2640,9 +2642,10 @@ def main(): final_messages.append({ "role": "system", "content": ( - "Aşağıdaki bilgiler konuşma devamlılığı için verilmiştir. " - "Bunlara doğrudan cevap verme, sadece son kullanıcı mesajını anlamak için kullan.\n\n" - + memory_context + get_ui_text( + cfg, + "o_RAG_Context_System_Message" + ) + "\n\n" + memory_context ) }) @@ -2695,9 +2698,9 @@ def main(): if recent_messages: final_messages.append({ "role": "system", - "content": ( - "Aşağıdaki mesajlar konuşmanın son kısmıdır. " - "Bunlara tek tek cevap verme; sadece en son kullanıcı mesajını anlamak için kullan." + "content": get_ui_text( + cfg, + "o_RAG_Recent_Messages_System_Message" ) }) @@ -3019,8 +3022,44 @@ def main(): ) ) + has_ref_images = False + + try: + for i in selected_indexes: + if not (0 <= i < len(messages)): + continue + + rm = messages[i] + + if rm.get("image"): + has_ref_images = True + break + + imgs = rm.get("images") + if isinstance(imgs, list) and imgs: + has_ref_images = True + break + + files = rm.get("files") + if isinstance(files, list): + for f in files: + if not isinstance(f, dict): + continue + + p = str(f.get("path") or "").lower() + + if p.endswith((".png", ".jpg", ".jpeg", ".webp")): + has_ref_images = True + break + + if has_ref_images: + break + except Exception: + has_ref_images = False + should_stream = ( (not has_input_images) + and (not has_ref_images) and (not wants_file_create) and (not is_pdf_text_only_request) and (not is_pdf_image_only_request) diff --git a/ui.py b/ui.py index 9726ac5..a15c280 100644 --- a/ui.py +++ b/ui.py @@ -2291,8 +2291,9 @@ class ChatApp(Gtk.Application): # Debian/Ubuntu/Raspberry tarafında daha stabil fallback return self.is_debian_like() - - def open_files_dialog_portable(self, title="Dosya Seç", image_only=False, multiple=True, callback=None): + def open_files_dialog_portable(self, title=None, image_only=False, multiple=True, callback=None): + if title is None: + title = self("o_Select_File") """ callback(paths: list[str]) çağrılır """ @@ -2304,7 +2305,9 @@ class ChatApp(Gtk.Application): self._open_files_dialog_modern(title=title, image_only=image_only, multiple=multiple, callback=callback) - def open_single_file_dialog_portable(self, title="Dosya Seç", image_only=False, callback=None): + def open_single_file_dialog_portable(self, title=None, image_only=False, callback=None): + if title is None: + title = self("o_Select_File") """ callback(path: str | None) çağrılır """ @@ -2334,8 +2337,9 @@ class ChatApp(Gtk.Application): return [img_filter] return [img_filter, any_filter] - - def _open_files_dialog_modern(self, title="Dosya Seç", image_only=False, multiple=True, callback=None): + def _open_files_dialog_modern(self, title=None, image_only=False, multiple=True, callback=None): + if title is None: + title = self("o_Select_File") callback = callback or (lambda paths: None) dialog = Gtk.FileDialog() @@ -2383,8 +2387,9 @@ class ChatApp(Gtk.Application): dialog.open(self.win, None, on_done) - - def _open_single_file_dialog_modern(self, title="Dosya Seç", image_only=False, callback=None): + def _open_single_file_dialog_modern(self, title=None, image_only=False, callback=None): + if title is None: + title = self("o_Select_File") callback = callback or (lambda path: None) def _cb(paths): @@ -2392,7 +2397,9 @@ class ChatApp(Gtk.Application): self._open_files_dialog_modern(title=title, image_only=image_only, multiple=False, callback=_cb) - def _open_files_dialog_native(self, title="Dosya Seç", image_only=False, multiple=True, callback=None): + def _open_files_dialog_native(self, title=None, image_only=False, multiple=True, callback=None): + if title is None: + title = self("o_Select_File") callback = callback or (lambda paths: None) dialog = Gtk.FileChooserDialog( @@ -2440,7 +2447,9 @@ class ChatApp(Gtk.Application): dialog.connect("response", on_response) dialog.present() - def _open_single_file_dialog_native(self, title="Dosya Seç", image_only=False, callback=None): + def _open_single_file_dialog_native(self, title=None, image_only=False, callback=None): + if title is None: + title = self("o_Select_File") callback = callback or (lambda path: None) def _cb(paths): @@ -3516,7 +3525,7 @@ class ChatApp(Gtk.Application): entry.set_placeholder_text("provider/model (exp: openai/gpt-4.1-mini)") content.append(entry) - local_check = Gtk.CheckButton(label="Local model") + local_check = Gtk.CheckButton(label=self("o_Local_Model")) content.append(local_check) # Ctrl+V ile yapıştırmayı garanti et @@ -3872,6 +3881,80 @@ class ChatApp(Gtk.Application): except Exception: pass + # --- CTRL C,V,X,A --- + + def add_entry_clipboard_shortcuts(self, entry_widget, dialog=None): + key_controller = Gtk.EventControllerKey() + key_controller.set_propagation_phase(Gtk.PropagationPhase.CAPTURE) + + def on_key_pressed(_controller, keyval, _keycode, state): + ctrl = bool(state & Gdk.ModifierType.CONTROL_MASK) + + if ctrl: + display = Gdk.Display.get_default() + if not display: + return False + + clipboard = display.get_clipboard() + + if keyval in (Gdk.KEY_a, Gdk.KEY_A): + entry_widget.grab_focus() + entry_widget.select_region(0, -1) + return True + + if keyval in (Gdk.KEY_c, Gdk.KEY_C): + text = entry_widget.get_text() or "" + sel = entry_widget.get_selection_bounds() + if sel: + s, e = sel + clipboard.set(text[s:e]) + return True + + if keyval in (Gdk.KEY_x, Gdk.KEY_X): + text = entry_widget.get_text() or "" + sel = entry_widget.get_selection_bounds() + + if sel: + s, e = sel + clipboard.set(text[s:e]) + entry_widget.delete_text(s, e) + else: + clipboard.set(text) + entry_widget.set_text("") + + return True + + if keyval in (Gdk.KEY_v, Gdk.KEY_V): + def on_text(cb, res): + try: + text = cb.read_text_finish(res) or "" + except Exception: + text = "" + + if not text: + return + + sel = entry_widget.get_selection_bounds() + if sel: + s, e = sel + entry_widget.delete_text(s, e) + + pos = entry_widget.get_position() + entry_widget.insert_text(text, pos) + entry_widget.set_position(pos + len(text)) + + clipboard.read_text_async(None, on_text) + return True + + if dialog and keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): + dialog.response(Gtk.ResponseType.OK) + return True + + return False + + key_controller.connect("key-pressed", on_key_pressed) + entry_widget.add_controller(key_controller) + def bind_i18n(self, widget, field: str, key: str): """ @@ -4036,6 +4119,7 @@ class ChatApp(Gtk.Application): stt_model_entry.set_placeholder_text("openai/gpt-audio-mini") stt_model_entry.set_text(str(cfg["stt_model_online"])) content.append(stt_model_entry) + self.add_entry_clipboard_shortcuts(stt_model_entry) # --- whisper.cpp binary --- whisper_bin_label = Gtk.Label(label=f"whisper.cpp {self('o_Binary_Path')}") @@ -4046,6 +4130,7 @@ class ChatApp(Gtk.Application): whisper_bin_entry.set_placeholder_text("/home/usr/whisper.cpp/build/bin/whisper-cli") whisper_bin_entry.set_text(str(cfg["whisper_cpp_bin"])) content.append(whisper_bin_entry) + self.add_entry_clipboard_shortcuts(whisper_bin_entry) # --- whisper.cpp model --- whisper_model_label = Gtk.Label(label=f"whisper.cpp {self('o_Model_Path')}") @@ -4056,6 +4141,7 @@ class ChatApp(Gtk.Application): whisper_model_entry.set_placeholder_text("/home/usr/.local/share/whisper/ggml-tiny.bin") whisper_model_entry.set_text(str(cfg["whisper_cpp_model"])) content.append(whisper_model_entry) + self.add_entry_clipboard_shortcuts(whisper_model_entry) # Link link = Gtk.LinkButton.new_with_label( @@ -4351,6 +4437,7 @@ class ChatApp(Gtk.Application): local_provider_name_entry.set_text(selected_provider_name) local_provider_name_entry.set_placeholder_text("ollama") content.append(local_provider_name_entry) + self.add_entry_clipboard_shortcuts(local_provider_name_entry) row_local_enabled = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) @@ -4389,6 +4476,7 @@ class ChatApp(Gtk.Application): local_base_url_entry.set_text(str(local_cfg.get("base_url", ""))) local_base_url_entry.set_placeholder_text("http://127.0.0.1:11434") content.append(local_base_url_entry) + self.add_entry_clipboard_shortcuts(local_base_url_entry) local_run_startup_label = Gtk.Label() self.bind_i18n(local_run_startup_label, "label", "o_Run_Startup") @@ -4399,15 +4487,18 @@ class ChatApp(Gtk.Application): local_run_startup_entry.set_text(str(local_cfg.get("run_startup", ""))) self.bind_i18n(local_run_startup_entry, "placeholder", "o_Run_Startup_Placeholder") content.append(local_run_startup_entry) + self.add_entry_clipboard_shortcuts(local_run_startup_entry) local_stop_command_label = Gtk.Label() self.bind_i18n(local_stop_command_label, "label", "o_Stop_Command") local_stop_command_label.set_xalign(0) content.append(local_stop_command_label) + local_stop_command_entry = Gtk.Entry() local_stop_command_entry.set_text(str(local_cfg.get("stop_command", ""))) self.bind_i18n(local_stop_command_entry, "placeholder", "o_Stop_Command_Placeholder") content.append(local_stop_command_entry) + self.add_entry_clipboard_shortcuts(local_stop_command_entry) local_system_error_label = Gtk.Label() self.bind_i18n(local_system_error_label, "label", "o_Custom_Error") @@ -4418,6 +4509,7 @@ class ChatApp(Gtk.Application): local_system_error_entry.set_text(str(local_cfg.get("system_error", ""))) self.bind_i18n(local_system_error_entry, "placeholder", "o_Custom_Error_Placeholder") content.append(local_system_error_entry) + self.add_entry_clipboard_shortcuts(local_system_error_entry) def add_local_param_row(title_text, key, placeholder=""): lab = Gtk.Label(label=title_text) @@ -4425,6 +4517,7 @@ class ChatApp(Gtk.Application): content.append(lab) entry = Gtk.Entry() + self.add_entry_clipboard_shortcuts(entry) entry.set_text(str(local_cfg.get(key, ""))) if placeholder: entry.set_placeholder_text(placeholder) @@ -4922,6 +5015,7 @@ class ChatApp(Gtk.Application): entry = Gtk.Entry() entry.set_text(colors[key]) entry.set_placeholder_text("#rrggbb") + self.add_entry_clipboard_shortcuts(entry) row.append(lab) row.append(entry) @@ -5019,6 +5113,7 @@ class ChatApp(Gtk.Application): token_value_entry.set_width_chars(8) token_value_entry.set_max_width_chars(10) token_value_entry.set_halign(Gtk.Align.END) + self.add_entry_clipboard_shortcuts(token_value_entry) row_token_value.append(token_value_left) row_token_value.append(token_value_entry) @@ -5049,6 +5144,7 @@ class ChatApp(Gtk.Application): bg_path_entry.set_hexpand(True) self.bind_i18n(bg_path_entry, "placeholder", "o_Background_Path_Placeholder") bg_path_entry.set_text(self._get_chat_bg_image_path()) + self.add_entry_clipboard_shortcuts(bg_path_entry) bg_pick_btn = Gtk.Button(label=f"{self('o_Select_Image')}") bg_clear_btn = Gtk.Button(label=f"{self('o_Clear')}") @@ -5145,6 +5241,7 @@ class ChatApp(Gtk.Application): entry = Gtk.Entry() + self.add_entry_clipboard_shortcuts(entry, dialog) self.bind_i18n(entry, "placeholder", "o_Response_Style_Placeholder") cfg = ensure_base_config(self.load_config()) @@ -5159,79 +5256,6 @@ class ChatApp(Gtk.Application): # Enter → Kaydet entry.connect("activate", lambda *_: dialog.response(Gtk.ResponseType.OK)) - key_controller = Gtk.EventControllerKey() - - def on_key_pressed(_controller, keyval, _keycode, state): - - ctrl = bool(state & Gdk.ModifierType.CONTROL_MASK) - - display = Gdk.Display.get_default() - clipboard = display.get_clipboard() if display else None - - # Ctrl+A → Hepsini seç - if ctrl and keyval in (Gdk.KEY_a, Gdk.KEY_A): - entry.select_region(0, -1) - return True - - # Ctrl+V → Yapıştır - if ctrl and keyval in (Gdk.KEY_v, Gdk.KEY_V): - if not clipboard: - return False - - def on_text(cb, res): - try: - text = cb.read_text_finish(res) or "" - except Exception: - text = "" - - if not text: - return - - sel = entry.get_selection_bounds() - if sel: - s, e = sel - entry.delete_text(s, e) - - pos = entry.get_position() - entry.insert_text(text, pos) - entry.set_position(pos + len(text)) - - clipboard.read_text_async(None, on_text) - return True - - # Ctrl+X → Kes - if ctrl and keyval in (Gdk.KEY_x, Gdk.KEY_X): - - if not clipboard: - return True - - text = entry.get_text() or "" - if not text: - return True - - sel = entry.get_selection_bounds() - - if sel: - s, e = sel - cut = text[s:e] - clipboard.set(cut) - entry.delete_text(s, e) - else: - clipboard.set(text) - entry.set_text("") - - return True - - # Enter → Kaydet - if keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): - dialog.response(Gtk.ResponseType.OK) - return True - - return False - - key_controller.connect("key-pressed", on_key_pressed) - entry.add_controller(key_controller) - def on_response(d, resp): if resp == Gtk.ResponseType.OK: cfg2 = self.load_config() @@ -5294,12 +5318,12 @@ class ChatApp(Gtk.Application): tavily_entry.set_text(str(cfg.get("tavily_api_key", ""))) content.append(tavily_entry) - # Ctrl+V ile yapıştırmayı garanti et - key_controller = Gtk.EventControllerKey() - dialog.add_button(self("o_Cancel"), Gtk.ResponseType.CANCEL) dialog.add_button(self("o_Save"), Gtk.ResponseType.OK) + self.add_entry_clipboard_shortcuts(openrouter_entry, dialog) + self.add_entry_clipboard_shortcuts(tavily_entry, dialog) + def on_response(dialog, response): if response == Gtk.ResponseType.OK: cfg = ensure_base_config(self.load_config()) @@ -5314,34 +5338,6 @@ class ChatApp(Gtk.Application): dialog.connect("response", on_response) dialog.present() - def on_key_pressed(controller, keyval, keycode, state): - ctrl = bool(state & Gdk.ModifierType.CONTROL_MASK) - - # hem v hem V yakala - if ctrl and keyval in (Gdk.KEY_v, Gdk.KEY_V): - display = Gdk.Display.get_default() - if not display: - return False - - clipboard = display.get_clipboard() - - def on_text(cb, res): - try: - text = cb.read_text_finish(res) or "" - except Exception: - text = "" - if text: - entry.set_text(text.strip()) - - clipboard.read_text_async(None, on_text) - return True - - return False - - key_controller.connect("key-pressed", on_key_pressed) - entry.add_controller(key_controller) - - def open_settings_menu(self, button): pop = Gtk.Popover() pop.set_parent(button) @@ -7807,7 +7803,11 @@ class ChatApp(Gtk.Application): query = str(req.get("query") or "").strip() if not query: - self._set_web_search_status(msg_index, "error", "Empty search query.") + self._set_web_search_status( + msg_index, + "error", + self("o_Empty_Search_Query") + ) self.load_chat() return @@ -8537,7 +8537,7 @@ class ChatApp(Gtk.Application): GLib.idle_add( self.handle_ai_error, - stderr_text or "AI process failed.", + stderr_text or self("o_AI_Process_Failed"), chat_path_for_ai ) return