Add files via upload
This commit is contained in:
@@ -301,6 +301,148 @@ def fix_mojibake(s: str) -> str:
|
||||
except Exception:
|
||||
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:
|
||||
"""
|
||||
@@ -548,16 +690,17 @@ def main():
|
||||
final_messages.append({"role": role, "content": last.get("content", "")})
|
||||
|
||||
# Payload
|
||||
is_image_model = _is_image_generation_model(model_name)
|
||||
payload = {
|
||||
"model": model_name,
|
||||
"messages": final_messages,
|
||||
"max_tokens": 5120,
|
||||
"stream": True
|
||||
"stream": False if is_image_model else True
|
||||
}
|
||||
|
||||
# Request (STREAMING)
|
||||
reply_parts = []
|
||||
usage_data = None
|
||||
reply = ""
|
||||
usage_obj = None
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
@@ -570,14 +713,56 @@ def main():
|
||||
},
|
||||
json=payload,
|
||||
timeout=120,
|
||||
stream=True
|
||||
stream=not is_image_model
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
sys.stderr.write(f"OpenRouter API ERROR ({response.status_code}): {response.text}\n")
|
||||
sys.exit(1)
|
||||
|
||||
# SSE'yi satır satır oku (en stabil yöntem)
|
||||
if is_image_model:
|
||||
data = response.json()
|
||||
|
||||
usage_data = data.get("usage")
|
||||
if usage_data:
|
||||
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)
|
||||
|
||||
usage_obj = {
|
||||
"prompt_tokens": prompt_t,
|
||||
"completion_tokens": comp_t,
|
||||
"total_tokens": total_t
|
||||
}
|
||||
|
||||
msg = {}
|
||||
try:
|
||||
msg = data["choices"][0]["message"]
|
||||
except Exception:
|
||||
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
|
||||
@@ -610,8 +795,6 @@ def main():
|
||||
sys.stdout.flush()
|
||||
|
||||
reply = "".join(reply_parts).strip()
|
||||
# token usage yazdır
|
||||
usage_obj = None
|
||||
|
||||
if usage_data:
|
||||
prompt_t = int(usage_data.get("prompt_tokens", 0) or 0)
|
||||
@@ -624,6 +807,7 @@ def main():
|
||||
"total_tokens": total_t
|
||||
}
|
||||
|
||||
|
||||
reply = fix_mojibake(reply)
|
||||
if not reply:
|
||||
sys.stderr.write("OpenRouter boş stream döndürdü.\n")
|
||||
@@ -640,30 +824,7 @@ def main():
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"AI ERROR: {str(e)}\n")
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -24,6 +24,8 @@ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
BASE_DIR = Path.home() / ".cache" / "capture-ai"
|
||||
CHAT_DIR = BASE_DIR / "chats"
|
||||
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)
|
||||
|
||||
@@ -1076,6 +1078,181 @@ class ChatApp(Gtk.Application):
|
||||
|
||||
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 ----------------
|
||||
|
||||
def _measure_text_px(self, widget: Gtk.Widget, text: str) -> int:
|
||||
@@ -1534,23 +1711,42 @@ class ChatApp(Gtk.Application):
|
||||
self.typing_dots = 0
|
||||
GLib.timeout_add(500, self.animate_typing)
|
||||
|
||||
def after_ai_response(self):
|
||||
def hide_typing_indicator(self):
|
||||
self.typing_active = False
|
||||
self.stream_active = False
|
||||
self.ai_proc = None
|
||||
self.ai_stop_requested = False
|
||||
self.set_generating_state(False)
|
||||
|
||||
try:
|
||||
self.typing_row.set_visible(False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.load_chat()
|
||||
self.scroll_to_bottom()
|
||||
def after_ai_response(self):
|
||||
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_label = None
|
||||
|
||||
self.load_chat()
|
||||
self.scroll_to_bottom(force=True)
|
||||
|
||||
# ---------------- MODELS LIST ----------------
|
||||
|
||||
def apply_models_visibility(self):
|
||||
@@ -2081,7 +2277,8 @@ class ChatApp(Gtk.Application):
|
||||
pass
|
||||
|
||||
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)
|
||||
|
||||
content = dialog.get_content_area()
|
||||
@@ -2094,7 +2291,8 @@ class ChatApp(Gtk.Application):
|
||||
current_lang = self.get_ui_language()
|
||||
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)
|
||||
content.append(label)
|
||||
|
||||
@@ -2199,7 +2397,7 @@ class ChatApp(Gtk.Application):
|
||||
is_dark = bool(cfg.get("dark_mode", True))
|
||||
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_default_size(460, 340)
|
||||
|
||||
@@ -2271,18 +2469,17 @@ class ChatApp(Gtk.Application):
|
||||
|
||||
rows[key] = entry
|
||||
|
||||
add_row("Genel arkaplan", "window_bg")
|
||||
add_row("Input arkaplan", "input_bg")
|
||||
add_row("Kullanıcı balon arkaplan", "user_bg")
|
||||
add_row("Kullanıcı yazı rengi", "user_text")
|
||||
add_row("Bot balon arkaplan", "bot_bg")
|
||||
add_row("Bot yazı rengi", "bot_text")
|
||||
add_row("Sidebar yazı rengi", "sidebar_text")
|
||||
add_row(self("o_Window_BG"), "window_bg")
|
||||
add_row(self("o_Input_BG"), "input_bg")
|
||||
add_row(self("o_User_BG"), "user_bg")
|
||||
add_row(self("o_User_Text"), "user_text")
|
||||
add_row(self("o_Bot_BG"), "bot_bg")
|
||||
add_row(self("o_Bot_Text"), "bot_text")
|
||||
add_row(self("o_Sidebar_Text"), "sidebar_text")
|
||||
|
||||
hint = Gtk.Label(
|
||||
label=(
|
||||
f"Şu anda {mode_name} renklerini düzenliyorsun.\n"
|
||||
"Hex formatı kullan: örn #303643"
|
||||
f"{self('o_Color_Edit_Hint')}"
|
||||
)
|
||||
)
|
||||
hint.set_xalign(0)
|
||||
@@ -2290,7 +2487,7 @@ class ChatApp(Gtk.Application):
|
||||
hint.add_css_class("dim-label")
|
||||
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)
|
||||
content.append(bg_path_label)
|
||||
|
||||
@@ -2298,11 +2495,11 @@ class ChatApp(Gtk.Application):
|
||||
|
||||
bg_path_entry = Gtk.Entry()
|
||||
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_pick_btn = Gtk.Button(label="Seç")
|
||||
bg_clear_btn = Gtk.Button(label="Temizle")
|
||||
bg_pick_btn = Gtk.Button(label=f"{self('o_Select_Image')}")
|
||||
bg_clear_btn = Gtk.Button(label=f"{self('o_Clear')}")
|
||||
bg_pick_btn.connect("clicked", on_pick_bg)
|
||||
bg_clear_btn.connect("clicked", on_clear_bg)
|
||||
|
||||
@@ -2312,7 +2509,7 @@ class ChatApp(Gtk.Application):
|
||||
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)
|
||||
|
||||
cancel_btn = Gtk.Button()
|
||||
@@ -2368,6 +2565,9 @@ class ChatApp(Gtk.Application):
|
||||
dialog.present()
|
||||
|
||||
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.set_modal(True)
|
||||
|
||||
@@ -2375,7 +2575,7 @@ class ChatApp(Gtk.Application):
|
||||
content.set_spacing(8)
|
||||
|
||||
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()
|
||||
entry.set_text(cfg.get("response_style", "") or "")
|
||||
@@ -2492,12 +2692,12 @@ class ChatApp(Gtk.Application):
|
||||
dialog = Gtk.Dialog()
|
||||
dialog.set_transient_for(self.win)
|
||||
dialog.set_modal(True)
|
||||
dialog.set_title("OpenRouter API Key")
|
||||
self.bind_i18n(dialog, "title", "o_OpenRouter_API_Key")
|
||||
|
||||
content = dialog.get_content_area()
|
||||
|
||||
entry = Gtk.Entry()
|
||||
entry.set_placeholder_text("OpenRouter API Key girin...")
|
||||
self.bind_i18n(entry, "placeholder", "o_OpenRouter_Placeholder")
|
||||
entry.set_visibility(False)
|
||||
content.append(entry)
|
||||
# Ctrl+V ile yapıştırmayı garanti et
|
||||
@@ -2823,6 +3023,9 @@ class ChatApp(Gtk.Application):
|
||||
if not (0 <= idx < len(messages)):
|
||||
return
|
||||
|
||||
if getattr(self, "is_generating", False):
|
||||
return
|
||||
|
||||
role = (messages[idx].get("role") or "").strip()
|
||||
content = (messages[idx].get("content") or "").strip()
|
||||
if not content:
|
||||
@@ -2953,6 +3156,9 @@ class ChatApp(Gtk.Application):
|
||||
self.load_chat()
|
||||
self.show_typing_indicator()
|
||||
|
||||
self.ai_stop_requested = False
|
||||
self.set_generating_state(True)
|
||||
|
||||
# 6) AI çağır: zincir referanslarla birlikte
|
||||
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)
|
||||
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.set_xalign(0)
|
||||
@@ -3314,7 +3524,29 @@ class ChatApp(Gtk.Application):
|
||||
bubble.add_css_class("selected")
|
||||
|
||||
# ---------------- 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":
|
||||
|
||||
# daha önce sonuç yazıldıysa buton göstermeyelim, sadece feedback gösterelim
|
||||
st = msg.get("apply_status")
|
||||
if isinstance(st, dict) and "status" in st:
|
||||
@@ -3322,11 +3554,11 @@ class ChatApp(Gtk.Application):
|
||||
err = (st.get("err") or "").strip()
|
||||
|
||||
if status == "ok":
|
||||
txt = "✅ Dosya değiştirildi."
|
||||
txt = f"✅ {self('o_File_Changed')}"
|
||||
elif status == "fail":
|
||||
txt = f"❌ APPLY FAILED: {err}"
|
||||
elif status == "cancel":
|
||||
txt = "⏭️ İşlem iptal edildi."
|
||||
txt = f"⏭️ {self('o_Cancelled')}"
|
||||
else:
|
||||
txt = ""
|
||||
|
||||
@@ -3458,11 +3690,14 @@ class ChatApp(Gtk.Application):
|
||||
|
||||
# AI düşünüyorsa typing satırını en alta geri ekle
|
||||
if getattr(self, "typing_active", False):
|
||||
if kept_typing_row is not None:
|
||||
self.chat_box.append(kept_typing_row)
|
||||
else:
|
||||
# typing_row yoksa güvenli fallback
|
||||
self.show_typing_indicator()
|
||||
try:
|
||||
if self.typing_row.get_parent() is not self.content_box:
|
||||
parent = self.typing_row.get_parent()
|
||||
if parent is not None:
|
||||
parent.remove(self.typing_row)
|
||||
self.content_box.append(self.typing_row)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
GLib.idle_add(self.scroll_to_bottom)
|
||||
|
||||
@@ -3824,20 +4059,30 @@ class ChatApp(Gtk.Application):
|
||||
messages = json.load(f) or []
|
||||
|
||||
changed = False
|
||||
|
||||
if messages and isinstance(messages[-1], dict):
|
||||
last = messages[-1]
|
||||
|
||||
# Son mesaj boş streaming bot placeholder ise sil
|
||||
if last.get("role") in ("bot", "assistant") and last.get("streaming"):
|
||||
content = (last.get("content") or "").strip()
|
||||
|
||||
if not content:
|
||||
messages.pop()
|
||||
changed = True
|
||||
else:
|
||||
last.pop("streaming", None)
|
||||
|
||||
# Cancel bilgisini son user mesajına yaz
|
||||
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:
|
||||
with open(self.current_chat, "w", encoding="utf-8") as f:
|
||||
json.dump(messages, f, ensure_ascii=False, indent=2)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -4260,6 +4505,8 @@ class ChatApp(Gtk.Application):
|
||||
partial = ""
|
||||
last_len = 0
|
||||
last_push = 0
|
||||
stderr_chunks = []
|
||||
json_mode = False
|
||||
|
||||
out_fd = proc.stdout.fileno()
|
||||
err_fd = proc.stderr.fileno()
|
||||
@@ -4274,18 +4521,28 @@ class ChatApp(Gtk.Application):
|
||||
text = decoder.decode(data)
|
||||
if 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)
|
||||
now = GLib.get_monotonic_time() # mikro-saniye
|
||||
if (now - last_push) > 8_000: # 8ms
|
||||
last_push = now
|
||||
new_part = partial[last_len:]
|
||||
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)
|
||||
|
||||
|
||||
if err_fd in rlist:
|
||||
# stderr'i biriktirmek istersen burada alabilirsin (şimdilik sadece drain)
|
||||
_ = os.read(err_fd, 4096)
|
||||
err_data = os.read(err_fd, 4096)
|
||||
if err_data:
|
||||
stderr_chunks.append(err_data.decode("utf-8", errors="replace"))
|
||||
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
@@ -4297,25 +4554,24 @@ class ChatApp(Gtk.Application):
|
||||
|
||||
new_part = partial[last_len:]
|
||||
last_len = len(partial)
|
||||
if new_part:
|
||||
if new_part and not json_mode:
|
||||
GLib.idle_add(self.update_stream_text, new_part)
|
||||
|
||||
proc.wait()
|
||||
rc = proc.returncode
|
||||
self.ai_proc = None
|
||||
|
||||
stderr_text = "".join(stderr_chunks).strip()
|
||||
|
||||
if self.ai_stop_requested:
|
||||
GLib.idle_add(self.after_ai_response)
|
||||
return
|
||||
|
||||
if rc != 0:
|
||||
err = ""
|
||||
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"
|
||||
err = stderr_text or partial.strip() or "Bilinmeyen hata"
|
||||
GLib.idle_add(self.handle_ai_error, err)
|
||||
else:
|
||||
GLib.idle_add(self.finalize_ai_response, partial)
|
||||
GLib.idle_add(self.after_ai_response)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user