tabby-ai-assistant 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/dist/components/chat/ai-sidebar.component.d.ts +147 -0
  2. package/dist/components/chat/chat-interface.component.d.ts +38 -6
  3. package/dist/components/settings/general-settings.component.d.ts +6 -3
  4. package/dist/components/settings/provider-config.component.d.ts +25 -12
  5. package/dist/components/terminal/command-preview.component.d.ts +38 -0
  6. package/dist/index-full.d.ts +8 -0
  7. package/dist/index-minimal.d.ts +3 -0
  8. package/dist/index.d.ts +7 -3
  9. package/dist/index.js +1 -2
  10. package/dist/providers/tabby/ai-config.provider.d.ts +57 -5
  11. package/dist/providers/tabby/ai-hotkey.provider.d.ts +8 -14
  12. package/dist/providers/tabby/ai-toolbar-button.provider.d.ts +8 -9
  13. package/dist/services/chat/ai-sidebar.service.d.ts +89 -0
  14. package/dist/services/chat/chat-history.service.d.ts +78 -0
  15. package/dist/services/chat/chat-session.service.d.ts +57 -2
  16. package/dist/services/context/compaction.d.ts +90 -0
  17. package/dist/services/context/manager.d.ts +69 -0
  18. package/dist/services/context/memory.d.ts +116 -0
  19. package/dist/services/context/token-budget.d.ts +105 -0
  20. package/dist/services/core/ai-assistant.service.d.ts +40 -1
  21. package/dist/services/core/checkpoint.service.d.ts +130 -0
  22. package/dist/services/platform/escape-sequence.service.d.ts +132 -0
  23. package/dist/services/platform/platform-detection.service.d.ts +146 -0
  24. package/dist/services/providers/anthropic-provider.service.d.ts +5 -0
  25. package/dist/services/providers/base-provider.service.d.ts +6 -1
  26. package/dist/services/providers/glm-provider.service.d.ts +5 -0
  27. package/dist/services/providers/minimax-provider.service.d.ts +10 -1
  28. package/dist/services/providers/ollama-provider.service.d.ts +76 -0
  29. package/dist/services/providers/openai-compatible.service.d.ts +5 -0
  30. package/dist/services/providers/openai-provider.service.d.ts +5 -0
  31. package/dist/services/providers/vllm-provider.service.d.ts +82 -0
  32. package/dist/services/terminal/buffer-analyzer.service.d.ts +128 -0
  33. package/dist/services/terminal/terminal-manager.service.d.ts +185 -0
  34. package/dist/services/terminal/terminal-tools.service.d.ts +79 -0
  35. package/dist/types/ai.types.d.ts +92 -0
  36. package/dist/types/provider.types.d.ts +1 -1
  37. package/package.json +7 -10
  38. package/src/components/chat/ai-sidebar.component.ts +945 -0
  39. package/src/components/chat/chat-input.component.html +9 -24
  40. package/src/components/chat/chat-input.component.scss +3 -2
  41. package/src/components/chat/chat-interface.component.html +77 -69
  42. package/src/components/chat/chat-interface.component.scss +54 -4
  43. package/src/components/chat/chat-interface.component.ts +250 -34
  44. package/src/components/chat/chat-settings.component.scss +4 -4
  45. package/src/components/chat/chat-settings.component.ts +22 -11
  46. package/src/components/common/error-message.component.html +15 -0
  47. package/src/components/common/error-message.component.scss +77 -0
  48. package/src/components/common/error-message.component.ts +2 -96
  49. package/src/components/common/loading-spinner.component.html +4 -0
  50. package/src/components/common/loading-spinner.component.scss +57 -0
  51. package/src/components/common/loading-spinner.component.ts +2 -63
  52. package/src/components/security/consent-dialog.component.html +22 -0
  53. package/src/components/security/consent-dialog.component.scss +34 -0
  54. package/src/components/security/consent-dialog.component.ts +2 -55
  55. package/src/components/security/password-prompt.component.html +19 -0
  56. package/src/components/security/password-prompt.component.scss +30 -0
  57. package/src/components/security/password-prompt.component.ts +2 -54
  58. package/src/components/security/risk-confirm-dialog.component.html +8 -12
  59. package/src/components/security/risk-confirm-dialog.component.scss +8 -5
  60. package/src/components/security/risk-confirm-dialog.component.ts +6 -6
  61. package/src/components/settings/ai-settings-tab.component.html +16 -20
  62. package/src/components/settings/ai-settings-tab.component.scss +8 -5
  63. package/src/components/settings/ai-settings-tab.component.ts +12 -12
  64. package/src/components/settings/general-settings.component.html +8 -17
  65. package/src/components/settings/general-settings.component.scss +6 -3
  66. package/src/components/settings/general-settings.component.ts +62 -22
  67. package/src/components/settings/provider-config.component.html +19 -39
  68. package/src/components/settings/provider-config.component.scss +182 -39
  69. package/src/components/settings/provider-config.component.ts +119 -7
  70. package/src/components/settings/security-settings.component.scss +1 -1
  71. package/src/components/terminal/ai-toolbar-button.component.html +8 -0
  72. package/src/components/terminal/ai-toolbar-button.component.scss +20 -0
  73. package/src/components/terminal/ai-toolbar-button.component.ts +2 -30
  74. package/src/components/terminal/command-preview.component.html +61 -0
  75. package/src/components/terminal/command-preview.component.scss +72 -0
  76. package/src/components/terminal/command-preview.component.ts +127 -140
  77. package/src/components/terminal/command-suggestion.component.html +23 -0
  78. package/src/components/terminal/command-suggestion.component.scss +55 -0
  79. package/src/components/terminal/command-suggestion.component.ts +2 -77
  80. package/src/index-minimal.ts +32 -0
  81. package/src/index.ts +94 -11
  82. package/src/index.ts.backup +165 -0
  83. package/src/providers/tabby/ai-config.provider.ts +60 -51
  84. package/src/providers/tabby/ai-hotkey.provider.ts +23 -39
  85. package/src/providers/tabby/ai-settings-tab.provider.ts +2 -2
  86. package/src/providers/tabby/ai-toolbar-button.provider.ts +29 -24
  87. package/src/services/chat/ai-sidebar.service.ts +258 -0
  88. package/src/services/chat/chat-history.service.ts +308 -0
  89. package/src/services/chat/chat-history.service.ts.backup +239 -0
  90. package/src/services/chat/chat-session.service.ts +276 -3
  91. package/src/services/context/compaction.ts +483 -0
  92. package/src/services/context/manager.ts +442 -0
  93. package/src/services/context/memory.ts +519 -0
  94. package/src/services/context/token-budget.ts +422 -0
  95. package/src/services/core/ai-assistant.service.ts +280 -5
  96. package/src/services/core/ai-provider-manager.service.ts +2 -2
  97. package/src/services/core/checkpoint.service.ts +619 -0
  98. package/src/services/platform/escape-sequence.service.ts +499 -0
  99. package/src/services/platform/platform-detection.service.ts +494 -0
  100. package/src/services/providers/anthropic-provider.service.ts +28 -1
  101. package/src/services/providers/base-provider.service.ts +7 -1
  102. package/src/services/providers/glm-provider.service.ts +28 -1
  103. package/src/services/providers/minimax-provider.service.ts +209 -11
  104. package/src/services/providers/ollama-provider.service.ts +445 -0
  105. package/src/services/providers/openai-compatible.service.ts +9 -0
  106. package/src/services/providers/openai-provider.service.ts +9 -0
  107. package/src/services/providers/vllm-provider.service.ts +463 -0
  108. package/src/services/security/risk-assessment.service.ts +6 -2
  109. package/src/services/terminal/buffer-analyzer.service.ts +594 -0
  110. package/src/services/terminal/terminal-manager.service.ts +748 -0
  111. package/src/services/terminal/terminal-tools.service.ts +441 -0
  112. package/src/styles/ai-assistant.scss +78 -6
  113. package/src/types/ai.types.ts +144 -0
  114. package/src/types/provider.types.ts +1 -1
  115. package/tsconfig.json +9 -9
  116. package/webpack.config.js +28 -6
