Skip to content

Commit 3b00728

Browse files
committed
refactor(ui): extract TokenUsagePanel to MVVM pattern
Separate UI concerns from business logic by introducing TokenUsageViewModel and TokenUsageUIComponents. This improves maintainability and testability by decoupling data management from UI rendering.
1 parent 7f45f07 commit 3b00728

File tree

2 files changed

+272
-137
lines changed

2 files changed

+272
-137
lines changed
Lines changed: 158 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
package cc.unitmesh.devti.gui.chat.ui
22

3-
import cc.unitmesh.devti.llm2.TokenUsageEvent
4-
import cc.unitmesh.devti.llm2.TokenUsageListener
3+
import cc.unitmesh.devti.gui.chat.ui.TokenUsageViewModel.TokenUsageData
54
import cc.unitmesh.devti.llms.custom.Usage
6-
import cc.unitmesh.devti.settings.AutoDevSettingsState
7-
import cc.unitmesh.devti.settings.model.LLMModelManager
8-
import com.intellij.openapi.application.ApplicationManager
95
import com.intellij.openapi.project.Project
106
import com.intellij.ui.JBColor
117
import com.intellij.ui.components.JBLabel
@@ -22,177 +18,202 @@ import javax.swing.SwingConstants
2218

2319
/**
2420
* Panel that displays token usage statistics for the current session
21+
* Refactored to separate UI concerns from business logic
2522
*/
2623
class TokenUsagePanel(private val project: Project) : BorderLayoutPanel() {
27-
private val modelLabel = JBLabel("", SwingConstants.LEFT)
28-
private val progressBar = JProgressBar(0, 100)
29-
private val usageRatioLabel = JBLabel("", SwingConstants.CENTER)
30-
31-
private var currentUsage = Usage()
32-
private var currentModel: String? = null
33-
private var maxContextWindowTokens: Long = 0
34-
24+
private val uiComponents = TokenUsageUIComponents()
25+
private val viewModel = TokenUsageViewModel(project)
26+
27+
private var currentData: TokenUsageData? = null
28+
3529
init {
3630
setupUI()
37-
setupTokenUsageListener()
31+
setupViewModel()
3832
}
39-
33+
4034
private fun setupUI() {
4135
isOpaque = false
4236
border = JBUI.Borders.empty(4, 8)
43-
44-
progressBar.apply {
45-
isStringPainted = false
46-
preferredSize = java.awt.Dimension(150, 8)
47-
minimumSize = java.awt.Dimension(100, 8)
48-
font = font.deriveFont(Font.PLAIN, 10f)
49-
isOpaque = false
50-
}
51-
52-
usageRatioLabel.apply {
53-
font = font.deriveFont(Font.PLAIN, 10f)
54-
foreground = UIUtil.getContextHelpForeground()
55-
}
56-
37+
38+
val mainPanel = createMainPanel()
39+
addToCenter(mainPanel)
40+
41+
isVisible = false
42+
}
43+
44+
private fun createMainPanel(): JPanel {
5745
val mainPanel = JPanel(GridBagLayout())
5846
mainPanel.isOpaque = false
59-
47+
6048
val gbc = GridBagConstraints()
61-
49+
50+
// Model label (left)
6251
gbc.gridx = 0
6352
gbc.gridy = 0
6453
gbc.anchor = GridBagConstraints.WEST
6554
gbc.fill = GridBagConstraints.NONE
66-
// Create left panel for model info
67-
val leftPanel = JPanel(BorderLayout())
68-
leftPanel.isOpaque = false
69-
modelLabel.font = modelLabel.font.deriveFont(Font.PLAIN, 11f)
70-
modelLabel.foreground = UIUtil.getContextHelpForeground()
71-
leftPanel.add(modelLabel, BorderLayout.WEST)
72-
73-
mainPanel.add(leftPanel, gbc)
74-
75-
// Progress bar and ratio in the middle
55+
mainPanel.add(uiComponents.createModelLabelPanel(), gbc)
56+
57+
// Progress bar (center)
7658
gbc.gridx = 1
7759
gbc.weightx = 0.9
7860
gbc.fill = GridBagConstraints.HORIZONTAL
7961
gbc.insets = JBUI.insets(0, 8)
80-
81-
val progressPanel = JPanel(BorderLayout())
82-
progressPanel.isOpaque = false
83-
progressPanel.add(progressBar, BorderLayout.CENTER)
84-
85-
mainPanel.add(progressPanel, gbc)
86-
87-
// Right panel for token count display (10% width)
88-
val rightPanel = JPanel()
89-
rightPanel.isOpaque = false
90-
91-
// Add usage ratio label to the right panel
92-
usageRatioLabel.horizontalAlignment = SwingConstants.RIGHT
93-
rightPanel.add(usageRatioLabel)
94-
62+
mainPanel.add(uiComponents.createProgressPanel(), gbc)
63+
64+
// Usage ratio label (right)
9565
gbc.gridx = 2
9666
gbc.weightx = 0.1
9767
gbc.fill = GridBagConstraints.NONE
9868
gbc.anchor = GridBagConstraints.EAST
9969
gbc.insets = JBUI.emptyInsets()
100-
mainPanel.add(rightPanel, gbc)
101-
102-
// Add panels to main layout
103-
addToCenter(mainPanel)
104-
105-
// Initially hidden
106-
isVisible = false
70+
mainPanel.add(uiComponents.createUsageRatioPanel(), gbc)
71+
72+
return mainPanel
10773
}
108-
109-
private fun setupTokenUsageListener() {
110-
val messageBus = ApplicationManager.getApplication().messageBus
111-
messageBus.connect().subscribe(TokenUsageListener.TOPIC, object : TokenUsageListener {
112-
override fun onTokenUsage(event: TokenUsageEvent) {
113-
updateTokenUsage(event)
114-
}
115-
})
116-
}
117-
118-
private fun updateTokenUsage(event: TokenUsageEvent) {
119-
ApplicationManager.getApplication().invokeLater {
120-
currentUsage = event.usage
121-
currentModel = event.model
122-
123-
updateMaxTokens()
124-
updateProgressBar(event.usage.totalTokens ?: 0)
125-
126-
if (!event.model.isNullOrBlank()) {
127-
modelLabel.text = "Model: ${event.model}"
128-
}
129-
130-
isVisible = true
131-
revalidate()
132-
repaint()
74+
75+
private fun setupViewModel() {
76+
viewModel.setOnTokenUsageUpdated { data ->
77+
updateUI(data)
13378
}
13479
}
135-
136-
private fun updateMaxTokens() {
137-
try {
138-
val settings = AutoDevSettingsState.getInstance()
139-
val modelManager = LLMModelManager(project, settings) {}
140-
val limits = modelManager.getUsedMaxToken()
141-
maxContextWindowTokens = limits.maxContextWindowTokens?.toLong() ?: 0
142-
} catch (e: Exception) {
143-
maxContextWindowTokens = 4096
80+
81+
private fun updateUI(data: TokenUsageData) {
82+
currentData = data
83+
84+
// Update model label
85+
val modelText = if (!data.model.isNullOrBlank()) {
86+
"Model: ${data.model}"
87+
} else {
88+
""
14489
}
145-
}
146-
147-
private fun updateProgressBar(totalTokens: Long) {
148-
if (maxContextWindowTokens <= 0) {
149-
progressBar.isVisible = false
150-
usageRatioLabel.isVisible = false
151-
return
90+
uiComponents.updateModelLabel(modelText)
91+
92+
// Update progress bar and ratio
93+
if (data.maxContextWindowTokens > 0) {
94+
val totalTokens = data.usage.totalTokens
95+
val usageRatio = (data.usageRatio * 100).toInt().coerceIn(0, 100)
96+
97+
uiComponents.updateProgressBar(usageRatio, createProgressBarColor(usageRatio))
98+
uiComponents.updateUsageRatioLabel(
99+
createUsageRatioText(
100+
totalTokens,
101+
data.maxContextWindowTokens,
102+
usageRatio
103+
)
104+
)
105+
uiComponents.setProgressBarTooltip("Token usage: $usageRatio% of context window")
106+
uiComponents.setProgressComponentsVisible(true)
107+
} else {
108+
uiComponents.setProgressComponentsVisible(false)
152109
}
153-
154-
val usageRatio = (totalTokens.toDouble() / maxContextWindowTokens * 100).toInt()
155-
progressBar.value = usageRatio.coerceIn(0, 100)
156-
157-
progressBar.foreground = when {
110+
111+
// Update panel visibility
112+
isVisible = data.isVisible
113+
revalidate()
114+
repaint()
115+
}
116+
117+
private fun createProgressBarColor(usageRatio: Int): JBColor {
118+
return when {
158119
usageRatio >= 90 -> JBColor.RED
159120
usageRatio >= 75 -> JBColor.ORANGE
160121
usageRatio >= 50 -> JBColor.YELLOW
161122
usageRatio >= 25 -> JBColor.GREEN
162-
else -> UIUtil.getPanelBackground().brighter()
163-
}
164-
165-
usageRatioLabel.text = "${formatTokenCount(totalTokens)}/${formatTokenCount(maxContextWindowTokens)} (${usageRatio}%)"
166-
progressBar.isVisible = true
167-
usageRatioLabel.isVisible = true
168-
progressBar.toolTipText = "Token usage: $usageRatio% of context window"
169-
}
170-
171-
private fun formatTokenCount(count: Long): String {
172-
return when {
173-
count >= 1_000_000 -> String.format("%.1fM", count / 1_000_000.0)
174-
count >= 1_000 -> String.format("%.1fK", count / 1_000.0)
175-
else -> count.toString()
123+
else -> UIUtil.getPanelBackground() as JBColor
176124
}
177125
}
178126

127+
private fun createUsageRatioText(totalTokens: Long, maxTokens: Long, usageRatio: Int): String {
128+
return "${TokenUsageViewModel.formatTokenCount(totalTokens)}/${TokenUsageViewModel.formatTokenCount(maxTokens)} (${usageRatio}%)"
129+
}
130+
179131
fun reset() {
180-
ApplicationManager.getApplication().invokeLater {
181-
currentUsage = Usage()
182-
currentModel = null
183-
maxContextWindowTokens = 0
184-
modelLabel.text = ""
185-
progressBar.value = 0
186-
progressBar.isVisible = false
187-
usageRatioLabel.text = ""
188-
usageRatioLabel.isVisible = false
189-
isVisible = false
190-
revalidate()
191-
repaint()
132+
viewModel.reset()
133+
}
134+
135+
fun dispose() {
136+
viewModel.dispose()
137+
}
138+
}
139+
140+
/**
141+
* Encapsulates UI component creation and management
142+
* Separates UI component logic from main panel logic
143+
*/
144+
private class TokenUsageUIComponents {
145+
val modelLabel = JBLabel("", SwingConstants.LEFT)
146+
val progressBar = JProgressBar(0, 100)
147+
val usageRatioLabel = JBLabel("", SwingConstants.CENTER)
148+
149+
init {
150+
setupComponents()
151+
}
152+
153+
private fun setupComponents() {
154+
// Setup progress bar
155+
progressBar.apply {
156+
isStringPainted = false
157+
preferredSize = java.awt.Dimension(150, 8)
158+
minimumSize = java.awt.Dimension(100, 8)
159+
font = font.deriveFont(Font.PLAIN, 10f)
160+
isOpaque = false
161+
}
162+
163+
// Setup usage ratio label
164+
usageRatioLabel.apply {
165+
font = font.deriveFont(Font.PLAIN, 10f)
166+
foreground = UIUtil.getContextHelpForeground()
167+
horizontalAlignment = SwingConstants.RIGHT
168+
}
169+
170+
// Setup model label
171+
modelLabel.apply {
172+
font = font.deriveFont(Font.PLAIN, 11f)
173+
foreground = UIUtil.getContextHelpForeground()
192174
}
193175
}
194176

195-
fun getCurrentUsage(): Usage = currentUsage
177+
fun createModelLabelPanel(): JPanel {
178+
val leftPanel = JPanel(BorderLayout())
179+
leftPanel.isOpaque = false
180+
leftPanel.add(modelLabel, BorderLayout.WEST)
181+
return leftPanel
182+
}
183+
184+
fun createProgressPanel(): JPanel {
185+
val progressPanel = JPanel(BorderLayout())
186+
progressPanel.isOpaque = false
187+
progressPanel.add(progressBar, BorderLayout.CENTER)
188+
return progressPanel
189+
}
190+
191+
fun createUsageRatioPanel(): JPanel {
192+
val rightPanel = JPanel()
193+
rightPanel.isOpaque = false
194+
rightPanel.add(usageRatioLabel)
195+
return rightPanel
196+
}
197+
198+
fun updateModelLabel(text: String) {
199+
modelLabel.text = text
200+
}
201+
202+
fun updateProgressBar(value: Int, color: JBColor) {
203+
progressBar.value = value
204+
progressBar.foreground = color
205+
}
206+
207+
fun updateUsageRatioLabel(text: String) {
208+
usageRatioLabel.text = text
209+
}
196210

197-
fun getCurrentModel(): String? = currentModel
211+
fun setProgressBarTooltip(tooltip: String) {
212+
progressBar.toolTipText = tooltip
213+
}
214+
215+
fun setProgressComponentsVisible(visible: Boolean) {
216+
progressBar.isVisible = visible
217+
usageRatioLabel.isVisible = visible
218+
}
198219
}

0 commit comments

Comments
 (0)