diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 1dc124c3f..d6f53f65d 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -298,7 +298,9 @@ export default { 'How is Qwen doing this session? (optional)': 'Wie macht sich Qwen in dieser Sitzung? (optional)', Bad: 'Schlecht', + Fine: 'In Ordnung', Good: 'Gut', + Dismiss: 'Ignorieren', 'Not Sure Yet': 'Noch nicht sicher', 'Any other key': 'Beliebige andere Taste', 'Disable Loading Phrases': 'Ladesprüche deaktivieren', diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 929ffc904..5e28a5e8e 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -315,7 +315,9 @@ export default { 'How is Qwen doing this session? (optional)': 'How is Qwen doing this session? (optional)', Bad: 'Bad', + Fine: 'Fine', Good: 'Good', + Dismiss: 'Dismiss', 'Not Sure Yet': 'Not Sure Yet', 'Any other key': 'Any other key', 'Disable Loading Phrases': 'Disable Loading Phrases', diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index c5108ec5d..76f5ef1a4 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -319,7 +319,9 @@ export default { 'How is Qwen doing this session? (optional)': 'Как дела у Qwen в этой сессии? (необязательно)', Bad: 'Плохо', + Fine: 'Нормально', Good: 'Хорошо', + Dismiss: 'Отклонить', 'Not Sure Yet': 'Пока не уверен', 'Any other key': 'Любая другая клавиша', 'Disable Loading Phrases': 'Отключить фразы при загрузке', diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index d6603207c..57a4cda60 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -305,7 +305,9 @@ export default { 'Enable User Feedback': '启用用户反馈', 'How is Qwen doing this session? (optional)': 'Qwen 这次表现如何?(可选)', Bad: '不满意', + Fine: '还行', Good: '满意', + Dismiss: '忽略', 'Not Sure Yet': '暂不评价', 'Any other key': '任意其他键', 'Disable Loading Phrases': '禁用加载短语', diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index c1bd7b80c..82ab66dd6 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1326,6 +1326,7 @@ export const AppContainer = (props: AppContainerProps) => { isFeedbackDialogOpen, openFeedbackDialog, closeFeedbackDialog, + temporaryCloseFeedbackDialog, submitFeedback, } = useFeedbackDialog({ config, @@ -1571,6 +1572,7 @@ export const AppContainer = (props: AppContainerProps) => { // Feedback dialog openFeedbackDialog, closeFeedbackDialog, + temporaryCloseFeedbackDialog, submitFeedback, }), [ @@ -1611,6 +1613,7 @@ export const AppContainer = (props: AppContainerProps) => { // Feedback dialog openFeedbackDialog, closeFeedbackDialog, + temporaryCloseFeedbackDialog, submitFeedback, ], ); diff --git a/packages/cli/src/ui/FeedbackDialog.tsx b/packages/cli/src/ui/FeedbackDialog.tsx index 7791dfb88..ec2bf3c40 100644 --- a/packages/cli/src/ui/FeedbackDialog.tsx +++ b/packages/cli/src/ui/FeedbackDialog.tsx @@ -5,19 +5,21 @@ import { useUIActions } from './contexts/UIActionsContext.js'; import { useUIState } from './contexts/UIStateContext.js'; import { useKeypress } from './hooks/useKeypress.js'; -const FEEDBACK_OPTIONS = { - GOOD: 1, - BAD: 2, - NOT_SURE: 3, +export const FEEDBACK_OPTIONS = { + BAD: 1, + FINE: 2, + GOOD: 3, + DISMISS: 0, } as const; const FEEDBACK_OPTION_KEYS = { - [FEEDBACK_OPTIONS.GOOD]: '1', - [FEEDBACK_OPTIONS.BAD]: '2', - [FEEDBACK_OPTIONS.NOT_SURE]: 'any', + [FEEDBACK_OPTIONS.BAD]: '1', + [FEEDBACK_OPTIONS.FINE]: '2', + [FEEDBACK_OPTIONS.GOOD]: '3', + [FEEDBACK_OPTIONS.DISMISS]: '0', } as const; -export const FEEDBACK_DIALOG_KEYS = ['1', '2'] as const; +export const FEEDBACK_DIALOG_KEYS = ['1', '2', '3', '0'] as const; export const FeedbackDialog: React.FC = () => { const uiState = useUIState(); @@ -25,15 +27,19 @@ export const FeedbackDialog: React.FC = () => { useKeypress( (key) => { - if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]) { - uiActions.submitFeedback(FEEDBACK_OPTIONS.GOOD); - } else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]) { + // Handle keys 0-3: permanent close with feedback/dismiss + if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]) { uiActions.submitFeedback(FEEDBACK_OPTIONS.BAD); + } else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.FINE]) { + uiActions.submitFeedback(FEEDBACK_OPTIONS.FINE); + } else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]) { + uiActions.submitFeedback(FEEDBACK_OPTIONS.GOOD); + } else if (key.name === FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.DISMISS]) { + uiActions.submitFeedback(FEEDBACK_OPTIONS.DISMISS); } else { - uiActions.submitFeedback(FEEDBACK_OPTIONS.NOT_SURE); + // Handle other keys: temporary close + uiActions.temporaryCloseFeedbackDialog(); } - - uiActions.closeFeedbackDialog(); }, { isActive: uiState.isFeedbackDialogOpen }, ); @@ -45,16 +51,24 @@ export const FeedbackDialog: React.FC = () => { {t('How is Qwen doing this session? (optional)')} + {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]}: + {t('Bad')} + + + {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.FINE]}:{' '} + + {t('Fine')} + {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.GOOD]}:{' '} {t('Good')} - {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.BAD]}: - {t('Bad')} + + {FEEDBACK_OPTION_KEYS[FEEDBACK_OPTIONS.DISMISS]}:{' '} + + {t('Dismiss')} - {t('Any other key')}: - {t('Not Sure Yet')} ); diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index de4cd1dee..584dc15f6 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -36,6 +36,11 @@ vi.mock('../utils/clipboardUtils.js'); vi.mock('../contexts/UIStateContext.js', () => ({ useUIState: vi.fn(() => ({ isFeedbackDialogOpen: false })), })); +vi.mock('../contexts/UIActionsContext.js', () => ({ + useUIActions: vi.fn(() => ({ + temporaryCloseFeedbackDialog: vi.fn(), + })), +})); const mockSlashCommands: SlashCommand[] = [ { diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 1d46d03ab..0e3c43806 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -37,6 +37,7 @@ import * as path from 'node:path'; import { SCREEN_READER_USER_PREFIX } from '../textConstants.js'; import { useShellFocusState } from '../contexts/ShellFocusContext.js'; import { useUIState } from '../contexts/UIStateContext.js'; +import { useUIActions } from '../contexts/UIActionsContext.js'; import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js'; export interface InputPromptProps { buffer: TextBuffer; @@ -109,6 +110,7 @@ export const InputPrompt: React.FC = ({ }) => { const isShellFocused = useShellFocusState(); const uiState = useUIState(); + const uiActions = useUIActions(); const [justNavigatedHistory, setJustNavigatedHistory] = useState(false); const [escPressCount, setEscPressCount] = useState(0); const [showEscapePrompt, setShowEscapePrompt] = useState(false); @@ -337,12 +339,16 @@ export const InputPrompt: React.FC = ({ return; } - // Intercept feedback dialog option keys (1, 2) when dialog is open - if ( - uiState.isFeedbackDialogOpen && - (FEEDBACK_DIALOG_KEYS as readonly string[]).includes(key.name) - ) { - return; + // Handle feedback dialog keyboard interactions when dialog is open + if (uiState.isFeedbackDialogOpen) { + // If it's one of the feedback option keys (1-4), let FeedbackDialog handle it + if ((FEEDBACK_DIALOG_KEYS as readonly string[]).includes(key.name)) { + return; + } else { + // For any other key, close feedback dialog temporarily and continue with normal processing + uiActions.temporaryCloseFeedbackDialog(); + // Continue processing the key for normal input handling + } } // Reset ESC count and hide prompt on any non-ESC key @@ -712,6 +718,7 @@ export const InputPrompt: React.FC = ({ onToggleShortcuts, showShortcuts, uiState, + uiActions, ], ); diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index a1e7f3b35..17d74dd4e 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -71,6 +71,7 @@ export interface UIActions { // Feedback dialog openFeedbackDialog: () => void; closeFeedbackDialog: () => void; + temporaryCloseFeedbackDialog: () => void; submitFeedback: (rating: number) => void; } diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts index 18865b1f0..432d6d15a 100644 --- a/packages/cli/src/ui/hooks/useFeedbackDialog.ts +++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts @@ -15,6 +15,7 @@ import { USER_SETTINGS_PATH, } from '../../config/settings.js'; import type { SessionStatsState } from '../contexts/SessionContext.js'; +import { FEEDBACK_OPTIONS } from '../FeedbackDialog.js'; import stripJsonComments from 'strip-json-comments'; const FEEDBACK_SHOW_PROBABILITY = 0.25; // 25% probability of showing feedback dialog @@ -96,37 +97,48 @@ export const useFeedbackDialog = ({ }: UseFeedbackDialogProps) => { // Feedback dialog state const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); + const [isFeedbackDismissedTemporarily, setIsFeedbackDismissedTemporarily] = + useState(false); const openFeedbackDialog = useCallback(() => { setIsFeedbackDialogOpen(true); - - // Record the timestamp when feedback dialog is shown (fire and forget) - settings.setValue( - SettingScope.User, - 'ui.feedbackLastShownTimestamp', - Date.now(), - ); - }, [settings]); + }, []); const closeFeedbackDialog = useCallback( () => setIsFeedbackDialogOpen(false), [], ); + const temporaryCloseFeedbackDialog = useCallback(() => { + setIsFeedbackDialogOpen(false); + setIsFeedbackDismissedTemporarily(true); + }, []); + const submitFeedback = useCallback( (rating: number) => { - // Create and log the feedback event - const feedbackEvent = new UserFeedbackEvent( - sessionStats.sessionId, - rating as UserFeedbackRating, - config.getModel(), - config.getApprovalMode(), - ); + // Only create and log feedback event for ratings 1-3 (BAD, FINE, GOOD) + // Rating 0 (DISMISS) should not trigger any telemetry + if (rating >= FEEDBACK_OPTIONS.BAD && rating <= FEEDBACK_OPTIONS.GOOD) { + const feedbackEvent = new UserFeedbackEvent( + sessionStats.sessionId, + rating as UserFeedbackRating, + config.getModel(), + config.getApprovalMode(), + ); + + logUserFeedback(config, feedbackEvent); + + // Record the timestamp when feedback dialog is submitted + settings.setValue( + SettingScope.User, + 'ui.feedbackLastShownTimestamp', + Date.now(), + ); + } - logUserFeedback(config, feedbackEvent); closeFeedbackDialog(); }, - [config, sessionStats, closeFeedbackDialog], + [closeFeedbackDialog, sessionStats.sessionId, config, settings], ); useEffect(() => { @@ -140,13 +152,15 @@ export const useFeedbackDialog = ({ // 5. Random chance (25% probability) // 6. Meets minimum requirements (tool calls > 10 OR user messages > 5) // 7. Fatigue mechanism allows showing (not shown recently across sessions) + // 8. Not temporarily dismissed if ( config.getAuthType() !== AuthType.QWEN_OAUTH || !config.getUsageStatisticsEnabled() || settings.merged.ui?.enableUserFeedback === false || !lastMessageIsAIResponse(history) || Math.random() > FEEDBACK_SHOW_PROBABILITY || - !meetsMinimumSessionRequirements(sessionStats) + !meetsMinimumSessionRequirements(sessionStats) || + isFeedbackDismissedTemporarily ) { return; } @@ -164,15 +178,27 @@ export const useFeedbackDialog = ({ history, sessionStats, isFeedbackDialogOpen, + isFeedbackDismissedTemporarily, openFeedbackDialog, settings.merged.ui?.enableUserFeedback, config, ]); + // Reset temporary dismissal when a new AI response starts streaming + useEffect(() => { + if ( + streamingState === StreamingState.Responding && + isFeedbackDismissedTemporarily + ) { + setIsFeedbackDismissedTemporarily(false); + } + }, [streamingState, isFeedbackDismissedTemporarily]); + return { isFeedbackDialogOpen, openFeedbackDialog, closeFeedbackDialog, + temporaryCloseFeedbackDialog, submitFeedback, }; };