Add files via upload

This commit is contained in:
FURK4NGG
2026-03-13 15:19:31 +03:00
committed by GitHub
parent c39e1e0c3f
commit e0fb1aeeb4
2 changed files with 530 additions and 113 deletions
+227 -66
View File
@@ -301,6 +301,148 @@ def fix_mojibake(s: str) -> str:
except Exception: except Exception:
return s return s
def _message_to_plain_text(content) -> str:
if isinstance(content, str):
return content.strip()
if isinstance(content, list):
parts = []
for item in content:
if not isinstance(item, dict):
continue
if item.get("type") == "text":
txt = item.get("text")
if isinstance(txt, str) and txt.strip():
parts.append(txt.strip())
return "\n".join(parts).strip()
return ""
def _extract_images_from_message(msg: dict) -> dict:
"""
OpenRouter / farklı provider cevaplarından image url veya base64 çıkar.
UI'nin finalize_ai_response() fonksiyonunun anlayacağı formatı döndürür.
"""
out = {
"type": "text",
"content": "",
}
if not isinstance(msg, dict):
return out
content = msg.get("content")
text_parts = []
image_urls = []
image_b64 = []
# 1) content string ise
if isinstance(content, str):
if content.strip():
text_parts.append(content.strip())
# 2) content liste ise
elif isinstance(content, list):
for item in content:
if not isinstance(item, dict):
continue
itype = str(item.get("type") or "").strip().lower()
if itype == "text":
txt = item.get("text")
if isinstance(txt, str) and txt.strip():
text_parts.append(txt.strip())
elif itype in ("image_url", "output_image"):
iu = item.get("image_url")
if isinstance(iu, str) and iu.strip():
image_urls.append(iu.strip())
elif isinstance(iu, dict):
u = iu.get("url")
if isinstance(u, str) and u.strip():
image_urls.append(u.strip())
b64 = item.get("b64_json")
if isinstance(b64, str) and b64.strip():
image_b64.append(b64.strip())
data = item.get("image")
if isinstance(data, str) and data.strip():
image_b64.append(data.strip())
# 3) message level fallback alanlar
for key in ("image", "url"):
val = msg.get(key)
if isinstance(val, str) and val.strip():
image_urls.append(val.strip())
one_b64 = msg.get("image_base64")
if isinstance(one_b64, str) and one_b64.strip():
image_b64.append(one_b64.strip())
imgs = msg.get("images")
if isinstance(imgs, list):
for item in imgs:
if isinstance(item, str) and item.strip():
image_urls.append(item.strip())
elif isinstance(item, dict):
u = item.get("url")
if isinstance(u, str) and u.strip():
image_urls.append(u.strip())
iu = item.get("image_url")
if isinstance(iu, str) and iu.strip():
image_urls.append(iu.strip())
elif isinstance(iu, dict):
uu = iu.get("url")
if isinstance(uu, str) and uu.strip():
image_urls.append(uu.strip())
b64 = item.get("b64_json")
if isinstance(b64, str) and b64.strip():
image_b64.append(b64.strip())
# uniq
def _uniq(seq):
seen = set()
out2 = []
for x in seq:
if x not in seen:
seen.add(x)
out2.append(x)
return out2
image_urls = _uniq(image_urls)
image_b64 = _uniq(image_b64)
out["content"] = "\n".join(text_parts).strip()
if image_urls:
out["type"] = "image"
out["images"] = image_urls
if image_b64:
out["type"] = "image"
out["images_base64"] = image_b64
if image_urls and image_b64:
out["type"] = "image"
return out
def _is_image_generation_model(model_name: str) -> bool:
m = str(model_name or "").strip().lower()
return any(x in m for x in [
"image",
"gpt-image",
"imagen",
"flux",
"recraft",
"stable-diffusion"
])
def _pick_model(cfg: dict, chat_file: Path, cli_model: str | None) -> str: def _pick_model(cfg: dict, chat_file: Path, cli_model: str | None) -> str:
""" """
@@ -548,16 +690,17 @@ def main():
final_messages.append({"role": role, "content": last.get("content", "")}) final_messages.append({"role": role, "content": last.get("content", "")})
# Payload # Payload
is_image_model = _is_image_generation_model(model_name)
payload = { payload = {
"model": model_name, "model": model_name,
"messages": final_messages, "messages": final_messages,
"max_tokens": 5120, "max_tokens": 5120,
"stream": True "stream": False if is_image_model else True
} }
# Request (STREAMING) # Request (STREAMING)
reply_parts = [] reply = ""
usage_data = None usage_obj = None
try: try:
response = requests.post( response = requests.post(
@@ -570,64 +713,105 @@ def main():
}, },
json=payload, json=payload,
timeout=120, timeout=120,
stream=True stream=not is_image_model
) )
if response.status_code != 200: if response.status_code != 200:
sys.stderr.write(f"OpenRouter API ERROR ({response.status_code}): {response.text}\n") sys.stderr.write(f"OpenRouter API ERROR ({response.status_code}): {response.text}\n")
sys.exit(1) sys.exit(1)
# SSE'yi satır satır oku (en stabil yöntem) if is_image_model:
for raw_line in response.iter_lines(decode_unicode=False, chunk_size=1024): data = response.json()
if not raw_line:
continue
line = raw_line.strip() usage_data = data.get("usage")
if not line.startswith(b"data:"): if usage_data:
continue prompt_t = int(usage_data.get("prompt_tokens", 0) or 0)
comp_t = int(usage_data.get("completion_tokens", 0) or 0)
total_t = int(usage_data.get("total_tokens", 0) or 0)
data_str = line[len(b"data:"):].strip() usage_obj = {
if data_str == b"[DONE]": "prompt_tokens": prompt_t,
break "completion_tokens": comp_t,
"total_tokens": total_t
}
msg = {}
try: try:
evt = json.loads(data_str.decode("utf-8", errors="replace")) msg = data["choices"][0]["message"]
if "usage" in evt:
usage_data = evt["usage"]
except Exception: except Exception:
continue msg = {}
result_obj = _extract_images_from_message(msg)
# hiç image yoksa düz text fallback
if not result_obj.get("content") and not result_obj.get("images") and not result_obj.get("images_base64"):
txt = _message_to_plain_text(msg.get("content"))
result_obj = {
"type": "text",
"content": fix_mojibake(txt)
}
if usage_obj is not None:
result_obj["usage"] = usage_obj
reply = json.dumps(result_obj, ensure_ascii=False)
sys.stdout.write(reply)
sys.stdout.flush()
else:
reply_parts = []
usage_data = None
for raw_line in response.iter_lines(decode_unicode=False, chunk_size=1024):
if not raw_line:
continue
line = raw_line.strip()
if not line.startswith(b"data:"):
continue
data_str = line[len(b"data:"):].strip()
if data_str == b"[DONE]":
break
try:
evt = json.loads(data_str.decode("utf-8", errors="replace"))
if "usage" in evt:
usage_data = evt["usage"]
except Exception:
continue
delta = None
try:
delta = evt["choices"][0]["delta"].get("content")
except Exception:
delta = None delta = None
try:
delta = evt["choices"][0]["delta"].get("content")
except Exception:
delta = None
if delta: if delta:
delta = fix_mojibake(delta) delta = fix_mojibake(delta)
reply_parts.append(delta) reply_parts.append(delta)
sys.stdout.write(delta) sys.stdout.write(delta)
sys.stdout.flush() sys.stdout.flush()
reply = "".join(reply_parts).strip() reply = "".join(reply_parts).strip()
# token usage yazdır
usage_obj = None
if usage_data: if usage_data:
prompt_t = int(usage_data.get("prompt_tokens", 0) or 0) prompt_t = int(usage_data.get("prompt_tokens", 0) or 0)
comp_t = int(usage_data.get("completion_tokens", 0) or 0) comp_t = int(usage_data.get("completion_tokens", 0) or 0)
total_t = int(usage_data.get("total_tokens", 0) or 0) total_t = int(usage_data.get("total_tokens", 0) or 0)
usage_obj = { usage_obj = {
"prompt_tokens": prompt_t, "prompt_tokens": prompt_t,
"completion_tokens": comp_t, "completion_tokens": comp_t,
"total_tokens": total_t "total_tokens": total_t
} }
reply = fix_mojibake(reply)
if not reply: reply = fix_mojibake(reply)
sys.stderr.write("OpenRouter boş stream döndürdü.\n") if not reply:
sys.exit(1) sys.stderr.write("OpenRouter boş stream döndürdü.\n")
sys.exit(1)
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
sys.stderr.write("İstek zaman aşımına uğradı (timeout).\n") sys.stderr.write("İstek zaman aşımına uğradı (timeout).\n")
@@ -640,30 +824,7 @@ def main():
except Exception as e: except Exception as e:
sys.stderr.write(f"AI ERROR: {str(e)}\n") sys.stderr.write(f"AI ERROR: {str(e)}\n")
sys.exit(1) sys.exit(1)
# Save bot reply
# UI tarafı streaming için placeholder bot mesajı eklediyse, ona yaz.
if messages and isinstance(messages[-1], dict) and messages[-1].get("role") in ("bot", "assistant") and messages[-1].get("streaming"):
messages[-1]["role"] = "bot"
messages[-1]["content"] = reply
messages[-1].pop("streaming", None)
# usage JSON'a kaydet
if usage_obj is not None:
messages[-1]["usage"] = usage_obj
else:
bot_msg: Dict[str, Any] = {"role": "bot", "content": reply}
if usage_obj is not None:
bot_msg["usage"] = usage_obj
messages.append(bot_msg)
try:
chat_file.write_text(json.dumps(messages, ensure_ascii=False, indent=2), encoding="utf-8")
except Exception as e:
sys.stderr.write(f"Chat yazılamadı: {e}\n")
sys.exit(1)
sys.exit(0) sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+303 -47
View File
@@ -24,6 +24,8 @@ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
BASE_DIR = Path.home() / ".cache" / "capture-ai" BASE_DIR = Path.home() / ".cache" / "capture-ai"
CHAT_DIR = BASE_DIR / "chats" CHAT_DIR = BASE_DIR / "chats"
AI_SCRIPT = str(Path.home() / "capture-ai/ai.py") AI_SCRIPT = str(Path.home() / "capture-ai/ai.py")
GENERATED_DIR = BASE_DIR / "generated_images"
GENERATED_DIR.mkdir(parents=True, exist_ok=True)
CHAT_DIR.mkdir(parents=True, exist_ok=True) CHAT_DIR.mkdir(parents=True, exist_ok=True)
@@ -1076,6 +1078,181 @@ class ChatApp(Gtk.Application):
return False return False
# ------------------------- AI IMAGES --------------------------------
def save_base64_image(self, b64_data: str, ext: str = "png") -> str | None:
try:
raw = base64.b64decode(b64_data)
stamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
out = GENERATED_DIR / f"gen-{stamp}.{ext}"
out.write_bytes(raw)
return str(out.resolve())
except Exception as e:
print("save_base64_image error:", e)
return None
def download_image_to_cache(self, url: str) -> str | None:
try:
import urllib.request
stamp = datetime.now().strftime("%Y%m%d-%H%M%S-%f")
ext = "png"
low = url.lower()
if ".jpg" in low or ".jpeg" in low:
ext = "jpg"
elif ".webp" in low:
ext = "webp"
out = GENERATED_DIR / f"gen-{stamp}.{ext}"
urllib.request.urlretrieve(url, str(out))
return str(out.resolve())
except Exception as e:
print("download_image_to_cache error:", e)
return None
def finalize_ai_response(self, raw_text: str):
raw_text = (raw_text or "").strip()
try:
data = json.loads(raw_text)
except Exception:
data = {"type": "text", "content": raw_text}
try:
with open(self.current_chat, "r", encoding="utf-8") as f:
messages = json.load(f) or []
except Exception:
messages = []
# son streaming bot placeholder'ını bul
bot_idx = None
for i in range(len(messages) - 1, -1, -1):
msg = messages[i]
if msg.get("role") == "bot" and msg.get("streaming"):
bot_idx = i
break
if bot_idx is None:
messages.append({"role": "bot", "content": ""})
bot_idx = len(messages) - 1
bot_msg = messages[bot_idx]
bot_msg.pop("streaming", None)
result_type = str(data.get("type") or "text").strip().lower()
bot_msg["content"] = str(data.get("content") or "").strip()
saved_images = []
seen_saved = set()
# usage varsa kaydet
usage = data.get("usage")
if isinstance(usage, dict):
bot_msg["usage"] = usage
else:
bot_msg.pop("usage", None)
# 1) URL tabanlı görseller
image_candidates = []
inline_data_urls = []
# tekil alanlar
for key in ("image", "url"):
val = data.get(key)
if isinstance(val, str) and val.strip():
sval = val.strip()
if sval.startswith("data:image/"):
inline_data_urls.append(sval)
else:
image_candidates.append(sval)
imgs = data.get("images")
if isinstance(imgs, list):
for item in imgs:
if isinstance(item, str) and item.strip():
sval = item.strip()
if sval.startswith("data:image/"):
inline_data_urls.append(sval)
else:
image_candidates.append(sval)
elif isinstance(item, dict):
u = item.get("url")
if isinstance(u, str) and u.strip():
sval = u.strip()
if sval.startswith("data:image/"):
inline_data_urls.append(sval)
else:
image_candidates.append(sval)
iu = item.get("image_url")
if isinstance(iu, str) and iu.strip():
sval = iu.strip()
if sval.startswith("data:image/"):
inline_data_urls.append(sval)
else:
image_candidates.append(sval)
elif isinstance(iu, dict):
uu = iu.get("url")
if isinstance(uu, str) and uu.strip():
sval = uu.strip()
if sval.startswith("data:image/"):
inline_data_urls.append(sval)
else:
image_candidates.append(sval)
for url in image_candidates:
local_path = self.download_image_to_cache(url)
if local_path and local_path not in seen_saved:
seen_saved.add(local_path)
saved_images.append(local_path)
# 2) base64 tabanlı görseller
base64_candidates = []
for item in inline_data_urls:
if item.startswith("data:image/") and "," in item:
base64_candidates.append(item.split(",", 1)[1].strip())
one_b64 = data.get("image_base64")
if isinstance(one_b64, str) and one_b64.strip():
base64_candidates.append(one_b64.strip())
many_b64 = data.get("images_base64")
if isinstance(many_b64, list):
for item in many_b64:
if isinstance(item, str) and item.strip():
base64_candidates.append(item.strip())
elif isinstance(item, dict):
b = item.get("b64_json")
if isinstance(b, str) and b.strip():
base64_candidates.append(b.strip())
for item in base64_candidates:
if item.startswith("data:image/") and "," in item:
item = item.split(",", 1)[1].strip()
local_path = self.save_base64_image(item, "png")
if local_path and local_path not in seen_saved:
seen_saved.add(local_path)
saved_images.append(local_path)
if saved_images:
bot_msg["images"] = saved_images
else:
bot_msg.pop("images", None)
messages[bot_idx] = bot_msg
try:
with open(self.current_chat, "w", encoding="utf-8") as f:
json.dump(messages, f, ensure_ascii=False, indent=2)
except Exception as e:
print("finalize_ai_response write error:", e)
# ---------------- IMAGE,DOCS PREVIEW AND ATTACH MENU ---------------- # ---------------- IMAGE,DOCS PREVIEW AND ATTACH MENU ----------------
def _measure_text_px(self, widget: Gtk.Widget, text: str) -> int: def _measure_text_px(self, widget: Gtk.Widget, text: str) -> int:
@@ -1534,23 +1711,42 @@ class ChatApp(Gtk.Application):
self.typing_dots = 0 self.typing_dots = 0
GLib.timeout_add(500, self.animate_typing) GLib.timeout_add(500, self.animate_typing)
def after_ai_response(self): def hide_typing_indicator(self):
self.typing_active = False self.typing_active = False
self.stream_active = False self.stream_active = False
self.ai_proc = None
self.ai_stop_requested = False
self.set_generating_state(False)
try: try:
self.typing_row.set_visible(False) self.typing_row.set_visible(False)
except Exception: except Exception:
pass pass
self.load_chat() def after_ai_response(self):
self.scroll_to_bottom() self.hide_typing_indicator()
self.set_generating_state(False)
if getattr(self, "ai_stop_requested", False):
try:
with open(self.current_chat, "r", encoding="utf-8") as f:
messages = json.load(f) or []
messages.append({
"role": "info",
"content": self("o_Cancelled")
})
with open(self.current_chat, "w", encoding="utf-8") as f:
json.dump(messages, f, ensure_ascii=False, indent=2)
except Exception:
pass
self.ai_proc = None
self.ai_stop_requested = False
self.streaming_row = None self.streaming_row = None
self.streaming_label = None self.streaming_label = None
self.load_chat()
self.scroll_to_bottom(force=True)
# ---------------- MODELS LIST ---------------- # ---------------- MODELS LIST ----------------
def apply_models_visibility(self): def apply_models_visibility(self):
@@ -2081,7 +2277,8 @@ class ChatApp(Gtk.Application):
pass pass
def open_language_dialog(self): def open_language_dialog(self):
dialog = Gtk.Dialog(title=self("Dil"), transient_for=self.win) dialog = Gtk.Dialog(transient_for=self.win)
self.bind_i18n(dialog, "title", "o_Language")
dialog.set_modal(True) dialog.set_modal(True)
content = dialog.get_content_area() content = dialog.get_content_area()
@@ -2094,7 +2291,8 @@ class ChatApp(Gtk.Application):
current_lang = self.get_ui_language() current_lang = self.get_ui_language()
langs = self.get_available_ui_languages() langs = self.get_available_ui_languages()
label = Gtk.Label(label=self("Dil")) label = Gtk.Label()
self.bind_i18n(label, "label", "o_Language")
label.set_xalign(0) label.set_xalign(0)
content.append(label) content.append(label)
@@ -2199,7 +2397,7 @@ class ChatApp(Gtk.Application):
is_dark = bool(cfg.get("dark_mode", True)) is_dark = bool(cfg.get("dark_mode", True))
mode_name = "Dark Mode" if is_dark else "Light Mode" mode_name = "Dark Mode" if is_dark else "Light Mode"
dialog = Gtk.Dialog(title=f"Kişiselleştirme ({mode_name})", transient_for=self.win) dialog = Gtk.Dialog(title=f"{self('o_Personalization')} ({mode_name})",transient_for=self.win)
dialog.set_modal(True) dialog.set_modal(True)
dialog.set_default_size(460, 340) dialog.set_default_size(460, 340)
@@ -2271,18 +2469,17 @@ class ChatApp(Gtk.Application):
rows[key] = entry rows[key] = entry
add_row("Genel arkaplan", "window_bg") add_row(self("o_Window_BG"), "window_bg")
add_row("Input arkaplan", "input_bg") add_row(self("o_Input_BG"), "input_bg")
add_row("Kullanıcı balon arkaplan", "user_bg") add_row(self("o_User_BG"), "user_bg")
add_row("Kullanıcı yazı rengi", "user_text") add_row(self("o_User_Text"), "user_text")
add_row("Bot balon arkaplan", "bot_bg") add_row(self("o_Bot_BG"), "bot_bg")
add_row("Bot yazı rengi", "bot_text") add_row(self("o_Bot_Text"), "bot_text")
add_row("Sidebar yazı rengi", "sidebar_text") add_row(self("o_Sidebar_Text"), "sidebar_text")
hint = Gtk.Label( hint = Gtk.Label(
label=( label=(
f"Şu anda {mode_name} renklerini düzenliyorsun.\n" f"{self('o_Color_Edit_Hint')}"
"Hex formatı kullan: örn #303643"
) )
) )
hint.set_xalign(0) hint.set_xalign(0)
@@ -2290,7 +2487,7 @@ class ChatApp(Gtk.Application):
hint.add_css_class("dim-label") hint.add_css_class("dim-label")
content.append(hint) content.append(hint)
bg_path_label = Gtk.Label(label="Chat arkaplan resmi") bg_path_label = Gtk.Label(label=f"{self('o_Background_Hint')}")
bg_path_label.set_xalign(0) bg_path_label.set_xalign(0)
content.append(bg_path_label) content.append(bg_path_label)
@@ -2298,11 +2495,11 @@ class ChatApp(Gtk.Application):
bg_path_entry = Gtk.Entry() bg_path_entry = Gtk.Entry()
bg_path_entry.set_hexpand(True) bg_path_entry.set_hexpand(True)
bg_path_entry.set_placeholder_text("/dosya-yolu/resim.png") self.bind_i18n(bg_path_entry, "placeholder", "o_Background_Path_Placeholder")
bg_path_entry.set_text(self._get_chat_bg_image_path()) bg_path_entry.set_text(self._get_chat_bg_image_path())
bg_pick_btn = Gtk.Button(label="Seç") bg_pick_btn = Gtk.Button(label=f"{self('o_Select_Image')}")
bg_clear_btn = Gtk.Button(label="Temizle") bg_clear_btn = Gtk.Button(label=f"{self('o_Clear')}")
bg_pick_btn.connect("clicked", on_pick_bg) bg_pick_btn.connect("clicked", on_pick_bg)
bg_clear_btn.connect("clicked", on_clear_bg) bg_clear_btn.connect("clicked", on_clear_bg)
@@ -2312,7 +2509,7 @@ class ChatApp(Gtk.Application):
content.append(bg_path_controls) content.append(bg_path_controls)
reset_btn = Gtk.Button(label="Varsayılana dön") reset_btn = Gtk.Button(label=f"{self('o_Reset_Default')}")
content.append(reset_btn) content.append(reset_btn)
cancel_btn = Gtk.Button() cancel_btn = Gtk.Button()
@@ -2368,6 +2565,9 @@ class ChatApp(Gtk.Application):
dialog.present() dialog.present()
def open_style_dialog(self): def open_style_dialog(self):
dialog = Gtk.Dialog(transient_for=self.win)
self.bind_i18n(dialog, "title", "o_Response_Style")
dialog = Gtk.Dialog(title="Cevap Tarzı", transient_for=self.win) dialog = Gtk.Dialog(title="Cevap Tarzı", transient_for=self.win)
dialog.set_modal(True) dialog.set_modal(True)
@@ -2375,7 +2575,7 @@ class ChatApp(Gtk.Application):
content.set_spacing(8) content.set_spacing(8)
entry = Gtk.Entry() entry = Gtk.Entry()
entry.set_placeholder_text("Örn: Samimi ve esprili konuş") self.bind_i18n(entry, "placeholder", "o_Response_Style_Placeholder")
cfg = self.load_config() cfg = self.load_config()
entry.set_text(cfg.get("response_style", "") or "") entry.set_text(cfg.get("response_style", "") or "")
@@ -2492,12 +2692,12 @@ class ChatApp(Gtk.Application):
dialog = Gtk.Dialog() dialog = Gtk.Dialog()
dialog.set_transient_for(self.win) dialog.set_transient_for(self.win)
dialog.set_modal(True) dialog.set_modal(True)
dialog.set_title("OpenRouter API Key") self.bind_i18n(dialog, "title", "o_OpenRouter_API_Key")
content = dialog.get_content_area() content = dialog.get_content_area()
entry = Gtk.Entry() entry = Gtk.Entry()
entry.set_placeholder_text("OpenRouter API Key girin...") self.bind_i18n(entry, "placeholder", "o_OpenRouter_Placeholder")
entry.set_visibility(False) entry.set_visibility(False)
content.append(entry) content.append(entry)
# Ctrl+V ile yapıştırmayı garanti et # Ctrl+V ile yapıştırmayı garanti et
@@ -2823,6 +3023,9 @@ class ChatApp(Gtk.Application):
if not (0 <= idx < len(messages)): if not (0 <= idx < len(messages)):
return return
if getattr(self, "is_generating", False):
return
role = (messages[idx].get("role") or "").strip() role = (messages[idx].get("role") or "").strip()
content = (messages[idx].get("content") or "").strip() content = (messages[idx].get("content") or "").strip()
if not content: if not content:
@@ -2953,6 +3156,9 @@ class ChatApp(Gtk.Application):
self.load_chat() self.load_chat()
self.show_typing_indicator() self.show_typing_indicator()
self.ai_stop_requested = False
self.set_generating_state(True)
# 6) AI çağır: zincir referanslarla birlikte # 6) AI çağır: zincir referanslarla birlikte
threading.Thread(target=self.call_ai, args=(selected_arg,), daemon=True).start() threading.Thread(target=self.call_ai, args=(selected_arg,), daemon=True).start()
@@ -3287,7 +3493,11 @@ class ChatApp(Gtk.Application):
c = int(u.get("completion_tokens", 0) or 0) c = int(u.get("completion_tokens", 0) or 0)
t = int(u.get("total_tokens", 0) or 0) t = int(u.get("total_tokens", 0) or 0)
usage_txt = f"Girdi token: {p} | Çıktı token: {c} | Toplam: {t}" usage_txt = (
f"{self('o_Input_Tokens')}: {p} | "
f"{self('o_Output_Tokens')}: {c} | "
f"{self('o_Total_Tokens')}: {t}"
)
usage_label = Gtk.Label(label=usage_txt) usage_label = Gtk.Label(label=usage_txt)
usage_label.set_xalign(0) usage_label.set_xalign(0)
@@ -3314,7 +3524,29 @@ class ChatApp(Gtk.Application):
bubble.add_css_class("selected") bubble.add_css_class("selected")
# ---------------- APPLY BUTTONS ---------------- # ---------------- APPLY BUTTONS ----------------
gen_st = msg.get("gen_status")
if isinstance(gen_st, dict) and "status" in gen_st:
gen_status = gen_st.get("status")
gen_err = (gen_st.get("err") or "").strip()
if gen_status == "cancel":
txt = f"⏭️ {self('o_Cancelled')}"
elif gen_status == "fail":
txt = f"{self('o_Generation_Failed')}: {gen_err}"
else:
txt = ""
if txt:
status_label = Gtk.Label(label=txt)
status_label.set_wrap(True)
status_label.set_wrap_mode(Pango.WrapMode.WORD_CHAR)
status_label.set_xalign(0)
status_label.add_css_class("refs-preview-line")
bubble.append(status_label)
if msg.get("role") != "user": if msg.get("role") != "user":
# daha önce sonuç yazıldıysa buton göstermeyelim, sadece feedback gösterelim # daha önce sonuç yazıldıysa buton göstermeyelim, sadece feedback gösterelim
st = msg.get("apply_status") st = msg.get("apply_status")
if isinstance(st, dict) and "status" in st: if isinstance(st, dict) and "status" in st:
@@ -3322,11 +3554,11 @@ class ChatApp(Gtk.Application):
err = (st.get("err") or "").strip() err = (st.get("err") or "").strip()
if status == "ok": if status == "ok":
txt = "Dosya değiştirildi." txt = f"{self('o_File_Changed')}"
elif status == "fail": elif status == "fail":
txt = f"❌ APPLY FAILED: {err}" txt = f"❌ APPLY FAILED: {err}"
elif status == "cancel": elif status == "cancel":
txt = "⏭️ İşlem iptal edildi." txt = f"⏭️ {self('o_Cancelled')}"
else: else:
txt = "" txt = ""
@@ -3458,11 +3690,14 @@ class ChatApp(Gtk.Application):
# AI düşünüyorsa typing satırını en alta geri ekle # AI düşünüyorsa typing satırını en alta geri ekle
if getattr(self, "typing_active", False): if getattr(self, "typing_active", False):
if kept_typing_row is not None: try:
self.chat_box.append(kept_typing_row) if self.typing_row.get_parent() is not self.content_box:
else: parent = self.typing_row.get_parent()
# typing_row yoksa güvenli fallback if parent is not None:
self.show_typing_indicator() parent.remove(self.typing_row)
self.content_box.append(self.typing_row)
except Exception:
pass
GLib.idle_add(self.scroll_to_bottom) GLib.idle_add(self.scroll_to_bottom)
@@ -3824,20 +4059,30 @@ class ChatApp(Gtk.Application):
messages = json.load(f) or [] messages = json.load(f) or []
changed = False changed = False
if messages and isinstance(messages[-1], dict): if messages and isinstance(messages[-1], dict):
last = messages[-1] last = messages[-1]
# Son mesaj boş streaming bot placeholder ise sil
if last.get("role") in ("bot", "assistant") and last.get("streaming"): if last.get("role") in ("bot", "assistant") and last.get("streaming"):
content = (last.get("content") or "").strip() content = (last.get("content") or "").strip()
if not content: if not content:
messages.pop() messages.pop()
changed = True changed = True
else:
last.pop("streaming", None) # Cancel bilgisini son user mesajına yaz
changed = True for j in range(len(messages) - 1, -1, -1):
msg = messages[j]
if msg.get("role") == "user":
msg["gen_status"] = {"status": "cancel", "err": ""}
changed = True
break
if changed: if changed:
with open(self.current_chat, "w", encoding="utf-8") as f: with open(self.current_chat, "w", encoding="utf-8") as f:
json.dump(messages, f, ensure_ascii=False, indent=2) json.dump(messages, f, ensure_ascii=False, indent=2)
except Exception: except Exception:
pass pass
@@ -4260,6 +4505,8 @@ class ChatApp(Gtk.Application):
partial = "" partial = ""
last_len = 0 last_len = 0
last_push = 0 last_push = 0
stderr_chunks = []
json_mode = False
out_fd = proc.stdout.fileno() out_fd = proc.stdout.fileno()
err_fd = proc.stderr.fileno() err_fd = proc.stderr.fileno()
@@ -4274,18 +4521,28 @@ class ChatApp(Gtk.Application):
text = decoder.decode(data) text = decoder.decode(data)
if text: if text:
partial += text partial += text
# Gelen çıktı JSON gibi mi?
stripped = partial.lstrip()
if stripped.startswith("{") or stripped.startswith("["):
json_mode = True
# daha sık güncelle (stream hissi) # daha sık güncelle (stream hissi)
now = GLib.get_monotonic_time() # mikro-saniye now = GLib.get_monotonic_time() # mikro-saniye
if (now - last_push) > 8_000: # 8ms if (now - last_push) > 8_000: # 8ms
last_push = now last_push = now
new_part = partial[last_len:] new_part = partial[last_len:]
last_len = len(partial) last_len = len(partial)
if new_part:
# Sadece düz metinse ekrana akıt
if new_part and not json_mode:
GLib.idle_add(self.update_stream_text, new_part) GLib.idle_add(self.update_stream_text, new_part)
if err_fd in rlist: if err_fd in rlist:
# stderr'i biriktirmek istersen burada alabilirsin (şimdilik sadece drain) err_data = os.read(err_fd, 4096)
_ = os.read(err_fd, 4096) if err_data:
stderr_chunks.append(err_data.decode("utf-8", errors="replace"))
if proc.poll() is not None: if proc.poll() is not None:
break break
@@ -4297,25 +4554,24 @@ class ChatApp(Gtk.Application):
new_part = partial[last_len:] new_part = partial[last_len:]
last_len = len(partial) last_len = len(partial)
if new_part: if new_part and not json_mode:
GLib.idle_add(self.update_stream_text, new_part) GLib.idle_add(self.update_stream_text, new_part)
proc.wait()
rc = proc.returncode rc = proc.returncode
self.ai_proc = None self.ai_proc = None
stderr_text = "".join(stderr_chunks).strip()
if self.ai_stop_requested: if self.ai_stop_requested:
GLib.idle_add(self.after_ai_response) GLib.idle_add(self.after_ai_response)
return return
if rc != 0: if rc != 0:
err = "" err = stderr_text or partial.strip() or "Bilinmeyen hata"
try:
err_bytes = proc.stderr.read() if proc.stderr else b""
err = err_bytes.decode("utf-8", errors="replace").strip() if err_bytes else "Bilinmeyen hata"
except Exception:
err = "Bilinmeyen hata"
GLib.idle_add(self.handle_ai_error, err) GLib.idle_add(self.handle_ai_error, err)
else: else:
GLib.idle_add(self.finalize_ai_response, partial)
GLib.idle_add(self.after_ai_response) GLib.idle_add(self.after_ai_response)
except Exception as e: except Exception as e: