feat(ui): 优化播客创建器文本输入体验和内容长度管理

实现前后端协同的内容长度控制机制。前端通过动态计数器展示
当前输入进度,接近上限时视觉提醒用户。后端API增强参数验证,
针对不同创作模式设定差异化阈值(标准20k/故事30k)。模式
切换时智能裁剪超长内容并友好提示。完善中英日三语国际化
文案支持,提升全球用户使用体验。
This commit is contained in:
hex2077
2025-10-22 13:27:59 +08:00
parent d7cb44de3a
commit 1d750ef616
9 changed files with 63 additions and 3 deletions

View File

@@ -98,6 +98,8 @@
"pleaseSelectSpeaker": "Please select a speaker",
"pleaseSelectAtLeastOneSpeaker": "Please select at least one podcast speaker.",
"podcastGenerationFailed": "Podcast generation failed:",
"textTruncated": "Text Truncated",
"textTruncatedMessage": "After switching to the current mode, the text has been automatically truncated to {{maxChars}} characters to comply with the limit.",
"maximum5Speakers": "You can select up to 5 speakers.",
"chinese": "Chinese",
"english": "English",

View File

@@ -26,6 +26,7 @@
"user_not_logged_in_or_session_expired": "User not logged in or session expired",
"request_body_cannot_be_empty": "Request body cannot be empty",
"tts_provider_cannot_be_empty": "TTS provider cannot be empty",
"input_text_exceeds_limit": "Input text exceeds limit, maximum {{limit}} characters allowed",
"please_select_at_least_one_speaker": "Please select at least one podcast speaker",
"invalid_speaker_config_format": "Invalid podcast speaker configuration format",
"insufficient_points_for_podcast": "Insufficient points, generating a podcast requires {{pointsNeeded}} points, you currently have {{currentPoints}} points.",

View File

@@ -98,6 +98,8 @@
"pleaseSelectSpeaker": "スピーカーを選択してください",
"pleaseSelectAtLeastOneSpeaker": "少なくとも1人のポッドキャストスピーカーを選択してください。",
"podcastGenerationFailed": "ポッドキャストの生成に失敗しました:",
"textTruncated": "テキストが切り詰められました",
"textTruncatedMessage": "現在のモードに切り替えた後、テキストは制限に準拠するために自動的に {{maxChars}} 文字に切り詰められました。",
"maximum5Speakers": "最大5人のスピーカーを選択できます。",
"chinese": "中国語",
"english": "英語",

View File

@@ -26,6 +26,7 @@
"user_not_logged_in_or_session_expired": "ユーザーがログインしていないか、セッションの有効期限が切れています",
"request_body_cannot_be_empty": "リクエストボディは空にできません",
"tts_provider_cannot_be_empty": "TTSプロバイダーは空にできません",
"input_text_exceeds_limit": "入力テキストが制限を超えています。最大 {{limit}} 文字まで許可されています",
"please_select_at_least_one_speaker": "少なくとも1人のポッドキャスト話者を選択してください",
"invalid_speaker_config_format": "無効なポッドキャスト話者設定フォーマット",
"insufficient_points_for_podcast": "ポイントが不足しています。ポッドキャストを生成するには{{pointsNeeded}}ポイントが必要です。現在{{currentPoints}}ポイントしかありません。",

View File

@@ -98,6 +98,8 @@
"pleaseSelectSpeaker": "请选择说话人",
"pleaseSelectAtLeastOneSpeaker": "请至少选择一位播客说话人。",
"podcastGenerationFailed": "播客生成失败:",
"textTruncated": "文本已截断",
"textTruncatedMessage": "切换到当前模式后,文本已自动截断至 {{maxChars}} 字符以符合限制。",
"maximum5Speakers": "最多只能选择5个说话人。",
"chinese": "中文",
"english": "英文",

View File