@@ -0,0 +1,945 @@
1
+ import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core';
2
+ import { Subject } from 'rxjs';
3
+ import { ChatMessage, MessageRole } from '../../types/ai.types';
4
+ import { AiAssistantService } from '../../services/core/ai-assistant.service';
5
+ import { ConfigProviderService } from '../../services/core/config-provider.service';
6
+ import { LoggerService } from '../../services/core/logger.service';
7
+ import { ChatHistoryService } from '../../services/chat/chat-history.service';
8
+ import { AiSidebarService } from '../../services/chat/ai-sidebar.service';
9
+
10
+ /**
11
+ * AI Sidebar 组件 - 替代 ChatInterfaceComponent
12
+ * 使用内联模板和样式,支持 Tabby 主题
13
+ */
14
+ @Component({
15
+ selector: 'app-ai-sidebar',
16
+ template: `
17
+ <div class="ai-sidebar-container">
18
+ <!-- Header -->
19
+ <div class="ai-sidebar-header">
20
+ <div class="header-title">
21
+ <svg class="header-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
22
+ <path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5ZM3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.58 26.58 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.933.933 0 0 1-.765.935c-.845.147-2.34.346-4.235.346-1.895 0-3.39-.2-4.235-.346A.933.933 0 0 1 3 9.219V8.062Zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a24.767 24.767 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25.286 25.286 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135Z"/>
23
+ <path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2V1.866ZM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5Z"/>
24
+ </svg>
25
+ <span>AI 助手</span>
26
+ <small class="provider-badge">{{ currentProvider }}</small>
27
+ </div>
28
+ <div class="header-actions">
29
+ <button class="btn btn-link btn-sm btn-close-sidebar" (click)="hideSidebar()" title="隐藏侧边栏">
30
+ <svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
31
+ <path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
32
+ </svg>
33
+ </button>
34
+ <button class="btn btn-link btn-sm" (click)="switchProvider()" title="切换提供商">
35
+ <svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
36
+ <path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/>
37
+ <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.292A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z"/>
38
+ </svg>
39
+ </button>
40
+ <button class="btn btn-link btn-sm" (click)="clearChat()" title="清空聊天">
41
+ <svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
42
+ <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
43
+ <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
44
+ </svg>
45
+ </button>
46
+ <button class="btn btn-link btn-sm" (click)="exportChat()" title="导出聊天">
47
+ <svg width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
48
+ <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
49
+ <path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z"/>
50
+ </svg>
51
+ </button>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Messages -->
56
+ <div class="ai-sidebar-messages" #chatContainer (scroll)="onScroll($event)">
57
+ <div *ngFor="let message of messages; let i = index" class="message-item" [ngClass]="message.role">
58
+ <div class="message-avatar">
59
+ <svg *ngIf="message.role === 'user'" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
60
+ <path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/>
61
+ </svg>
62
+ <svg *ngIf="message.role === 'assistant'" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
63
+ <path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5ZM3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.58 26.58 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.933.933 0 0 1-.765.935c-.845.147-2.34.346-4.235.346-1.895 0-3.39-.2-4.235-.346A.933.933 0 0 1 3 9.219V8.062Zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a24.767 24.767 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25.286 25.286 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135Z"/>
64
+ <path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2V1.866ZM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5Z"/>
65
+ </svg>
66
+ </div>
67
+ <div class="message-content">
68
+ <div class="message-header">
69
+ <span class="message-role">
70
+ {{ message.role === 'user' ? '用户' : message.role === 'assistant' ? 'AI' : '系统' }}
71
+ </span>
72
+ <span class="message-time">{{ formatTimestamp(message.timestamp) }}</span>
73
+ </div>
74
+ <div class="message-text" [innerHTML]="formatMessage(message.content)"></div>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Loading indicator -->
79
+ <div *ngIf="isLoading" class="message-item assistant loading">
80
+ <div class="message-avatar">
81
+ <svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
82
+ <path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5ZM3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.58 26.58 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.933.933 0 0 1-.765.935c-.845.147-2.34.346-4.235.346-1.895 0-3.39-.2-4.235-.346A.933.933 0 0 1 3 9.219V8.062Z"/>
83
+ </svg>
84
+ </div>
85
+ <div class="message-content">
86
+ <div class="loading-dots">
87
+ <span></span>
88
+ <span></span>
89
+ <span></span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ <!-- Scroll buttons -->
96
+ <button *ngIf="showScrollTop" class="scroll-btn scroll-top" (click)="scrollToTop()" title="回到顶部">
97
+ <svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
98
+ <path fill-rule="evenodd" d="M7.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1-.708.708L8 5.707l-5.646 5.647a.5.5 0 0 1-.708-.708l6-6z"/>
99
+ </svg>
100
+ </button>
101
+ <button *ngIf="showScrollBottom" class="scroll-btn scroll-bottom" (click)="scrollToBottom()" title="回到底部">
102
+ <svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
103
+ <path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L7.293 8 1.646 2.354a.5.5 0 0 1 0-.708z"/>
104
+ </svg>
105
+ </button>
106
+
107
+ <!-- Input -->
108
+ <div class="ai-sidebar-input">
109
+ <div class="input-container">
110
+ <textarea
111
+ #textInput
112
+ class="message-input"
113
+ [(ngModel)]="inputValue"
114
+ [disabled]="isLoading"
115
+ [placeholder]="isLoading ? 'AI 正在思考...' : '输入您的问题或描述要执行的命令...'"
116
+ (keydown)="onKeydown($event)"
117
+ (input)="onInput($event)"
118
+ (compositionstart)="isComposing = true"
119
+ (compositionend)="isComposing = false"
120
+ rows="1">
121
+ </textarea>
122
+ <button
123
+ class="send-btn"
124
+ [disabled]="!inputValue.trim() || isLoading"
125
+ (click)="submit()"
126
+ title="发送消息">
127
+ <svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
128
+ <path d="M15.964.686a.5.5 0 0 0-.65-.65L.767 5.855H.766l-.452.18a.5.5 0 0 0-.082.887l.41.26.001.002 4.995 3.178 3.178 4.995.002.002.26.41a.5.5 0 0 0 .886-.083l6-15Zm-1.833 1.89L6.637 10.07l-.215-.338a.5.5 0 0 0-.154-.154l-.338-.215 7.494-7.494 1.178-.471-.47 1.178Z"/>
129
+ </svg>
130
+ </button>
131
+ </div>
132
+ <div class="input-footer">
133
+ <small class="char-count" [ngClass]="{ 'warning': isNearLimit(), 'danger': isOverLimit() }">
134
+ {{ inputValue.length }} / {{ charLimit }}
135
+ </small>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ `,
140
+ styles: [`
141
+ :host {
142
+ display: block;
143
+ height: 100%;
144
+ width: 100%;
145
+ }
146
+
147
+ .ai-sidebar-container {
148
+ display: flex;
149
+ flex-direction: column;
150
+ height: 100%;
151
+ background: var(--bs-body-bg, #1e1e1e);
152
+ color: var(--bs-body-color, #e0e0e0);
153
+ overflow: hidden;
154
+ }
155
+
156
+ .ai-sidebar-header {
157
+ display: flex;
158
+ justify-content: space-between;
159
+ align-items: center;
160
+ padding: 12px 16px;
161
+ border-bottom: 1px solid var(--bs-border-color, #333);
162
+ background: var(--bs-tertiary-bg, #252525);
163
+ }
164
+
165
+ .header-title {
166
+ display: flex;
167
+ align-items: center;
168
+ gap: 8px;
169
+ font-weight: 600;
170
+ font-size: 14px;
171
+ }
172
+
173
+ .header-icon {
174
+ color: var(--bs-primary, #0d6efd);
175
+ }
176
+
177
+ .provider-badge {
178
+ padding: 2px 6px;
179
+ background: var(--bs-secondary-bg, #2a2a2a);
180
+ border-radius: 4px;
181
+ font-size: 11px;
182
+ color: var(--bs-secondary-color, #aaa);
183
+ font-weight: normal;
184
+ }
185
+
186
+ .header-actions {
187
+ display: flex;
188
+ gap: 4px;
189
+ }
190
+
191
+ .header-actions .btn-link {
192
+ padding: 4px 8px;
193
+ color: var(--bs-secondary-color, #aaa);
194
+ border: none;
195
+ background: transparent;
196
+ cursor: pointer;
197
+ border-radius: 4px;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ transition: all 0.2s;
202
+ }
203
+
204
+ .header-actions .btn-link:hover {
205
+ color: var(--bs-primary, #0d6efd);
206
+ background: var(--bs-hover-bg, rgba(255, 255, 255, 0.05));
207
+ }
208
+
209
+ .header-actions .btn-close-sidebar {
210
+ margin-right: 4px;
211
+ padding-right: 8px;
212
+ border-right: 1px solid var(--bs-border-color, #333);
213
+ }
214
+
215
+ .header-actions .btn-close-sidebar:hover {
216
+ color: var(--bs-danger, #dc3545);
217
+ }
218
+
219
+ .ai-sidebar-messages {
220
+ flex: 1;
221
+ overflow-y: auto;
222
+ overflow-x: hidden;
223
+ padding: 16px;
224
+ scroll-behavior: smooth;
225
+ }
226
+
227
+ .ai-sidebar-messages::-webkit-scrollbar {
228
+ width: 8px;
229
+ }
230
+
231
+ .ai-sidebar-messages::-webkit-scrollbar-track {
232
+ background: var(--bs-body-bg, #1e1e1e);
233
+ }
234
+
235
+ .ai-sidebar-messages::-webkit-scrollbar-thumb {
236
+ background: var(--bs-border-color, #333);
237
+ border-radius: 4px;
238
+ }
239
+
240
+ .ai-sidebar-messages::-webkit-scrollbar-thumb:hover {
241
+ background: var(--bs-secondary-color, #666);
242
+ }
243
+
244
+ .message-item {
245
+ display: flex;
246
+ gap: 12px;
247
+ margin-bottom: 16px;
248
+ animation: fadeIn 0.3s ease-in;
249
+ }
250
+
251
+ @keyframes fadeIn {
252
+ from {
253
+ opacity: 0;
254
+ transform: translateY(10px);
255
+ }
256
+ to {
257
+ opacity: 1;
258
+ transform: translateY(0);
259
+ }
260
+ }
261
+
262
+ .message-avatar {
263
+ flex-shrink: 0;
264
+ width: 32px;
265
+ height: 32px;
266
+ border-radius: 50%;
267
+ display: flex;
268
+ align-items: center;
269
+ justify-content: center;
270
+ background: var(--bs-secondary-bg, #2a2a2a);
271
+ color: var(--bs-primary, #0d6efd);
272
+ }
273
+
274
+ .message-item.assistant .message-avatar {
275
+ color: var(--bs-primary, #0d6efd);
276
+ }
277
+
278
+ .message-item.user .message-avatar {
279
+ color: var(--bs-success, #28a745);
280
+ }
281
+
282
+ .message-content {
283
+ flex: 1;
284
+ min-width: 0;
285
+ }
286
+
287
+ .message-header {
288
+ display: flex;
289
+ gap: 8px;
290
+ align-items: center;
291
+ margin-bottom: 4px;
292
+ }
293
+
294
+ .message-role {
295
+ font-size: 12px;
296
+ font-weight: 600;
297
+ color: var(--bs-primary, #0d6efd);
298
+ }
299
+
300
+ .message-time {
301
+ font-size: 11px;
302
+ color: var(--bs-secondary-color, #999);
303
+ }
304
+
305
+ .message-text {
306
+ font-size: 13px;
307
+ line-height: 1.6;
308
+ color: var(--bs-body-color, #e0e0e0);
309
+ word-wrap: break-word;
310
+ white-space: pre-wrap;
311
+ }
312
+
313
+ .message-item.loading {
314
+ opacity: 0.7;
315
+ }
316
+
317
+ .loading-dots {
318
+ display: flex;
319
+ gap: 4px;
320
+ padding: 8px 0;
321
+ }
322
+
323
+ .loading-dots span {
324
+ width: 6px;
325
+ height: 6px;
326
+ border-radius: 50%;
327
+ background: var(--bs-primary, #0d6efd);
328
+ animation: bounce 1.4s infinite ease-in-out;
329
+ }
330
+
331
+ .loading-dots span:nth-child(1) {
332
+ animation-delay: -0.32s;
333
+ }
334
+
335
+ .loading-dots span:nth-child(2) {
336
+ animation-delay: -0.16s;
337
+ }
338
+
339
+ @keyframes bounce {
340
+ 0%, 80%, 100% {
341
+ transform: scale(0);
342
+ }
343
+ 40% {
344
+ transform: scale(1);
345
+ }
346
+ }
347
+
348
+ .scroll-btn {
349
+ position: absolute;
350
+ right: 16px;
351
+ width: 32px;
352
+ height: 32px;
353
+ border-radius: 50%;
354
+ border: 1px solid var(--bs-border-color, #333);
355
+ background: var(--bs-body-bg, #1e1e1e);
356
+ color: var(--bs-primary, #0d6efd);
357
+ cursor: pointer;
358
+ display: flex;
359
+ align-items: center;
360
+ justify-content: center;
361
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
362
+ transition: all 0.2s;
363
+ z-index: 10;
364
+ }
365
+
366
+ .scroll-btn:hover {
367
+ background: var(--bs-hover-bg, rgba(255, 255, 255, 0.05));
368
+ transform: translateY(-2px);
369
+ }
370
+
371
+ .scroll-btn.scroll-top {
372
+ bottom: 120px;
373
+ }
374
+
375
+ .scroll-btn.scroll-bottom {
376
+ bottom: 80px;
377
+ }
378
+
379
+ .ai-sidebar-input {
380
+ padding: 12px 16px;
381
+ border-top: 1px solid var(--bs-border-color, #333);
382
+ background: var(--bs-tertiary-bg, #252525);
383
+ }
384
+
385
+ .input-container {
386
+ position: relative;
387
+ display: flex;
388
+ gap: 8px;
389
+ align-items: flex-end;
390
+ }
391
+
392
+ .message-input {
393
+ flex: 1;
394
+ min-height: 38px;
395
+ max-height: 120px;
396
+ padding: 8px 12px;
397
+ border: 1px solid var(--bs-border-color, #333);
398
+ border-radius: 8px;
399
+ background: var(--bs-body-bg, #1e1e1e);
400
+ color: var(--bs-body-color, #e0e0e0);
401
+ font-size: 13px;
402
+ font-family: inherit;
403
+ resize: none;
404
+ outline: none;
405
+ transition: border-color 0.2s;
406
+ }
407
+
408
+ .message-input:focus {
409
+ border-color: var(--bs-primary, #0d6efd);
410
+ }
411
+
412
+ .message-input:disabled {
413
+ opacity: 0.6;
414
+ cursor: not-allowed;
415
+ }
416
+
417
+ .message-input::placeholder {
418
+ color: var(--bs-secondary-color, #999);
419
+ }
420
+
421
+ .send-btn {
422
+ width: 38px;
423
+ height: 38px;
424
+ border-radius: 8px;
425
+ border: 1px solid var(--bs-border-color, #333);
426
+ background: var(--bs-primary, #0d6efd);
427
+ color: white;
428
+ cursor: pointer;
429
+ display: flex;
430
+ align-items: center;
431
+ justify-content: center;
432
+ transition: all 0.2s;
433
+ flex-shrink: 0;
434
+ }
435
+
436
+ .send-btn:hover:not(:disabled) {
437
+ background: var(--bs-primary-hover, #0b5ed7);
438
+ transform: translateY(-1px);
439
+ }
440
+
441
+ .send-btn:disabled {
442
+ opacity: 0.4;
443
+ cursor: not-allowed;
444
+ background: var(--bs-secondary-bg, #2a2a2a);
445
+ }
446
+
447
+ .input-footer {
448
+ margin-top: 6px;
449
+ display: flex;
450
+ justify-content: flex-end;
451
+ }
452
+
453
+ .char-count {
454
+ color: var(--bs-secondary-color, #999);
455
+ font-size: 11px;
456
+ }
457
+
458
+ .char-count.warning {
459
+ color: var(--bs-warning, #ffc107);
460
+ }
461
+
462
+ .char-count.danger {
463
+ color: var(--bs-danger, #dc3545);
464
+ }
465
+
466
+ /* Responsive adjustments */
467
+ @media (max-width: 768px) {
468
+ .ai-sidebar-header {
469
+ padding: 10px 12px;
470
+ }
471
+
472
+ .ai-sidebar-messages {
473
+ padding: 12px;
474
+ }
475
+
476
+ .ai-sidebar-input {
477
+ padding: 10px 12px;
478
+ }
479
+ }
480
+ `]
481
+ })
482
+ export class AiSidebarComponent implements OnInit, OnDestroy, AfterViewChecked {
483
+ @ViewChild('chatContainer') chatContainerRef!: ElementRef;
484
+ @ViewChild('textInput') textInput!: ElementRef<HTMLTextAreaElement>;
485
+
486
+ // 服务引用(由 AiSidebarService 注入)
487
+ public sidebarService!: AiSidebarService;
488
+
489
+ // 组件状态
490
+ messages: ChatMessage[] = [];
491
+ isLoading = false;
492
+ currentProvider: string = '';
493
+ currentSessionId: string = '';
494
+ showScrollTop = false;
495
+ showScrollBottom = false;
496
+ inputValue = '';
497
+ isComposing = false;
498
+ charLimit = 4000;
499
+
500
+ private destroy$ = new Subject<void>();
501
+ private shouldScrollToBottom = false;
502
+
503
+ constructor(
504
+ private aiService: AiAssistantService,
505
+ private config: ConfigProviderService,
506
+ private logger: LoggerService,
507
+ private chatHistory: ChatHistoryService
508
+ ) { }
509
+
510
+ ngOnInit(): void {
511
+ // 生成或加载会话 ID
512
+ this.currentSessionId = this.generateSessionId();
513
+
514
+ // 加载当前提供商信息
515
+ this.loadCurrentProvider();
516
+
517
+ // 加载聊天历史
518
+ this.loadChatHistory();
519
+
520
+ // 发送欢迎消息(仅在没有历史记录时)
521
+ if (this.messages.length === 0) {
522
+ this.sendWelcomeMessage();
523
+ }
524
+
525
+ // 延迟检查滚动状态(等待 DOM 渲染)
526
+ setTimeout(() => this.checkScrollState(), 100);
527
+ }
528
+
529
+ ngOnDestroy(): void {
530
+ // 保存当前会话
531
+ this.saveChatHistory();
532
+ this.destroy$.next();
533
+ this.destroy$.complete();
534
+ }
535
+
536
+ ngAfterViewChecked(): void {
537
+ if (this.shouldScrollToBottom) {
538
+ this.performScrollToBottom();
539
+ this.shouldScrollToBottom = false;
540
+ }
541
+ }
542
+
543
+ /**
544
+ * 加载当前提供商信息
545
+ */
546
+ private loadCurrentProvider(): void {
547
+ const defaultProvider = this.config.getDefaultProvider();
548
+ if (defaultProvider) {
549
+ const providerConfig = this.config.getProviderConfig(defaultProvider);
550
+ this.currentProvider = providerConfig?.displayName || defaultProvider;
551
+ } else {
552
+ // 尝试获取第一个已配置的提供商
553
+ const allConfigs = this.config.getAllProviderConfigs();
554
+ const configuredProviders = Object.keys(allConfigs).filter(k => allConfigs[k]?.apiKey);
555
+ if (configuredProviders.length > 0) {
556
+ const firstProvider = configuredProviders[0];
557
+ const providerConfig = allConfigs[firstProvider];
558
+ this.currentProvider = providerConfig?.displayName || firstProvider;
559
+ this.config.setDefaultProvider(firstProvider);
560
+ } else {
561
+ this.currentProvider = '未配置';
562
+ }
563
+ }
564
+ }
565
+
566
+ /**
567
+ * 加载聊天历史
568
+ */
569
+ private loadChatHistory(): void {
570
+ try {
571
+ // 尝试加载最近的会话
572
+ const recentSessions = this.chatHistory.getRecentSessions(1);
573
+ if (recentSessions.length > 0) {
574
+ const lastSession = recentSessions[0];
575
+ this.currentSessionId = lastSession.sessionId;
576
+ this.messages = lastSession.messages.map(msg => ({
577
+ ...msg,
578
+ timestamp: new Date(msg.timestamp)
579
+ }));
580
+ this.logger.info('Loaded chat history', {
581
+ sessionId: this.currentSessionId,
582
+ messageCount: this.messages.length
583
+ });
584
+ }
585
+ } catch (error) {
586
+ this.logger.error('Failed to load chat history', error);
587
+ this.messages = [];
588
+ }
589
+ }
590
+
591
+ /**
592
+ * 发送欢迎消息
593
+ */
594
+ private sendWelcomeMessage(): void {
595
+ const welcomeMessage: ChatMessage = {
596
+ id: this.generateId(),
597
+ role: MessageRole.ASSISTANT,
598
+ content: `您好!我是AI助手。\n\n我可以帮助您:\n• 将自然语言转换为终端命令\n• 解释复杂的命令\n• 分析命令执行结果\n• 提供错误修复建议\n\n当前使用:${this.currentProvider}\n\n请输入您的问题或描述您想执行的命令。`,
599
+ timestamp: new Date()
600
+ };
601
+ this.messages.push(welcomeMessage);
602
+ }
603
+
604
+ /**
605
+ * 处理发送消息
606
+ */
607
+ async onSendMessage(content: string): Promise<void> {
608
+ if (!content.trim() || this.isLoading) {
609
+ return;
610
+ }
611
+
612
+ // 添加用户消息
613
+ const userMessage: ChatMessage = {
614
+ id: this.generateId(),
615
+ role: MessageRole.USER,
616
+ content: content.trim(),
617
+ timestamp: new Date()
618
+ };
619
+ this.messages.push(userMessage);
620
+
621
+ // 滚动到底部
622
+ setTimeout(() => this.scrollToBottom(), 0);
623
+
624
+ // 清空输入框
625
+ content = '';
626
+
627
+ // 显示加载状态
628
+ this.isLoading = true;
629
+
630
+ try {
631
+ // 发送请求到AI
632
+ const response = await this.aiService.chat({
633
+ messages: this.messages,
634
+ maxTokens: 1000,
635
+ temperature: 0.7
636
+ });
637
+
638
+ // 添加AI响应
639
+ this.messages.push(response.message);
640
+
641
+ // 保存聊天历史
642
+ this.saveChatHistory();
643
+
644
+ } catch (error) {
645
+ this.logger.error('Failed to send message', error);
646
+
647
+ // 添加错误消息
648
+ const errorMessage: ChatMessage = {
649
+ id: this.generateId(),
650
+ role: MessageRole.ASSISTANT,
651
+ content: `抱歉,我遇到了一些问题:${error instanceof Error ? error.message : 'Unknown error'}\n\n请稍后重试。`,
652
+ timestamp: new Date()
653
+ };
654
+ this.messages.push(errorMessage);
655
+ } finally {
656
+ this.isLoading = false;
657
+ // 滚动到底部
658
+ setTimeout(() => this.scrollToBottom(), 0);
659
+ }
660
+ }
661
+
662
+ /**
663
+ * 清空聊天记录
664
+ */
665
+ clearChat(): void {
666
+ if (confirm('确定要清空聊天记录吗?')) {
667
+ // 删除当前会话
668
+ if (this.currentSessionId) {
669
+ this.chatHistory.deleteSession(this.currentSessionId);
670
+ }
671
+ // 创建新会话
672
+ this.currentSessionId = this.generateSessionId();
673
+ this.messages = [];
674
+ this.sendWelcomeMessage();
675
+ this.logger.info('Chat cleared, new session created', { sessionId: this.currentSessionId });
676
+ }
677
+ }
678
+
679
+ /**
680
+ * 导出聊天记录
681
+ */
682
+ exportChat(): void {
683
+ const chatData = {
684
+ provider: this.currentProvider,
685
+ exportTime: new Date().toISOString(),
686
+ messages: this.messages
687
+ };
688
+
689
+ const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' });
690
+ const url = window.URL.createObjectURL(blob);
691
+ const a = document.createElement('a');
692
+ a.href = url;
693
+ a.download = `ai-chat-${new Date().toISOString().slice(0, 10)}.json`;
694
+ a.click();
695
+ window.URL.revokeObjectURL(url);
696
+ }
697
+
698
+ /**
699
+ * 切换提供商
700
+ */
701
+ async switchProvider(): Promise<void> {
702
+ // 从配置服务获取已配置的提供商
703
+ const allConfigs = this.config.getAllProviderConfigs();
704
+ const configuredProviders = Object.keys(allConfigs)
705
+ .filter(key => allConfigs[key] && allConfigs[key].enabled !== false)
706
+ .map(key => ({
707
+ name: key,
708
+ displayName: allConfigs[key].displayName || key
709
+ }));
710
+
711
+ if (configuredProviders.length === 0) {
712
+ alert('没有可用的AI提供商,请先在设置中配置。');
713
+ return;
714
+ }
715
+
716
+ // 构建提供商列表
717
+ const providerList = configuredProviders.map((p, i) =>
718
+ `${i + 1}. ${p.displayName}`
719
+ ).join('\n');
720
+
721
+ const choice = prompt(
722
+ `当前使用: ${this.currentProvider}\n\n可用的AI提供商:\n${providerList}\n\n请输入序号选择提供商:`,
723
+ '1'
724
+ );
725
+
726
+ if (choice) {
727
+ const index = parseInt(choice, 10) - 1;
728
+ if (index >= 0 && index < configuredProviders.length) {
729
+ const selectedProvider = configuredProviders[index];
730
+ this.config.setDefaultProvider(selectedProvider.name);
731
+ this.currentProvider = selectedProvider.displayName;
732
+ this.logger.info('Provider switched', { provider: selectedProvider.name });
733
+
734
+ // 添加系统消息
735
+ const systemMessage: ChatMessage = {
736
+ id: this.generateId(),
737
+ role: MessageRole.SYSTEM,
738
+ content: `已切换到 ${this.currentProvider}`,
739
+ timestamp: new Date()
740
+ };
741
+ this.messages.push(systemMessage);
742
+ } else {
743
+ alert('无效的选择');
744
+ }
745
+ }
746
+ }
747
+
748
+ /**
749
+ * 隐藏侧边栏
750
+ */
751
+ hideSidebar(): void {
752
+ if (this.sidebarService) {
753
+ this.sidebarService.hide();
754
+ }
755
+ }
756
+
757
+ /**
758
+ * 滚动到底部(公开方法)
759
+ */
760
+ scrollToBottom(): void {
761
+ this.shouldScrollToBottom = true;
762
+ }
763
+
764
+ /**
765
+ * 滚动到顶部
766
+ */
767
+ scrollToTop(): void {
768
+ const chatContainer = this.chatContainerRef?.nativeElement;
769
+ if (chatContainer) {
770
+ chatContainer.scrollTo({ top: 0, behavior: 'smooth' });
771
+ }
772
+ }
773
+
774
+ /**
775
+ * 实际执行滚动到底部
776
+ */
777
+ private performScrollToBottom(): void {
778
+ const chatContainer = this.chatContainerRef?.nativeElement;
779
+ if (chatContainer) {
780
+ chatContainer.scrollTo({ top: chatContainer.scrollHeight, behavior: 'smooth' });
781
+ }
782
+ }
783
+
784
+ /**
785
+ * 处理滚动事件
786
+ */
787
+ onScroll(event: Event): void {
788
+ const target = event.target as HTMLElement;
789
+ if (!target) return;
790
+ this.updateScrollButtons(target);
791
+ }
792
+
793
+ /**
794
+ * 检查滚动状态(初始化时调用)
795
+ */
796
+ private checkScrollState(): void {
797
+ const chatContainer = this.chatContainerRef?.nativeElement;
798
+ if (chatContainer) {
799
+ this.updateScrollButtons(chatContainer);
800
+ }
801
+ }
802
+
803
+ /**
804
+ * 更新滚动按钮显示状态
805
+ */
806
+ private updateScrollButtons(container: HTMLElement): void {
807
+ const scrollTop = container.scrollTop;
808
+ const scrollHeight = container.scrollHeight;
809
+ const clientHeight = container.clientHeight;
810
+
811
+ // 判断是否显示滚动按钮
812
+ this.showScrollTop = scrollTop > 50;
813
+ this.showScrollBottom = scrollHeight > clientHeight && scrollTop < scrollHeight - clientHeight - 50;
814
+ }
815
+
816
+ /**
817
+ * 保存聊天历史
818
+ */
819
+ private saveChatHistory(): void {
820
+ try {
821
+ if (this.messages.length > 0 && this.currentSessionId) {
822
+ this.chatHistory.saveSession(this.currentSessionId, this.messages);
823
+ this.logger.info('Chat history saved', {
824
+ sessionId: this.currentSessionId,
825
+ messageCount: this.messages.length
826
+ });
827
+ }
828
+ } catch (error) {
829
+ this.logger.error('Failed to save chat history', error);
830
+ }
831
+ }
832
+
833
+ /**
834
+ * 生成会话 ID
835
+ */
836
+ private generateSessionId(): string {
837
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
838
+ }
839
+
840
+ /**
841
+ * 生成唯一ID
842
+ */
843
+ private generateId(): string {
844
+ return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
845
+ }
846
+
847
+ /**
848
+ * 获取消息时间格式
849
+ */
850
+ formatTimestamp(timestamp: Date): string {
851
+ return timestamp.toLocaleTimeString('zh-CN', {
852
+ hour: '2-digit',
853
+ minute: '2-digit'
854
+ });
855
+ }
856
+
857
+ /**
858
+ * 格式化消息内容(支持换行和基本格式化)
859
+ */
860
+ formatMessage(content: string): string {
861
+ return content
862
+ .replace(/\n/g, '<br>')
863
+ .replace(/•/g, '&#8226;');
864
+ }
865
+
866
+ /**
867
+ * 检查是否为今天的消息
868
+ */
869
+ isToday(date: Date): boolean {
870
+ const today = new Date();
871
+ return date.toDateString() === today.toDateString();
872
+ }
873
+
874
+ /**
875
+ * 检查是否为同一天的消息
876
+ */
877
+ isSameDay(date1: Date, date2: Date): boolean {
878
+ return date1.toDateString() === date2.toDateString();
879
+ }
880
+
881
+ /**
882
+ * 处理键盘事件
883
+ */
884
+ onKeydown(event: KeyboardEvent): void {
885
+ // Enter 发送(不包含Shift)
886
+ if (event.key === 'Enter' && !event.shiftKey && !this.isComposing) {
887
+ event.preventDefault();
888
+ this.submit();
889
+ }
890
+ }
891
+
892
+ /**
893
+ * 处理输入事件
894
+ */
895
+ onInput(event: Event): void {
896
+ const target = event.target as HTMLTextAreaElement;
897
+ this.inputValue = target.value;
898
+ this.autoResize();
899
+ }
900
+
901
+ /**
902
+ * 提交消息
903
+ */
904
+ submit(): void {
905
+ const message = this.inputValue.trim();
906
+ if (message && !this.isLoading) {
907
+ this.onSendMessage(message);
908
+ this.inputValue = '';
909
+ setTimeout(() => this.autoResize(), 0);
910
+ this.textInput?.nativeElement.focus();
911
+ }
912
+ }
913
+
914
+ /**
915
+ * 自动调整输入框高度
916
+ */
917
+ private autoResize(): void {
918
+ if (this.textInput?.nativeElement) {
919
+ const textarea = this.textInput.nativeElement;
920
+ textarea.style.height = 'auto';
921
+ textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
922
+ }
923
+ }
924
+
925
+ /**
926
+ * 获取字符计数
927
+ */
928
+ getCharCount(): number {
929
+ return this.inputValue.length;
930
+ }
931
+
932
+ /**
933
+ * 检查是否接近限制
934
+ */
935
+ isNearLimit(): boolean {
936
+ return this.getCharCount() > this.charLimit * 0.8;
937
+ }
938
+
939
+ /**
940
+ * 检查是否超过限制
941
+ */
942
+ isOverLimit(): boolean {
943
+ return this.getCharCount() > this.charLimit;
944
+ }
945
+ }