mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 11:37:38 +00:00
Telegram: add inline button model selection for /models and /model commands
This commit is contained in:
committed by
Ayaan Zaidi
parent
efb4a34be4
commit
16349b6e93
210
src/telegram/model-buttons.ts
Normal file
210
src/telegram/model-buttons.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Telegram inline button utilities for model selection.
|
||||
*
|
||||
* Callback data patterns (max 64 bytes for Telegram):
|
||||
* - mdl_prov - show providers list
|
||||
* - mdl_list_{prov}_{pg} - show models for provider (page N, 1-indexed)
|
||||
* - mdl_sel_{provider/id} - select model
|
||||
* - mdl_back - back to providers list
|
||||
*/
|
||||
|
||||
export type ButtonRow = Array<{ text: string; callback_data: string }>;
|
||||
|
||||
export type ParsedModelCallback =
|
||||
| { type: "providers" }
|
||||
| { type: "list"; provider: string; page: number }
|
||||
| { type: "select"; provider: string; model: string }
|
||||
| { type: "back" };
|
||||
|
||||
export type ProviderInfo = {
|
||||
id: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type ModelsKeyboardParams = {
|
||||
provider: string;
|
||||
models: string[];
|
||||
currentModel?: string;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
pageSize?: number;
|
||||
};
|
||||
|
||||
const MODELS_PAGE_SIZE = 8;
|
||||
|
||||
/**
|
||||
* Parse a model callback_data string into a structured object.
|
||||
* Returns null if the data doesn't match a known pattern.
|
||||
*/
|
||||
export function parseModelCallbackData(data: string): ParsedModelCallback | null {
|
||||
const trimmed = data.trim();
|
||||
if (!trimmed.startsWith("mdl_")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (trimmed === "mdl_prov" || trimmed === "mdl_back") {
|
||||
return { type: trimmed === "mdl_prov" ? "providers" : "back" };
|
||||
}
|
||||
|
||||
// mdl_list_{provider}_{page}
|
||||
const listMatch = trimmed.match(/^mdl_list_([a-z0-9_-]+)_(\d+)$/i);
|
||||
if (listMatch) {
|
||||
const [, provider, pageStr] = listMatch;
|
||||
const page = Number.parseInt(pageStr ?? "1", 10);
|
||||
if (provider && Number.isFinite(page) && page >= 1) {
|
||||
return { type: "list", provider, page };
|
||||
}
|
||||
}
|
||||
|
||||
// mdl_sel_{provider/model}
|
||||
const selMatch = trimmed.match(/^mdl_sel_(.+)$/);
|
||||
if (selMatch) {
|
||||
const modelRef = selMatch[1];
|
||||
if (modelRef) {
|
||||
const slashIndex = modelRef.indexOf("/");
|
||||
if (slashIndex > 0 && slashIndex < modelRef.length - 1) {
|
||||
return {
|
||||
type: "select",
|
||||
provider: modelRef.slice(0, slashIndex),
|
||||
model: modelRef.slice(slashIndex + 1),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build provider selection keyboard with 2 providers per row.
|
||||
*/
|
||||
export function buildProviderKeyboard(providers: ProviderInfo[]): ButtonRow[] {
|
||||
if (providers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows: ButtonRow[] = [];
|
||||
let currentRow: ButtonRow = [];
|
||||
|
||||
for (const provider of providers) {
|
||||
const button = {
|
||||
text: `${provider.id} (${provider.count})`,
|
||||
callback_data: `mdl_list_${provider.id}_1`,
|
||||
};
|
||||
|
||||
currentRow.push(button);
|
||||
|
||||
if (currentRow.length === 2) {
|
||||
rows.push(currentRow);
|
||||
currentRow = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Push any remaining button
|
||||
if (currentRow.length > 0) {
|
||||
rows.push(currentRow);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build model list keyboard with pagination and back button.
|
||||
*/
|
||||
export function buildModelsKeyboard(params: ModelsKeyboardParams): ButtonRow[] {
|
||||
const { provider, models, currentModel, currentPage, totalPages } = params;
|
||||
const pageSize = params.pageSize ?? MODELS_PAGE_SIZE;
|
||||
|
||||
if (models.length === 0) {
|
||||
return [[{ text: "<< Back", callback_data: "mdl_back" }]];
|
||||
}
|
||||
|
||||
const rows: ButtonRow[] = [];
|
||||
|
||||
// Calculate page slice
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = Math.min(startIndex + pageSize, models.length);
|
||||
const pageModels = models.slice(startIndex, endIndex);
|
||||
|
||||
// Model buttons - one per row
|
||||
const currentModelId = currentModel?.includes("/")
|
||||
? currentModel.split("/").slice(1).join("/")
|
||||
: currentModel;
|
||||
|
||||
for (const model of pageModels) {
|
||||
const isCurrentModel = model === currentModelId;
|
||||
const displayText = truncateModelId(model, 38);
|
||||
const text = isCurrentModel ? `${displayText} ✓` : displayText;
|
||||
|
||||
rows.push([
|
||||
{
|
||||
text,
|
||||
callback_data: `mdl_sel_${provider}/${model}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// Pagination row
|
||||
if (totalPages > 1) {
|
||||
const paginationRow: ButtonRow = [];
|
||||
|
||||
if (currentPage > 1) {
|
||||
paginationRow.push({
|
||||
text: "◀ Prev",
|
||||
callback_data: `mdl_list_${provider}_${currentPage - 1}`,
|
||||
});
|
||||
}
|
||||
|
||||
paginationRow.push({
|
||||
text: `${currentPage}/${totalPages}`,
|
||||
callback_data: `mdl_list_${provider}_${currentPage}`, // noop
|
||||
});
|
||||
|
||||
if (currentPage < totalPages) {
|
||||
paginationRow.push({
|
||||
text: "Next ▶",
|
||||
callback_data: `mdl_list_${provider}_${currentPage + 1}`,
|
||||
});
|
||||
}
|
||||
|
||||
rows.push(paginationRow);
|
||||
}
|
||||
|
||||
// Back button
|
||||
rows.push([{ text: "<< Back", callback_data: "mdl_back" }]);
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build "Browse providers" button for /model summary.
|
||||
*/
|
||||
export function buildBrowseProvidersButton(): ButtonRow[] {
|
||||
return [[{ text: "Browse providers", callback_data: "mdl_prov" }]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate model ID for display, preserving end if too long.
|
||||
*/
|
||||
function truncateModelId(modelId: string, maxLen: number): string {
|
||||
if (modelId.length <= maxLen) {
|
||||
return modelId;
|
||||
}
|
||||
// Show last part with ellipsis prefix
|
||||
return `…${modelId.slice(-(maxLen - 1))}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page size for model list pagination.
|
||||
*/
|
||||
export function getModelsPageSize(): number {
|
||||
return MODELS_PAGE_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate total pages for a model list.
|
||||
*/
|
||||
export function calculateTotalPages(totalModels: number, pageSize?: number): number {
|
||||
const size = pageSize ?? MODELS_PAGE_SIZE;
|
||||
return size > 0 ? Math.ceil(totalModels / size) : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user