mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat(admin): предупреждение о разбивке длинных текстов системных страниц в боте
Кабинет, в отличие от админки бота (лимит 4000 символов), позволяет сохранять юридические тексты любой длины — и до парного фикса в боте (BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot#3049) текст длиннее ~4096 символов ронял показ правил и политики при /start (MESSAGE_TOO_LONG). Теперь под текстовым полем политики, оферты и правил постоянно показывается счётчик символов, а для текстов длиннее одного сообщения — предупреждение с оценкой, на сколько сообщений бот разобьёт текст. Оценка зеркалит жадную упаковку абзацев из split_telegram_text бота (порог 3500 символов на кусок). Предупреждение не показывается там, где оно неуместно: - «Рекуррентные платежи» — документ не отображается в боте вовсе; - режим отображения «Только веб» — бот документ не показывает. Переводы добавлены в ru/en (с плюральными формами); остальные локали используют английский fallback через defaultValue.
This commit is contained in:
@@ -4196,6 +4196,9 @@
|
||||
"language": "Content language",
|
||||
"content": "Content",
|
||||
"contentPlaceholder": "HTML page content...",
|
||||
"charCount": "Characters: {{count}}",
|
||||
"botSplitEstimate_one": "⚠️ The bot will show it split into ~{{count}} message",
|
||||
"botSplitEstimate_other": "⚠️ The bot will show it split into ~{{count}} messages",
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"saveError": "Failed to save. Please try again.",
|
||||
|
||||
@@ -4741,6 +4741,10 @@
|
||||
"language": "Язык контента",
|
||||
"content": "Текст",
|
||||
"contentPlaceholder": "HTML-текст страницы...",
|
||||
"charCount": "Символов: {{count}}",
|
||||
"botSplitEstimate_one": "⚠️ В боте будет показано по частям: ~{{count}} сообщение",
|
||||
"botSplitEstimate_few": "⚠️ В боте будет показано по частям: ~{{count}} сообщения",
|
||||
"botSplitEstimate_many": "⚠️ В боте будет показано по частям: ~{{count}} сообщений",
|
||||
"save": "Сохранить",
|
||||
"saving": "Сохранение...",
|
||||
"saveError": "Не удалось сохранить. Попробуйте ещё раз.",
|
||||
|
||||
@@ -20,6 +20,62 @@ const DISPLAY_MODES: LegalDisplayMode[] = ['bot', 'web', 'both'];
|
||||
|
||||
type DocumentKind = 'privacy-policy' | 'public-offer' | 'recurrent-payments';
|
||||
|
||||
// Bot chunk/page size (split_telegram_text max_length): longer texts are
|
||||
// delivered by the bot in several messages / paginated pages
|
||||
const TELEGRAM_SPLIT_THRESHOLD = 3500;
|
||||
|
||||
// Mirrors the bot's split_telegram_text greedy paragraph packing to estimate
|
||||
// how many messages/pages the bot will produce for this text
|
||||
function estimateTelegramParts(text: string): number {
|
||||
const normalized = text.replace(/\r\n/g, '\n').trim();
|
||||
if (!normalized) return 0;
|
||||
if (normalized.length <= TELEGRAM_SPLIT_THRESHOLD) return 1;
|
||||
const paragraphs = normalized.split('\n\n').filter((p) => p.trim());
|
||||
let parts = 0;
|
||||
let current = '';
|
||||
for (const paragraph of paragraphs) {
|
||||
const candidate = current ? `${current}\n\n${paragraph}` : paragraph;
|
||||
if (candidate.length <= TELEGRAM_SPLIT_THRESHOLD) {
|
||||
current = candidate;
|
||||
continue;
|
||||
}
|
||||
if (current) {
|
||||
parts += 1;
|
||||
current = '';
|
||||
}
|
||||
if (paragraph.length <= TELEGRAM_SPLIT_THRESHOLD) {
|
||||
current = paragraph;
|
||||
} else {
|
||||
parts += Math.ceil(paragraph.length / TELEGRAM_SPLIT_THRESHOLD);
|
||||
}
|
||||
}
|
||||
if (current) parts += 1;
|
||||
return parts;
|
||||
}
|
||||
|
||||
function ContentLengthMeta({ text, botVisible }: { text: string; botVisible: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const parts = botVisible ? estimateTelegramParts(text) : 0;
|
||||
return (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
||||
<span className="text-dark-500">
|
||||
{t('admin.legalPages.charCount', {
|
||||
count: text.length,
|
||||
defaultValue: 'Characters: {{count}}',
|
||||
})}
|
||||
</span>
|
||||
{parts > 1 && (
|
||||
<span className="text-warning-400">
|
||||
{t('admin.legalPages.botSplitEstimate', {
|
||||
count: parts,
|
||||
defaultValue: '⚠️ The bot will show it split into ~{{count}} messages',
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DOCUMENT_API: Record<
|
||||
DocumentKind,
|
||||
{
|
||||
@@ -236,6 +292,10 @@ function DocumentEditor({
|
||||
className="input min-h-[320px] w-full font-mono text-sm"
|
||||
placeholder={t('admin.legalPages.contentPlaceholder')}
|
||||
/>
|
||||
<ContentLengthMeta
|
||||
text={contents[activeLang] ?? ''}
|
||||
botVisible={kind !== 'recurrent-payments' && displayMode !== 'web'}
|
||||
/>
|
||||
</div>
|
||||
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
|
||||
<button
|
||||
@@ -336,6 +396,7 @@ function RulesEditor({ onDirtyChange }: { onDirtyChange: (dirty: boolean) => voi
|
||||
className="input min-h-[320px] w-full font-mono text-sm"
|
||||
placeholder={t('admin.legalPages.contentPlaceholder')}
|
||||
/>
|
||||
<ContentLengthMeta text={contents[activeLang] ?? ''} botVisible={displayMode !== 'web'} />
|
||||
</div>
|
||||
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user