@@ -26,6 +26,7 @@
"user_not_logged_in_or_session_expired": "用户未登录或会话已过期",
"request_body_cannot_be_empty": "请求正文不能为空",
"tts_provider_cannot_be_empty": "TTS服务提供商不能为空",
"input_text_exceeds_limit": "输入文本超过限制,最多允许 {{limit}} 字符",
"please_select_at_least_one_speaker": "请至少选择一位播客说话人",
"invalid_speaker_config_format": "播客说话人配置格式无效",
"insufficient_points_for_podcast": "积分不足,生成一个播客需要 {{pointsNeeded}} 积分,您当前只有 {{currentPoints}} 积分。",

View File

@@ -33,6 +33,15 @@ export async function POST(request: NextRequest) {
{ status: 400 }
);
}
// 字符数限制校验 - 沉浸故事模式限制30000字符
const MAX_CHARS_AI_STORY = 30000;
if (body.input_txt_content.length > MAX_CHARS_AI_STORY) {
return NextResponse.json(
{ success: false, error: t('input_text_exceeds_limit', { limit: MAX_CHARS_AI_STORY }) },
{ status: 400 }
);
}
if (!body.tts_provider || body.tts_provider.trim().length === 0) {
return NextResponse.json(
{ success: false, error: t('tts_provider_cannot_be_empty') },

View File

@@ -33,6 +33,15 @@ export async function POST(request: NextRequest) {
{ status: 400 }
);
}
// 字符数限制校验 - AI播客模式限制20000字符
const MAX_CHARS_AI_PODCAST = 20000;
if (body.input_txt_content.length > MAX_CHARS_AI_PODCAST) {
return NextResponse.json(
{ success: false, error: t('input_text_exceeds_limit', { limit: MAX_CHARS_AI_PODCAST }) },
{ status: 400 }
);
}
if (!body.tts_provider || body.tts_provider.trim().length === 0) {
return NextResponse.json(
{ success: false, error: t('tts_provider_cannot_be_empty') },

View File

@@ -68,6 +68,26 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
const [topic, setTopic] = useState('');
const [customInstructions, setCustomInstructions] = useState('');
const [selectedMode, setSelectedMode] = useState<'ai-podcast' | 'ai-story'>('ai-podcast');
// 字符数限制常量
const MAX_CHARS_AI_PODCAST = 20000;
const MAX_CHARS_AI_STORY = 30000;
// 获取当前模式的字符数限制
const maxChars = selectedMode === 'ai-podcast' ? MAX_CHARS_AI_PODCAST : MAX_CHARS_AI_STORY;
// 监听模式切换,如果文本超过新模式的限制,则截断
useEffect(() => {
if (topic.length > maxChars) {
const truncatedTopic = topic.substring(0, maxChars);
setTopic(truncatedTopic);
setItem('podcast-topic', truncatedTopic);
error(
t('podcastCreator.textTruncated'),
t('podcastCreator.textTruncatedMessage', { maxChars })
);
}
}, [selectedMode, maxChars]); // 只在模式切换时触发
// 初始化时从 localStorage 加载 topic 和 customInstructions
useEffect(() => {
@@ -366,14 +386,27 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
<textarea
value={topic}
onChange={(e) => {
setTopic(e.target.value);
setItem('podcast-topic', e.target.value); // 实时保存到 localStorage
const newValue = e.target.value;
// 如果超过字符数限制,截取到最大长度
const finalValue = newValue.length > maxChars ? newValue.substring(0, maxChars) : newValue;
setTopic(finalValue);
setItem('podcast-topic', finalValue); // 实时保存到 localStorage
}}
placeholder={t('podcastCreator.enterTextPlaceholder')}
className="w-full h-32 resize-none border-none outline-none text-lg placeholder-neutral-400 bg-white"
className="w-full h-48 resize-none border-none outline-none text-lg placeholder-neutral-400 bg-white"
disabled={isGenerating}
/>
{/* 字符数统计 */}
<div className="flex justify-end mt-2">
<span className={cn(
"text-sm",
topic.length > maxChars * 0.9 ? "text-red-500 font-medium" : "text-neutral-400"
)}>
{topic.length} / {maxChars}
</span>
</div>
{/* 自定义指令 */}
{customInstructions !== undefined && selectedMode === 'ai-podcast' && (
<div className="mt-4 pt-4 border-t border-neutral-100">