mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +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",
|
"language": "Content language",
|
||||||
"content": "Content",
|
"content": "Content",
|
||||||
"contentPlaceholder": "HTML page 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",
|
"save": "Save",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"saveError": "Failed to save. Please try again.",
|
"saveError": "Failed to save. Please try again.",
|
||||||
|
|||||||
@@ -4741,6 +4741,10 @@
|
|||||||
"language": "Язык контента",
|
"language": "Язык контента",
|
||||||
"content": "Текст",
|
"content": "Текст",
|
||||||
"contentPlaceholder": "HTML-текст страницы...",
|
"contentPlaceholder": "HTML-текст страницы...",
|
||||||
|
"charCount": "Символов: {{count}}",
|
||||||
|
"botSplitEstimate_one": "⚠️ В боте будет показано по частям: ~{{count}} сообщение",
|
||||||
|
"botSplitEstimate_few": "⚠️ В боте будет показано по частям: ~{{count}} сообщения",
|
||||||
|
"botSplitEstimate_many": "⚠️ В боте будет показано по частям: ~{{count}} сообщений",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"saving": "Сохранение...",
|
"saving": "Сохранение...",
|
||||||
"saveError": "Не удалось сохранить. Попробуйте ещё раз.",
|
"saveError": "Не удалось сохранить. Попробуйте ещё раз.",
|
||||||
|
|||||||
@@ -20,6 +20,62 @@ const DISPLAY_MODES: LegalDisplayMode[] = ['bot', 'web', 'both'];
|
|||||||
|
|
||||||
type DocumentKind = 'privacy-policy' | 'public-offer' | 'recurrent-payments';
|
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<
|
const DOCUMENT_API: Record<
|
||||||
DocumentKind,
|
DocumentKind,
|
||||||
{
|
{
|
||||||
@@ -236,6 +292,10 @@ function DocumentEditor({
|
|||||||
className="input min-h-[320px] w-full font-mono text-sm"
|
className="input min-h-[320px] w-full font-mono text-sm"
|
||||||
placeholder={t('admin.legalPages.contentPlaceholder')}
|
placeholder={t('admin.legalPages.contentPlaceholder')}
|
||||||
/>
|
/>
|
||||||
|
<ContentLengthMeta
|
||||||
|
text={contents[activeLang] ?? ''}
|
||||||
|
botVisible={kind !== 'recurrent-payments' && displayMode !== 'web'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
|
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
|
||||||
<button
|
<button
|
||||||
@@ -336,6 +396,7 @@ function RulesEditor({ onDirtyChange }: { onDirtyChange: (dirty: boolean) => voi
|
|||||||
className="input min-h-[320px] w-full font-mono text-sm"
|
className="input min-h-[320px] w-full font-mono text-sm"
|
||||||
placeholder={t('admin.legalPages.contentPlaceholder')}
|
placeholder={t('admin.legalPages.contentPlaceholder')}
|
||||||
/>
|
/>
|
||||||
|
<ContentLengthMeta text={contents[activeLang] ?? ''} botVisible={displayMode !== 'web'} />
|
||||||
</div>
|
</div>
|
||||||
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
|
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user