@llblab/pi-telegram 0.6.3 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Telegram status menu UI helpers
3
+ * Zones: telegram ui, status controls, menu composition
4
+ * Owns status-menu payloads, status callback handling, and status-menu message rendering
5
+ */
6
+
7
+ import { formatTelegramCommandEmojiPrefix } from "./commands.ts";
8
+ import {
9
+ formatStatusButtonLabel,
10
+ type TelegramMenuMessageRuntimeDeps,
11
+ type TelegramMenuRenderPayload,
12
+ type TelegramModelMenuState,
13
+ type TelegramReplyMarkup,
14
+ } from "./menu-model.ts";
15
+ import {
16
+ getCanonicalModelId,
17
+ type MenuModel,
18
+ type ThinkingLevel,
19
+ } from "./model.ts";
20
+
21
+ export interface TelegramStatusMenuCallbackDeps {
22
+ updateModelMenuMessage: () => Promise<void>;
23
+ updateThinkingMenuMessage: () => Promise<void>;
24
+ answerCallbackQuery: (
25
+ callbackQueryId: string,
26
+ text?: string,
27
+ ) => Promise<void>;
28
+ }
29
+
30
+ export interface TelegramStatusMenuOpenDeps<
31
+ TModel extends MenuModel = MenuModel,
32
+ > {
33
+ isIdle: () => boolean;
34
+ sendBusyMessage: () => Promise<void>;
35
+ getModelMenuState: () => Promise<TelegramModelMenuState<TModel>>;
36
+ buildStatusHtml: () => string;
37
+ getActiveModel: () => TModel | undefined;
38
+ getThinkingLevel: () => ThinkingLevel;
39
+ getQueueItemCount?: () => number;
40
+ sendStatusMenu: (
41
+ state: TelegramModelMenuState<TModel>,
42
+ statusHtml: string,
43
+ activeModel: TModel | undefined,
44
+ thinkingLevel: ThinkingLevel,
45
+ queueItemCount: number,
46
+ ) => Promise<number | undefined>;
47
+ storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
48
+ }
49
+
50
+ function isTelegramStatusMenuCallbackAction(
51
+ data: string | undefined,
52
+ action: "model" | "thinking",
53
+ ): boolean {
54
+ return data === `status:${action}`;
55
+ }
56
+
57
+ function applyTelegramMenuRenderPayload(
58
+ state: TelegramModelMenuState,
59
+ payload: TelegramMenuRenderPayload,
60
+ ): TelegramMenuRenderPayload {
61
+ state.mode = payload.nextMode;
62
+ return payload;
63
+ }
64
+
65
+ async function editTelegramMenuMessage(
66
+ state: TelegramModelMenuState,
67
+ payload: TelegramMenuRenderPayload,
68
+ deps: TelegramMenuMessageRuntimeDeps,
69
+ ): Promise<void> {
70
+ const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
71
+ await deps.editInteractiveMessage(
72
+ state.chatId,
73
+ state.messageId,
74
+ appliedPayload.text,
75
+ appliedPayload.mode,
76
+ appliedPayload.replyMarkup,
77
+ );
78
+ }
79
+
80
+ function sendTelegramMenuMessage(
81
+ state: TelegramModelMenuState,
82
+ payload: TelegramMenuRenderPayload,
83
+ deps: TelegramMenuMessageRuntimeDeps,
84
+ ): Promise<number | undefined> {
85
+ const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
86
+ return deps.sendInteractiveMessage(
87
+ state.chatId,
88
+ appliedPayload.text,
89
+ appliedPayload.mode,
90
+ appliedPayload.replyMarkup,
91
+ );
92
+ }
93
+
94
+ export async function openTelegramStatusMenu<
95
+ TModel extends MenuModel = MenuModel,
96
+ >(deps: TelegramStatusMenuOpenDeps<TModel>): Promise<void> {
97
+ const state = await deps.getModelMenuState();
98
+ const messageId = await deps.sendStatusMenu(
99
+ state,
100
+ deps.buildStatusHtml(),
101
+ deps.getActiveModel(),
102
+ deps.getThinkingLevel(),
103
+ deps.getQueueItemCount?.() ?? 0,
104
+ );
105
+ if (messageId === undefined) return;
106
+ state.messageId = messageId;
107
+ state.mode = "status";
108
+ deps.storeModelMenuState(state);
109
+ }
110
+
111
+ export async function handleTelegramStatusMenuCallbackAction(
112
+ callbackQueryId: string,
113
+ data: string | undefined,
114
+ activeModel: MenuModel | undefined,
115
+ deps: TelegramStatusMenuCallbackDeps,
116
+ ): Promise<boolean> {
117
+ if (isTelegramStatusMenuCallbackAction(data, "model")) {
118
+ await deps.updateModelMenuMessage();
119
+ await deps.answerCallbackQuery(callbackQueryId);
120
+ return true;
121
+ }
122
+ if (!isTelegramStatusMenuCallbackAction(data, "thinking")) return false;
123
+ if (!activeModel?.reasoning) {
124
+ await deps.answerCallbackQuery(
125
+ callbackQueryId,
126
+ "This model has no reasoning controls.",
127
+ );
128
+ return true;
129
+ }
130
+ await deps.updateThinkingMenuMessage();
131
+ await deps.answerCallbackQuery(callbackQueryId);
132
+ return true;
133
+ }
134
+
135
+ export function buildStatusReplyMarkup(
136
+ activeModel: MenuModel | undefined,
137
+ currentThinkingLevel: ThinkingLevel,
138
+ queueItemCount = 0,
139
+ ): TelegramReplyMarkup {
140
+ const rows: Array<Array<{ text: string; callback_data: string }>> = [];
141
+ rows.push([
142
+ {
143
+ text: formatStatusButtonLabel(
144
+ `${formatTelegramCommandEmojiPrefix("model")}Model`,
145
+ activeModel ? getCanonicalModelId(activeModel) : "unknown",
146
+ ),
147
+ callback_data: "status:model",
148
+ },
149
+ ]);
150
+ if (activeModel?.reasoning) {
151
+ rows.push([
152
+ {
153
+ text: formatStatusButtonLabel(
154
+ `${formatTelegramCommandEmojiPrefix("thinking")}Thinking`,
155
+ currentThinkingLevel,
156
+ ),
157
+ callback_data: "status:thinking",
158
+ },
159
+ ]);
160
+ }
161
+ rows.push([
162
+ {
163
+ text: `🔢 Queue: ${queueItemCount}`,
164
+ callback_data: "status:queue",
165
+ },
166
+ ]);
167
+ return { inline_keyboard: rows };
168
+ }
169
+
170
+ export function buildTelegramStatusMenuRenderPayload(
171
+ statusText: string,
172
+ activeModel: MenuModel | undefined,
173
+ currentThinkingLevel: ThinkingLevel,
174
+ queueItemCount = 0,
175
+ ): TelegramMenuRenderPayload {
176
+ return {
177
+ nextMode: "status",
178
+ text: statusText,
179
+ mode: "html",
180
+ replyMarkup: buildStatusReplyMarkup(
181
+ activeModel,
182
+ currentThinkingLevel,
183
+ queueItemCount,
184
+ ),
185
+ };
186
+ }
187
+
188
+ export async function updateTelegramStatusMessage(
189
+ state: TelegramModelMenuState,
190
+ statusText: string,
191
+ activeModel: MenuModel | undefined,
192
+ currentThinkingLevel: ThinkingLevel,
193
+ deps: TelegramMenuMessageRuntimeDeps,
194
+ queueItemCount = 0,
195
+ ): Promise<void> {
196
+ await editTelegramMenuMessage(
197
+ state,
198
+ buildTelegramStatusMenuRenderPayload(
199
+ statusText,
200
+ activeModel,
201
+ currentThinkingLevel,
202
+ queueItemCount,
203
+ ),
204
+ deps,
205
+ );
206
+ }
207
+
208
+ export function sendTelegramStatusMessage(
209
+ state: TelegramModelMenuState,
210
+ statusText: string,
211
+ activeModel: MenuModel | undefined,
212
+ currentThinkingLevel: ThinkingLevel,
213
+ deps: TelegramMenuMessageRuntimeDeps,
214
+ queueItemCount = 0,
215
+ ): Promise<number | undefined> {
216
+ return sendTelegramMenuMessage(
217
+ state,
218
+ buildTelegramStatusMenuRenderPayload(
219
+ statusText,
220
+ activeModel,
221
+ currentThinkingLevel,
222
+ queueItemCount,
223
+ ),
224
+ deps,
225
+ );
226
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Telegram thinking menu UI helpers
3
+ * Zones: telegram ui, thinking controls, menu composition
4
+ * Owns thinking-menu text, reply markup, callback handling, and thinking-menu message rendering
5
+ */
6
+
7
+ import type {
8
+ TelegramMenuMessageRuntimeDeps,
9
+ TelegramMenuRenderPayload,
10
+ TelegramModelMenuState,
11
+ TelegramReplyMarkup,
12
+ } from "./menu-model.ts";
13
+ import {
14
+ isThinkingLevel,
15
+ type MenuModel,
16
+ THINKING_LEVELS,
17
+ type ThinkingLevel,
18
+ } from "./model.ts";
19
+
20
+ export interface TelegramThinkingMenuCallbackDeps {
21
+ setThinkingLevel: (level: ThinkingLevel) => void;
22
+ getCurrentThinkingLevel: () => ThinkingLevel;
23
+ updateStatusMessage: () => Promise<void>;
24
+ answerCallbackQuery: (
25
+ callbackQueryId: string,
26
+ text?: string,
27
+ ) => Promise<void>;
28
+ }
29
+
30
+ export interface TelegramThinkingMenuOpenDeps<
31
+ TModel extends MenuModel = MenuModel,
32
+ > extends TelegramMenuMessageRuntimeDeps {
33
+ getModelMenuState: () => Promise<TelegramModelMenuState<TModel>>;
34
+ getActiveModel: () => TModel | undefined;
35
+ getThinkingLevel: () => ThinkingLevel;
36
+ storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
37
+ }
38
+
39
+ function parseTelegramThinkingMenuCallbackAction(
40
+ data: string | undefined,
41
+ ): { kind: "thinking:set"; level: string } | undefined {
42
+ if (!data?.startsWith("thinking:set:")) return undefined;
43
+ return { kind: "thinking:set", level: data.slice("thinking:set:".length) };
44
+ }
45
+
46
+ function applyTelegramMenuRenderPayload(
47
+ state: TelegramModelMenuState,
48
+ payload: TelegramMenuRenderPayload,
49
+ ): TelegramMenuRenderPayload {
50
+ state.mode = payload.nextMode;
51
+ return payload;
52
+ }
53
+
54
+ async function editTelegramMenuMessage(
55
+ state: TelegramModelMenuState,
56
+ payload: TelegramMenuRenderPayload,
57
+ deps: TelegramMenuMessageRuntimeDeps,
58
+ ): Promise<void> {
59
+ const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
60
+ await deps.editInteractiveMessage(
61
+ state.chatId,
62
+ state.messageId,
63
+ appliedPayload.text,
64
+ appliedPayload.mode,
65
+ appliedPayload.replyMarkup,
66
+ );
67
+ }
68
+
69
+ function sendTelegramMenuMessage(
70
+ state: TelegramModelMenuState,
71
+ payload: TelegramMenuRenderPayload,
72
+ deps: TelegramMenuMessageRuntimeDeps,
73
+ ): Promise<number | undefined> {
74
+ const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
75
+ return deps.sendInteractiveMessage(
76
+ state.chatId,
77
+ appliedPayload.text,
78
+ appliedPayload.mode,
79
+ appliedPayload.replyMarkup,
80
+ );
81
+ }
82
+
83
+ export async function handleTelegramThinkingMenuCallbackAction(
84
+ callbackQueryId: string,
85
+ data: string | undefined,
86
+ activeModel: MenuModel | undefined,
87
+ deps: TelegramThinkingMenuCallbackDeps,
88
+ ): Promise<boolean> {
89
+ const action = parseTelegramThinkingMenuCallbackAction(data);
90
+ if (!action) return false;
91
+ if (!isThinkingLevel(action.level)) {
92
+ await deps.answerCallbackQuery(callbackQueryId, "Invalid thinking level.");
93
+ return true;
94
+ }
95
+ if (!activeModel?.reasoning) {
96
+ await deps.answerCallbackQuery(
97
+ callbackQueryId,
98
+ "This model has no reasoning controls.",
99
+ );
100
+ return true;
101
+ }
102
+ deps.setThinkingLevel(action.level);
103
+ await deps.updateStatusMessage();
104
+ await deps.answerCallbackQuery(
105
+ callbackQueryId,
106
+ `Thinking: ${deps.getCurrentThinkingLevel()}`,
107
+ );
108
+ return true;
109
+ }
110
+
111
+ export function buildThinkingMenuText(): string {
112
+ return "<b>Choose a thinking level:</b>";
113
+ }
114
+
115
+ export function buildThinkingMenuReplyMarkup(
116
+ currentThinkingLevel: ThinkingLevel,
117
+ ): TelegramReplyMarkup {
118
+ const rows = [[{ text: "⬆️ Main menu", callback_data: "menu:back" }]];
119
+ rows.push(
120
+ ...THINKING_LEVELS.map((level) => [
121
+ {
122
+ text: level === currentThinkingLevel ? `✅ ${level}` : level,
123
+ callback_data: `thinking:set:${level}`,
124
+ },
125
+ ]),
126
+ );
127
+ return { inline_keyboard: rows };
128
+ }
129
+
130
+ export function buildTelegramThinkingMenuRenderPayload(
131
+ _activeModel: MenuModel | undefined,
132
+ currentThinkingLevel: ThinkingLevel,
133
+ ): TelegramMenuRenderPayload {
134
+ return {
135
+ nextMode: "thinking",
136
+ text: buildThinkingMenuText(),
137
+ mode: "html",
138
+ replyMarkup: buildThinkingMenuReplyMarkup(currentThinkingLevel),
139
+ };
140
+ }
141
+
142
+ export async function openTelegramThinkingMenu<
143
+ TModel extends MenuModel = MenuModel,
144
+ >(deps: TelegramThinkingMenuOpenDeps<TModel>): Promise<void> {
145
+ const state = await deps.getModelMenuState();
146
+ const messageId = await sendTelegramMenuMessage(
147
+ state,
148
+ buildTelegramThinkingMenuRenderPayload(
149
+ deps.getActiveModel(),
150
+ deps.getThinkingLevel(),
151
+ ),
152
+ deps,
153
+ );
154
+ if (messageId === undefined) return;
155
+ state.messageId = messageId;
156
+ state.mode = "thinking";
157
+ deps.storeModelMenuState(state);
158
+ }
159
+
160
+ export async function updateTelegramThinkingMenuMessage(
161
+ state: TelegramModelMenuState,
162
+ activeModel: MenuModel | undefined,
163
+ currentThinkingLevel: ThinkingLevel,
164
+ deps: TelegramMenuMessageRuntimeDeps,
165
+ ): Promise<void> {
166
+ await editTelegramMenuMessage(
167
+ state,
168
+ buildTelegramThinkingMenuRenderPayload(activeModel, currentThinkingLevel),
169
+ deps,
170
+ );
171
+ }