Files
chat-summaries/references/unread_summary.plugin
T
2026-09-10 12:47:54 +02:00

2125 lines
95 KiB
Plaintext

"""
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⡿⠟⠋⠉⠉⠉⠉⠛⠿⣿⣿⣿⣿⡿⠛⠉⠉⠄⠈⠉⠙⠿⣿⣿⣿⣿
⣿⣿⡿⠋⠄⣠⣶⣿⣿⣿⣷⣦⣄⠈⠛⢟⢁⣠⣤⣴⣶⣤⣄⠄⠄⠄⠈⢿⣿⣿
⣿⡿⠁⢠⣾⣿⣿⣿⣿⣿⣿⣿⡿⣿⣦⣀⠈⠛⠛⠋⣸⣿⣿⣷⡄⠄⠄⠄⢻⣿
⣿⠁⢀⣿⣿⣿⣿⣿⣿⣿⠋⠄⠄⣿⣿⣿⣿⣶⣶⣾⣿⣿⣿⣿⣧⠄⠄⠄⠄⣿
⣿⠄⢸⣿⣿⣿⣿⣿⠟⠁⠄⠄⠄⠄⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⣿
⣿⠄⠘⣿⣿⣿⣿⡏⠄⠄⠄⠄⠄⠄⠄⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⠄⣿
⣿⠄⠄⢻⣿⣿⣿⠁⠄⠄⠄⠄⠄⠄⠄⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⠄⠄⠄⢀⣿
⣿⡆⠄⠈⠿⠿⠋⠄⠄⠄⠄⠄⠄⢰⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠄⠄⠄⣸⣿
⣿⣿⡀⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠄⠄⣰⣿⣿
⣿⣿⣷⡄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠄⠄⣰⣿⣿⣿
⣿⣿⣿⣿⣄⠄⠄⠄⠄⠄⠄⠄⠄⣰⣿⣿⣿⣿⣿⣿⣿⣿⠏⠄⢀⣴⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣷⣄⠄⠄⠄⠄⠄⣰⣿⣿⣿⣿⣿⣿⣿⡿⠃⠄⣠⣾⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣷⣄⠄⠄⠄⢿⣿⣿⣿⣿⣿⡿⠋⢀⣠⣾⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣀⠄⠙⢿⣿⠟⠋⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⣨⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
by @mihailkotovski
Перед копированием/изменением кода уведомите @mihailkotovski
"""
import requests
import traceback
from typing import Any, Dict, Optional, List
from datetime import datetime
import weakref
from base_plugin import BasePlugin, XposedHook
from android_utils import OnClickListener, run_on_ui_thread
from client_utils import (
get_messages_controller, run_on_queue, send_request,
RequestCallback, get_last_fragment, get_user_config
)
from markdown_utils import parse_markdown
from ui.settings import Header, Input, Selector, Switch, Text, Divider
from ui.bulletin import BulletinHelper
from ui.alert import AlertDialogBuilder
from hook_utils import find_class, get_private_field
from java.util import Locale
from org.telegram.tgnet import TLRPC
from org.telegram.messenger import R, MessageObject
from org.telegram.ui import LaunchActivity
from org.telegram.ui.Components import EditTextBoldCursor
from org.telegram.ui.ActionBar import Theme
from android.text import InputType
__id__ = "unread_summary"
__name__ = "Unread Summary"
__description__ = "Summarize unread messages with AI providers by clicking the unread badge"
__author__ = "@mihailkotovski & @mishabotov"
__version__ = "1.0.0"
__icon__ = "pk_4377080_by_Ctikerubot/24"
__min_version__ = "12.1.1"
GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/"
MODEL_DISPLAY_NAMES = [
"Gemini 2.5 Pro",
"Gemini 2.5 Flash",
"Gemini 2.5 Flash Lite"
]
MODEL_API_NAMES = [
"gemini-2.5-pro",
"gemini-flash-latest",
"gemini-flash-lite-latest"
]
PROVIDER_TYPES = [
"Gemini",
"OpenAI-Compatible"
]
DEFAULT_UNREAD_SUMMARY_PROMPT_RU = """Проанализируй следующие непрочитанные сообщения и создай краткую сводку.
Сосредоточься на:
1. Основных темах обсуждения
2. Важной информации или решениях
3. Вопросах или задачах, адресованных пользователю
Сводка должна быть краткой (максимум 150 слов) и написана простым текстом без какого либо форматирования.
Сообщения для анализа:
{messages}
"""
DEFAULT_UNREAD_SUMMARY_PROMPT_EN = """Analyze the following unread messages and create a brief summary.
Focus on:
1. Main discussion topics
2. Important information or decisions
3. Questions or tasks addressed to the user
The summary should be concise (maximum 150 words) and written in plain text without any formatting.
Messages to analyze:
{messages}
"""
class LocalizationManager:
strings = {
"ru": {
"SETTINGS_HEADER": "Настройки суммаризации",
"API_KEY_INPUT": "API",
"API_KEY_SUBTEXT": "Получите ключ в Google AI Studio",
"GET_API_KEY_BUTTON": "Получить API ключ",
"MODEL_SELECTOR": "Модель Gemini",
"ENABLE_SWITCH": "Включить плагин",
"SUMMARY_PROMPT_INPUT": "Промпт для суммаризации",
"TEMPERATURE_INPUT": "Температура",
"TEMPERATURE_SUBTEXT": "0.0-2.0. Контролирует креативность",
"MAX_TOKENS_INPUT": "Максимум токенов",
"MAX_TOKENS_SUBTEXT": "Максимальная длина ответа",
"API_KEY_MISSING": "API ключ Gemini не найден. Укажите его в настройках.",
"ANALYZING_MESSAGE": "Анализирую непрочитанные сообщения...",
"API_ERROR": "Ошибка API: {error}",
"NO_UNREAD_MESSAGES": "Нет непрочитанных сообщений.",
"UNEXPECTED_ERROR": "Произошла ошибка: {error}",
"MENU_SUMMARIZE": "Суммаризовать непрочитанные",
"MENU_CANCEL": "Отмена",
"DIALOG_TITLE": "Сводка непрочитанных сообщений",
"CLOSE_BUTTON": "Закрыть",
"PLUGIN_DISABLED": "Плагин отключен в настройках",
"CANNOT_GET_DIALOG": "Не удалось определить текущий чат",
"FETCHING_MESSAGES": "Загружаю сообщения...",
"MENU_PROVIDER": "Провайдер",
"MENU_LANGUAGE": "Язык",
"MENU_AI_SETTINGS": "Настройки AI",
"MENU_SHARE": "Поделиться",
"SELECT_PROVIDER_TITLE": "Выбор провайдера",
"SELECT_LANGUAGE_TITLE": "Выбор языка",
"SELECTOR_ERROR": "Не удалось открыть селектор",
"AI_SETTINGS_HEADER": "Настройки AI",
"PROMPT_HEADER": "Промпт",
"PROVIDER_SELECTOR": "Провайдер",
"OPENAI_SECTION": "OpenAI-совместимый",
"OPENAI_API_KEY_INPUT": "API Key",
"OPENAI_BASE_URL_INPUT": "URL",
"OPENAI_MODEL_INPUT": "Модель",
"OPENAI_API_KEY_MISSING": "API ключ OpenAI не найден. Укажите его в настройках.",
"OPENAI_HEADER_NAME_INPUT": "Заголовок API-ключа",
"OPENAI_HEADER_PREFIX_INPUT": "Префикс заголовка",
"MESSAGE_LIMIT_INPUT": "Лимит сообщений",
"MESSAGE_LIMIT_SUBTEXT": "0 — без лимита. Сколько непрочитанных анализировать",
},
"en": {
"SETTINGS_HEADER": "Summary Settings",
"API_KEY_INPUT": "API Key",
"API_KEY_SUBTEXT": "Get your key from Google AI Studio",
"GET_API_KEY_BUTTON": "Get API Key",
"MODEL_SELECTOR": "Gemini Model",
"ENABLE_SWITCH": "Enable Plugin",
"SUMMARY_PROMPT_INPUT": "Summary Prompt",
"TEMPERATURE_INPUT": "Temperature",
"TEMPERATURE_SUBTEXT": "0.0-2.0. Controls response creativity",
"MAX_TOKENS_INPUT": "Max Tokens",
"MAX_TOKENS_SUBTEXT": "Maximum response length",
"API_KEY_MISSING": "Gemini API key not found. Set it in settings.",
"ANALYZING_MESSAGE": "Analyzing unread messages...",
"API_ERROR": "AI API Error: {error}",
"NO_UNREAD_MESSAGES": "No unread messages found.",
"UNEXPECTED_ERROR": "An error occurred: {error}",
"MENU_SUMMARIZE": "Summarize Unread Messages",
"MENU_CANCEL": "Cancel",
"DIALOG_TITLE": "Unread Messages Summary",
"CLOSE_BUTTON": "Close",
"PLUGIN_DISABLED": "Plugin is disabled in settings",
"CANNOT_GET_DIALOG": "Cannot determine current chat",
"FETCHING_MESSAGES": "Fetching messages...",
"MENU_PROVIDER": "Provider",
"MENU_LANGUAGE": "Language",
"MENU_AI_SETTINGS": "AI Settings",
"MENU_SHARE": "Share",
"SELECT_PROVIDER_TITLE": "Select Provider",
"SELECT_LANGUAGE_TITLE": "Select Language",
"SELECTOR_ERROR": "Failed to open selector",
"AI_SETTINGS_HEADER": "AI Settings",
"PROMPT_HEADER": "Prompt",
"PROVIDER_SELECTOR": "Provider",
"OPENAI_SECTION": "OpenAI-compatible",
"OPENAI_API_KEY_INPUT": "API Key",
"OPENAI_BASE_URL_INPUT": "URL",
"OPENAI_MODEL_INPUT": "Model",
"OPENAI_API_KEY_MISSING": "OpenAI API key not found. Set it in settings.",
"OPENAI_HEADER_NAME_INPUT": "Header Name",
"OPENAI_HEADER_PREFIX_INPUT": "Header Prefix",
"MESSAGE_LIMIT_INPUT": "Message limit",
"MESSAGE_LIMIT_SUBTEXT": "0 = unlimited. How many unread messages to analyze",
}
}
def __init__(self):
self.language = Locale.getDefault().getLanguage()
self.language = self.language if self.language in self.strings else "en"
def get_string(self, key: str, **kwargs) -> str:
string = self.strings[self.language].get(key, self.strings["en"].get(key, key))
if kwargs:
try:
return string.format(**kwargs)
except (KeyError, ValueError):
return string
return string
locali = LocalizationManager()
class GeminiAPIHandler:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
"Content-Type": "application/json",
"User-Agent": f"ExteraPlugin/{__id__}/{__version__}"
})
def analyze_messages(self, api_key: str, model_name: str, prompt: str, temperature: float, max_tokens: int) -> Dict[str, Any]:
url = f"{GEMINI_BASE_URL}{model_name}:generateContent?key={api_key}"
payload = {
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {
"temperature": temperature,
"maxOutputTokens": max_tokens,
}
}
prompt_size = len(prompt.encode('utf-8'))
try:
response = self.session.post(url, json=payload, timeout=90)
response.raise_for_status()
data = response.json()
if "candidates" not in data:
error_msg = data.get("error", {}).get("message", "No candidates in API response")
return {"success": False, "error": f"API Error: {error_msg}"}
candidates = data["candidates"]
if not candidates or len(candidates) == 0:
return {"success": False, "error": "Empty candidates array in API response"}
first_candidate = candidates[0]
finish_reason = first_candidate.get("finishReason", "")
if finish_reason:
if finish_reason == "SAFETY":
return {"success": False, "error": "Content blocked by safety filters"}
elif finish_reason == "MAX_TOKENS":
return {"success": False, "error": "Response truncated due to token limit"}
elif finish_reason not in ["STOP", ""]:
return {"success": False, "error": f"Generation stopped: {finish_reason}"}
content = first_candidate.get("content", {})
if not content:
return {"success": False, "error": "No content in API response"}
parts = content.get("parts", [])
if not parts or len(parts) == 0:
return {"success": False, "error": "No parts in content"}
text = parts[0].get("text", "")
if not text or not text.strip():
return {"success": False, "error": "Empty text in API response"}
return {"success": True, "text": text}
except requests.exceptions.HTTPError as e:
error_text = f"HTTP {e.response.status_code}"
try:
error_json = e.response.json()
error_text += f": {error_json.get('error',{}).get('message', e.response.text)}"
except:
error_text += f": {e.response.text}"
return {"success": False, "error": error_text}
except requests.exceptions.RequestException as e:
return {"success": False, "error": f"Network error: {str(e)}"}
except Exception as e:
return {"success": False, "error": f"Unexpected error: {str(e)}"}
class OpenAICompatibleAPIHandler:
def __init__(self):
self.session = requests.Session()
self.session.headers.update({
"Content-Type": "application/json",
"User-Agent": f"ExteraPlugin/{__id__}/{__version__}"
})
def analyze_messages(self, base_url: str, api_key: str, model_name: str, prompt: str,
temperature: float, max_tokens: int,
api_key_header: str = "Authorization", api_key_prefix: str = "Bearer") -> Dict[str, Any]:
try:
bu = (base_url or "https://api.openai.com").rstrip('/')
if bu.endswith("/v1") or bu.endswith("/v1/"):
url = bu.rstrip('/') + "/chat/completions"
else:
url = bu + "/v1/chat/completions"
headers = {}
if api_key_header:
if api_key_prefix:
headers[api_key_header] = f"{api_key_prefix} {api_key}"
else:
headers[api_key_header] = api_key
payload = {
"model": model_name,
"messages": [
{"role": "user", "content": prompt}
],
"temperature": float(temperature),
"max_tokens": int(max_tokens),
"stream": False
}
prompt_size = len(prompt.encode('utf-8'))
response = self.session.post(url, json=payload, headers=headers, timeout=90)
response.raise_for_status()
data = response.json()
if "choices" not in data or not data["choices"]:
err = data.get("error", {}).get("message", "No choices in API response")
return {"success": False, "error": err}
first_choice = data["choices"][0]
finish_reason = first_choice.get("finish_reason") or first_choice.get("finishReason", "")
if finish_reason and finish_reason not in ["stop", ""]:
if finish_reason == "length":
return {"success": False, "error": "Response truncated due to token limit"}
if finish_reason == "content_filter":
return {"success": False, "error": "Blocked by content filter"}
message = first_choice.get("message") or {}
text = (message.get("content") if isinstance(message, dict) else None) or first_choice.get("text", "")
if not text or not str(text).strip():
return {"success": False, "error": "Empty content in API response"}
return {"success": True, "text": text}
except requests.exceptions.HTTPError as e:
error_text = f"HTTP {e.response.status_code}"
try:
error_json = e.response.json()
error_text += f": {error_json.get('error',{}).get('message', e.response.text)}"
except Exception:
error_text += f": {e.response.text}"
return {"success": False, "error": error_text}
except requests.exceptions.RequestException as e:
return {"success": False, "error": f"Network error: {str(e)}"}
except Exception as e:
return {"success": False, "error": f"Unexpected error: {str(e)}"}
class ChatUnreadCellConstructorHook(XposedHook):
def __init__(self, plugin: 'UnreadSummaryPlugin'):
super().__init__()
self.plugin = plugin
def after_hooked_method(self, param):
try:
unread_cell = param.thisObject
try:
from java.lang.ref import WeakReference as JWeakReference
_cell_wr = JWeakReference(unread_cell)
unread_cell_ref = (lambda ref=_cell_wr: ref.get())
except Exception:
try:
import weakref as _pyweakref
_cell_wr_py = _pyweakref.ref(unread_cell)
unread_cell_ref = (lambda ref=_cell_wr_py: ref())
except Exception:
unread_cell_ref = (lambda obj=unread_cell: obj)
def setup_click_listener():
try:
background_layout = None
cell_obj = unread_cell_ref()
if cell_obj is None:
return
background_layout = get_private_field(cell_obj, "backgroundLayout")
if not background_layout:
if cell_obj.getChildCount() > 0:
background_layout = cell_obj.getChildAt(0)
if not background_layout:
try:
field = cell_obj.getClass().getDeclaredField("backgroundLayout")
field.setAccessible(True)
background_layout = field.get(cell_obj)
except Exception:
pass
if background_layout:
from android.view import View
background_layout.setClickable(True)
background_layout.setFocusable(True)
def on_unread_cell_click(*_args, **_kwargs):
try:
if not self.plugin.is_analyzing:
try:
view_location = [0, 0]
background_layout.getLocationOnScreen(view_location)
center_x = view_location[0] + background_layout.getWidth() / 2.0
center_y = view_location[1] + background_layout.getHeight() / 2.0
LaunchActivity.makeRipple(center_x, center_y, 1.5)
try:
view_obj = _args[0] if (_args and _args[0] is not None) else background_layout
self.plugin._perform_click_vibration(view_obj)
except Exception:
pass
except Exception:
pass
cell_for_action = unread_cell_ref()
if cell_for_action is None:
return
text_view = get_private_field(cell_for_action, "textView")
if text_view:
current_text = str(text_view.getText())
if ("•" in current_text and ("сообщений" in current_text or "message" in current_text)) and self.plugin.last_summary:
self.plugin._show_summary_bottom_sheet(self.plugin.last_summary, self.plugin.last_message_count)
return
self.plugin._start_summarization(cell_for_action)
except Exception as e:
pass
background_layout.setOnClickListener(OnClickListener(on_unread_cell_click))
else:
pass
except Exception:
pass
from android_utils import R
unread_cell.post(R(setup_click_listener))
except Exception:
pass
class UnreadSummaryPlugin(BasePlugin):
def __init__(self):
super().__init__()
self.api_handler = GeminiAPIHandler()
self.openai_handler = OpenAICompatibleAPIHandler()
self.progress_dialog: Optional[AlertDialogBuilder] = None
self.hook_ref = None
self.last_summary = None
self.last_message_count = 0
self.locale_unhook = None
self.is_analyzing = False
def on_plugin_load(self):
self._apply_hook()
self._apply_locale_hook()
def on_plugin_unload(self):
self._remove_hook()
if self.progress_dialog:
run_on_ui_thread(lambda: self.progress_dialog.dismiss())
if self.locale_unhook:
try:
self.unhook_method(self.locale_unhook)
except Exception:
pass
finally:
self.locale_unhook = None
self.is_analyzing = False
def _apply_hook(self):
try:
self._remove_hook()
cls = find_class("org.telegram.ui.Cells.ChatUnreadCell")
if cls:
constructors = cls.getClass().getDeclaredConstructors()
if constructors and len(constructors) > 0:
self.hook_ref = self.hook_method(constructors[0], ChatUnreadCellConstructorHook(self))
else:
pass
else:
pass
except Exception:
pass
def _apply_locale_hook(self):
try:
LocaleController = find_class("org.telegram.messenger.LocaleController")
if not LocaleController:
return
proceed_id = None
try:
proceed_id = R.string.Proceed
except Exception:
proceed_id = None
from java.lang import Integer
try:
getStringMethod = LocaleController.getClass().getDeclaredMethod("getString", Integer.TYPE)
except Exception as e:
return
class _LocaleHook(XposedHook):
def __init__(self, proceed_res_id):
super().__init__()
self.proceed_res_id = proceed_res_id
def after_hooked_method(self, param):
try:
if not param or not hasattr(param, 'args') or not param.args:
return
res_id = param.args[0]
if self.proceed_res_id is not None and res_id == self.proceed_res_id:
param.setResult("Summarize")
except Exception:
pass
self.locale_unhook = self.hook_method(getStringMethod, _LocaleHook(proceed_id))
if self.locale_unhook:
pass
except Exception:
pass
def _remove_hook(self):
if self.hook_ref:
try:
self.unhook_method(self.hook_ref)
self.hook_ref = None
except Exception:
pass
def _show_summary_menu(self, unread_cell):
try:
self._start_summarization(unread_cell)
except Exception as e:
BulletinHelper.show_error(locali.get_string("UNEXPECTED_ERROR").format(error=str(e)))
def _start_summarization(self, unread_cell):
try:
if self.is_analyzing:
return
api_key = self.get_setting("gemini_api_key", "")
if not api_key:
BulletinHelper.show_error(locali.get_string("API_KEY_MISSING"))
return
dialog_id = self._get_current_dialog_id()
if not dialog_id:
BulletinHelper.show_error(locali.get_string("CANNOT_GET_DIALOG"))
return
self.is_analyzing = True
text_view = get_private_field(unread_cell, "textView")
if text_view:
run_on_ui_thread(lambda: text_view.setText("Анализирую..."))
run_on_queue(lambda: self._fetch_and_summarize(dialog_id, unread_cell))
except Exception as e:
self.is_analyzing = False
BulletinHelper.show_error(locali.get_string("UNEXPECTED_ERROR").format(error=str(e)))
def _get_current_dialog_id(self) -> Optional[int]:
try:
fragment = get_last_fragment()
if fragment and hasattr(fragment, 'getDialogId'):
return fragment.getDialogId()
elif fragment and hasattr(fragment, 'dialog_id'):
return getattr(fragment, 'dialog_id')
return None
except Exception:
return None
def _get_current_topic_id(self) -> int:
try:
fragment = get_last_fragment()
if fragment and hasattr(fragment, 'getTopicId'):
topic_id = fragment.getTopicId()
return int(topic_id) if topic_id else 0
return 0
except Exception:
return 0
def _resolve_read_bounds(self, dialog_id: int, topic_id: int) -> Dict[str, int]:
in_max = 0
out_max = 0
unread_count = 0
try:
mc = get_messages_controller()
dialog = None
try:
dialog = mc.getDialog(dialog_id)
except Exception:
pass
if dialog:
try:
in_max = getattr(dialog, 'read_inbox_max_id', 0) or 0
out_max = getattr(dialog, 'read_outbox_max_id', 0) or 0
unread_count = getattr(dialog, 'unread_count', 0) or 0
except Exception:
pass
if topic_id and dialog_id < 0:
try:
chat_id = -int(dialog_id)
topics_controller = mc.getTopicsController()
if topics_controller:
topic = topics_controller.findTopic(chat_id, topic_id)
if topic:
in_max = getattr(topic, 'read_inbox_max_id', in_max) or in_max
out_max = getattr(topic, 'read_outbox_max_id', out_max) or out_max
unread_count = getattr(topic, 'unread_count', unread_count) or unread_count
except Exception:
pass
except Exception:
pass
return {"in_max": int(in_max or 0), "out_max": int(out_max or 0), "unread_count": int(unread_count or 0)}
def _fetch_and_summarize_for_dialog(self, dialog_id: int):
try:
if self.is_analyzing:
BulletinHelper.show_info("Анализ уже выполняется...")
return
self.is_analyzing = True
topic_id = self._get_current_topic_id()
read_bounds = self._resolve_read_bounds(dialog_id, topic_id)
def handle_messages(messages, error):
try:
if error:
BulletinHelper.show_error(f"Ошибка: {error}")
return
if not messages or len(messages) == 0:
BulletinHelper.show_error(locali.get_string("NO_UNREAD_MESSAGES"))
return
formatted_messages = self._format_messages_for_analysis(messages)
provider_type = self.get_setting("provider_type", 0)
try:
provider_type = int(provider_type)
except (ValueError, TypeError):
provider_type = 0
temperature = self._validate_temperature(self.get_setting("temperature", "0.7"))
max_tokens = self._validate_max_tokens(self.get_setting("max_tokens", "256000"))
default_prompt = self._get_default_prompt()
prompt_template = self.get_setting("summary_prompt", default_prompt)
final_prompt = prompt_template.format(messages=formatted_messages)
if provider_type == 1:
base_url = self.get_setting("openai_base_url", "https://api.openai.com").strip() or "https://api.openai.com"
api_key = self.get_setting("openai_api_key", "").strip()
if not api_key:
BulletinHelper.show_error(locali.get_string("OPENAI_API_KEY_MISSING"))
return
model_name = self.get_setting("openai_model", "gpt-5-chat").strip() or "gpt-5-chat"
header_name = self.get_setting("openai_api_key_header", "Authorization").strip() or "Authorization"
header_prefix = self.get_setting("openai_api_key_prefix", "Bearer").strip()
result = self.openai_handler.analyze_messages(base_url, api_key, model_name, final_prompt, temperature, max_tokens, header_name, header_prefix)
else:
api_key = self.get_setting("gemini_api_key", "").strip()
model_idx = self._validate_model_index(self.get_setting("model_selection", 1))
model_name = MODEL_API_NAMES[model_idx]
result = self.api_handler.analyze_messages(api_key, model_name, final_prompt, temperature, max_tokens)
if result.get("success"):
self.last_summary = result["text"]
self.last_message_count = len(messages)
run_on_ui_thread(lambda: self._show_summary_bottom_sheet(result["text"], len(messages)))
else:
error_msg = result.get("error", "Unknown")
BulletinHelper.show_error(locali.get_string("API_ERROR").format(error=error_msg))
except Exception as handle_error:
BulletinHelper.show_error(locali.get_string("UNEXPECTED_ERROR").format(error=str(handle_error)))
finally:
self.is_analyzing = False
self._fetch_unread_messages(dialog_id, handle_messages, read_bounds, topic_id)
except Exception as e:
self.is_analyzing = False
BulletinHelper.show_error(locali.get_string("UNEXPECTED_ERROR").format(error=str(e)))
def _fetch_and_summarize(self, dialog_id: int, unread_cell):
try:
topic_id = self._get_current_topic_id()
read_bounds = self._resolve_read_bounds(dialog_id, topic_id)
def handle_messages(messages, error):
try:
if error:
self._show_error_in_cell(unread_cell, error)
return
if not messages or len(messages) == 0:
self._show_error_in_cell(unread_cell, locali.get_string("NO_UNREAD_MESSAGES"))
return
formatted_messages = self._format_messages_for_analysis(messages)
provider_type = self.get_setting("provider_type", 0)
try:
provider_type = int(provider_type)
except (ValueError, TypeError):
provider_type = 0
temperature = self._validate_temperature(self.get_setting("temperature", "0.7"))
max_tokens = self._validate_max_tokens(self.get_setting("max_tokens", "256000"))
default_prompt = self._get_default_prompt()
prompt_template = self.get_setting("summary_prompt", default_prompt)
final_prompt = prompt_template.format(messages=formatted_messages)
if provider_type == 1:
base_url = self.get_setting("openai_base_url", "https://api.openai.com").strip() or "https://api.openai.com"
api_key = self.get_setting("openai_api_key", "").strip()
if not api_key:
self._show_error_in_cell(unread_cell, locali.get_string("OPENAI_API_KEY_MISSING"))
return
model_name = self.get_setting("openai_model", "gpt-5-chat").strip() or "gpt-5-chat"
header_name = self.get_setting("openai_api_key_header", "Authorization").strip() or "Authorization"
header_prefix = self.get_setting("openai_api_key_prefix", "Bearer").strip()
result = self.openai_handler.analyze_messages(base_url, api_key, model_name, final_prompt, temperature, max_tokens, header_name, header_prefix)
else:
api_key = self.get_setting("gemini_api_key", "").strip()
model_idx = self._validate_model_index(self.get_setting("model_selection", 1))
model_name = MODEL_API_NAMES[model_idx]
result = self.api_handler.analyze_messages(api_key, model_name, final_prompt, temperature, max_tokens)
if result.get("success"):
self._show_summary_in_cell(unread_cell, result["text"], len(messages))
else:
error_msg = result.get("error", "Unknown")
self._show_error_in_cell(unread_cell, locali.get_string("API_ERROR").format(error=error_msg))
except Exception as handle_error:
self._show_error_in_cell(unread_cell, locali.get_string("UNEXPECTED_ERROR").format(error=str(handle_error)))
finally:
self.is_analyzing = False
self._fetch_unread_messages(dialog_id, handle_messages, read_bounds, topic_id)
except Exception as e:
self.is_analyzing = False
self._show_error_in_cell(unread_cell, locali.get_string("UNEXPECTED_ERROR").format(error=str(e)))
def _fetch_unread_messages(self, dialog_id: int, callback, read_bounds: Dict[str, int], topic_id: int):
try:
message_limit = self._validate_message_limit(self.get_setting("message_limit", "0"))
self._fetch_messages_paginated(dialog_id, 0, [], {}, {}, callback, read_bounds, topic_id, message_limit)
except Exception as e:
callback(None, f"Error: {str(e)}")
def _fetch_messages_paginated(self, dialog_id: int, offset_id: int,
accumulated_messages: List, users: Dict, chats: Dict, callback,
read_bounds: Dict[str, int], topic_id: int, message_limit: int):
try:
current_limit = 100
req = TLRPC.TL_messages_getHistory()
req.peer = get_messages_controller().getInputPeer(dialog_id)
req.offset_id = offset_id
req.limit = current_limit
req.add_offset = 0
req.max_id = 0
req.min_id = 0
req.hash = 0
def handle_response(response, error):
try:
if error:
error_msg = error.text if hasattr(error, 'text') else str(error)
if accumulated_messages:
callback(accumulated_messages, None)
else:
callback(None, f"Error fetching messages: {error_msg}")
return
if not response or not hasattr(response, 'messages'):
if accumulated_messages:
callback(accumulated_messages, None)
else:
callback(None, "Empty response from server")
return
messages_count = response.messages.size()
if messages_count == 0:
callback(accumulated_messages, None)
return
if hasattr(response, 'users') and response.users and response.users.size() > 0:
for i in range(response.users.size()):
try:
user = response.users.get(i)
if hasattr(user, 'id'):
users[user.id] = user
except Exception as user_error:
continue
if hasattr(response, 'chats') and response.chats and response.chats.size() > 0:
for i in range(response.chats.size()):
try:
chat = response.chats.get(i)
if hasattr(chat, 'id'):
chats[chat.id] = chat
except Exception as chat_error:
continue
batch_messages = []
last_message_id = offset_id
found_read_message = False
in_max = int(read_bounds.get("in_max", 0) or 0)
out_max = int(read_bounds.get("out_max", 0) or 0)
unread_expected = int(read_bounds.get("unread_count", 0) or 0)
current_account = 0
try:
current_account = int(get_user_config().getCurrentAccount())
except Exception:
current_account = 0
for i in range(messages_count):
msg = response.messages.get(i)
try:
if hasattr(msg, 'action') and msg.action:
continue
if topic_id:
try:
msg_topic_id = MessageObject.getTopicId(current_account, msg, True)
if int(msg_topic_id) != int(topic_id):
continue
except Exception as e:
continue
try:
is_out = bool(getattr(msg, 'out', False))
msg_id = int(getattr(msg, 'id', 0) or 0)
unread_by_id = (msg_id > in_max) and (not is_out)
unread_by_flag = bool(getattr(msg, 'unread', False))
is_unread = unread_by_flag or unread_by_id
except Exception:
is_unread = False
if hasattr(msg, 'id') and msg.id <= in_max:
found_read_message = True
break
message_text = self._format_message_text_or_media(msg)
if not message_text:
continue
if not is_unread:
continue
sender_name = self._get_sender_name(msg, users, chats)
msg_time = self._format_message_time(msg)
batch_messages.append({
'sender': sender_name,
'text': message_text,
'time': msg_time,
'id': msg.id if hasattr(msg, 'id') else 0
})
if hasattr(msg, 'id'):
last_message_id = msg.id
except Exception as msg_error:
continue
accumulated_messages.extend(batch_messages)
reached_limit = (message_limit > 0 and len(accumulated_messages) >= message_limit)
should_finish = (unread_expected > 0 and len(accumulated_messages) >= unread_expected) or found_read_message or len(batch_messages) == 0 or messages_count < current_limit or reached_limit
if should_finish:
result_messages = accumulated_messages
if unread_expected > 0 and len(result_messages) > unread_expected:
try:
try:
result_messages = sorted(result_messages, key=lambda m: m.get('id', 0))
except Exception:
pass
result_messages = result_messages[:unread_expected]
except Exception:
pass
if message_limit > 0 and len(result_messages) > message_limit:
try:
try:
result_messages = sorted(result_messages, key=lambda m: m.get('id', 0))
except Exception:
pass
result_messages = result_messages[:message_limit]
except Exception:
pass
callback(result_messages, None)
else:
self._fetch_messages_paginated(dialog_id, last_message_id,
accumulated_messages, users, chats, callback,
read_bounds, topic_id, message_limit)
except Exception as response_error:
if accumulated_messages:
callback(accumulated_messages, None)
else:
callback(None, f"Error processing response: {str(response_error)}")
request_callback = RequestCallback(handle_response)
send_request(req, request_callback)
except Exception as e:
if accumulated_messages:
callback(accumulated_messages, None)
else:
callback(None, f"Error: {str(e)}")
def _get_sender_name(self, msg, users: Dict, chats: Dict) -> str:
try:
if not hasattr(msg, 'from_id') or not msg.from_id:
return "Unknown"
if hasattr(msg.from_id, 'user_id') and msg.from_id.user_id in users:
user = users[msg.from_id.user_id]
return self._get_user_display_name(user)
elif hasattr(msg.from_id, 'chat_id') and msg.from_id.chat_id in chats:
chat = chats[msg.from_id.chat_id]
return chat.title if hasattr(chat, 'title') else f"Chat {chat.id}"
elif hasattr(msg.from_id, 'channel_id') and msg.from_id.channel_id in chats:
chat = chats[msg.from_id.channel_id]
return chat.title if hasattr(chat, 'title') else f"Channel {chat.id}"
else:
return "Unknown"
except Exception as e:
return "Unknown"
def _format_message_time(self, msg) -> str:
try:
if hasattr(msg, 'date') and msg.date:
return datetime.fromtimestamp(msg.date).strftime("%H:%M")
return ""
except Exception as e:
return ""
def _get_user_display_name(self, user) -> str:
try:
if not user:
return "Unknown"
name_parts = []
if hasattr(user, 'first_name') and user.first_name:
name_parts.append(user.first_name)
if hasattr(user, 'last_name') and user.last_name:
name_parts.append(user.last_name)
if name_parts:
return " ".join(name_parts)
elif hasattr(user, 'username') and user.username:
return f"@{user.username}"
else:
return f"User {user.id}"
except Exception as e:
return "Unknown"
def _format_messages_for_analysis(self, messages: List[Dict]) -> str:
if not messages:
return ""
formatted_messages = []
for msg in messages:
formatted_msg = f"[{msg['time']}] {msg['sender']}: {msg['text']}"
formatted_messages.append(formatted_msg)
return "\n".join(formatted_messages)
def _get_media_label(self, msg) -> str:
try:
if not hasattr(msg, 'media') or msg.media is None:
return ""
media = msg.media
cls = None
try:
cls = media.getClass().getSimpleName()
except Exception:
cls = str(media.__class__.__name__)
if hasattr(media, 'photo') and media.photo is not None:
return "[Photo]"
if hasattr(media, 'document') and media.document is not None:
try:
mime = getattr(media.document, 'mime_type', None) or ""
if 'gif' in mime.lower():
return "[GIF]"
if mime.startswith('video/'):
return "[Video]"
if mime.startswith('audio/'):
return "[Audio]"
return "[File]"
except Exception:
return "[Document]"
if 'WebPage' in cls:
return "[Link]"
if 'Geo' in cls or 'Venue' in cls:
return "[Location]"
if 'Contact' in cls:
return "[Contact]"
if 'Poll' in cls:
return "[Poll]"
return "[Media]"
except Exception:
return "[Media]"
def _format_message_text_or_media(self, msg) -> str:
try:
text = getattr(msg, 'message', None)
if text and str(text).strip():
return str(text)
label = self._get_media_label(msg)
return label
except Exception:
return ""
def _get_default_prompt(self) -> str:
lang_index = self.get_setting("summary_language", 0)
try:
lang_index = int(lang_index)
except (ValueError, TypeError):
lang_index = 0
return DEFAULT_UNREAD_SUMMARY_PROMPT_RU if lang_index == 0 else DEFAULT_UNREAD_SUMMARY_PROMPT_EN
def _validate_model_index(self, model_idx) -> int:
try:
idx = int(model_idx)
return max(0, min(len(MODEL_API_NAMES) - 1, idx))
except (ValueError, TypeError):
return 1
def _validate_temperature(self, temp_str) -> float:
try:
temp = float(temp_str)
return max(0.0, min(2.0, temp))
except (ValueError, TypeError):
return 0.7
def _validate_max_tokens(self, tokens_str) -> int:
try:
tokens = int(tokens_str)
return max(100, min(32768, tokens))
except (ValueError, TypeError):
return 2048
def _validate_message_limit(self, limit_str) -> int:
try:
value = int(limit_str)
if value <= 0:
return 0
return max(1, min(100000, value))
except (ValueError, TypeError):
return 0
def _show_summary_in_cell(self, unread_cell, summary_text: str, message_count: int):
self.last_summary = summary_text
self.last_message_count = message_count
def update_and_show():
try:
text_view = get_private_field(unread_cell, "textView")
if text_view:
preview = self._make_summary_preview(summary_text, message_count)
text_view.setText(preview)
BulletinHelper.show_success(f"Проанализировано {message_count} сообщений")
except Exception as e:
BulletinHelper.show_error(f"Ошибка: {str(e)}")
run_on_ui_thread(update_and_show)
def _make_summary_preview(self, summary_text: str, message_count: int) -> str:
try:
if locali.language == "ru":
msg_word = "сообщений"
else:
msg_word = "messages" if message_count != 1 else "message"
return f"{message_count} • {msg_word}"
except Exception:
return str(message_count)
def _show_dots_menu(self, anchor_view, summary_text: str):
try:
from org.telegram.ui.ActionBar import ActionBarPopupWindow, Theme
from org.telegram.ui.Components import LayoutHelper
from org.telegram.messenger import AndroidUtilities, R
from android.view import View, Gravity
from android.widget import FrameLayout, LinearLayout, TextView, ImageView
from android.util import TypedValue
from androidx.core.content import ContextCompat
context = anchor_view.getContext()
popup_layout = ActionBarPopupWindow.ActionBarPopupWindowLayout(context)
popup_layout.setBackgroundColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuBackground))
popup_layout.setFitItems(True)
def create_menu_item(icon_res: int, title: str, on_click_action):
item_frame = FrameLayout(context)
item_frame.setMinimumWidth(AndroidUtilities.dp(200))
item_frame.setClickable(True)
item_frame.setFocusable(True)
item_frame.setBackground(Theme.createSelectorDrawable(Theme.getColor(Theme.key_listSelector), 2))
item_content = LinearLayout(context)
item_content.setOrientation(LinearLayout.HORIZONTAL)
item_content.setGravity(Gravity.CENTER_VERTICAL)
item_content.setPadding(AndroidUtilities.dp(16), AndroidUtilities.dp(12), AndroidUtilities.dp(16), AndroidUtilities.dp(12))
icon = ImageView(context)
icon.setScaleType(ImageView.ScaleType.CENTER)
try:
icon_drawable = ContextCompat.getDrawable(context, icon_res)
from android.graphics import PorterDuff
icon_drawable.setColorFilter(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem), PorterDuff.Mode.SRC_IN)
icon.setImageDrawable(icon_drawable)
except Exception:
icon.setImageResource(icon_res)
item_content.addView(icon, LayoutHelper.createLinear(24, 24, Gravity.CENTER_VERTICAL, 0, 0, 12, 0))
title_tv = TextView(context)
title_tv.setText(title)
title_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16)
try:
title_tv.setTextColor(Theme.getColor(Theme.key_actionBarDefaultSubmenuItem))
except Exception:
pass
item_content.addView(title_tv, LayoutHelper.createLinear(-1, -2, 1.0, Gravity.CENTER_VERTICAL))
item_frame.addView(item_content)
return item_frame, on_click_action
menu_items = [
(R.drawable.msg_bot, locali.get_string("MENU_PROVIDER"), lambda: self._show_provider_selector()),
(R.drawable.msg_translate, locali.get_string("MENU_LANGUAGE"), lambda: self._show_language_selector()),
(R.drawable.msg_share, locali.get_string("MENU_SHARE"), lambda: self._share_summary(summary_text)),
]
popup_window_ref = [None]
for icon_res, title, action in menu_items:
item_view, on_click_action = create_menu_item(icon_res, title, action)
item_view.setOnClickListener(OnClickListener(lambda *_args, act=on_click_action, pw_ref=popup_window_ref: (
pw_ref[0].dismiss() if pw_ref[0] else None,
act()
)))
popup_layout.addView(item_view, LayoutHelper.createLinear(-1, -2))
popup_window = ActionBarPopupWindow(
popup_layout,
-2,
-2
)
popup_window_ref[0] = popup_window
popup_window.setOutsideTouchable(True)
popup_window.setClippingEnabled(True)
popup_window.setAnimationStyle(R.style.PopupContextAnimation)
popup_window.setFocusable(True)
popup_layout.measure(
View.MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(1000), View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(1000), View.MeasureSpec.AT_MOST)
)
location = [0, 0]
anchor_view.getLocationInWindow(location)
popup_x = location[0] + anchor_view.getWidth() - popup_layout.getMeasuredWidth()
popup_y = location[1] - popup_layout.getMeasuredHeight()
popup_window.showAtLocation(
anchor_view,
Gravity.TOP | Gravity.LEFT,
popup_x,
popup_y
)
popup_window.dimBehind()
except Exception as e:
BulletinHelper.show_error(f"Ошибка: {str(e)}")
def _select_provider(self, index: int):
try:
self.set_setting("provider_type", index)
provider_name = PROVIDER_TYPES[index] if 0 <= index < len(PROVIDER_TYPES) else str(index)
msg = f"Выбран провайдер: {provider_name}" if locali.language == "ru" else f"Provider selected: {provider_name}"
BulletinHelper.show_success(msg)
except Exception as e:
pass
def _select_language(self, index: int):
try:
current_prompt = self.get_setting("summary_prompt", "")
old_lang_index = self.get_setting("summary_language", 0)
try:
old_lang_index = int(old_lang_index)
except (ValueError, TypeError):
old_lang_index = 0
old_default = DEFAULT_UNREAD_SUMMARY_PROMPT_RU if old_lang_index == 0 else DEFAULT_UNREAD_SUMMARY_PROMPT_EN
new_default = DEFAULT_UNREAD_SUMMARY_PROMPT_RU if index == 0 else DEFAULT_UNREAD_SUMMARY_PROMPT_EN
self.set_setting("summary_language", index)
if not current_prompt or current_prompt.strip() == old_default.strip():
self.set_setting("summary_prompt", new_default)
lang_text = "Русский" if index == 0 else "English"
BulletinHelper.show_success(f"Язык изменен: {lang_text}")
except Exception as e:
pass
def _share_summary(self, summary_text: str):
try:
fragment = get_last_fragment()
if not fragment:
BulletinHelper.show_error("Не удалось открыть диалог выбора")
return
from org.telegram.ui import LaunchActivity
from android.content import Intent
try:
activity = fragment.getParentActivity()
if activity and isinstance(activity, LaunchActivity):
share_text = f"📝 Сводка непрочитанных сообщений:\n\n{summary_text}"
intent = Intent(Intent.ACTION_SEND)
intent.setType("text/plain")
intent.putExtra(Intent.EXTRA_TEXT, share_text)
chooser = Intent.createChooser(intent, "Поделиться сводкой")
activity.startActivity(chooser)
else:
from android.content import ClipData, ClipboardManager, Context
clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE)
clip = ClipData.newPlainText("Summary", summary_text)
clipboard.setPrimaryClip(clip)
BulletinHelper.show_success("Сводка скопирована в буфер обмена")
except Exception as share_error:
from android.content import ClipData, ClipboardManager, Context
context = fragment.getParentActivity()
clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE)
clip = ClipData.newPlainText("Summary", summary_text)
clipboard.setPrimaryClip(clip)
BulletinHelper.show_success("📋 Сводка скопирована в буфер обмена")
except Exception as e:
BulletinHelper.show_error("Ошибка при попытке поделиться")
def _show_provider_selector(self):
try:
fragment = get_last_fragment()
if not fragment or not hasattr(fragment, 'getParentActivity'):
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
return
context = fragment.getParentActivity()
if not context:
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
return
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
builder.set_title(locali.get_string("SELECT_PROVIDER_TITLE"))
def on_provider_selected(dialog_builder, which):
self._select_provider(which)
dialog_builder.dismiss()
builder.set_items(PROVIDER_TYPES, on_provider_selected)
builder.show()
except Exception as e:
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
def _show_language_selector(self):
try:
fragment = get_last_fragment()
if not fragment or not hasattr(fragment, 'getParentActivity'):
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
return
context = fragment.getParentActivity()
if not context:
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
return
current_lang_index = self.get_setting("summary_language", 0)
try:
current_lang_index = int(current_lang_index)
except (ValueError, TypeError):
current_lang_index = 0
lang_items = ["🇷🇺 Русский", "🇬🇧 English"]
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
builder.set_title(locali.get_string("SELECT_LANGUAGE_TITLE"))
def on_language_selected(dialog_builder, which):
self._select_language(which)
dialog_builder.dismiss()
builder.set_items(lang_items, on_language_selected)
builder.show()
except Exception as e:
BulletinHelper.show_error(locali.get_string("SELECTOR_ERROR"))
def _show_summary_bottom_sheet(self, summary_text: str, message_count: int):
try:
fragment = get_last_fragment()
if not fragment or not hasattr(fragment, 'getParentActivity'):
BulletinHelper.show_error("Не удалось показать сводку")
return
context = fragment.getParentActivity()
if not context:
BulletinHelper.show_error("Не удалось показать сводку")
return
from org.telegram.ui.ActionBar import BottomSheet, Theme
from android.widget import LinearLayout, TextView, ScrollView, FrameLayout
from android.view import Gravity, View
from android.util import TypedValue
from org.telegram.ui.Components import LayoutHelper
from org.telegram.messenger import AndroidUtilities
from android.graphics.drawable import GradientDrawable
from android.graphics import Color
sheet = BottomSheet(context, False)
root_layout = LinearLayout(context)
root_layout.setOrientation(LinearLayout.VERTICAL)
root_layout.setPadding(AndroidUtilities.dp(20), AndroidUtilities.dp(16), AndroidUtilities.dp(20), AndroidUtilities.dp(20))
try:
root_layout.setBackgroundColor(Theme.getColor(Theme.key_dialogBackground))
except Exception:
pass
title_view = TextView(context)
title_view.setTypeface(AndroidUtilities.bold())
title_view.setGravity(Gravity.LEFT)
title_view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20)
title_view.setText(locali.get_string('DIALOG_TITLE'))
try:
title_view.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
except Exception:
pass
root_layout.addView(title_view, LayoutHelper.createLinear(-1, -2, Gravity.LEFT, 0, 0, 0, 12))
tags_container = LinearLayout(context)
tags_container.setOrientation(LinearLayout.HORIZONTAL)
tags_container.setGravity(Gravity.LEFT)
def create_tag(text: str, bg_color: int):
tag_frame = FrameLayout(context)
tag_bg = GradientDrawable()
tag_bg.setCornerRadius(AndroidUtilities.dp(12))
tag_bg.setColor(bg_color)
tag_frame.setBackground(tag_bg)
tag_text = TextView(context)
tag_text.setText(text)
tag_text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13)
tag_text.setTypeface(AndroidUtilities.bold())
try:
tag_text.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText))
except Exception:
tag_text.setTextColor(Color.parseColor("#3390EC"))
tag_text.setPadding(AndroidUtilities.dp(10), AndroidUtilities.dp(4), AndroidUtilities.dp(10), AndroidUtilities.dp(4))
tag_frame.addView(tag_text)
return tag_frame
lang_index = self.get_setting("summary_language", 0)
try:
lang_index = int(lang_index)
except (ValueError, TypeError):
lang_index = 0
lang_display = "RU" if lang_index == 0 else "EN"
try:
tag_bg_color = Theme.getColor(Theme.key_chat_inLoader) & 0x30FFFFFF | 0x20000000
except Exception:
tag_bg_color = Color.parseColor("#E8F4FC")
lang_tag = create_tag(lang_display, tag_bg_color)
tags_container.addView(lang_tag, LayoutHelper.createLinear(-2, -2, 0, 0, 8, 0))
provider_type = self.get_setting("provider_type", 0)
try:
provider_type = int(provider_type)
except (ValueError, TypeError):
provider_type = 0
if provider_type == 1:
provider_display = "OpenAI"
else:
model_idx = self._validate_model_index(self.get_setting("model_selection", 1))
provider_display = MODEL_DISPLAY_NAMES[model_idx].split()[0]
model_tag = create_tag(provider_display, tag_bg_color)
tags_container.addView(model_tag, LayoutHelper.createLinear(-2, -2))
root_layout.addView(tags_container, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 8))
message_info = TextView(context)
message_info.setText(f"Проанализировано сообщений: {message_count}")
message_info.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13)
try:
message_info.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText))
except Exception:
message_info.setTextColor(Color.parseColor("#808080"))
root_layout.addView(message_info, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 12))
body_scroll = ScrollView(context)
body_scroll.setVerticalScrollBarEnabled(False)
body_scroll.setPadding(AndroidUtilities.dp(4), 0, AndroidUtilities.dp(4), 0)
body_tv = TextView(context)
try:
parsed = parse_markdown(summary_text)
body_tv.setText(parsed.text)
except Exception:
body_tv.setText(summary_text)
body_tv.setTextIsSelectable(True)
body_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15)
try:
body_tv.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
except Exception:
pass
try:
body_tv.setLineSpacing(AndroidUtilities.dp(4), 1.15)
except Exception:
pass
body_scroll.addView(body_tv)
root_layout.addView(body_scroll, LayoutHelper.createLinear(-1, 0, 1.0))
divider = View(context)
try:
divider_color = Theme.getColor(Theme.key_divider)
except Exception:
divider_color = Color.parseColor("#E0E0E0")
divider.setBackgroundColor(divider_color)
root_layout.addView(divider, LayoutHelper.createLinear(-1, 1, 0, 16, 0, 12))
def create_action_button(icon_res: str, on_click):
btn_frame = FrameLayout(context)
btn_bg = GradientDrawable()
btn_bg.setCornerRadius(AndroidUtilities.dp(20))
try:
btn_bg.setColor(Theme.getColor(Theme.key_chat_inLoader) & 0x15FFFFFF | 0x0A000000)
except Exception:
btn_bg.setColor(Color.parseColor("#F5F5F5"))
btn_frame.setBackground(btn_bg)
btn_frame.setPadding(AndroidUtilities.dp(16), AndroidUtilities.dp(10), AndroidUtilities.dp(16), AndroidUtilities.dp(10))
btn_frame.setClickable(True)
btn_frame.setFocusable(True)
btn_content = LinearLayout(context)
btn_content.setOrientation(LinearLayout.HORIZONTAL)
btn_content.setGravity(Gravity.CENTER)
try:
icon_id = R.drawable.__getattribute__(R.drawable, icon_res)
icon_view = TextView(context)
icon_view.setCompoundDrawablesWithIntrinsicBounds(icon_id, 0, 0, 0)
icon_view.setCompoundDrawablePadding(AndroidUtilities.dp(4))
except Exception:
icon_view = TextView(context)
btn_content.addView(icon_view)
btn_frame.addView(btn_content)
btn_frame.setOnClickListener(OnClickListener(lambda *_: on_click(btn_frame)))
return btn_frame
actions_row = LinearLayout(context)
actions_row.setOrientation(LinearLayout.HORIZONTAL)
actions_row.setGravity(Gravity.CENTER_VERTICAL)
def on_repeat(v):
try:
sheet.dismiss()
dialog_id = self._get_current_dialog_id()
if dialog_id:
BulletinHelper.show_info("Обновляю сводку...")
self.last_summary = None
self.last_message_count = 0
run_on_queue(lambda: self._fetch_and_summarize_for_dialog(dialog_id))
else:
BulletinHelper.show_error("Не удалось определить текущий чат")
except Exception as e:
pass
def on_copy(v):
try:
from android.content import ClipData, ClipboardManager, Context
clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE)
clip = ClipData.newPlainText("Unread summary", summary_text)
clipboard.setPrimaryClip(clip)
BulletinHelper.show_success("Скопировано в буфер обмена")
except Exception as copy_error:
BulletinHelper.show_error("Ошибка копирования")
def on_more(v):
try:
self._show_dots_menu(v, summary_text)
except Exception as e:
BulletinHelper.show_error("Ошибка открытия меню")
def create_rounded_button(icon_res: int, text: str, on_click):
from androidx.core.content import ContextCompat
btn_frame = FrameLayout(context)
btn_bg = GradientDrawable()
btn_bg.setCornerRadius(AndroidUtilities.dp(18))
try:
bg_color = Theme.getColor(Theme.key_chat_inLoader) & 0x20FFFFFF | 0x10000000
except Exception:
bg_color = Color.parseColor("#F0F0F0")
btn_bg.setColor(bg_color)
try:
from android.graphics.drawable import RippleDrawable
from android.content.res import ColorStateList
ripple_color = ColorStateList.valueOf(Color.parseColor("#40000000"))
ripple_drawable = RippleDrawable(ripple_color, btn_bg, None)
btn_frame.setBackground(ripple_drawable)
except Exception:
btn_frame.setBackground(btn_bg)
btn_layout = LinearLayout(context)
btn_layout.setOrientation(LinearLayout.HORIZONTAL)
btn_layout.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL)
btn_layout.setPadding(AndroidUtilities.dp(14), AndroidUtilities.dp(10), AndroidUtilities.dp(14), AndroidUtilities.dp(10))
btn_layout.setMinimumHeight(AndroidUtilities.dp(40))
if icon_res:
from android.widget import ImageView
icon_view = ImageView(context)
icon_view.setScaleType(ImageView.ScaleType.FIT_CENTER)
try:
from android.graphics import PorterDuff
icon_drawable = ContextCompat.getDrawable(context, icon_res)
icon_drawable.setColorFilter(Theme.getColor(Theme.key_dialogTextBlack), PorterDuff.Mode.SRC_IN)
icon_view.setImageDrawable(icon_drawable)
except Exception:
icon_view.setImageResource(icon_res)
btn_layout.addView(icon_view, LayoutHelper.createLinear(16, 16, Gravity.CENTER_VERTICAL))
if text:
spacer = View(context)
btn_layout.addView(spacer, LayoutHelper.createLinear(6, 0))
if text:
label_text = TextView(context)
label_text.setText(text)
label_text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14)
label_text.setGravity(Gravity.CENTER_VERTICAL)
try:
label_text.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
except Exception:
label_text.setTextColor(Color.parseColor("#000000"))
btn_layout.addView(label_text, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_VERTICAL))
btn_frame.addView(btn_layout)
btn_frame.setClickable(True)
btn_frame.setFocusable(True)
btn_frame.setOnClickListener(OnClickListener(lambda *_: on_click(btn_frame)))
return btn_frame
repeat_btn_frame = create_rounded_button(R.drawable.msg_retry, "Повторить", on_repeat)
copy_btn_frame = create_rounded_button(R.drawable.msg_copy, "Копировать", on_copy)
more_btn_frame = create_rounded_button(R.drawable.ic_ab_other, "", on_more)
actions_row.addView(repeat_btn_frame, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_VERTICAL, 0, 0, 6, 0))
actions_row.addView(copy_btn_frame, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_VERTICAL, 0, 0, 6, 0))
actions_row.addView(more_btn_frame, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_VERTICAL, 0, 0, 0, 0))
root_layout.addView(actions_row, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 12))
close_btn_frame = FrameLayout(context)
close_btn_bg = GradientDrawable()
close_btn_bg.setCornerRadius(AndroidUtilities.dp(10))
try:
btn_color = Theme.getColor(Theme.key_windowBackgroundWhiteBlueText)
except Exception:
btn_color = Color.parseColor("#3390EC")
close_btn_bg.setColor(btn_color)
try:
from android.graphics.drawable import RippleDrawable
from android.content.res import ColorStateList
ripple_color = ColorStateList.valueOf(Color.parseColor("#30FFFFFF"))
ripple_drawable = RippleDrawable(ripple_color, close_btn_bg, None)
close_btn_frame.setBackground(ripple_drawable)
except Exception:
close_btn_frame.setBackground(close_btn_bg)
close_btn_frame.setPadding(0, AndroidUtilities.dp(14), 0, AndroidUtilities.dp(14))
close_btn_frame.setClickable(True)
close_btn_frame.setFocusable(True)
close_btn_text = TextView(context)
close_btn_text.setText("Закрыть сводку")
close_btn_text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16)
close_btn_text.setTypeface(AndroidUtilities.bold())
close_btn_text.setGravity(Gravity.CENTER)
close_btn_text.setTextColor(Color.WHITE)
close_btn_frame.addView(close_btn_text, FrameLayout.LayoutParams(-1, -2))
close_btn_frame.setOnClickListener(OnClickListener(lambda *_: sheet.dismiss()))
root_layout.addView(close_btn_frame, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 0))
sheet.setCustomView(root_layout)
sheet.show()
except Exception as e:
pass
def _show_summary_dialog(self, summary_text: str, message_count: int):
try:
fragment = get_last_fragment()
if not fragment or not hasattr(fragment, 'getParentActivity'):
BulletinHelper.show_error("Не удалось показать сводку")
return
context = fragment.getParentActivity()
if not context:
BulletinHelper.show_error("Не удалось показать сводку")
return
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
title = f"📝 Сводка ({message_count} сообщений)"
builder.set_title(title)
builder.set_message(summary_text)
builder.set_message_text_view_clickable(True)
def copy_to_clipboard(dialog_builder, which):
try:
from android.content import ClipData, ClipboardManager, Context
clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE)
clip = ClipData.newPlainText("Сводка непрочитанных", summary_text)
clipboard.setPrimaryClip(clip)
BulletinHelper.show_success("Скопировано в буфер обмена")
except Exception as copy_error:
BulletinHelper.show_error("Ошибка копирования")
builder.set_neutral_button("Закрыть", lambda b, w: b.dismiss())
builder.set_positive_button("Копировать", copy_to_clipboard)
builder.set_cancelable(True)
builder.show()
BulletinHelper.show_success(f"Проанализировано {message_count} сообщений")
except Exception as e:
BulletinHelper.show_error(f"Ошибка: {str(e)}")
def _show_error_in_cell(self, unread_cell, error_text: str):
def update_cell():
try:
text_view = get_private_field(unread_cell, "textView")
if text_view:
text_view.setText(error_text)
else:
BulletinHelper.show_error(error_text)
except Exception as e:
BulletinHelper.show_error(error_text)
run_on_ui_thread(update_cell)
def _perform_click_vibration(self, view):
try:
context = None
try:
context = view.getContext()
while context and not hasattr(context, 'getSystemService'):
if hasattr(context, 'getBaseContext'):
context = context.getBaseContext()
else:
break
except Exception:
pass
if not context:
fragment = get_last_fragment()
if fragment and hasattr(fragment, 'getParentActivity'):
context = fragment.getParentActivity()
if not context:
return
try:
from java.lang import Class as JClass
HFC = JClass.forName("android.view.HapticFeedbackConstants")
haptic_constant = getattr(HFC, "KEYBOARD_TAP", getattr(HFC, "VIRTUAL_KEY", 1))
if hasattr(view, 'performHapticFeedback'):
view.performHapticFeedback(haptic_constant)
return
except Exception as haptic_error:
pass
try:
vibrator = context.getSystemService("vibrator")
if vibrator:
try:
from java.lang import Class as JClass
VibrationEffect = JClass.forName("android.os.VibrationEffect")
effect = VibrationEffect.createOneShot(20, 120)
vibrator.vibrate(effect)
except Exception:
vibrator.vibrate(20)
except Exception as vib_error:
pass
except Exception as e:
pass
def _open_link(self, url: str):
try:
from android.content import Intent
from android.net import Uri
fragment = get_last_fragment()
if not fragment:
return
context = fragment.getParentActivity()
if not context:
return
intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
context.startActivity(intent)
except Exception as e:
pass
def _show_api_key_dialog(self, view):
try:
fragment = get_last_fragment()
if not fragment or not hasattr(fragment, 'getParentActivity'):
BulletinHelper.show_error("Cannot get current context")
return
context = fragment.getParentActivity()
if not context:
BulletinHelper.show_error("Cannot get current context")
return
from org.telegram.messenger import AndroidUtilities
current_value = self.get_setting("gemini_api_key", "")
edit_text = EditTextBoldCursor(context)
edit_text.setText(current_value)
edit_text.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
edit_text.setHint(locali.get_string("API_KEY_SUBTEXT"))
edit_text.setSingleLine(True)
try:
from android.graphics.drawable import ColorDrawable
from android.content.res import ColorStateList
from android.graphics import Color
edit_text.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
edit_text.setHintTextColor(Theme.getColor(Theme.key_dialogTextGray3))
edit_text.setLineColors(
Theme.getColor(Theme.key_dialogInputField),
Theme.getColor(Theme.key_dialogInputFieldActivated),
Theme.getColor(Theme.key_text_RedBold)
)
edit_text.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText))
edit_text.setBackground(ColorDrawable(0))
edit_text.setBackgroundTintList(ColorStateList.valueOf(Color.TRANSPARENT))
except Exception:
pass
padding_h = AndroidUtilities.dp(20)
padding_v = AndroidUtilities.dp(12)
edit_text.setPadding(padding_h, padding_v, padding_h, padding_v)
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
builder.set_title(locali.get_string("API_KEY_INPUT"))
builder.set_view(edit_text)
def on_ok_click(dialog_builder, which):
new_value = str(edit_text.getText()).strip()
self.set_setting("gemini_api_key", new_value)
dialog_builder.dismiss()
BulletinHelper.show_success("API ключ сохранен" if locali.language == "ru" else "API key saved")
builder.set_positive_button("OK", on_ok_click)
builder.set_negative_button(locali.get_string("MENU_CANCEL") if locali.language == "ru" else "Cancel", lambda b, w: b.dismiss())
builder.show()
def focus_edit():
edit_text.requestFocus()
from android.view.inputmethod import InputMethodManager
from android.content import Context
imm = context.getSystemService(Context.INPUT_METHOD_SERVICE)
if imm:
imm.showSoftInput(edit_text, InputMethodManager.SHOW_IMPLICIT)
from android_utils import R
run_on_ui_thread(lambda: edit_text.post(R(focus_edit)))
except Exception as e:
BulletinHelper.show_error(f"Error: {str(e)}")
def _show_openai_api_key_dialog(self, view):
try:
fragment = get_last_fragment()
if not fragment or not hasattr(fragment, 'getParentActivity'):
BulletinHelper.show_error("Cannot get current context")
return
context = fragment.getParentActivity()
if not context:
BulletinHelper.show_error("Cannot get current context")
return
from org.telegram.messenger import AndroidUtilities
current_value = self.get_setting("openai_api_key", "")
edit_text = EditTextBoldCursor(context)
edit_text.setText(current_value)
edit_text.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
edit_text.setHint(locali.get_string("OPENAI_API_KEY_INPUT"))
edit_text.setSingleLine(True)
try:
from android.graphics.drawable import ColorDrawable
from android.content.res import ColorStateList
from android.graphics import Color
edit_text.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
edit_text.setHintTextColor(Theme.getColor(Theme.key_dialogTextGray3))
edit_text.setLineColors(
Theme.getColor(Theme.key_dialogInputField),
Theme.getColor(Theme.key_dialogInputFieldActivated),
Theme.getColor(Theme.key_text_RedBold)
)
edit_text.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText))
edit_text.setBackground(ColorDrawable(0))
edit_text.setBackgroundTintList(ColorStateList.valueOf(Color.TRANSPARENT))
except Exception:
pass
padding_h = AndroidUtilities.dp(20)
padding_v = AndroidUtilities.dp(12)
edit_text.setPadding(padding_h, padding_v, padding_h, padding_v)
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
builder.set_title(locali.get_string("OPENAI_API_KEY_INPUT"))
builder.set_view(edit_text)
def on_ok_click(dialog_builder, which):
new_value = str(edit_text.getText()).strip()
self.set_setting("openai_api_key", new_value)
dialog_builder.dismiss()
BulletinHelper.show_success("API ключ сохранен" if locali.language == "ru" else "API key saved")
builder.set_positive_button("OK", on_ok_click)
builder.set_negative_button(locali.get_string("MENU_CANCEL") if locali.language == "ru" else "Cancel", lambda b, w: b.dismiss())
builder.show()
def focus_edit():
edit_text.requestFocus()
from android.view.inputmethod import InputMethodManager
from android.content import Context
imm = context.getSystemService(Context.INPUT_METHOD_SERVICE)
if imm:
imm.showSoftInput(edit_text, InputMethodManager.SHOW_IMPLICIT)
from android_utils import R
run_on_ui_thread(lambda: edit_text.post(R(focus_edit)))
except Exception as e:
BulletinHelper.show_error(f"Error: {str(e)}")
def _show_prompt_dialog(self, view):
try:
fragment = get_last_fragment()
if not fragment or not hasattr(fragment, 'getParentActivity'):
BulletinHelper.show_error("Cannot get current context")
return
context = fragment.getParentActivity()
if not context:
BulletinHelper.show_error("Cannot get current context")
return
from org.telegram.messenger import AndroidUtilities
from android.widget import ScrollView
from android.util import TypedValue
default_prompt = self._get_default_prompt()
current_value = self.get_setting("summary_prompt", default_prompt)
scroll_view = ScrollView(context)
edit_text = EditTextBoldCursor(context)
edit_text.setText(current_value)
edit_text.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
edit_text.setHint("Введите промпт для суммаризации..." if locali.language == "ru" else "Enter summarization prompt...")
edit_text.setMaxLines(15)
edit_text.setSingleLine(False)
edit_text.setVerticalScrollBarEnabled(True)
edit_text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14)
try:
from android.graphics.drawable import ColorDrawable
from android.content.res import ColorStateList
from android.graphics import Color
edit_text.setTextColor(Theme.getColor(Theme.key_dialogTextBlack))
edit_text.setHintTextColor(Theme.getColor(Theme.key_dialogTextGray3))
edit_text.setLineColors(
Theme.getColor(Theme.key_dialogInputField),
Theme.getColor(Theme.key_dialogInputFieldActivated),
Theme.getColor(Theme.key_text_RedBold)
)
edit_text.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText))
edit_text.setBackground(ColorDrawable(0))
edit_text.setBackgroundTintList(ColorStateList.valueOf(Color.TRANSPARENT))
except Exception:
pass
padding = AndroidUtilities.dp(20)
edit_text.setPadding(padding, padding, padding, padding)
scroll_view.addView(edit_text)
builder = AlertDialogBuilder(context, AlertDialogBuilder.ALERT_TYPE_MESSAGE)
builder.set_title(locali.get_string("SUMMARY_PROMPT_INPUT"))
builder.set_view(scroll_view)
def on_ok_click(dialog_builder, which):
new_value = str(edit_text.getText()).strip()
if new_value:
self.set_setting("summary_prompt", new_value)
dialog_builder.dismiss()
BulletinHelper.show_success("Промпт сохранен" if locali.language == "ru" else "Prompt saved")
else:
BulletinHelper.show_error("Промпт не может быть пустым" if locali.language == "ru" else "Prompt cannot be empty")
builder.set_positive_button("OK", on_ok_click)
builder.set_negative_button(locali.get_string("MENU_CANCEL") if locali.language == "ru" else "Cancel", lambda b, w: b.dismiss())
def on_reset_click(dialog_builder, which):
edit_text.setText(default_prompt)
BulletinHelper.show_info("Промпт сброшен" if locali.language == "ru" else "Prompt reset")
builder.set_neutral_button("Сброс" if locali.language == "ru" else "Reset", on_reset_click)
builder.show()
def focus_edit():
edit_text.requestFocus()
edit_text.setSelection(len(str(edit_text.getText())))
from android.view.inputmethod import InputMethodManager
from android.content import Context
imm = context.getSystemService(Context.INPUT_METHOD_SERVICE)
if imm:
imm.showSoftInput(edit_text, InputMethodManager.SHOW_IMPLICIT)
from android_utils import R
run_on_ui_thread(lambda: edit_text.post(R(focus_edit)))
except Exception as e:
BulletinHelper.show_error(f"Error: {str(e)}")
def create_settings(self):
provider_type = self.get_setting("provider_type", 0)
try:
provider_type = int(provider_type)
except (ValueError, TypeError):
provider_type = 0
items = [
Header(text=locali.get_string("AI_SETTINGS_HEADER")),
Selector(
key="provider_type",
text=locali.get_string("PROVIDER_SELECTOR"),
icon="msg_bot",
default=provider_type,
items=PROVIDER_TYPES
),
]
if provider_type == 0:
items.extend([
Text(
text=locali.get_string("API_KEY_INPUT"),
icon="msg_pin_code",
on_click=self._show_api_key_dialog
),
Text(
text=locali.get_string("GET_API_KEY_BUTTON"),
icon="msg_link",
accent=True,
on_click=lambda view: self._open_link("https://aistudio.google.com/app/apikey")
),
Divider(),
Selector(
key="model_selection",
text=locali.get_string("MODEL_SELECTOR"),
icon="msg_language_solar",
default=1,
items=MODEL_DISPLAY_NAMES
),
])
else:
items.extend([
Header(text=locali.get_string("OPENAI_SECTION")),
Text(
text=locali.get_string("OPENAI_API_KEY_INPUT"),
icon="msg_pin_code",
on_click=self._show_openai_api_key_dialog
),
Input(
key="openai_base_url",
text=locali.get_string("OPENAI_BASE_URL_INPUT"),
icon="msg_link",
default="https://api.openai.com"
),
Input(
key="openai_model",
text=locali.get_string("OPENAI_MODEL_INPUT"),
icon="msg_language_solar",
default="gpt-5-chat"
),
Input(
key="openai_api_key_header",
text=locali.get_string("OPENAI_HEADER_NAME_INPUT"),
icon="msg_settings",
default="Authorization"
),
Input(
key="openai_api_key_prefix",
text=locali.get_string("OPENAI_HEADER_PREFIX_INPUT"),
icon="msg_settings",
default="Bearer"
),
Divider(),
])
items.extend([
Selector(
key="summary_language",
text="Summary Language" if locali.language == "en" else "Язык сводки",
icon="msg_translate",
default=0,
items=["🇷🇺 Русский", "🇬🇧 English"]
),
Input(
key="temperature",
text=locali.get_string("TEMPERATURE_INPUT"),
icon="msg_settings",
default="0.7",
subtext=locali.get_string("TEMPERATURE_SUBTEXT")
),
Input(
key="max_tokens",
text=locali.get_string("MAX_TOKENS_INPUT"),
icon="msg_data",
default="256000",
subtext=locali.get_string("MAX_TOKENS_SUBTEXT")
),
Input(
key="message_limit",
text=locali.get_string("MESSAGE_LIMIT_INPUT"),
icon="msg_data",
default="0",
subtext=locali.get_string("MESSAGE_LIMIT_SUBTEXT")
),
Divider(),
Header(text=locali.get_string("PROMPT_HEADER")),
Text(
text=locali.get_string("SUMMARY_PROMPT_INPUT"),
icon="msg_edit",
on_click=self._show_prompt_dialog
),
])
return items