Add files via upload
This commit is contained in:
@@ -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,14 +713,56 @@ 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:
|
||||||
|
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):
|
for raw_line in response.iter_lines(decode_unicode=False, chunk_size=1024):
|
||||||
if not raw_line:
|
if not raw_line:
|
||||||
continue
|
continue
|
||||||
@@ -610,8 +795,6 @@ def main():
|
|||||||
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)
|
||||||
@@ -624,6 +807,7 @@ def main():
|
|||||||
"total_tokens": total_t
|
"total_tokens": total_t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
reply = fix_mojibake(reply)
|
reply = fix_mojibake(reply)
|
||||||
if not reply:
|
if not reply:
|
||||||
sys.stderr.write("OpenRouter boş stream döndürdü.\n")
|
sys.stderr.write("OpenRouter boş stream döndürdü.\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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
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
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user