generated from exteraGramPlugins/pluginBase
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.
130 lines
4.5 KiB
Python
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()
|
|
|
|
|