Files
chatSummaries/src/ui/summarize_dialog.py
T
lyuksovannyy 196da742de Implement Chat Summaries plugin
Replace the sample plugin with an AI chat summarizer that renders its
output as a native RichMessage injected into the chat.

Summarization pipeline:
- Fetch chat/topic history via paginated TL requests.
- Send a system prompt, the user's instructions and an anonymized JSON
  transcript as three separate roles. Only message text and attachment
  names leave the device; senders and message ids are replaced by
  indices, and out-of-range reply parents are included with negative ids
  so replies keep their context.
- Stream the response over SSE, reading reasoning from reasoning_content,
  reasoning or inline <think> tags.
- Substitute the model's {userN}/{msgN} placeholders back into real names
  and deep links. Placeholders are used instead of Markdown links because
  a half-streamed link renders as broken literal text.

Rendering:
- Compile Markdown into TL_iv page blocks (headings, lists, quotes, code,
  dividers, inline styles) since the app's own converter works off
  Android Spanned spans rather than Markdown source.
- Show progress stages immediately on click, animate reasoning while it
  streams, then collapse it into a tappable details block.
- Attribute the card to a client-side mock sender and insert it directly
  after the newest analyzed message, scrolling it into view.

Persistence:
- Store summaries in SQLite keyed by (dialog, topic, anchor) and
  re-inject them when the chat is reopened.

Also add tools/release.py and a Gitea workflow that builds, verifies and
publishes dist/<id>.plugin to the Releases tab, and extend
tools/verify_bundle.py to 104 offline checks covering the transcript,
streaming, rendering, injection and storage behaviour.
2026-09-12 23:52:15 +02:00

130 lines
4.5 KiB
Python

"""Bottom-line UI: prompts for message count/offset/prompt, runs the
summarization, and shows the result. Deliberately plain — no bottom sheets,
sliders, or styling copied from any other plugin.
"""
from typing import Any, Callable
from android.widget import LinearLayout, TextView
from org.telegram.messenger import AndroidUtilities
from org.telegram.ui.Components import EditTextBoldCursor
import diagnostics
from ui.alert import AlertDialogBuilder
from ui.bulletin import BulletinHelper
from ui.theme import DialogColors
from config import (
DEFAULT_MSG_COUNT,
DEFAULT_MSG_OFFSET,
MAX_MSG_COUNT,
MIN_MSG_COUNT,
)
def _dp(value: int) -> int:
return AndroidUtilities.dp(value)
def _match_wrap_params() -> Any:
"""LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT).
Uses plain ints (-1 / -2, the real values behind
ViewGroup.LayoutParams.MATCH_PARENT/WRAP_CONTENT) instead of
org.telegram.ui.Components.LayoutHelper: on this device/SDK build,
Chaquopy raises AttributeError resolving LayoutHelper.MATCH_PARENT even
though the Java field exists, while WRAP_CONTENT alone was not enough
to prove the whole class is reachable. Plain LinearLayout.LayoutParams
sidesteps that static-field lookup entirely.
"""
return LinearLayout.LayoutParams(-1, -2)
def _make_label(activity, text: str, colors: DialogColors) -> TextView:
label = TextView(activity)
label.setText(text)
label.setTextSize(14)
label.setPadding(0, _dp(12), 0, _dp(4))
label.setTextColor(colors.secondary_text)
return label
def _make_edit_text(activity, hint: str, initial: str, colors: DialogColors) -> Any:
field = EditTextBoldCursor(activity)
field.setHint(hint)
field.setText(initial)
field.setSingleLine(True)
field.setTextColor(colors.input_text)
field.setHintTextColor(colors.hint_text)
field.setLineColors(colors.input_line, colors.input_line_active, colors.input_line_active)
field.setCursorColor(colors.cursor)
return field
def show_summarize_dialog(
activity: Any,
fragment: Any,
default_count: int,
default_offset: int,
default_prompt: str,
on_submit: Callable[[int, int, str], None],
) -> None:
"""Shows a dialog collecting message count, offset, and prompt, then
invokes on_submit(count, offset, prompt) if the user confirms.
"""
colors = DialogColors()
root = LinearLayout(activity)
root.setOrientation(LinearLayout.VERTICAL)
root.setPadding(_dp(24), _dp(8), _dp(24), _dp(8))
root.setBackgroundColor(colors.background)
count_label = _make_label(activity, "Messages to summarize (1-1000)", colors)
root.addView(count_label)
count_field = _make_edit_text(
activity, "e.g. 100", str(max(MIN_MSG_COUNT, min(MAX_MSG_COUNT, default_count))), colors
)
root.addView(count_field, _match_wrap_params())
offset_label = _make_label(activity, "Offset from newest message", colors)
root.addView(offset_label)
offset_field = _make_edit_text(activity, "e.g. 0", str(max(0, default_offset)), colors)
root.addView(offset_field, _match_wrap_params())
prompt_label = _make_label(activity, "Prompt (saved for this chat/topic)", colors)
root.addView(prompt_label)
prompt_field = _make_edit_text(activity, "Summarize this conversation…", default_prompt, colors)
prompt_field.setSingleLine(False)
prompt_field.setMinLines(2)
prompt_field.setMaxLines(5)
root.addView(prompt_field, _match_wrap_params())
builder = AlertDialogBuilder(activity)
builder.set_title("Summarize Chat")
builder.set_view(root)
def on_positive(bld, which):
try:
count = int(str(count_field.getText()).strip() or DEFAULT_MSG_COUNT)
except ValueError:
count = DEFAULT_MSG_COUNT
try:
offset = int(str(offset_field.getText()).strip() or DEFAULT_MSG_OFFSET)
except ValueError:
offset = DEFAULT_MSG_OFFSET
count = max(MIN_MSG_COUNT, min(MAX_MSG_COUNT, count))
offset = max(0, offset)
prompt = str(prompt_field.getText()).strip()
bld.dismiss()
try:
on_submit(count, offset, prompt)
except Exception as exc:
diagnostics.record_error("summarize_dialog.on_submit", exc)
BulletinHelper.show_error(f"Summarize failed to start: {exc}", fragment)
def on_negative(bld, which):
bld.dismiss()
builder.set_positive_button("Summarize", on_positive)
builder.set_negative_button("Cancel", on_negative)
builder.show()