From 42eebfd4683827169868e049fdaca525ff5f4cdd Mon Sep 17 00:00:00 2001 From: Hai Nguyen <3423575+haiphucnguyen@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:58:40 -0700 Subject: [PATCH] Update --- .../core/chat/service/ChatSessionServiceIT.kt | 17 +- .../io/askimo/ui/discover/DiscoverView.kt | 26 +- .../io/askimo/ui/shell/NativeMenuBar.kt | 40 +- .../io/askimo/ui/shell/StarPromptDialog.kt | 502 ++++++++++++++---- .../main/resources/i18n/messages.properties | 39 +- .../resources/i18n/messages_de.properties | 39 +- .../resources/i18n/messages_es.properties | 40 +- .../resources/i18n/messages_fr.properties | 40 +- .../resources/i18n/messages_ja_JP.properties | 39 +- .../resources/i18n/messages_ko_KR.properties | 40 +- .../resources/i18n/messages_pt_BR.properties | 39 +- .../resources/i18n/messages_vi_VN.properties | 39 +- .../resources/i18n/messages_zh_CN.properties | 39 +- .../resources/i18n/messages_zh_TW.properties | 39 +- .../src/main/kotlin/io/askimo/desktop/Main.kt | 47 +- .../io/askimo/core/analytics/Analytics.kt | 50 ++ .../askimo/core/analytics/AnalyticsEvent.kt | 9 + .../core/chat/service/ChatSessionService.kt | 13 +- 18 files changed, 889 insertions(+), 208 deletions(-) diff --git a/cli/src/test/kotlin/io/askimo/core/chat/service/ChatSessionServiceIT.kt b/cli/src/test/kotlin/io/askimo/core/chat/service/ChatSessionServiceIT.kt index 76bd31ed6..24fb51197 100644 --- a/cli/src/test/kotlin/io/askimo/core/chat/service/ChatSessionServiceIT.kt +++ b/cli/src/test/kotlin/io/askimo/core/chat/service/ChatSessionServiceIT.kt @@ -760,21 +760,24 @@ class ChatSessionServiceIT { fun `forkSession should not copy outdated messages`() { val baseTime = Instant.now() val session = sessionRepository.createSession(ChatSession(id = "", title = "Outdated Test")) - service.addMessage(ChatMessage(id = "", sessionId = session.id, role = MessageRole.USER, content = "Q1", createdAt = baseTime)) - val outdatedAi = service.addMessage(ChatMessage(id = "", sessionId = session.id, role = MessageRole.ASSISTANT, content = "Old answer", createdAt = baseTime.plusSeconds(1))) - // Mark the outdated branch - service.markMessagesAsOutdatedAfter(session.id, outdatedAi.id) - messageRepository.markMessageAsOutdated(outdatedAi.id) - // New active branch + // Simulate a user edit: Q1 is sent, AI replies "Old answer", user edits Q1 which + // marks Q1 and everything after it (including "Old answer") as outdated via greaterEq. + val q1 = service.addMessage(ChatMessage(id = "", sessionId = session.id, role = MessageRole.USER, content = "Q1", createdAt = baseTime)) + service.addMessage(ChatMessage(id = "", sessionId = session.id, role = MessageRole.ASSISTANT, content = "Old answer", createdAt = baseTime.plusSeconds(1))) + // Mark the outdated branch: calling with q1.id marks Q1 (createdAt = baseTime) AND + // "Old answer" (createdAt > baseTime) as outdated — matching the real edit flow. + service.markMessagesAsOutdatedAfter(session.id, q1.id) + // New active branch after the edit service.addMessage(ChatMessage(id = "", sessionId = session.id, role = MessageRole.USER, content = "Q2", createdAt = baseTime.plusSeconds(2))) val activeAi = service.addMessage(ChatMessage(id = "", sessionId = session.id, role = MessageRole.ASSISTANT, content = "New answer", createdAt = baseTime.plusSeconds(3))) val forked = service.forkSession(session.id, activeAi.id) val forkedMessages = service.getMessages(forked.id) - // Only the two active messages should be copied + // Only the two active messages should be copied (Q1 and "Old answer" are both outdated) assertEquals(2, forkedMessages.size) assertFalse(forkedMessages.any { it.content == "Old answer" }) + assertFalse(forkedMessages.any { it.content == "Q1" }) assertTrue(forkedMessages.any { it.content == "Q2" }) assertTrue(forkedMessages.any { it.content == "New answer" }) } 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 cd4cd879f..79295f2d5 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 @@ -277,7 +277,7 @@ private fun statCardsSection( statCard( label = stringResource("discover.stat.chats"), value = totalChats?.let { LocalizationManager.formatNumber(it) } ?: "—", - icon = { Icon(Icons.Default.ChatBubbleOutline, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, + icon = { Icon(Icons.Default.ChatBubbleOutline, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = onNavigateToSessions, modifier = Modifier.weight(1f), ) @@ -286,7 +286,7 @@ private fun statCardsSection( statCard( label = stringResource("discover.stat.projects"), value = totalProjects?.let { LocalizationManager.formatNumber(it) } ?: "—", - icon = { Icon(Icons.Default.FolderOpen, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, + icon = { Icon(Icons.Default.FolderOpen, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = onNavigateToProjects, modifier = Modifier.weight(1f), ) @@ -296,7 +296,7 @@ private fun statCardsSection( statCard( label = stringResource("discover.stat.mcp"), value = LocalizationManager.formatNumber(totalMcpServers), - icon = { Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, + icon = { Icon(Icons.Default.Settings, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = onNavigateToMcpSettings, modifier = Modifier.weight(1f), ) @@ -306,7 +306,7 @@ private fun statCardsSection( statCard( label = stringResource("discover.stat.plans"), value = totalPlans?.let { LocalizationManager.formatNumber(it) } ?: "—", - icon = { Icon(Icons.Default.PlayCircle, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, + icon = { Icon(Icons.Default.PlayCircle, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = onNavigateToPlans, modifier = Modifier.weight(1f), ) @@ -316,7 +316,7 @@ private fun statCardsSection( statCard( label = stringResource("discover.stat.skills"), value = totalSkills?.let { LocalizationManager.formatNumber(it) } ?: "—", - icon = { Icon(Icons.Default.Extension, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onPrimaryContainer) }, + icon = { Icon(Icons.Default.Extension, contentDescription = null, modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = onNavigateToSkills, modifier = Modifier.weight(1f), ) @@ -332,7 +332,7 @@ private fun statCard( modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, ) { - clickableCard(onClick = onClick, modifier = modifier, colors = AppComponents.primaryCardColors()) { + clickableCard(onClick = onClick, modifier = modifier, colors = AppComponents.secondaryCardColors()) { Column( modifier = Modifier.fillMaxWidth().padding(Spacing.large), verticalArrangement = Arrangement.spacedBy(Spacing.medium), @@ -342,12 +342,12 @@ private fun statCard( text = value, style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onPrimaryContainer, + color = MaterialTheme.colorScheme.onSecondaryContainer, ) Text( text = label, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.82f), + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.82f), ) } } @@ -573,7 +573,7 @@ private fun exploreFeaturesSection() { ) { 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.onSurfaceVariant) }, title = stringResource(card.titleKey), description = stringResource(card.descKey), url = card.url, @@ -595,7 +595,7 @@ private fun exploreCard( clickableCard( onClick = { runCatching { Desktop.getDesktop().browse(URI(url)) } }, modifier = modifier, - colors = AppComponents.secondaryCardColors(), + colors = AppComponents.surfaceVariantCardColors(), ) { Column( modifier = Modifier.fillMaxWidth().fillMaxHeight().padding(Spacing.large), @@ -607,10 +607,10 @@ 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.onSurfaceVariant.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.onSurfaceVariant) + Text(text = description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.82f)) } } } diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/shell/NativeMenuBar.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/shell/NativeMenuBar.kt index 7916ca002..34259317d 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/shell/NativeMenuBar.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/shell/NativeMenuBar.kt @@ -92,11 +92,13 @@ object NativeMenuBar { isProjectsVisible: Boolean = true, onShowSystemDiagnostics: () -> Unit = {}, onNavigateToBookmarks: () -> Unit, + onSupportAskimo: () -> Unit = {}, + onShareFeedback: () -> Unit = {}, ) { val window = frameWindowScope.window // Setup AWT menu bar for all platforms (includes Documentation) - setupAWTMenuBar(window, onShowAbout, onNewChat, onNewProject, onSearchInSessions, onShowSettings, onShowEventLog, onCheckForUpdates, onEnterFullScreen, onNavigateToSessions, onNavigateToProjects, onNavigateToDiscover, onToggleSidebar, onInvalidateCaches, onExportBackup, onImportBackup, onShowGettingStarted, onOpenTerminal, onClearPreferences, onClearAccountPreferences, onTogglePlans, onToggleSkills, onToggleProjects, isPlansVisible, isSkillsVisible, isProjectsVisible, onShowSystemDiagnostics, onNavigateToBookmarks) + setupAWTMenuBar(window, onShowAbout, onNewChat, onNewProject, onSearchInSessions, onShowSettings, onShowEventLog, onCheckForUpdates, onEnterFullScreen, onNavigateToSessions, onNavigateToProjects, onNavigateToDiscover, onToggleSidebar, onInvalidateCaches, onExportBackup, onImportBackup, onShowGettingStarted, onOpenTerminal, onClearPreferences, onClearAccountPreferences, onTogglePlans, onToggleSkills, onToggleProjects, isPlansVisible, isSkillsVisible, isProjectsVisible, onShowSystemDiagnostics, onNavigateToBookmarks, onSupportAskimo, onShareFeedback) // On macOS, also register the About handler for the app menu if (Platform.isMac) { @@ -147,6 +149,8 @@ object NativeMenuBar { isProjectsVisible: Boolean, onShowSystemDiagnostics: () -> Unit, onNavigateToBookmarks: () -> Unit, + onSupportAskimo: () -> Unit, + onShareFeedback: () -> Unit, ) { if (window is Frame) { val menuBar = MenuBar() @@ -395,14 +399,10 @@ object NativeMenuBar { } helpMenu.add(gettingStartedItem) - // Share Feedback — moved here from the footer status bar for better discoverability + // Share Feedback — opens the inline feedback dialog val shareFeedbackItem = MenuItem(menuLabel("system.share.feedback")) shareFeedbackItem.addActionListener { - runCatching { - if (Desktop.isDesktopSupported()) { - Desktop.getDesktop().browse(URI("https://$DOMAIN/contact/")) - } - } + onShareFeedback() } helpMenu.add(shareFeedbackItem) @@ -420,18 +420,6 @@ object NativeMenuBar { } helpMenu.add(releaseNotesItem) - // Star on GitHub - val starGitHubItem = MenuItem(menuLabel("menu.help.star.github")) - starGitHubItem.addActionListener { - runCatching { - if (Desktop.isDesktopSupported()) { - Desktop.getDesktop() - .browse(URI("https://github.com/askimo-ai/askimo")) - } - } - } - helpMenu.add(starGitHubItem) - // Join Discord Community val discordItem = MenuItem(menuLabel("menu.help.discord")) discordItem.addActionListener { @@ -441,16 +429,10 @@ object NativeMenuBar { } helpMenu.add(discordItem) - // Share Askimo submenu - val shareMenu = Menu(menuLabel("menu.help.share")) - - ShareTarget.entries.forEach { target -> - val item = MenuItem(ShareUtils.labelFor(target)) - item.addActionListener { ShareUtils.share(target) } - shareMenu.add(item) - } - - helpMenu.add(shareMenu) + // Support Askimo — opens the star/share prompt (star + share in one place) + val supportAskimoItem = MenuItem(menuLabel("menu.help.support.askimo")) + supportAskimoItem.addActionListener { onSupportAskimo() } + helpMenu.add(supportAskimoItem) helpMenu.addSeparator() diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/shell/StarPromptDialog.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/shell/StarPromptDialog.kt index 674798563..2a8597fc9 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/shell/StarPromptDialog.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/shell/StarPromptDialog.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -19,9 +20,11 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -40,8 +43,10 @@ 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 androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import io.askimo.core.analytics.Analytics import io.askimo.core.analytics.AnalyticsEvent import io.askimo.core.service.StatsService @@ -51,6 +56,18 @@ import io.askimo.ui.common.theme.Spacing import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +/** Pre-defined feedback categories shown in [feedbackPromptDialog]. */ +enum class FeedbackReason(val emoji: String, val i18nKey: String) { + INACCURATE("😕", "feedback.reason.inaccurate"), + SLOW("🐌", "feedback.reason.slow"), + MISSING_FEATURE("🧩", "feedback.reason.missing.feature"), + BROKEN("🐛", "feedback.reason.broken"), + HARD_TO_USE("😵", "feedback.reason.hard.to.use"), + INTEGRATION("🔌", "feedback.reason.integration"), + PRIVACY("🔒", "feedback.reason.privacy"), + OTHER("🤔", "feedback.reason.other"), +} + /** * Happiness gate shown before [starPromptDialog]. * @@ -65,9 +82,12 @@ fun happinessGateDialog( onNeutral: () -> Unit, onUnhappy: () -> Unit, ) { - Dialog(onDismissRequest = {}) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { Surface( - modifier = Modifier.width(400.dp), + modifier = Modifier.width(480.dp), shape = MaterialTheme.shapes.large, tonalElevation = 8.dp, ) { @@ -161,18 +181,36 @@ private fun sentimentButton( } /** - * Shown after neutral/unhappy sentiment — asks if user wants to share feedback. - * - Yes → [onConfirm] caller opens the contact/feedback page - * - No → [onDecline] caller dismisses + * Inline feedback dialog shown after neutral/unhappy sentiment, or opened directly from the Help menu. + * + * Lets the user pick one or more pre-defined [FeedbackReason]s and optionally add a comment. + * No browser is opened — structured data is surfaced via [onSubmit]. + * + * - Send Feedback → [onSubmit] receives selected reasons + comment + email; shows a thank-you screen. + * - Close (after thank-you) → [onClose] — caller permanently dismisses. + * - Skip → shows a reminder screen when [showReminderOnSkip] is true (automatic flow), + * or calls [onSnooze] immediately when false (opened from menu — user already knows where to find it). + * - Got it (after reminder) → [onSnooze] — caller snoozes so user can return later. */ @Composable fun feedbackPromptDialog( - onConfirm: () -> Unit, - onDecline: () -> Unit, + onSubmit: (reasons: Set, comment: String, email: String) -> Unit, + onClose: () -> Unit, + onSnooze: () -> Unit, + showReminderOnSkip: Boolean = true, ) { - Dialog(onDismissRequest = {}) { + var selectedReasons by remember { mutableStateOf(emptySet()) } + var comment by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + var submitted by remember { mutableStateOf(false) } + var showReminder by remember { mutableStateOf(false) } + + Dialog( + onDismissRequest = {}, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { Surface( - modifier = Modifier.width(400.dp), + modifier = Modifier.width(700.dp), shape = MaterialTheme.shapes.large, tonalElevation = 8.dp, ) { @@ -181,48 +219,217 @@ fun feedbackPromptDialog( verticalArrangement = Arrangement.spacedBy(20.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Text( - text = "💬", - style = MaterialTheme.typography.displaySmall, - ) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(Spacing.small), - ) { - Text( - text = stringResource("happiness.gate.feedback.title"), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - ) - Text( - text = stringResource("happiness.gate.feedback.message"), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) - } - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(Spacing.small), - ) { - sentimentButton( - label = stringResource("happiness.gate.feedback.yes"), - onClick = onConfirm, - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - ) - sentimentButton( - label = stringResource("happiness.gate.feedback.no"), - onClick = onDecline, - ) + when { + submitted -> { + // ── Thank-you screen ─────────────────────────────────── + Text(text = "🙏", style = MaterialTheme.typography.displaySmall) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(Spacing.small), + ) { + Text( + text = stringResource("feedback.thanks.title"), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource("feedback.thanks.message"), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = onClose, modifier = Modifier.pointerHoverIcon(PointerIcon.Hand)) { + Text(text = stringResource("feedback.thanks.close"), style = MaterialTheme.typography.bodySmall) + } + } + } + + showReminder -> { + // ── Skip reminder screen ─────────────────────────────── + Text(text = "💡", style = MaterialTheme.typography.displaySmall) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(Spacing.small), + ) { + Text( + text = stringResource("feedback.remind.title"), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource("feedback.remind.message"), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton(onClick = onSnooze, modifier = Modifier.pointerHoverIcon(PointerIcon.Hand)) { + Text(text = stringResource("feedback.remind.got.it"), style = MaterialTheme.typography.bodySmall) + } + } + } + + else -> { + // ── Input screen ─────────────────────────────────────── + Text(text = "💬", style = MaterialTheme.typography.displaySmall) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(Spacing.small), + ) { + Text( + text = stringResource("feedback.dialog.title"), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource("feedback.dialog.subtitle"), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + // ── Reason chips (2-column grid) ─────────────────────── + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(Spacing.small), + ) { + FeedbackReason.entries.chunked(2).forEach { row -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Spacing.small), + ) { + row.forEach { reason -> + feedbackReasonChip( + reason = reason, + selected = reason in selectedReasons, + onToggle = { + selectedReasons = if (reason in selectedReasons) { + selectedReasons - reason + } else { + selectedReasons + reason + } + }, + modifier = Modifier.weight(1f), + ) + } + if (row.size == 1) Spacer(modifier = Modifier.weight(1f)) + } + } + } + // ── Optional comment ─────────────────────────────────── + OutlinedTextField( + value = comment, + onValueChange = { comment = it }, + modifier = Modifier.fillMaxWidth(), + label = { + Text(text = stringResource("feedback.comment.label"), style = MaterialTheme.typography.bodySmall) + }, + placeholder = { + Text(text = stringResource("feedback.comment.placeholder"), style = MaterialTheme.typography.bodySmall) + }, + minLines = 3, + maxLines = 5, + shape = MaterialTheme.shapes.medium, + ) + // ── Optional email ───────────────────────────────────── + OutlinedTextField( + value = email, + onValueChange = { email = it }, + modifier = Modifier.fillMaxWidth(), + label = { + Text(text = stringResource("feedback.email.label"), style = MaterialTheme.typography.bodySmall) + }, + placeholder = { + Text(text = stringResource("feedback.email.placeholder"), style = MaterialTheme.typography.bodySmall) + }, + supportingText = { + Text( + text = stringResource("feedback.email.hint"), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + singleLine = true, + shape = MaterialTheme.shapes.medium, + ) + // ── Action buttons ───────────────────────────────────── + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton( + onClick = { if (showReminderOnSkip) showReminder = true else onSnooze() }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) { + Text( + text = stringResource("feedback.action.skip"), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Button( + onClick = { + onSubmit(selectedReasons, comment.trim(), email.trim()) + submitted = true + }, + enabled = selectedReasons.isNotEmpty() || comment.isNotBlank(), + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) { + Text(text = stringResource("feedback.action.send"), style = MaterialTheme.typography.labelLarge) + } + } + } } } } } } +@Composable +private fun feedbackReasonChip( + reason: FeedbackReason, + selected: Boolean, + onToggle: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier + .clip(MaterialTheme.shapes.medium) + .clickable(onClick = onToggle) + .pointerHoverIcon(PointerIcon.Hand), + shape = MaterialTheme.shapes.medium, + color = if (selected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant, + contentColor = if (selected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text(text = reason.emoji, style = MaterialTheme.typography.bodyMedium) + Text( + text = stringResource(reason.i18nKey), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + /** * Dialog prompting users to support the project. * Fetches the current star count from the public API and shows social proof when loaded. @@ -236,8 +443,11 @@ fun starPromptDialog( onDismiss: () -> Unit, onStar: () -> Unit, onAlreadyStarred: () -> Unit, + showReminderOnMaybeLater: Boolean = true, ) { var starCount by remember { mutableStateOf(null) } + var thanked by remember { mutableStateOf(false) } + var showReminder by remember { mutableStateOf(false) } LaunchedEffect(Unit) { withContext(Dispatchers.IO) { @@ -245,9 +455,12 @@ fun starPromptDialog( } } - Dialog(onDismissRequest = {}) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { Surface( - modifier = Modifier.width(520.dp), + modifier = Modifier.width(700.dp), shape = MaterialTheme.shapes.large, tonalElevation = 8.dp, ) { @@ -255,70 +468,155 @@ fun starPromptDialog( modifier = Modifier.padding(Spacing.extraLarge), verticalArrangement = Arrangement.spacedBy(20.dp), ) { - // Header - Column(verticalArrangement = Arrangement.spacedBy(Spacing.small)) { - Text( - text = stringResource("star.prompt.title"), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = stringResource("star.prompt.message"), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - val count = starCount - if (count != null) { + if (thanked) { + // ── Thank-you screen ─────────────────────────────────── + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(Spacing.medium), + ) { + Text( + text = "🙏", + style = MaterialTheme.typography.displaySmall, + ) Text( - text = stringResource("star.prompt.social.proof", count), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.secondary, + text = stringResource("star.prompt.thanks.title"), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource("star.prompt.thanks.message"), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, ) } - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(Spacing.medium), - ) { - supportActionCard( - icon = Icons.Default.Star, - iconTint = Color(0xFFFFC107), - label = stringResource("star.prompt.star.button"), - description = stringResource("star.prompt.star.description"), - onClick = onStar, - modifier = Modifier.weight(1f), - ) - shareActionCard(modifier = Modifier.weight(1f)) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - TextButton( - onClick = onAlreadyStarred, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, ) { + TextButton( + onClick = onAlreadyStarred, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) { + Text( + text = stringResource("star.prompt.thanks.done"), + style = MaterialTheme.typography.bodySmall, + ) + } + } + } else if (showReminder) { + // ── Maybe-later reminder screen ──────────────────────── + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(Spacing.medium), + ) { + Text( + text = "💡", + style = MaterialTheme.typography.displaySmall, + ) Text( - text = stringResource("star.prompt.already.starred"), - style = MaterialTheme.typography.bodySmall, + text = stringResource("star.prompt.remind.title"), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource("star.prompt.remind.message"), + style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, ) } - TextButton( - onClick = onDismiss, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, ) { + TextButton( + onClick = onDismiss, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) { + Text( + text = stringResource("star.prompt.remind.got.it"), + style = MaterialTheme.typography.bodySmall, + ) + } + } + } else { + // ── Default screen ───────────────────────────────────── + Column(verticalArrangement = Arrangement.spacedBy(Spacing.small)) { + Text( + text = stringResource("star.prompt.title"), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) Text( - text = stringResource("star.prompt.maybe.later"), - style = MaterialTheme.typography.bodySmall, + text = stringResource("star.prompt.message"), + style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) + val count = starCount + if (count != null) { + Text( + text = stringResource("star.prompt.social.proof", count), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.secondary, + ) + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(Spacing.medium), + ) { + supportActionCard( + icon = Icons.Default.Star, + iconTint = Color(0xFFFFC107), + label = stringResource("star.prompt.star.button"), + description = stringResource("star.prompt.star.description"), + onClick = { + onStar() + thanked = true + }, + modifier = Modifier.weight(1f), + ) + shareActionCard( + onShared = { thanked = true }, + modifier = Modifier.weight(1f), + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton( + onClick = onAlreadyStarred, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) { + Text( + text = stringResource("star.prompt.already.starred"), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + TextButton( + onClick = { if (showReminderOnMaybeLater) showReminder = true else onDismiss() }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) { + Text( + text = stringResource("star.prompt.maybe.later"), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } } @@ -327,7 +625,10 @@ fun starPromptDialog( } @Composable -private fun shareActionCard(modifier: Modifier = Modifier) { +private fun shareActionCard( + onShared: () -> Unit = {}, + modifier: Modifier = Modifier, +) { val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() var showMenu by remember { mutableStateOf(false) } @@ -383,6 +684,7 @@ private fun shareActionCard(modifier: Modifier = Modifier) { onClick = { showMenu = false ShareUtils.share(target) + onShared() }, colors = AppComponents.menuItemColors(), modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), diff --git a/desktop-shared/src/main/resources/i18n/messages.properties b/desktop-shared/src/main/resources/i18n/messages.properties index 8d11add05..bc285251b 100644 --- a/desktop-shared/src/main/resources/i18n/messages.properties +++ b/desktop-shared/src/main/resources/i18n/messages.properties @@ -52,6 +52,7 @@ menu.dev.clear.account.preferences=[Dev] Clear Account Preferences menu.about=About Askimo menu.help.check.updates=Check for Updates... menu.help.diagnostics=System Diagnostics... +menu.help.support.askimo=Support Askimo ⭐ # Common Dialog Buttons dialog.cancel=Cancel @@ -513,10 +514,34 @@ happiness.gate.subtitle=Let us know how we're doing happiness.gate.happy=Yes, I'm loving it! 😍 happiness.gate.neutral=It's okay 🙂 happiness.gate.unhappy=Not really 😕 -happiness.gate.feedback.title=Help us improve -happiness.gate.feedback.message=Would you like to share your feedback? It helps us make Askimo better for you and everyone else. -happiness.gate.feedback.yes=Yes, share feedback -happiness.gate.feedback.no=No thanks + +# Inline Feedback Dialog (shown after neutral/unhappy sentiment) +feedback.dialog.title=What's not working for you? +feedback.dialog.subtitle=Select all that apply — your input goes straight to the team. +feedback.comment.label=Tell us more (optional) +feedback.comment.placeholder=Describe the issue or share your thoughts… +feedback.email.label=Your email (optional) +feedback.email.placeholder=you@example.com +feedback.email.hint=Only used to follow up on your feedback. We'll never spam you. +feedback.action.skip=Skip +feedback.action.send=Send Feedback +feedback.thanks.title=Thank you for your feedback! +feedback.thanks.message=Your input helps us make Askimo better. We really appreciate it. +feedback.thanks.close=Close + +feedback.remind.title=No worries! +feedback.remind.message=You can share feedback anytime from the Help menu → Share Feedback. +feedback.remind.got.it=Got it + +# Feedback reasons +feedback.reason.inaccurate=AI responses aren't accurate +feedback.reason.slow=It's too slow +feedback.reason.missing.feature=Missing a feature I need +feedback.reason.broken=Something is broken +feedback.reason.hard.to.use=It's hard to use +feedback.reason.integration=Integration issues +feedback.reason.privacy=Privacy concerns +feedback.reason.other=Something else # Star Prompt star.prompt.title=Enjoying Askimo? Help spread the word 💙 @@ -528,6 +553,12 @@ star.prompt.share.description=Tell your network about Askimo on X, LinkedIn, or star.prompt.maybe.later=Maybe later star.prompt.already.starred=Already starred ✓ star.prompt.social.proof=⭐ Join {0} people who already starred Askimo +star.prompt.thanks.title=Thank you so much! 🌟 +star.prompt.thanks.message=Your support means a lot and helps us keep building Askimo. +star.prompt.thanks.done=Done ✓ +star.prompt.remind.title=No worries! +star.prompt.remind.message=You can support us anytime from the Help menu → Support Askimo. +star.prompt.remind.got.it=Got it # Analytics Settings (Advanced section toggle) settings.analytics.title=Anonymous Analytics diff --git a/desktop-shared/src/main/resources/i18n/messages_de.properties b/desktop-shared/src/main/resources/i18n/messages_de.properties index 2f789b737..98383249b 100644 --- a/desktop-shared/src/main/resources/i18n/messages_de.properties +++ b/desktop-shared/src/main/resources/i18n/messages_de.properties @@ -51,6 +51,7 @@ menu.dev.clear.account.preferences= [Dev] Kontoeinstellungen löschen menu.about=Über Askimo menu.help.check.updates=Nach Updates suchen... menu.help.diagnostics=Systemdiagnose... +menu.help.support.askimo=Askimo unterstützen ⭐ # Common Dialog Buttons dialog.cancel=Abbrechen @@ -487,10 +488,34 @@ happiness.gate.subtitle=Sagen Sie uns, wie wir uns machen happiness.gate.happy=Ich liebe es! 😍 happiness.gate.neutral=Es geht 🙂 happiness.gate.unhappy=Nicht wirklich 😕 -happiness.gate.feedback.title=Helfen Sie uns, uns zu verbessern -happiness.gate.feedback.message=Möchten Sie uns Ihr Feedback mitteilen? Es hilft uns, Askimo für Sie und alle anderen besser zu machen. -happiness.gate.feedback.yes=Ja, Feedback teilen -happiness.gate.feedback.no=Nein, danke + +# Inline Feedback Dialog (shown after neutral/unhappy sentiment) +feedback.dialog.title=Was funktioniert für dich nicht? +feedback.dialog.subtitle=Wähle alle zutreffenden Punkte aus — dein Feedback geht direkt an das Team. +feedback.comment.label=Erzähl uns mehr (optional) +feedback.comment.placeholder=Beschreibe das Problem oder teile uns deine Gedanken mit… +feedback.email.label=Deine E-Mail (optional) +feedback.email.placeholder=you@example.com +feedback.email.hint=Wird nur verwendet, um bei deinem Feedback nachzufassen. Wir werden dich niemals zuspammen. +feedback.action.skip=Überspringen +feedback.action.send=Feedback senden +feedback.thanks.title=Vielen Dank für dein Feedback! +feedback.thanks.message=Dein Input hilft uns, Askimo zu verbessern. Wir wissen das wirklich zu schätzen. +feedback.thanks.close=Schließen + +feedback.remind.title=Kein Problem! +feedback.remind.message=Du kannst jederzeit über das Hilfe-Menü → Feedback senden Rückmeldung geben. +feedback.remind.got.it=Verstanden + +# Feedback reasons +feedback.reason.inaccurate=Die Antworten der KI sind nicht korrekt +feedback.reason.slow=Es ist zu langsam +feedback.reason.missing.feature=Ein benötigtes Feature fehlt +feedback.reason.broken=Etwas ist kaputt +feedback.reason.hard.to.use=Es ist schwer zu bedienen +feedback.reason.integration=Integrationsprobleme +feedback.reason.privacy=Datenschutzbedenken +feedback.reason.other=Etwas anderes # Star Prompt star.prompt.title=Genießt du Askimo? Verbreite die Nachricht 💙 @@ -502,6 +527,12 @@ star.prompt.share.description=Erzählen Sie Ihrem Netzwerk von Askimo auf X, Lin star.prompt.maybe.later=Jetzt nicht star.prompt.already.starred=Bereits mit Stern markiert ✓ star.prompt.social.proof=⭐ Schließen Sie sich {0} Personen an, die Askimo bereits mit einem Stern markiert haben +star.prompt.thanks.title=Vielen Dank! 🌟 +star.prompt.thanks.message=Deine Unterstützung bedeutet uns viel und hilft uns, Askimo weiterzuentwickeln. +star.prompt.thanks.done=Fertig ✓ +star.prompt.remind.title=Kein Problem! +star.prompt.remind.message=Du kannst uns jederzeit über das Hilfe-Menü → Askimo unterstützen. +star.prompt.remind.got.it=Verstanden # Analytics Settings (Advanced section toggle) settings.analytics.title=Anonyme Analysen diff --git a/desktop-shared/src/main/resources/i18n/messages_es.properties b/desktop-shared/src/main/resources/i18n/messages_es.properties index ea0d9bda7..d5ef6b680 100644 --- a/desktop-shared/src/main/resources/i18n/messages_es.properties +++ b/desktop-shared/src/main/resources/i18n/messages_es.properties @@ -51,7 +51,7 @@ menu.dev.clear.account.preferences=[Dev] Eliminar preferencias de cuenta menu.about=Acerca de Askimo menu.help.check.updates=Buscar actualizaciones... menu.help.diagnostics=Diagnóstico del sistema... - +menu.help.support.askimo=Apoyar a Askimo ⭐ # Common Dialog Buttons dialog.cancel=Cancelar @@ -487,10 +487,34 @@ happiness.gate.subtitle=Cuéntanos cómo nos estamos desenvolviendo happiness.gate.happy=¡Me encanta! 😍 happiness.gate.neutral=Está bien 🙂 happiness.gate.unhappy=No mucho 😕 -happiness.gate.feedback.title=Ayúdanos a mejorar -happiness.gate.feedback.message=¿Te gustaría compartir tus comentarios? Nos ayuda a hacer que Askimo sea mejor para ti y para todos los demás. -happiness.gate.feedback.yes=Sí, compartir comentarios -happiness.gate.feedback.no=No, gracias + +# Inline Feedback Dialog (shown after neutral/unhappy sentiment) +feedback.dialog.title=¿Qué no te está funcionando? +feedback.dialog.subtitle=Selecciona todo lo que aplique — tu opinión llegará directamente al equipo. +feedback.comment.label=Cuéntanos más (opcional) +feedback.comment.placeholder=Describe el problema o comparte tus pensamientos… +feedback.email.label=Tu correo electrónico (opcional) +feedback.email.placeholder=you@example.com +feedback.email.hint=Solo se usará para hacer seguimiento de tu feedback. Nunca te enviaremos spam. +feedback.action.skip=Omitir +feedback.action.send=Enviar feedback +feedback.thanks.title=¡Gracias por tu feedback! +feedback.thanks.message=Tu aporte nos ayuda a mejorar Askimo. Lo apreciamos mucho. +feedback.thanks.close=Cerrar + +feedback.remind.title=¡No te preocupes! +feedback.remind.message=Puedes compartir tus comentarios en cualquier momento desde el menú de Ayuda → Compartir Feedback. +feedback.remind.got.it=Entendido + +# Feedback reasons +feedback.reason.inaccurate=Las respuestas de la IA no son precisas +feedback.reason.slow=Es demasiado lento +feedback.reason.missing.feature=Falta una función que necesito +feedback.reason.broken=Algo está roto +feedback.reason.hard.to.use=Es difícil de usar +feedback.reason.integration=Problemas de integración +feedback.reason.privacy=Preocupaciones de privacidad +feedback.reason.other=Otra cosa # Star Prompt star.prompt.title=¿Disfrutando de Askimo? Ayuda a difundir la palabra 💙 @@ -502,6 +526,12 @@ star.prompt.share.description=Cuéntale a tu red sobre Askimo en X, LinkedIn o F star.prompt.maybe.later=Ahora no star.prompt.already.starred=Ya destacado ✓ star.prompt.social.proof=⭐ Únete a {0} personas que ya han destacado Askimo +star.prompt.thanks.title=¡Muchas gracias! 🌟 +star.prompt.thanks.message=Tu apoyo significa mucho y nos ayuda a seguir construyendo Askimo. +star.prompt.thanks.done=Hecho ✓ +star.prompt.remind.title=No te preocupes. +star.prompt.remind.message=Puedes apoyarnos en cualquier momento desde el menú Ayuda → Apoyar a Askimo. +star.prompt.remind.got.it=Entendido # Analytics Settings (Advanced section toggle) settings.analytics.title=Analítica anónima diff --git a/desktop-shared/src/main/resources/i18n/messages_fr.properties b/desktop-shared/src/main/resources/i18n/messages_fr.properties index 61a659a07..073d20138 100644 --- a/desktop-shared/src/main/resources/i18n/messages_fr.properties +++ b/desktop-shared/src/main/resources/i18n/messages_fr.properties @@ -51,7 +51,7 @@ menu.dev.clear.account.preferences=[Dev] Effacer les préférences du compte menu.about=À propos d’Askimo menu.help.check.updates=Vérifier les mises à jour... menu.help.diagnostics=Diagnostics système... - +menu.help.support.askimo=Soutenir Askimo ⭐ # Common Dialog Buttons dialog.cancel=Annuler @@ -487,10 +487,34 @@ happiness.gate.subtitle=Dites-nous comment nous nous en sortons happiness.gate.happy=Je l'adore ! 😍 happiness.gate.neutral=Ça va 🙂 happiness.gate.unhappy=Pas vraiment 😕 -happiness.gate.feedback.title=Aidez-nous à nous améliorer -happiness.gate.feedback.message=Souhaitez-vous partager vos commentaires ? Cela nous aide à rendre Askimo meilleur pour vous et pour tout le monde. -happiness.gate.feedback.yes=Oui, partager des commentaires -happiness.gate.feedback.no=Non merci + +# Inline Feedback Dialog (shown after neutral/unhappy sentiment) +feedback.dialog.title=Qu'est-ce qui ne vous convient pas ? +feedback.dialog.subtitle=Sélectionnez toutes les options correspondantes — votre retour sera envoyé directement à l'équipe. +feedback.comment.label=Dites-nous en plus (facultatif) +feedback.comment.placeholder=Décrivez le problème ou partagez vos réflexions… +feedback.email.label=Votre e-mail (facultatif) +feedback.email.placeholder=you@example.com +feedback.email.hint=Utilisé uniquement pour assurer le suivi de votre retour. Nous ne vous enverrons jamais de spam. +feedback.action.skip=Passer +feedback.action.send=Envoyer le feedback +feedback.thanks.title=Merci pour votre retour ! +feedback.thanks.message=Votre contribution nous aide à améliorer Askimo. Nous l'apprécions vraiment. +feedback.thanks.close=Fermer + +feedback.remind.title=Pas de souci ! +feedback.remind.message=Vous pouvez envoyer un feedback à tout moment depuis le menu Aide → Partager un feedback. +feedback.remind.got.it=C'est noté + +# Feedback reasons +feedback.reason.inaccurate=Les réponses de l'IA ne sont pas exactes +feedback.reason.slow=C'est trop lent +feedback.reason.missing.feature=Il manque une fonctionnalité dont j'ai besoin +feedback.reason.broken=Quelque chose est cassé +feedback.reason.hard.to.use=C'est difficile à utiliser +feedback.reason.integration=Problèmes d'intégration +feedback.reason.privacy=Problèmes de confidentialité +feedback.reason.other=Autre chose # Star Prompt star.prompt.title=Vous appréciez Askimo? Aidez à faire connaître 💙 @@ -502,6 +526,12 @@ star.prompt.share.description=Parlez d'Askimo à votre réseau sur X, LinkedIn o star.prompt.maybe.later=Pas maintenant star.prompt.already.starred=Déjà mis en favori ✓ star.prompt.social.proof=⭐ Rejoignez {0} personnes qui ont déjà ajouté une étoile à Askimo +star.prompt.thanks.title=Merci beaucoup ! 🌟 +star.prompt.thanks.message=Votre soutien compte beaucoup et nous aide à continuer à développer Askimo. +star.prompt.thanks.done=Terminé ✓ +star.prompt.remind.title=Pas de souci ! +star.prompt.remind.message=Vous pouvez nous soutenir à tout moment via le menu Aide → Soutenir Askimo. +star.prompt.remind.got.it=Compris # Analytics Settings (Advanced section toggle) settings.analytics.title=Analytique anonyme 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 2dca450f5..6b7cb1d19 100644 --- a/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties +++ b/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties @@ -51,6 +51,7 @@ menu.dev.clear.account.preferences=[Dev] アカウント設定をクリア menu.about=Askimo について menu.help.check.updates=アップデートを確認... menu.help.diagnostics=システム診断... +menu.help.support.askimo=Askimo をサポート ⭐ # Common Dialog Buttons dialog.cancel=キャンセル @@ -486,10 +487,34 @@ happiness.gate.subtitle=ご意見をお聞かせください happiness.gate.happy=とても気に入っています!😍 happiness.gate.neutral=まあまあです 🙂 happiness.gate.unhappy=あまりよくないです 😕 -happiness.gate.feedback.title=改善にご協力ください -happiness.gate.feedback.message=フィードバックを共有していただけますか?Askimoを皆様にとってより良いものにするために役立ちます。 -happiness.gate.feedback.yes=はい、フィードバックを共有します -happiness.gate.feedback.no=いいえ、結構です + +# Inline Feedback Dialog (shown after neutral/unhappy sentiment) +feedback.dialog.title=何かお困りですか? +feedback.dialog.subtitle=当てはまるものをすべて選択してください。ご意見は直接チームに届きます。 +feedback.comment.label=詳細(任意) +feedback.comment.placeholder=問題の説明やご意見をお聞かせください… +feedback.email.label=メールアドレス(任意) +feedback.email.placeholder=you@example.com +feedback.email.hint=フィードバックのフォローアップのみに使用します。スパムメールを送ることはありません。 +feedback.action.skip=スキップ +feedback.action.send=フィードバックを送信 +feedback.thanks.title=ご意見ありがとうございます! +feedback.thanks.message=お客様からのご意見は、Askimoの改善に役立てられます。ご協力に感謝いたします。 +feedback.thanks.close=閉じる + +feedback.remind.title=問題ありません! +feedback.remind.message=フィードバックはいつでも「ヘルプ」メニュー → 「フィードバックを送信」から共有できます。 +feedback.remind.got.it=了解しました + +# Feedback reasons +feedback.reason.inaccurate=AIの回答が正確ではない +feedback.reason.slow=動作が遅い +feedback.reason.missing.feature=必要な機能が不足している +feedback.reason.broken=どこか壊れている +feedback.reason.hard.to.use=使いにくい +feedback.reason.integration=連携に関する問題 +feedback.reason.privacy=プライバシーに関する懸念 +feedback.reason.other=その他 # Star Prompt star.prompt.title=Askimoを楽しんでいますか?広めてください 💙 @@ -501,6 +526,12 @@ star.prompt.share.description=X、LinkedIn、またはFacebookでAskimoについ star.prompt.maybe.later=今はしない star.prompt.already.starred=すでにスター付き ✓ star.prompt.social.proof=⭐ すでにAskimoにスターを付けた{0}人に参加しましょう +star.prompt.thanks.title=ありがとうございます! 🌟 +star.prompt.thanks.message=皆様のご支援は大きな励みとなり、Askimoの開発を続ける力になります。 +star.prompt.thanks.done=完了 ✓ +star.prompt.remind.title=問題ありません! +star.prompt.remind.message=いつでも「ヘルプ」メニュー → 「Askimoを支援」からご協力いただけます。 +star.prompt.remind.got.it=了解 # Analytics Settings (Advanced section toggle) settings.analytics.title=匿名分析 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 afce9c91b..40d106ee1 100644 --- a/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties +++ b/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties @@ -51,7 +51,7 @@ menu.dev.clear.account.preferences=[Dev] 계정 환경 설정 지우기 menu.about=Askimo 정보 menu.help.check.updates=업데이트 확인... menu.help.diagnostics=시스템 진단... - +menu.help.support.askimo=Askimo 지원하기 ⭐ # Common Dialog Buttons dialog.cancel=취소 @@ -487,10 +487,34 @@ happiness.gate.subtitle=어떻게 사용하고 계신지 알려주세요 happiness.gate.happy=정말 마음에 들어요! 😍 happiness.gate.neutral=괜찮아요 🙂 happiness.gate.unhappy=별로예요 😕 -happiness.gate.feedback.title=개선에 도움을 주세요 -happiness.gate.feedback.message=피드백을 공유해 주시겠습니까? Askimo를 귀하와 모든 분들을 위해 더 나은 서비스로 만드는 데 도움이 됩니다. -happiness.gate.feedback.yes=네, 피드백을 공유합니다 -happiness.gate.feedback.no=아니요, 괜찮습니다 + +# Inline Feedback Dialog (shown after neutral/unhappy sentiment) +feedback.dialog.title=어떤 점이 불편하신가요? +feedback.dialog.subtitle=해당하는 모든 항목을 선택해주세요. 귀하의 의견은 팀에 직접 전달됩니다. +feedback.comment.label=상세 내용 (선택 사항) +feedback.comment.placeholder=문제에 대한 설명이나 의견을 공유해주세요… +feedback.email.label=이메일 (선택 사항) +feedback.email.placeholder=you@example.com +feedback.email.hint=피드백에 대한 후속 조치를 위해서만 사용됩니다. 스팸 메일은 절대 보내지 않습니다. +feedback.action.skip=건너뛰기 +feedback.action.send=피드백 보내기 +feedback.thanks.title=소중한 의견 감사합니다! +feedback.thanks.message=보내주신 의견은 Askimo를 개선하는 데 큰 도움이 됩니다. 정말 감사합니다. +feedback.thanks.close=닫기 + +feedback.remind.title=괜찮습니다! +feedback.remind.message=도움말 메뉴 → 피드백 보내기를 통해 언제든지 의견을 보내주실 수 있습니다. +feedback.remind.got.it=알겠습니다 + +# Feedback reasons +feedback.reason.inaccurate=AI 응답이 정확하지 않음 +feedback.reason.slow=너무 느림 +feedback.reason.missing.feature=필요한 기능이 없음 +feedback.reason.broken=무언가 작동하지 않음 +feedback.reason.hard.to.use=사용하기 어려움 +feedback.reason.integration=통합 관련 문제 +feedback.reason.privacy=개인정보 보호 문제 +feedback.reason.other=기타 # Star Prompt star.prompt.title=Askimo를 즐기고 있나요? 전파해 주세요 💙 @@ -502,6 +526,12 @@ star.prompt.share.description=X, LinkedIn 또는 Facebook에서 네트워크에 star.prompt.maybe.later=지금은 아니요 star.prompt.already.starred=이미 별표 표시됨 ✓ star.prompt.social.proof=⭐ 이미 Askimo에 별표를 표시한 {0}명과 함께하세요 +star.prompt.thanks.title=정말 감사합니다! 🌟 +star.prompt.thanks.message=여러분의 성원은 저희가 Askimo를 계속 발전시키는 데 큰 힘이 됩니다. +star.prompt.thanks.done=완료 ✓ +star.prompt.remind.title=괜찮습니다! +star.prompt.remind.message=언제든지 도움말 메뉴 → Askimo 지원하기를 통해 저희를 후원하실 수 있습니다. +star.prompt.remind.got.it=확인 # Analytics Settings (Advanced section toggle) settings.analytics.title=익명 분석 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 08d9fad0c..85191b665 100644 --- a/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties +++ b/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties @@ -51,6 +51,7 @@ menu.dev.clear.account.preferences=[Dev] Limpar preferências da conta menu.about=Sobre o Askimo menu.help.check.updates=Verificar atualizações... menu.help.diagnostics=Diagnóstico do sistema... +menu.help.support.askimo=Apoiar o Askimo ⭐ # Common Dialog Buttons dialog.cancel=Cancelar @@ -488,10 +489,34 @@ happiness.gate.subtitle=Nos diga como estamos indo happiness.gate.happy=Estou adorando! 😍 happiness.gate.neutral=Está ok 🙂 happiness.gate.unhappy=Não muito 😕 -happiness.gate.feedback.title=Ajude-nos a melhorar -happiness.gate.feedback.message=Gostaria de compartilhar o seu feedback? Ele nos ajuda a tornar o Askimo melhor para você e para todos os outros. -happiness.gate.feedback.yes=Sim, compartilhar feedback -happiness.gate.feedback.no=Não, obrigado + +# Inline Feedback Dialog (shown after neutral/unhappy sentiment) +feedback.dialog.title=O que não está a funcionar para si? +feedback.dialog.subtitle=Selecione tudo o que se aplicar — o seu feedback vai direto para a equipa. +feedback.comment.label=Conte-nos mais (opcional) +feedback.comment.placeholder=Descreva o problema ou partilhe os seus pensamentos… +feedback.email.label=O seu e-mail (opcional) +feedback.email.placeholder=you@example.com +feedback.email.hint=Usado apenas para acompanhar o seu feedback. Nunca enviaremos spam. +feedback.action.skip=Ignorar +feedback.action.send=Enviar feedback +feedback.thanks.title=Obrigado pelo seu feedback! +feedback.thanks.message=O seu contributo ajuda-nos a tornar o Askimo melhor. Agradecemos muito. +feedback.thanks.close=Fechar + +feedback.remind.title=Não se preocupe! +feedback.remind.message=Pode partilhar feedback a qualquer momento no menu Ajuda → Partilhar Feedback. +feedback.remind.got.it=Entendido + +# Feedback reasons +feedback.reason.inaccurate=As respostas da IA não são precisas +feedback.reason.slow=É demasiado lento +feedback.reason.missing.feature=Falta uma funcionalidade que necessito +feedback.reason.broken=Algo não está a funcionar +feedback.reason.hard.to.use=É difícil de usar +feedback.reason.integration=Problemas de integração +feedback.reason.privacy=Preocupações com a privacidade +feedback.reason.other=Outro # Star Prompt star.prompt.title=Você está curtindo Askimo? Ajude a divulgar 💙 @@ -503,6 +528,12 @@ star.prompt.share.description=Conte à sua rede sobre o Askimo no X, LinkedIn ou star.prompt.maybe.later=Agora não star.prompt.already.starred=Já com estrela ✓ star.prompt.social.proof=⭐ Junte-se a {0} pessoas que já deram estrela no Askimo +star.prompt.thanks.title=Muito obrigado! 🌟 +star.prompt.thanks.message=O seu apoio significa muito e ajuda-nos a continuar a desenvolver o Askimo. +star.prompt.thanks.done=Concluído ✓ +star.prompt.remind.title=Sem problemas! +star.prompt.remind.message=Pode apoiar-nos a qualquer momento através do menu Ajuda → Apoiar o Askimo. +star.prompt.remind.got.it=Entendido # Analytics Settings (Advanced section toggle) settings.analytics.title=Análises anônimas 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 62cdeb6fa..0f857b934 100644 --- a/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties +++ b/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties @@ -61,6 +61,7 @@ menu.dev.clear.account.preferences=[Dev] Xóa cài đặt tài khoản menu.about=Giới thiệu Askimo menu.help.check.updates=Kiểm tra cập nhật... menu.help.diagnostics=Chẩn đoán hệ thống... +menu.help.support.askimo=Hỗ trợ Askimo ⭐ # Common Dialog Buttons dialog.cancel=Hủy @@ -494,10 +495,34 @@ happiness.gate.subtitle=Cho chúng tôi biết trải nghiệm của bạn happiness.gate.happy=Tôi rất thích! 😍 happiness.gate.neutral=Ổn 🙂 happiness.gate.unhappy=Chưa thực sự ưng 😕 -happiness.gate.feedback.title=Giúp chúng tôi cải thiện -happiness.gate.feedback.message=Bạn có muốn chia sẻ phản hồi của mình không? Điều này giúp chúng tôi làm cho Askimo trở nên tốt hơn cho bạn và mọi người khác. -happiness.gate.feedback.yes=Có, chia sẻ phản hồi -happiness.gate.feedback.no=Không, cảm ơn + +# Inline Feedback Dialog (shown after neutral/unhappy sentiment) +feedback.dialog.title=Điều gì đang làm bạn chưa hài lòng? +feedback.dialog.subtitle=Chọn tất cả những mục phù hợp — phản hồi của bạn sẽ được gửi trực tiếp đến nhóm của chúng tôi. +feedback.comment.label=Cho chúng tôi biết thêm (tùy chọn) +feedback.comment.placeholder=Mô tả vấn đề hoặc chia sẻ suy nghĩ của bạn… +feedback.email.label=Email của bạn (tùy chọn) +feedback.email.placeholder=you@example.com +feedback.email.hint=Chỉ được sử dụng để theo dõi phản hồi của bạn. Chúng tôi sẽ không bao giờ gửi thư rác. +feedback.action.skip=Bỏ qua +feedback.action.send=Gửi phản hồi +feedback.thanks.title=Cảm ơn bạn đã phản hồi! +feedback.thanks.message=Đóng góp của bạn giúp chúng tôi cải thiện Askimo tốt hơn. Chúng tôi rất trân trọng điều đó. +feedback.thanks.close=Đóng + +feedback.remind.title=Không sao đâu! +feedback.remind.message=Bạn có thể chia sẻ phản hồi bất cứ lúc nào từ menu Trợ giúp → Gửi phản hồi. +feedback.remind.got.it=Đã hiểu + +# Feedback reasons +feedback.reason.inaccurate=Câu trả lời của AI không chính xác +feedback.reason.slow=Quá chậm +feedback.reason.missing.feature=Thiếu tính năng tôi cần +feedback.reason.broken=Có lỗi xảy ra +feedback.reason.hard.to.use=Khó sử dụng +feedback.reason.integration=Vấn đề về tích hợp +feedback.reason.privacy=Lo ngại về quyền riêng tư +feedback.reason.other=Vấn đề khác # Star Prompt star.prompt.title=Bạn thích Askimo? Giúp lan tỏa 💙 @@ -509,6 +534,12 @@ star.prompt.share.description=Kể cho mạng lưới của bạn về Askimo tr star.prompt.maybe.later=Để sau star.prompt.already.starred=Đã gắn sao ✓ star.prompt.social.proof=⭐ Tham gia cùng {0} người đã gắn sao cho Askimo +star.prompt.thanks.title=Cảm ơn bạn rất nhiều! 🌟 +star.prompt.thanks.message=Sự ủng hộ của bạn có ý nghĩa rất lớn và giúp chúng tôi tiếp tục phát triển Askimo. +star.prompt.thanks.done=Xong ✓ +star.prompt.remind.title=Không vấn đề gì! +star.prompt.remind.message=Bạn có thể ủng hộ chúng tôi bất cứ lúc nào từ menu Trợ giúp → Ủng hộ Askimo. +star.prompt.remind.got.it=Đã hiểu # Analytics Settings (Advanced section toggle) settings.analytics.title=Phân tích ẩn danh 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 972758c0e..539e998f1 100644 --- a/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties +++ b/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties @@ -51,6 +51,7 @@ menu.dev.clear.account.preferences=[Dev] 清除账号首选项 menu.about=关于 Askimo menu.help.check.updates=检查更新... menu.help.diagnostics=系统诊断... +menu.help.support.askimo=支持 Askimo ⭐ # Common Dialog Buttons dialog.cancel=取消 @@ -486,10 +487,34 @@ happiness.gate.subtitle=让我们知道您的使用体验 happiness.gate.happy=非常喜欢!😍 happiness.gate.neutral=还不错 🙂 happiness.gate.unhappy=不太好 😕 -happiness.gate.feedback.title=帮助我们改进 -happiness.gate.feedback.message=您愿意分享您的反馈吗?这有助于我们为您和其他所有人把 Askimo 做得更好。 -happiness.gate.feedback.yes=是的,分享反馈 -happiness.gate.feedback.no=不了,谢谢 + +# Inline Feedback Dialog (shown after neutral/unhappy sentiment) +feedback.dialog.title=什么地方没能满足您的需求? +feedback.dialog.subtitle=请勾选所有适用项 — 您的反馈将直接发送给团队。 +feedback.comment.label=详细说明(可选) +feedback.comment.placeholder=描述问题或分享您的想法… +feedback.email.label=您的电子邮箱(可选) +feedback.email.placeholder=you@example.com +feedback.email.hint=仅用于跟进您的反馈。我们绝不会发送垃圾邮件。 +feedback.action.skip=跳过 +feedback.action.send=发送反馈 +feedback.thanks.title=感谢您的反馈! +feedback.thanks.message=您的意见有助于我们完善 Askimo。非常感谢。 +feedback.thanks.close=关闭 + +feedback.remind.title=没关系! +feedback.remind.message=您可以随时通过“帮助”菜单 → “发送反馈”分享您的意见。 +feedback.remind.got.it=知道了 + +# Feedback reasons +feedback.reason.inaccurate=AI 回答不准确 +feedback.reason.slow=速度太慢 +feedback.reason.missing.feature=缺少我需要的功能 +feedback.reason.broken=出现错误 +feedback.reason.hard.to.use=很难使用 +feedback.reason.integration=集成问题 +feedback.reason.privacy=隐私顾虑 +feedback.reason.other=其他 # Star Prompt star.prompt.title=正在享受 Askimo 吗?帮忙传播消息 💙 @@ -501,6 +526,12 @@ star.prompt.share.description=在 X、LinkedIn 或 Facebook 上向您的网络 star.prompt.maybe.later=暂不 star.prompt.already.starred=已标星 ✓ star.prompt.social.proof=⭐ 加入 {0} 位已为 Askimo 标星的用户 +star.prompt.thanks.title=非常感谢! 🌟 +star.prompt.thanks.message=您的支持对我们意义重大,并能帮助我们继续打造 Askimo。 +star.prompt.thanks.done=完成 ✓ +star.prompt.remind.title=没关系! +star.prompt.remind.message=您可以随时通过“帮助”菜单 → “支持 Askimo”来给予我们支持。 +star.prompt.remind.got.it=知道了 # Analytics Settings (Advanced section toggle) settings.analytics.title=匿名分析 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 2fa72afca..9ef0ba706 100644 --- a/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties +++ b/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties @@ -51,6 +51,7 @@ menu.dev.clear.account.preferences=[Dev] 清除帳戶首選項 menu.about=關於 Askimo menu.help.check.updates=檢查更新... menu.help.diagnostics=系統診斷... +menu.help.support.askimo=支援 Askimo ⭐ # Common Dialog Buttons dialog.cancel=取消 @@ -486,10 +487,34 @@ happiness.gate.subtitle=讓我們知道您的使用體驗 happiness.gate.happy=非常喜歡!😍 happiness.gate.neutral=還不錯 🙂 happiness.gate.unhappy=不太好 😕 -happiness.gate.feedback.title=幫助我們改進 -happiness.gate.feedback.message=您願意分享您的回饋嗎?這有助於我們為您和其他所有人將 Askimo 變得更好。 -happiness.gate.feedback.yes=是的,分享回饋 -happiness.gate.feedback.no=不了,謝謝 + +# Inline Feedback Dialog (shown after neutral/unhappy sentiment) +feedback.dialog.title=什麼地方沒能滿足您的需求? +feedback.dialog.subtitle=請勾選所有適用項 — 您的意見將直接發送給團隊。 +feedback.comment.label=詳細說明(選填) +feedback.comment.placeholder=描述問題或分享您的想法… +feedback.email.label=您的電子郵件(選填) +feedback.email.placeholder=you@example.com +feedback.email.hint=僅用於跟進您的意見。我們絕對不會發送垃圾郵件。 +feedback.action.skip=略過 +feedback.action.send=發送意見回饋 +feedback.thanks.title=感謝您的意見回饋! +feedback.thanks.message=您的意見有助於我們完善 Askimo。非常感謝。 +feedback.thanks.close=關閉 + +feedback.remind.title=沒關係! +feedback.remind.message=您可以隨時透過「說明」選單 → 「發送意見回饋」分享您的意見。 +feedback.remind.got.it=知道了 + +# Feedback reasons +feedback.reason.inaccurate=AI 回答不準確 +feedback.reason.slow=速度太慢 +feedback.reason.missing.feature=缺少我需要的功能 +feedback.reason.broken=出現錯誤 +feedback.reason.hard.to.use=很難使用 +feedback.reason.integration=整合問題 +feedback.reason.privacy=隱私顧慮 +feedback.reason.other=其他 # Star Prompt star.prompt.title=正在享受 Askimo 吗?帮忙传播消息 💙 @@ -501,6 +526,12 @@ star.prompt.share.description=在 X、LinkedIn 或 Facebook 上向您的網絡 star.prompt.maybe.later=暫時不要 star.prompt.already.starred=已標星 ✓ star.prompt.social.proof=⭐ 加入 {0} 位已為 Askimo 標星的用戶 +star.prompt.thanks.title=非常感謝! 🌟 +star.prompt.thanks.message=您的支持對我們意義重大,並能幫助我們繼續打造 Askimo。 +star.prompt.thanks.done=完成 ✓ +star.prompt.remind.title=沒關係! +star.prompt.remind.message=您可以隨時透過「說明」選單 → 「支持 Askimo」來給予我們支持。 +star.prompt.remind.got.it=知道了 # Analytics Settings (Advanced section toggle) settings.analytics.title=匿名分析 diff --git a/desktop/src/main/kotlin/io/askimo/desktop/Main.kt b/desktop/src/main/kotlin/io/askimo/desktop/Main.kt index ccf44d485..9cfd9102e 100644 --- a/desktop/src/main/kotlin/io/askimo/desktop/Main.kt +++ b/desktop/src/main/kotlin/io/askimo/desktop/Main.kt @@ -55,7 +55,6 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application -import io.askimo.core.AppConstants.DOMAIN import io.askimo.core.analytics.Analytics import io.askimo.core.analytics.AnalyticsEvent import io.askimo.core.backup.BackupManager @@ -338,9 +337,11 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? = var terminalPanelSize by remember { mutableStateOf(300.dp) } // Default size var pendingTerminalCommand by remember { mutableStateOf(null) } var showStarPromptDialog by remember { mutableStateOf(false) } + var starPromptOpenedFromMenu by remember { mutableStateOf(false) } var showHappinessGateDialog by remember { mutableStateOf(false) } var showFeedbackPromptDialog by remember { mutableStateOf(false) } var feedbackSentiment by remember { mutableStateOf("neutral") } + var feedbackOpenedFromMenu by remember { mutableStateOf(false) } var showNewProjectDialog by remember { mutableStateOf(false) } var showEditProjectDialog by remember { mutableStateOf(false) } var showGlobalSearchDialog by remember { mutableStateOf(false) } @@ -729,6 +730,15 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? = isProjectsVisible = showProjectsInSidebar, onShowSystemDiagnostics = { showSystemDiagnosticsDialog = true }, onNavigateToBookmarks = { currentView = View.BOOKMARKS }, + onSupportAskimo = { + starPromptOpenedFromMenu = true + showStarPromptDialog = true + }, + onShareFeedback = { + feedbackSentiment = "direct" + feedbackOpenedFromMenu = true + showFeedbackPromptDialog = true + }, ) } } @@ -1730,13 +1740,15 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? = starPromptDialog( onDismiss = { Analytics.track(AnalyticsEvent.STAR_PROMPT_DISMISSED) - AccountPreferences.device().snoozeStarPrompt() + if (!starPromptOpenedFromMenu) { + AccountPreferences.device().snoozeStarPrompt() + } + starPromptOpenedFromMenu = false showStarPromptDialog = false }, onStar = { Analytics.track(AnalyticsEvent.STAR_PROMPT_ACCEPTED) AccountPreferences.device().dismissStarPromptPermanently() - showStarPromptDialog = false runCatching { if (Desktop.isDesktopSupported()) { Desktop.getDesktop().browse( @@ -1748,8 +1760,10 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? = onAlreadyStarred = { Analytics.track(AnalyticsEvent.STAR_PROMPT_ACCEPTED) AccountPreferences.device().dismissStarPromptPermanently() + starPromptOpenedFromMenu = false showStarPromptDialog = false }, + showReminderOnMaybeLater = !starPromptOpenedFromMenu, ) } @@ -1758,6 +1772,7 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? = happinessGateDialog( onHappy = { showHappinessGateDialog = false + starPromptOpenedFromMenu = false showStarPromptDialog = true Analytics.track(AnalyticsEvent.STAR_PROMPT_SHOWN) }, @@ -1774,24 +1789,28 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? = ) } - // Feedback prompt — shown after neutral/unhappy; user opts in to open contact form + // Feedback prompt — shown after neutral/unhappy; user picks reasons and optionally adds a comment if (showFeedbackPromptDialog) { feedbackPromptDialog( - onConfirm = { + onSubmit = { reasons, comment, email -> + Analytics.sendFeedbackDirect( + sentiment = feedbackSentiment, + reasons = reasons.joinToString(",") { it.name.lowercase() }, + hasComment = comment.isNotBlank(), + email = email.trim(), + ) + }, + onClose = { AccountPreferences.device().dismissStarPromptPermanently() + feedbackOpenedFromMenu = false showFeedbackPromptDialog = false - runCatching { - if (Desktop.isDesktopSupported()) { - Desktop.getDesktop().browse( - URI("https://$DOMAIN/feedback/?sentiment=$feedbackSentiment"), - ) - } - }.onFailure { log.error("Cannot open browser for feedback", it) } }, - onDecline = { - AccountPreferences.device().dismissStarPromptPermanently() + onSnooze = { + AccountPreferences.device().snoozeStarPrompt() + feedbackOpenedFromMenu = false showFeedbackPromptDialog = false }, + showReminderOnSkip = !feedbackOpenedFromMenu, ) } diff --git a/shared/src/main/kotlin/io/askimo/core/analytics/Analytics.kt b/shared/src/main/kotlin/io/askimo/core/analytics/Analytics.kt index 624e5c0d3..b5ca5da97 100644 --- a/shared/src/main/kotlin/io/askimo/core/analytics/Analytics.kt +++ b/shared/src/main/kotlin/io/askimo/core/analytics/Analytics.kt @@ -132,6 +132,56 @@ object Analytics { }, "askimo-install-ping").also { it.isDaemon = true }.start() } + /** + * Sends [AnalyticsEvent.USER_FEEDBACK_SUBMITTED] directly + * + * @param sentiment "neutral" | "unhappy" | "direct" + * @param reasons Comma-separated lowercase reason names (e.g. "slow,broken") + * @param hasComment Whether the user wrote an optional comment (text itself is never sent) + * @param email Optional follow-up email address; empty string → omitted from payload + */ + fun sendFeedbackDirect( + sentiment: String, + reasons: String, + hasComment: Boolean, + email: String, + ) { + val endpoint = runCatching { AppConfig.analytics.endpoint }.getOrNull() ?: return + val payload = buildFeedbackPayload(sentiment, reasons, hasComment, email) + Thread({ + runCatching { + val (status, _) = httpPost( + url = endpoint, + body = payload, + connectTimeoutMs = 10_000, + readTimeoutMs = 15_000, + httpVersion = HttpClient.Version.HTTP_2, + ) + if (status in 200..299) { + log.debug("Feedback submitted (HTTP $status)") + } else { + log.trace("Feedback HTTP $status — not retried") + } + }.onFailure { log.trace("Feedback submission failed: ${it.message}") } + }, "askimo-feedback").also { it.isDaemon = true }.start() + } + + private fun buildFeedbackPayload( + sentiment: String, + reasons: String, + hasComment: Boolean, + email: String, + ): String { + fun String.jsonSafe() = replace("\\", "\\\\").replace("\"", "\\\"") + val (os, version) = osAndVersion() + val installId = AnalyticsDeviceInfo.installId.jsonSafe() + val safeEmail = email.trim().jsonSafe() + val hasEmail = safeEmail.isNotEmpty() + // Email appended only when the user provided it — their act of entering it is consent + val emailField = if (hasEmail) ""","email":"$safeEmail"""" else "" + return """[{"event":"${AnalyticsEvent.USER_FEEDBACK_SUBMITTED.eventName}","appVersion":"$version","os":"$os","installId":"$installId","properties":{"sentiment":"$sentiment","reasons":"$reasons","has_comment":"$hasComment","has_email":"$hasEmail"$emailField}}]""" + } + private fun buildInstallPingPayload(): String { val (os, version) = osAndVersion() return """[{"event":"${AnalyticsEvent.INSTALL_PING.eventName}","appVersion":"$version","os":"$os"}]""" diff --git a/shared/src/main/kotlin/io/askimo/core/analytics/AnalyticsEvent.kt b/shared/src/main/kotlin/io/askimo/core/analytics/AnalyticsEvent.kt index a145d3244..8f74a69f6 100644 --- a/shared/src/main/kotlin/io/askimo/core/analytics/AnalyticsEvent.kt +++ b/shared/src/main/kotlin/io/askimo/core/analytics/AnalyticsEvent.kt @@ -334,6 +334,15 @@ enum class AnalyticsEvent( "User indicated they are unhappy with Askimo in the happiness gate.", ), + /** + * User submitted inline feedback from [feedbackPromptDialog]. + * Properties: `sentiment` (neutral|unhappy), `reasons` (comma-separated reason keys), `has_comment` (true|false). + */ + USER_FEEDBACK_SUBMITTED( + "user_feedback_submitted", + "User submitted inline feedback. Properties: sentiment (neutral|unhappy), reasons (comma-separated), has_comment (true|false).", + ), + // ── Consent ────────────────────────────────────────────────────────────── /** User explicitly opted in via the consent dialog or flag. */ diff --git a/shared/src/main/kotlin/io/askimo/core/chat/service/ChatSessionService.kt b/shared/src/main/kotlin/io/askimo/core/chat/service/ChatSessionService.kt index 88e13f232..e34cc46e8 100644 --- a/shared/src/main/kotlin/io/askimo/core/chat/service/ChatSessionService.kt +++ b/shared/src/main/kotlin/io/askimo/core/chat/service/ChatSessionService.kt @@ -432,8 +432,17 @@ class ChatSessionService( // Update the session's updatedAt timestamp to reflect the bulk write. sessionRepository.touchSession(forkedSession.id) - // Warm up a chat client for the new session so it is ready immediately. - getOrCreateClientForSession(forkedSession.id) + // Warm up a chat client for the new session in the background. + // This is optional and doesn't block the fork — if it fails (e.g., no model + // configured in tests), the client will be created on-demand when needed. + eventScope.launch { + try { + getOrCreateClientForSession(forkedSession.id) + log.debug("Pre-created chat client for forked session ${forkedSession.id} in background") + } catch (e: Exception) { + log.debug("Could not pre-create chat client for forked session ${forkedSession.id}: ${e.message}") + } + } eventScope.launch { EventBus.emit(