diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/common/preferences/ApplicationPreferences.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/common/preferences/ApplicationPreferences.kt index 3c1e767bd..97bce410d 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/common/preferences/ApplicationPreferences.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/common/preferences/ApplicationPreferences.kt @@ -74,6 +74,7 @@ object ApplicationPreferences { private const val SHOW_PLANS_IN_SIDEBAR_KEY = "ui.show_plans_in_sidebar" private const val SHOW_SKILLS_IN_SIDEBAR_KEY = "ui.show_skills_in_sidebar" private const val SHOW_PROJECTS_IN_SIDEBAR_KEY = "ui.show_projects_in_sidebar" + private const val SHOW_TOKEN_USAGE_CARD_KEY = "ui.show_token_usage_card" fun getProjectSidePanelWidth(): Int = safeGetInt(PROJECT_SIDE_PANEL_WIDTH_KEY, DEFAULT_PROJECT_SIDE_PANEL_WIDTH) fun setProjectSidePanelWidth(width: Int) = safePutInt(PROJECT_SIDE_PANEL_WIDTH_KEY, width) @@ -120,6 +121,9 @@ object ApplicationPreferences { fun getShowProjectsInSidebar(): Boolean = safeGetBoolean(SHOW_PROJECTS_IN_SIDEBAR_KEY, true) fun setShowProjectsInSidebar(show: Boolean) = safePutBoolean(SHOW_PROJECTS_IN_SIDEBAR_KEY, show) + fun getShowTokenUsageCard(): Boolean = safeGetBoolean(SHOW_TOKEN_USAGE_CARD_KEY, true) + fun setShowTokenUsageCard(show: Boolean) = safePutBoolean(SHOW_TOKEN_USAGE_CARD_KEY, show) + // ============================================================ // LIFECYCLE // ============================================================ diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/discover/DiscoverView.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/discover/DiscoverView.kt index 5a0856780..cd4cd879f 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/discover/DiscoverView.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/discover/DiscoverView.kt @@ -26,36 +26,48 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.LibraryBooks +import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ChatBubbleOutline import androidx.compose.material.icons.filled.Extension import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Token +import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.askimo.core.AppConstants.DOMAIN import io.askimo.core.chat.domain.ChatSession import io.askimo.core.config.FeatureFlags +import io.askimo.core.i18n.LocalizationManager +import io.askimo.core.telemetry.TelemetryMetrics import io.askimo.core.user.domain.UserProfile import io.askimo.core.util.TimeUtil import io.askimo.ui.common.components.clickableCard @@ -63,11 +75,24 @@ import io.askimo.ui.common.i18n.stringResource import io.askimo.ui.common.theme.AppComponents import io.askimo.ui.common.theme.Spacing import io.askimo.ui.common.theme.ThemePreferences +import io.askimo.ui.common.ui.themedTooltip import io.askimo.ui.session.sessionTooltip import java.awt.Desktop import java.net.URI import java.time.LocalTime +/** + * Abbreviates a token count to a compact, locale-aware string. + * + * Examples (en-US): 1_234_567 → "1.2M", 45_000 → "45.0K", 999 → "999" + * Examples (de-DE): → "1,2M", → "45,0K", 999 → "999" + */ +private fun abbreviateTokens(value: Long): String = when { + value >= 1_000_000 -> "${LocalizationManager.formatDouble(value / 1_000_000.0, 1)}M" + value >= 1_000 -> "${LocalizationManager.formatDouble(value / 1_000.0, 1)}K" + else -> LocalizationManager.formatNumber(value) +} + @Composable fun discoverView( userProfile: UserProfile?, @@ -80,15 +105,17 @@ fun discoverView( onNavigateToPlans: () -> Unit, onNavigateToSkills: () -> Unit, onNavigateToMcpSettings: () -> Unit, + showTokenUsageCard: Boolean, + onToggleTokenUsageCard: (Boolean) -> Unit, + onOpenSystemDiagnostics: () -> Unit, + telemetryMetrics: TelemetryMetrics, modifier: Modifier = Modifier, ) { val scrollState = rememberScrollState() Box(modifier = modifier.fillMaxSize()) { Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState), + modifier = Modifier.fillMaxSize().verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally, ) { Column( @@ -101,6 +128,8 @@ fun discoverView( headerSection( userProfile = userProfile, onNewChat = onNewChat, + showTokenUsageCard = showTokenUsageCard, + onToggleTokenUsageCard = onToggleTokenUsageCard, ) statCardsSection( @@ -118,6 +147,13 @@ fun discoverView( exploreFeaturesSection() + if (showTokenUsageCard) { + tokenUsageSection( + telemetryMetrics = telemetryMetrics, + onOpenSystemDiagnostics = onOpenSystemDiagnostics, + ) + } + recentSessionsSection( sessions = recentSessions, onResumeSession = onResumeSession, @@ -133,10 +169,14 @@ fun discoverView( } } +// ── Header ──────────────────────────────────────────────────────────────────── + @Composable private fun headerSection( userProfile: UserProfile?, onNewChat: () -> Unit, + showTokenUsageCard: Boolean, + onToggleTokenUsageCard: (Boolean) -> Unit, ) { val hour = LocalTime.now().hour val greetingKey = when { @@ -145,32 +185,78 @@ private fun headerSection( else -> "discover.greeting.evening" } val firstName = userProfile?.name?.split(" ")?.firstOrNull() ?: stringResource("user.profile.default_name") + var showCustomizeMenu by remember { mutableStateOf(false) } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Column(verticalArrangement = Arrangement.spacedBy(Spacing.extraSmall)) { - Text( - text = stringResource(greetingKey, firstName), - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onBackground, - ) - } + Text( + text = stringResource(greetingKey, firstName), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + ) - Button( - onClick = onNewChat, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + Row( + horizontalArrangement = Arrangement.spacedBy(Spacing.small), + verticalAlignment = Alignment.CenterVertically, ) { - Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.size(6.dp)) - Text(stringResource("chat.new")) + Box { + IconButton( + onClick = { showCustomizeMenu = true }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) { + Icon( + Icons.Default.Tune, + contentDescription = stringResource("discover.customize"), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + AppComponents.dropdownMenu( + expanded = showCustomizeMenu, + onDismissRequest = { showCustomizeMenu = false }, + ) { + DropdownMenuItem( + text = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource("discover.customize.show_token_usage"), + style = MaterialTheme.typography.bodyMedium, + ) + Spacer(modifier = Modifier.width(Spacing.large)) + Switch( + checked = showTokenUsageCard, + onCheckedChange = { onToggleTokenUsageCard(it) }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) + } + }, + onClick = { onToggleTokenUsageCard(!showTokenUsageCard) }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) + } + } + + Button( + onClick = onNewChat, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) { + Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.size(6.dp)) + Text(stringResource("chat.new")) + } } } } +// ── Stat cards ──────────────────────────────────────────────────────────────── + @Composable private fun statCardsSection( totalChats: Int?, @@ -190,7 +276,7 @@ private fun statCardsSection( ) { statCard( label = stringResource("discover.stat.chats"), - value = totalChats?.toString() ?: "—", + value = totalChats?.let { LocalizationManager.formatNumber(it) } ?: "—", icon = { Icon(Icons.Default.ChatBubbleOutline, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, onClick = onNavigateToSessions, modifier = Modifier.weight(1f), @@ -199,7 +285,7 @@ private fun statCardsSection( if (FeatureFlags.projectsEnabled) { statCard( label = stringResource("discover.stat.projects"), - value = totalProjects?.toString() ?: "—", + value = totalProjects?.let { LocalizationManager.formatNumber(it) } ?: "—", icon = { Icon(Icons.Default.FolderOpen, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, onClick = onNavigateToProjects, modifier = Modifier.weight(1f), @@ -209,7 +295,7 @@ private fun statCardsSection( if (FeatureFlags.mcpIntegrationEnabled) { statCard( label = stringResource("discover.stat.mcp"), - value = totalMcpServers.toString(), + value = LocalizationManager.formatNumber(totalMcpServers), icon = { Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, onClick = onNavigateToMcpSettings, modifier = Modifier.weight(1f), @@ -219,7 +305,7 @@ private fun statCardsSection( if (FeatureFlags.plansEnabled) { statCard( label = stringResource("discover.stat.plans"), - value = totalPlans?.toString() ?: "—", + value = totalPlans?.let { LocalizationManager.formatNumber(it) } ?: "—", icon = { Icon(Icons.Default.PlayCircle, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, onClick = onNavigateToPlans, modifier = Modifier.weight(1f), @@ -229,7 +315,7 @@ private fun statCardsSection( if (FeatureFlags.skillsEnabled) { statCard( label = stringResource("discover.stat.skills"), - value = totalSkills?.toString() ?: "—", + value = totalSkills?.let { LocalizationManager.formatNumber(it) } ?: "—", icon = { Icon(Icons.Default.Extension, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, onClick = onNavigateToSkills, modifier = Modifier.weight(1f), @@ -246,31 +332,12 @@ private fun statCard( modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, ) { - val colors = AppComponents.primaryCardColors() - - clickableCard( - onClick = onClick, - modifier = modifier, - colors = colors, - ) { + clickableCard(onClick = onClick, modifier = modifier, colors = AppComponents.primaryCardColors()) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(Spacing.large), + modifier = Modifier.fillMaxWidth().padding(Spacing.large), verticalArrangement = Arrangement.spacedBy(Spacing.medium), ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier.size(40.dp), - contentAlignment = Alignment.Center, - ) { - icon() - } - } + Box(modifier = Modifier.size(40.dp), contentAlignment = Alignment.Center) { icon() } Text( text = value, style = MaterialTheme.typography.headlineLarge, @@ -286,63 +353,211 @@ private fun statCard( } } +// ── Token Usage section ─────────────────────────────────────────────────────── + /** - * Data class for explore feature card configuration. + * Full-width section with a horizontal bar chart of the top 5 models by token + * usage. All numbers are formatted via [LocalizationManager] so grouping separators + * and decimal marks follow the active locale. Abbreviated counts (K/M) also use + * [LocalizationManager.formatDouble] for locale-correct decimal separators. */ -private data class ExploreCardData( - val icon: ImageVector, - val titleKey: String, - val descKey: String, - val url: String, -) +@Composable +private fun tokenUsageSection( + telemetryMetrics: TelemetryMetrics, + onOpenSystemDiagnostics: () -> Unit, +) { + val totalTokens = telemetryMetrics.totalTokensUsed + val topModels = telemetryMetrics.llmTokensByProvider + .entries + .sortedByDescending { it.value } + .take(5) + + Column(verticalArrangement = Arrangement.spacedBy(Spacing.medium)) { + // ── Section header ───────────────────────────────────────── + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(Spacing.small), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Default.Token, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onBackground, + ) + Text( + text = stringResource("discover.tokens.title"), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onBackground, + ) + } + + if (totalTokens > 0) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + // abbreviateTokens uses LocalizationManager internally + text = stringResource("discover.tokens.total", abbreviateTokens(totalTokens)), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + IconButton( + onClick = onOpenSystemDiagnostics, + modifier = Modifier.size(24.dp).pointerHoverIcon(PointerIcon.Hand), + ) { + Icon( + Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = stringResource("discover.tokens.view_details"), + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + + // ── Chart card ───────────────────────────────────────────── + Surface( + shape = MaterialTheme.shapes.large, + tonalElevation = 1.dp, + modifier = Modifier.fillMaxWidth(), + ) { + if (totalTokens == 0L) { + Box( + modifier = Modifier.fillMaxWidth().padding(Spacing.large), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource("discover.tokens.empty"), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } else { + Column( + modifier = Modifier.fillMaxWidth().padding(Spacing.large), + verticalArrangement = Arrangement.spacedBy(Spacing.medium), + ) { + topModels.forEachIndexed { index, (key, tokens) -> + val parts = key.split(":", limit = 2) + val provider = parts.getOrElse(0) { key } + .replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + val model = parts.getOrElse(1) { "" }.ifBlank { provider } + val fraction = tokens.toFloat() / totalTokens.toFloat() + // Percentage: locale-aware integer, e.g. "62 %" in fr vs "62%" in en + val pctFormatted = LocalizationManager.formatNumber((fraction * 100).toInt()) + "%" + + tokenBarRow( + provider = provider, + model = model, + tokens = tokens, + fraction = fraction, + pctFormatted = pctFormatted, + ) + + if (index < topModels.lastIndex) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f), + ) + } + } + } + } + } + } +} @Composable -private fun exploreFeaturesSection() { - // Build list of enabled explore cards based on feature flags - val enabledCards = buildList { - if (FeatureFlags.mcpIntegrationEnabled) { - add( - ExploreCardData( - icon = Icons.Default.Extension, - titleKey = "discover.explore.mcp.title", - descKey = "discover.explore.mcp.desc", - url = "https://$DOMAIN/docs/desktop/mcp-integration/", - ), - ) +private fun tokenBarRow( + provider: String, + model: String, + tokens: Long, + fraction: Float, + pctFormatted: String, +) { + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(Spacing.medium), + ) { + // Model + provider name — tooltip shows full names if truncated + themedTooltip(text = "$model · $provider") { + Column(modifier = Modifier.width(160.dp)) { + Text( + text = model, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = provider, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } - if (FeatureFlags.ragEnabled) { - add( - ExploreCardData( - icon = Icons.AutoMirrored.Filled.LibraryBooks, - titleKey = "discover.explore.rag.title", - descKey = "discover.explore.rag.desc", - url = "https://$DOMAIN/docs/desktop/rag/", - ), + + // Horizontal bar (track + fill) + Box(modifier = Modifier.weight(1f).height(8.dp)) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(4.dp)), ) - } - if (FeatureFlags.plansEnabled) { - add( - ExploreCardData( - icon = Icons.Default.PlayCircle, - titleKey = "discover.explore.plans.title", - descKey = "discover.explore.plans.desc", - url = "https://$DOMAIN/docs/desktop/plans/", - ), + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(fraction.coerceAtLeast(0.02f)) + .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(4.dp)), ) } - if (FeatureFlags.skillsEnabled) { - add( - ExploreCardData( - icon = Icons.Default.Extension, - titleKey = "discover.explore.skills.title", - descKey = "discover.explore.skills.desc", - url = "https://$DOMAIN/docs/desktop/skills/", - ), + + // Abbreviated token count — tooltip shows exact locale-formatted number + themedTooltip(text = LocalizationManager.formatNumber(tokens)) { + Text( + text = abbreviateTokens(tokens), + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.End, + modifier = Modifier.width(52.dp), ) } + + // Percentage + Text( + text = pctFormatted, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.End, + modifier = Modifier.width(40.dp), + ) } +} - // Don't show section if no cards are enabled +// ── Explore features ────────────────────────────────────────────────────────── + +private data class ExploreCardData(val icon: ImageVector, val titleKey: String, val descKey: String, val url: String) + +@Composable +private fun exploreFeaturesSection() { + val enabledCards = buildList { + if (FeatureFlags.mcpIntegrationEnabled) add(ExploreCardData(Icons.Default.Extension, "discover.explore.mcp.title", "discover.explore.mcp.desc", "https://$DOMAIN/docs/desktop/mcp-integration/")) + if (FeatureFlags.ragEnabled) add(ExploreCardData(Icons.AutoMirrored.Filled.LibraryBooks, "discover.explore.rag.title", "discover.explore.rag.desc", "https://$DOMAIN/docs/desktop/rag/")) + if (FeatureFlags.plansEnabled) add(ExploreCardData(Icons.Default.PlayCircle, "discover.explore.plans.title", "discover.explore.plans.desc", "https://$DOMAIN/docs/desktop/plans/")) + if (FeatureFlags.skillsEnabled) add(ExploreCardData(Icons.Default.Extension, "discover.explore.skills.title", "discover.explore.skills.desc", "https://$DOMAIN/docs/desktop/skills/")) + } if (enabledCards.isEmpty()) return Column(verticalArrangement = Arrangement.spacedBy(Spacing.medium)) { @@ -353,21 +568,12 @@ private fun exploreFeaturesSection() { color = MaterialTheme.colorScheme.onBackground, ) Row( - modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Max), + modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Max), horizontalArrangement = Arrangement.spacedBy(Spacing.large), ) { enabledCards.forEach { card -> exploreCard( - icon = { - Icon( - card.icon, - contentDescription = null, - modifier = Modifier.size(22.dp), - tint = MaterialTheme.colorScheme.onSecondaryContainer, - ) - }, + icon = { Icon(card.icon, contentDescription = null, modifier = Modifier.size(22.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) }, title = stringResource(card.titleKey), description = stringResource(card.descKey), url = card.url, @@ -392,10 +598,7 @@ private fun exploreCard( colors = AppComponents.secondaryCardColors(), ) { Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(Spacing.large), + modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(Spacing.large), verticalArrangement = Arrangement.spacedBy(Spacing.medium), ) { Row( @@ -404,28 +607,16 @@ private fun exploreCard( verticalAlignment = Alignment.Top, ) { icon() - Icon( - Icons.AutoMirrored.Filled.ArrowForward, - contentDescription = null, - modifier = Modifier.size(14.dp), - tint = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.5f), - ) + Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = null, modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.5f)) } - Text( - text = title, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSecondaryContainer, - ) - Text( - text = description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.82f), - ) + Text(text = title, style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSecondaryContainer) + Text(text = description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.82f)) } } } +// ── Recent sessions ─────────────────────────────────────────────────────────── + @Composable private fun recentSessionsSection( sessions: List, @@ -447,21 +638,12 @@ private fun recentSessionsSection( modifier = Modifier.padding(vertical = 8.dp), ) } else { - Surface( - shape = MaterialTheme.shapes.large, - tonalElevation = 1.dp, - modifier = Modifier.fillMaxWidth(), - ) { + Surface(shape = MaterialTheme.shapes.large, tonalElevation = 1.dp, modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) { sessions.forEachIndexed { index, session -> - recentSessionRow( - session = session, - onResumeSession = onResumeSession, - ) + recentSessionRow(session = session, onResumeSession = onResumeSession) if (index < sessions.lastIndex) { - HorizontalDivider( - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), - ) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)) } } } @@ -489,11 +671,7 @@ private fun recentSessionRow( MaterialTheme.colorScheme.surface.copy(alpha = 0f) }, ) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { onResumeSession(session.id) }, - ) + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { onResumeSession(session.id) } .pointerHoverIcon(PointerIcon.Hand) .padding(horizontal = Spacing.large, vertical = 14.dp), horizontalArrangement = Arrangement.SpaceBetween, @@ -504,27 +682,12 @@ private fun recentSessionRow( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f), ) { - Icon( - Icons.Default.ChatBubbleOutline, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(16.dp), - ) + Icon(Icons.Default.ChatBubbleOutline, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(16.dp)) sessionTooltip(session = session) { - Text( - text = session.title, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Text(text = session.title, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis) } } Spacer(modifier = Modifier.width(16.dp)) - Text( - text = TimeUtil.formatDisplay(session.updatedAt), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Text(text = TimeUtil.formatDisplay(session.updatedAt), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } } diff --git a/desktop-shared/src/main/resources/i18n/messages.properties b/desktop-shared/src/main/resources/i18n/messages.properties index dd0891c8c..9584f37cb 100644 --- a/desktop-shared/src/main/resources/i18n/messages.properties +++ b/desktop-shared/src/main/resources/i18n/messages.properties @@ -107,6 +107,12 @@ discover.stat.projects=Projects discover.stat.mcp=MCP Servers discover.stat.plans=Plans discover.stat.skills=Skills +discover.customize=Customize +discover.customize.show_token_usage=Show token usage card +discover.tokens.title=Top Models by Token Usage +discover.tokens.total={0} total +discover.tokens.view_details=View details +discover.tokens.empty=No token usage recorded yet. Start chatting to see stats here. discover.explore.title=Explore Features discover.explore.mcp.title=MCP Servers discover.explore.mcp.desc=Extend Askimo with external tools and data sources via the Model Context Protocol. diff --git a/desktop-shared/src/main/resources/i18n/messages_de.properties b/desktop-shared/src/main/resources/i18n/messages_de.properties index 591145127..b98c61e88 100644 --- a/desktop-shared/src/main/resources/i18n/messages_de.properties +++ b/desktop-shared/src/main/resources/i18n/messages_de.properties @@ -107,6 +107,12 @@ discover.stat.projects=Projekte discover.stat.mcp=MCP-Server discover.stat.plans=Pläne discover.stat.skills=Fähigkeiten +discover.customize=Anpassen +discover.customize.show_token_usage=Token-Nutzungskarte anzeigen +discover.tokens.title=Top-Modelle nach Token-Nutzung +discover.tokens.total={0} insgesamt +discover.tokens.view_details=Details anzeigen +discover.tokens.empty=Noch keine Token-Nutzung aufgezeichnet. Beginnen Sie zu chatten, um hier Statistiken zu sehen. discover.explore.title=Funktionen entdecken discover.explore.mcp.title=MCP-Server discover.explore.mcp.desc=Erweitern Sie Askimo mit externen Tools und Datenquellen über das Model Context Protocol. diff --git a/desktop-shared/src/main/resources/i18n/messages_es.properties b/desktop-shared/src/main/resources/i18n/messages_es.properties index 90b73cc89..48612b57d 100644 --- a/desktop-shared/src/main/resources/i18n/messages_es.properties +++ b/desktop-shared/src/main/resources/i18n/messages_es.properties @@ -108,6 +108,12 @@ discover.stat.projects=Proyectos discover.stat.mcp=Servidores MCP discover.stat.plans=Planes discover.stat.skills=Habilidades +discover.customize=Personalizar +discover.customize.show_token_usage=Mostrar tarjeta de uso de tokens +discover.tokens.title=Modelos principales por uso de tokens +discover.tokens.total={0} en total +discover.tokens.view_details=Ver detalles +discover.tokens.empty=Aún no se ha registrado el uso de tokens. Empiece a chatear para ver las estadísticas aquí. discover.explore.title=Explorar funciones discover.explore.mcp.title=Servidores MCP discover.explore.mcp.desc=Amplía Askimo con herramientas externas y fuentes de datos mediante el Model Context Protocol. diff --git a/desktop-shared/src/main/resources/i18n/messages_fr.properties b/desktop-shared/src/main/resources/i18n/messages_fr.properties index 904c48fb8..c9998d841 100644 --- a/desktop-shared/src/main/resources/i18n/messages_fr.properties +++ b/desktop-shared/src/main/resources/i18n/messages_fr.properties @@ -108,6 +108,12 @@ discover.stat.projects=Projets discover.stat.mcp=Serveurs MCP discover.stat.plans=Plans discover.stat.skills=Compétences +discover.customize=Personnaliser +discover.customize.show_token_usage=Afficher la carte d'utilisation des jetons +discover.tokens.title=Meilleurs modèles par utilisation de jetons +discover.tokens.total={0} au total +discover.tokens.view_details=Voir les détails +discover.tokens.empty=Aucune utilisation de jetons enregistrée pour le moment. Commencez à discuter pour voir les statistiques ici. discover.explore.title=Explorer les fonctionnalités discover.explore.mcp.title=Serveurs MCP discover.explore.mcp.desc=Étendez Askimo avec des outils externes et des sources de données via le Model Context Protocol. diff --git a/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties b/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties index 089ea4593..4b79a0080 100644 --- a/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties +++ b/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties @@ -107,6 +107,12 @@ discover.stat.projects=プロジェクト discover.stat.mcp=MCPサーバー discover.stat.plans=プラン discover.stat.skills=スキル +discover.customize=カスタマイズ +discover.customize.show_token_usage=トークン使用量カードを表示 +discover.tokens.title=トークン使用量別のトップモデル +discover.tokens.total=合計 {0} +discover.tokens.view_details=詳細を表示 +discover.tokens.empty=トークンの使用記録がまだありません。チャットを開始して統計を表示してください。 discover.explore.title=機能を探索 discover.explore.mcp.title=MCPサーバー discover.explore.mcp.desc=Model Context Protocolを介して、外部ツールやデータソースでAskimoを拡張します。 diff --git a/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties b/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties index 81f10aaa4..9001d8c98 100644 --- a/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties +++ b/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties @@ -108,6 +108,12 @@ discover.stat.projects=프로젝트 discover.stat.mcp=MCP 서버 discover.stat.plans=플랜 discover.stat.skills=스킬 +discover.customize=사용자 지정 +discover.customize.show_token_usage=토큰 사용량 카드 표시 +discover.tokens.title=토큰 사용량 기준 상위 모델 +discover.tokens.total=총 {0} +discover.tokens.view_details=세부 정보 보기 +discover.tokens.empty=아직 기록된 토큰 사용량이 없습니다. 채팅을 시작하여 여기에서 통계를 확인하세요. discover.explore.title=기능 살펴보기 discover.explore.mcp.title=MCP 서버 discover.explore.mcp.desc=Model Context Protocol을 통해 외부 도구와 데이터 소스로 Askimo를 확장하세요. diff --git a/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties b/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties index 2688ae21b..a6aeea888 100644 --- a/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties +++ b/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties @@ -107,6 +107,12 @@ discover.stat.projects=Projetos discover.stat.mcp=Servidores MCP discover.stat.plans=Planos discover.stat.skills=Habilidades +discover.customize=Personalizar +discover.customize.show_token_usage=Mostrar cartão de uso de tokens +discover.tokens.title=Principais modelos por uso de tokens +discover.tokens.total={0} no total +discover.tokens.view_details=Ver detalhes +discover.tokens.empty=Nenhum uso de token registrado ainda. Comece a conversar para ver as estatísticas aqui. discover.explore.title=Explorar funcionalidades discover.explore.mcp.title=Servidores MCP discover.explore.mcp.desc=Expanda o Askimo com ferramentas externas e fontes de dados através do Model Context Protocol. diff --git a/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties b/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties index fc367de11..e5df301f8 100644 --- a/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties +++ b/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties @@ -117,6 +117,12 @@ discover.stat.projects=Dự án discover.stat.mcp=Máy chủ MCP discover.stat.plans=Kế hoạch discover.stat.skills=Kỹ năng +discover.customize=Tùy chỉnh +discover.customize.show_token_usage=Hiển thị thẻ sử dụng token +discover.tokens.title=Các model hàng đầu theo mức sử dụng token +discover.tokens.total=Tổng {0} +discover.tokens.view_details=Xem chi tiết +discover.tokens.empty=Chưa có dữ liệu sử dụng token. Hãy bắt đầu trò chuyện để xem thống kê tại đây. discover.explore.title=Khám phá tính năng discover.explore.mcp.title=Máy chủ MCP discover.explore.mcp.desc=Mở rộng Askimo bằng các công cụ bên ngoài và nguồn dữ liệu thông qua Model Context Protocol. diff --git a/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties b/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties index a9eb67253..e199022c7 100644 --- a/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties +++ b/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties @@ -107,6 +107,12 @@ discover.stat.projects=项目 discover.stat.mcp=MCP 服务器 discover.stat.plans=计划 discover.stat.skills=技能 +discover.customize=自定义 +discover.customize.show_token_usage=显示 Token 使用情况卡片 +discover.tokens.title=按 Token 使用量排名的模型 +discover.tokens.total=共 {0} +discover.tokens.view_details=查看详情 +discover.tokens.empty=暂无 Token 使用记录。开始聊天以在此处查看统计信息。 discover.explore.title=探索功能 discover.explore.mcp.title=MCP 服务器 discover.explore.mcp.desc=通过 Model Context Protocol,使用外部工具和数据源扩展 Askimo。 diff --git a/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties b/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties index 5e9de8b45..ab4bf799a 100644 --- a/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties +++ b/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties @@ -107,6 +107,12 @@ discover.stat.projects=專案 discover.stat.mcp=MCP 伺服器 discover.stat.plans=計劃 discover.stat.skills=技能 +discover.customize=自訂 +discover.customize.show_token_usage=顯示 Token 使用量卡片 +discover.tokens.title=按 Token 使用量排名的模型 +discover.tokens.total=共 {0} +discover.tokens.view_details=查看詳情 +discover.tokens.empty=暫無 Token 使用記錄。開始聊天以在此處查看統計資訊。 discover.explore.title=探索功能 discover.explore.mcp.title=MCP 伺服器 discover.explore.mcp.desc=透過 Model Context Protocol,使用外部工具和資料來源擴充 Askimo。 diff --git a/desktop/src/main/kotlin/io/askimo/desktop/Main.kt b/desktop/src/main/kotlin/io/askimo/desktop/Main.kt index da4b3d55a..20a680cc5 100644 --- a/desktop/src/main/kotlin/io/askimo/desktop/Main.kt +++ b/desktop/src/main/kotlin/io/askimo/desktop/Main.kt @@ -318,6 +318,7 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? = var showPlansInSidebar by remember { mutableStateOf(ApplicationPreferences.getShowPlansInSidebar()) } var showSkillsInSidebar by remember { mutableStateOf(ApplicationPreferences.getShowSkillsInSidebar()) } var showProjectsInSidebar by remember { mutableStateOf(ApplicationPreferences.getShowProjectsInSidebar()) } + var showTokenUsageCard by remember { mutableStateOf(ApplicationPreferences.getShowTokenUsageCard()) } var selectedProjectId by remember { mutableStateOf(null) } var sidebarWidthFraction by remember { mutableStateOf(ThemePreferences.getMainSidebarWidthFraction()) } var showQuitDialog by remember { mutableStateOf(false) } @@ -1140,6 +1141,14 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? = userAvatarPath = userProfile?.preferences?.get("avatarPath"), userProfile = userProfile, discoverViewModel = discoverViewModel, + showTokenUsageCard = showTokenUsageCard, + onToggleTokenUsageCard = { enabled -> + showTokenUsageCard = enabled + ApplicationPreferences.setShowTokenUsageCard(enabled) + }, + onOpenSystemDiagnostics = { + showSystemDiagnosticsDialog = true + }, onNavigateToMcpSettings = { settingsSection = SettingsSection.MCP_SERVERS previousView = currentView @@ -1961,7 +1970,11 @@ fun mainContent( userAvatarPath: String? = null, userProfile: UserProfile? = null, discoverViewModel: DiscoverViewModel, + showTokenUsageCard: Boolean = true, + onToggleTokenUsageCard: (Boolean) -> Unit = {}, + onOpenSystemDiagnostics: () -> Unit = {}, ) { + val discoverMetrics by appContext.telemetry.metricsFlow.collectAsState() Box( modifier = Modifier .fillMaxSize() @@ -1985,6 +1998,10 @@ fun mainContent( onNavigateToPlans = onNavigateToPlans, onNavigateToSkills = onNavigateToSkills, onNavigateToMcpSettings = onNavigateToMcpSettings, + showTokenUsageCard = showTokenUsageCard, + onToggleTokenUsageCard = onToggleTokenUsageCard, + onOpenSystemDiagnostics = onOpenSystemDiagnostics, + telemetryMetrics = discoverMetrics, modifier = Modifier.fillMaxSize(), )