diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 46f847aa6..c4b39696c 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -37,9 +37,6 @@ import { getErrorMessage, getAllGeminiMdFilenames, ShellExecutionService, - logUserFeedback, - UserFeedbackEvent, - type UserFeedbackRating, } from '@qwen-code/qwen-code-core'; import { buildResumedHistoryItems } from './utils/resumeHistoryUtils.js'; import { validateAuthMethod } from '../config/auth.js'; @@ -48,6 +45,7 @@ import process from 'node:process'; import { useHistory } from './hooks/useHistoryManager.js'; import { useMemoryMonitor } from './hooks/useMemoryMonitor.js'; import { useThemeCommand } from './hooks/useThemeCommand.js'; +import { useFeedbackDialog } from './hooks/useFeedbackDialog.js'; import { useAuthCommand } from './auth/useAuth.js'; import { useEditorSettings } from './hooks/useEditorSettings.js'; import { useSettingsCommand } from './hooks/useSettingsCommand.js'; @@ -185,18 +183,6 @@ export const AppContainer = (props: AppContainerProps) => { // Helper to determine the current model (polled, since Config has no model-change event). const getCurrentModel = useCallback(() => config.getModel(), [config]); - // Feedback dialog state - const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); - const [feedbackShownForSession, setFeedbackShownForSession] = useState(false); - const openFeedbackDialog = useCallback(() => { - setIsFeedbackDialogOpen(true); - setFeedbackShownForSession(true); - }, []); - const closeFeedbackDialog = useCallback( - () => setIsFeedbackDialogOpen(false), - [], - ); - const [currentModel, setCurrentModel] = useState(getCurrentModel()); const [isConfigInitialized, setConfigInitialized] = useState(false); @@ -213,40 +199,6 @@ export const AppContainer = (props: AppContainerProps) => { const logger = useLogger(config.storage, sessionStats.sessionId); const branchName = useGitBranchName(config.getTargetDir()); - // Submit user feedback function - const submitFeedback = useCallback( - (rating: number) => { - // Calculate session duration and turn count - const sessionDurationMs = - Date.now() - sessionStats.sessionStartTime.getTime(); - let lastUserMessageIndex = -1; - for (let i = historyManager.history.length - 1; i >= 0; i--) { - if (historyManager.history[i].type === MessageType.USER) { - lastUserMessageIndex = i; - break; - } - } - const turnCount = - lastUserMessageIndex === -1 - ? 0 - : historyManager.history.length - lastUserMessageIndex; - - // Create and log the feedback event - const feedbackEvent = new UserFeedbackEvent( - sessionStats.sessionId, - rating as UserFeedbackRating, - sessionDurationMs, - turnCount, - config.getModel(), - config.getApprovalMode(), - ); - - logUserFeedback(config, feedbackEvent); - closeFeedbackDialog(); - }, - [sessionStats, historyManager.history, config, closeFeedbackDialog], - ); - // Layout measurements const mainControlsRef = useRef(null); const originalTitleRef = useRef( @@ -1222,6 +1174,19 @@ export const AppContainer = (props: AppContainerProps) => { const nightly = props.version.includes('nightly'); + const { + isFeedbackDialogOpen, + openFeedbackDialog, + closeFeedbackDialog, + submitFeedback, + } = useFeedbackDialog({ + config, + settings, + streamingState, + history: historyManager.history, + sessionStats, + }); + const dialogsVisible = showWelcomeBackDialog || showWorkspaceMigrationDialog || @@ -1246,78 +1211,6 @@ export const AppContainer = (props: AppContainerProps) => { isResumeDialogOpen || isFeedbackDialogOpen; - // Track when to show feedback dialog - useEffect(() => { - if ( - streamingState === StreamingState.Idle && - historyManager.history.length > 0 - ) { - // Find the last user message and check if there's AI response after it - let lastUserMessageIndex = -1; - let hasAIResponseAfterLastUser = false; - - for (let i = historyManager.history.length - 1; i >= 0; i--) { - if (historyManager.history[i].type === MessageType.USER) { - lastUserMessageIndex = i; - break; - } - } - - // Check if there's any AI response (GEMINI message) after the last user message - if (lastUserMessageIndex !== -1) { - for ( - let i = lastUserMessageIndex + 1; - i < historyManager.history.length; - i++ - ) { - if (historyManager.history[i].type === MessageType.GEMINI) { - hasAIResponseAfterLastUser = true; - break; - } - } - } - - const sessionDurationMs = - Date.now() - sessionStats.sessionStartTime.getTime(); - - // Show feedback dialog if: - // 1. Telemetry is enabled (required for feedback submission) - // 2. User feedback is enabled in settings - // 3. There's an AI response after the last user message (real AI conversation) - // 4. Session duration > 10 seconds (meaningful interaction) - // 5. No other dialogs are open - // 6. Not already shown for this session - // 7. Random chance (25% probability) - if ( - config.getUsageStatisticsEnabled() && // Only show if telemetry is enabled - settings.merged.ui?.enableUserFeedback !== false && // Default to true if not set - hasAIResponseAfterLastUser && - sessionDurationMs > 10000 && // 10 seconds minimum for meaningful interaction - !dialogsVisible && - !isFeedbackDialogOpen && - !feedbackShownForSession && - Math.random() < 0.25 // 25% probability - ) { - setTimeout(() => { - // Double check no dialogs opened in the meantime - if (!dialogsVisible && !isFeedbackDialogOpen) { - openFeedbackDialog(); - } - }, 1000); // Delay to ensure user has time to see the completion - } - } - }, [ - streamingState, - historyManager.history, - sessionStats, - dialogsVisible, - isFeedbackDialogOpen, - feedbackShownForSession, - openFeedbackDialog, - settings.merged.ui?.enableUserFeedback, - config, - ]); - const pendingHistoryItems = useMemo( () => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems], [pendingSlashCommandHistoryItems, pendingGeminiHistoryItems], diff --git a/packages/cli/src/ui/hooks/useFeedbackDialog.ts b/packages/cli/src/ui/hooks/useFeedbackDialog.ts new file mode 100644 index 000000000..6c4d356b2 --- /dev/null +++ b/packages/cli/src/ui/hooks/useFeedbackDialog.ts @@ -0,0 +1,145 @@ +import { useState, useCallback, useEffect } from 'react'; +import { + type Config, + logUserFeedback, + UserFeedbackEvent, + type UserFeedbackRating, +} from '@qwen-code/qwen-code-core'; +import { StreamingState, MessageType, type HistoryItem } from '../types.js'; +import type { LoadedSettings } from '../../config/settings.js'; +import type { SessionStatsState } from '../contexts/SessionContext.js'; + +export interface UseFeedbackDialogProps { + config: Config; + settings: LoadedSettings; + streamingState: StreamingState; + history: HistoryItem[]; + sessionStats: SessionStatsState; +} + +export const useFeedbackDialog = ({ + config, + settings, + streamingState, + history, + sessionStats, +}: UseFeedbackDialogProps) => { + // Feedback dialog state + const [isFeedbackDialogOpen, setIsFeedbackDialogOpen] = useState(false); + const [feedbackShownForSession, setFeedbackShownForSession] = useState(false); + + const openFeedbackDialog = useCallback(() => { + setIsFeedbackDialogOpen(true); + setFeedbackShownForSession(true); + }, []); + + const closeFeedbackDialog = useCallback( + () => setIsFeedbackDialogOpen(false), + [], + ); + + const submitFeedback = useCallback( + (rating: number) => { + // Calculate session duration and turn count + const sessionDurationMs = + Date.now() - sessionStats.sessionStartTime.getTime(); + let lastUserMessageIndex = -1; + for (let i = history.length - 1; i >= 0; i--) { + if (history[i].type === MessageType.USER) { + lastUserMessageIndex = i; + break; + } + } + const turnCount = + lastUserMessageIndex === -1 ? 0 : history.length - lastUserMessageIndex; + + // Create and log the feedback event + const feedbackEvent = new UserFeedbackEvent( + sessionStats.sessionId, + rating as UserFeedbackRating, + sessionDurationMs, + turnCount, + config.getModel(), + config.getApprovalMode(), + ); + + logUserFeedback(config, feedbackEvent); + closeFeedbackDialog(); + }, + [config, sessionStats, history, closeFeedbackDialog], + ); + + // Track when to show feedback dialog + useEffect(() => { + let timeoutId: NodeJS.Timeout; + + if (streamingState === StreamingState.Idle && history.length > 0) { + // Find the last user message and check if there's AI response after it + let lastUserMessageIndex = -1; + let hasAIResponseAfterLastUser = false; + + for (let i = history.length - 1; i >= 0; i--) { + if (history[i].type === MessageType.USER) { + lastUserMessageIndex = i; + break; + } + } + + // Check if there's any AI response (GEMINI message) after the last user message + if (lastUserMessageIndex !== -1) { + for (let i = lastUserMessageIndex + 1; i < history.length; i++) { + if (history[i].type === MessageType.GEMINI) { + hasAIResponseAfterLastUser = true; + break; + } + } + } + + const sessionDurationMs = + Date.now() - sessionStats.sessionStartTime.getTime(); + + // Show feedback dialog if: + // 1. Telemetry is enabled (required for feedback submission) + // 2. User feedback is enabled in settings + // 3. There's an AI response after the last user message (real AI conversation) + // 4. Session duration > 10 seconds (meaningful interaction) + // 5. Not already shown for this session + // 6. Random chance (25% probability) + // Note: We check !isFeedbackDialogOpen to ensure it's not already open + if ( + config.getUsageStatisticsEnabled() && // Only show if telemetry is enabled + settings.merged.ui?.enableUserFeedback !== false && // Default to true if not set + hasAIResponseAfterLastUser && + sessionDurationMs > 10000 && // 10 seconds minimum for meaningful interaction + !feedbackShownForSession && + Math.random() < 0.25 // 25% probability + ) { + timeoutId = setTimeout(() => { + openFeedbackDialog(); + }, 1000); // Delay to ensure user has time to see the completion + } + } + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [ + streamingState, + history, + sessionStats, + isFeedbackDialogOpen, + feedbackShownForSession, + openFeedbackDialog, + settings.merged.ui?.enableUserFeedback, + config, + ]); + + return { + isFeedbackDialogOpen, + openFeedbackDialog, + closeFeedbackDialog, + submitFeedback, + }; +};