@@ -430,7 +430,8 @@ function createChatSettingsSidePanel(manager, prefix, popup) {
430430 container . style . flexDirection = 'column' ;
431431 container . style . alignItems = 'stretch' ;
432432 container . style . gap = '2px' ;
433- container . style . minWidth = '160px' ;
433+ container . style . width = '200px' ;
434+ container . style . minWidth = '0' ;
434435 container . style . padding = '4px 4px' ;
435436
436437 const chatToggles = [
@@ -443,10 +444,187 @@ function createChatSettingsSidePanel(manager, prefix, popup) {
443444 container . appendChild ( toggleItem ) ;
444445 } ) ;
445446
447+ // 字数限制滑动条
448+ const textGuardContainer = manager . _createTextGuardSlider ( ) ;
449+ container . appendChild ( textGuardContainer ) ;
450+
446451 document . body . appendChild ( container ) ;
447452 return container ;
448453}
449454
455+ /**
456+ * 创建字数限制滑动条
457+ */
458+ function createTextGuardSlider ( manager , prefix ) {
459+ const container = document . createElement ( 'div' ) ;
460+ Object . assign ( container . style , {
461+ display : 'flex' ,
462+ flexDirection : 'column' ,
463+ gap : '4px' ,
464+ padding : '4px 0'
465+ } ) ;
466+
467+ // 标签和数值行
468+ const labelRow = document . createElement ( 'div' ) ;
469+ Object . assign ( labelRow . style , {
470+ display : 'flex' ,
471+ justifyContent : 'space-between' ,
472+ alignItems : 'center' ,
473+ gap : '8px'
474+ } ) ;
475+
476+ const label = document . createElement ( 'span' ) ;
477+ label . textContent = window . t ? window . t ( 'settings.toggles.textGuardMaxLength' ) : '回复字数限制' ;
478+ label . setAttribute ( 'data-i18n' , 'settings.toggles.textGuardMaxLength' ) ;
479+ Object . assign ( label . style , {
480+ fontSize : '12px' ,
481+ color : 'var(--neko-popup-text, #333)' ,
482+ flexShrink : '0'
483+ } ) ;
484+
485+ const valueDisplay = document . createElement ( 'span' ) ;
486+ Object . assign ( valueDisplay . style , {
487+ fontSize : '12px' ,
488+ color : 'var(--neko-popup-active, #2a7bc4)' ,
489+ fontWeight : '500' ,
490+ minWidth : '60px' ,
491+ textAlign : 'right'
492+ } ) ;
493+
494+ labelRow . appendChild ( label ) ;
495+ labelRow . appendChild ( valueDisplay ) ;
496+
497+ // 滑动条行
498+ const sliderRow = document . createElement ( 'div' ) ;
499+ Object . assign ( sliderRow . style , {
500+ display : 'flex' ,
501+ alignItems : 'center' ,
502+ gap : '8px' ,
503+ width : '100%'
504+ } ) ;
505+
506+ const slider = document . createElement ( 'input' ) ;
507+ slider . type = 'range' ;
508+ // 滑动条位置:0-10 对应 50-1500(每档150字),11 对应无限制
509+ // 默认值 350字 = (350-50)/150 = 2
510+ slider . min = '0' ;
511+ slider . max = '11' ;
512+ slider . step = '1' ;
513+
514+ // 当前值转换:数值 -> 滑动条位置
515+ const currentValue = typeof window . textGuardMaxLength !== 'undefined' ? window . textGuardMaxLength : 350 ;
516+ let currentPosition ;
517+ if ( currentValue === 0 || currentValue === null || currentValue === undefined ) {
518+ currentPosition = 11 ; // 无限制
519+ } else {
520+ // 找到最接近的档位:50 + position * 150
521+ currentPosition = Math . min ( 10 , Math . max ( 0 , Math . round ( ( currentValue - 50 ) / 150 ) ) ) ;
522+ }
523+ slider . value = currentPosition ;
524+
525+ Object . assign ( slider . style , {
526+ flex : '1' ,
527+ height : '4px' ,
528+ cursor : 'pointer' ,
529+ accentColor : 'var(--neko-popup-accent, #44b7fe)'
530+ } ) ;
531+
532+ // 更新显示文本
533+ const updateDisplay = ( position ) => {
534+ if ( parseInt ( position ) === 11 ) {
535+ const unlimitedText = ( typeof window . t === 'function' ) ? window . t ( 'settings.toggles.unlimited' ) : '无限制' ;
536+ valueDisplay . textContent = unlimitedText ;
537+ valueDisplay . setAttribute ( 'data-i18n' , 'settings.toggles.unlimited' ) ;
538+ } else {
539+ const value = 50 + parseInt ( position ) * 150 ;
540+ const unit = ( typeof window . t === 'function' ) ? window . t ( 'settings.toggles.characters' ) : '字' ;
541+ valueDisplay . textContent = `${ value } ${ unit } ` ;
542+ valueDisplay . removeAttribute ( 'data-i18n' ) ;
543+ }
544+ } ;
545+
546+ updateDisplay ( currentPosition ) ;
547+
548+ // 警告提示
549+ const warningRow = document . createElement ( 'div' ) ;
550+ Object . assign ( warningRow . style , {
551+ fontSize : '11px' ,
552+ color : '#ff6b6b' ,
553+ lineHeight : '1.4' ,
554+ minHeight : '16px' ,
555+ opacity : '0' ,
556+ transition : 'opacity 0.2s ease'
557+ } ) ;
558+
559+ const updateWarning = ( position ) => {
560+ const pos = parseInt ( position ) ;
561+ const value = 50 + pos * 150 ;
562+ if ( pos === 11 ) {
563+ // 无限制
564+ const warningText = ( typeof window . t === 'function' )
565+ ? window . t ( 'settings.toggles.textGuardUnlimitedWarning' )
566+ : '无限制可能导致模型生成过长回复,消耗较多Token' ;
567+ warningRow . textContent = warningText ;
568+ warningRow . style . opacity = '1' ;
569+ } else if ( value > 500 ) {
570+ // 超过500字显示提示
571+ const warningText = ( typeof window . t === 'function' )
572+ ? window . t ( 'settings.toggles.textGuardHighWarning' )
573+ : '设置过大可能导致回复过长,建议保持在500字以内' ;
574+ warningRow . textContent = warningText ;
575+ warningRow . style . opacity = '1' ;
576+ } else {
577+ warningRow . style . opacity = '0' ;
578+ }
579+ } ;
580+
581+ updateWarning ( currentPosition ) ;
582+
583+ slider . addEventListener ( 'input' , ( ) => {
584+ const position = parseInt ( slider . value ) ;
585+ updateDisplay ( position ) ;
586+ updateWarning ( position ) ;
587+ } ) ;
588+
589+ slider . addEventListener ( 'change' , ( ) => {
590+ const position = parseInt ( slider . value ) ;
591+ let value ;
592+ if ( position === 11 ) {
593+ value = 0 ; // 0 表示无限制
594+ } else {
595+ value = 50 + position * 150 ;
596+ }
597+ window . textGuardMaxLength = value ;
598+ if ( typeof window . saveNEKOSettings === 'function' ) window . saveNEKOSettings ( ) ;
599+ console . log ( `[TextGuard] 回复字数限制已设置为 ${ value === 0 ? '无限制' : value + '字' } ` ) ;
600+ } ) ;
601+
602+ slider . addEventListener ( 'click' , ( e ) => e . stopPropagation ( ) ) ;
603+ slider . addEventListener ( 'mousedown' , ( e ) => e . stopPropagation ( ) ) ;
604+
605+ sliderRow . appendChild ( slider ) ;
606+
607+ // 底部提示(仅对文本回复有效)
608+ const noteRow = document . createElement ( 'div' ) ;
609+ Object . assign ( noteRow . style , {
610+ fontSize : '10px' ,
611+ color : '#888' ,
612+ lineHeight : '1.4' ,
613+ marginTop : '4px'
614+ } ) ;
615+ const noteText = ( typeof window . t === 'function' )
616+ ? window . t ( 'settings.toggles.textGuardNote' )
617+ : '仅对文本回复有效,不影响语音对话' ;
618+ noteRow . textContent = noteText ;
619+
620+ container . appendChild ( labelRow ) ;
621+ container . appendChild ( sliderRow ) ;
622+ container . appendChild ( warningRow ) ;
623+ container . appendChild ( noteRow ) ;
624+
625+ return container ;
626+ }
627+
450628/**
451629 * 创建角色设置侧边面板
452630 */
@@ -455,7 +633,8 @@ function createCharacterSettingsSidePanel(manager, prefix) {
455633 container . style . flexDirection = 'column' ;
456634 container . style . alignItems = 'stretch' ;
457635 container . style . gap = '2px' ;
458- container . style . minWidth = '140px' ;
636+ container . style . width = '160px' ;
637+ container . style . minWidth = '0' ;
459638 container . style . padding = '4px 8px' ;
460639
461640 const items = manager . _characterMenuItems || [ ] ;
@@ -1727,6 +1906,10 @@ const AvatarPopupMixin = {
17271906 return createAnimationSettingsSidePanel ( this , prefix ) ;
17281907 } ;
17291908
1909+ ManagerProto . _createTextGuardSlider = function ( ) {
1910+ return createTextGuardSlider ( this , prefix ) ;
1911+ } ;
1912+
17301913 ManagerProto . _createSidePanelContainer = function ( panelOptions = { } ) {
17311914 return createSidePanelContainer ( this , prefix , options . sidePanelContainerLayout || panelOptions ) ;
17321915 } ;
0 commit comments