From 119f1977c73480ac3dd525daf739f0a8ca608155 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:59:58 +0000 Subject: [PATCH] Auto-apply paid defaults (model + web search) for pro/max/team users - Add one-time paid defaults flip: when billing confirms pro/max/team and paidDefaultsApplied is not set, auto-select 'Powerful' model (kimi-k2-5) and enable web search - Store paidDefaultsApplied as ISO date string for future extensibility - Add getInitialWebSearchEnabled() for billing-aware fast initial load - Add setModelInternal() to distinguish system vs user model changes - Respect manual user choices: once defaults are applied, never override - Handle downgrade: clear paid defaults when switching to free/starter - Remove WebSearchInfoDialog entirely (no longer needed with auto-enable) - Simplify web search toggle handlers (remove first-time dialog logic) Co-Authored-By: tony@opensecret.cloud --- frontend/src/components/UnifiedChat.tsx | 58 ++------ .../src/components/WebSearchInfoDialog.tsx | 77 ----------- frontend/src/state/LocalStateContext.tsx | 124 +++++++++++++++--- 3 files changed, 119 insertions(+), 140 deletions(-) delete mode 100644 frontend/src/components/WebSearchInfoDialog.tsx diff --git a/frontend/src/components/UnifiedChat.tsx b/frontend/src/components/UnifiedChat.tsx index efcd466e..6031c57c 100644 --- a/frontend/src/components/UnifiedChat.tsx +++ b/frontend/src/components/UnifiedChat.tsx @@ -52,7 +52,7 @@ import { useIsMobile } from "@/utils/utils"; import { fileToDataURL } from "@/utils/file"; import { truncateMarkdownPreservingLinks } from "@/utils/markdown"; import { useOpenAI } from "@/ai/useOpenAi"; -import { DEFAULT_MODEL_ID } from "@/state/LocalStateContext"; +import { DEFAULT_MODEL_ID, getInitialWebSearchEnabled } from "@/state/LocalStateContext"; import { Markdown, ThinkingBlock } from "@/components/markdown"; import { ModelSelector } from "@/components/ModelSelector"; import { useLocalState } from "@/state/useLocalState"; @@ -61,7 +61,6 @@ import { UpgradePromptDialog } from "@/components/UpgradePromptDialog"; import { DocumentPlatformDialog } from "@/components/DocumentPlatformDialog"; import { ContextLimitDialog } from "@/components/ContextLimitDialog"; import { RecordingOverlay } from "@/components/RecordingOverlay"; -import { WebSearchInfoDialog } from "@/components/WebSearchInfoDialog"; import { TTSDownloadDialog } from "@/components/TTSDownloadDialog"; import { useTTS } from "@/services/tts/TTSContext"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -936,7 +935,6 @@ export function UnifiedChat() { >("image"); const [documentPlatformDialogOpen, setDocumentPlatformDialogOpen] = useState(false); const [contextLimitDialogOpen, setContextLimitDialogOpen] = useState(false); - const [webSearchInfoDialogOpen, setWebSearchInfoDialogOpen] = useState(false); const [ttsSetupDialogOpen, setTtsSetupDialogOpen] = useState(false); // Audio recording states @@ -945,10 +943,8 @@ export function UnifiedChat() { const [isProcessingSend, setIsProcessingSend] = useState(false); const [audioError, setAudioError] = useState(null); - // Web search toggle state - persisted in localStorage - const [isWebSearchEnabled, setIsWebSearchEnabled] = useState(() => { - return localStorage.getItem("webSearchEnabled") === "true"; - }); + // Web search toggle state - persisted in localStorage, billing-aware initial default + const [isWebSearchEnabled, setIsWebSearchEnabled] = useState(getInitialWebSearchEnabled); // Fullscreen mode for power users - persisted in localStorage const [isFullscreen, setIsFullscreen] = useState(() => { @@ -966,6 +962,15 @@ export function UnifiedChat() { localStorage.setItem("webSearchEnabled", isWebSearchEnabled.toString()); }, [isWebSearchEnabled]); + // Sync web search state when billing status changes (handles the one-time paid defaults flip). + // When setBillingStatus writes "webSearchEnabled" to localStorage, we need to pick that up. + useEffect(() => { + if (localState.billingStatus) { + const storedValue = localStorage.getItem("webSearchEnabled") === "true"; + setIsWebSearchEnabled(storedValue); + } + }, [localState.billingStatus]); + // Toggle fullscreen with animation const toggleFullscreen = useCallback(() => { setIsFullscreenAnimating(true); @@ -2993,24 +2998,11 @@ export function UnifiedChat() { size="sm" className="h-8 w-8 p-0" onClick={() => { - // Step 1: Check if user has access (free/starter users see upsell) if (!canUseWebSearch) { setUpgradeFeature("websearch"); setUpgradeDialogOpen(true); return; } - - // Step 2: Check if this is their first time (enable web search, set flag, show popup) - const hasSeenWebSearchInfo = - localStorage.getItem("hasSeenWebSearchInfo") === "true"; - if (!hasSeenWebSearchInfo) { - localStorage.setItem("hasSeenWebSearchInfo", "true"); - setIsWebSearchEnabled(true); - setWebSearchInfoDialogOpen(true); - return; - } - - // Step 3: Toggle web search directly setIsWebSearchEnabled(!isWebSearchEnabled); }} aria-label={ @@ -3236,24 +3228,11 @@ export function UnifiedChat() { size="sm" className="h-8 w-8 p-0" onClick={() => { - // Step 1: Check if user has access (free/starter users see upsell) if (!canUseWebSearch) { setUpgradeFeature("websearch"); setUpgradeDialogOpen(true); return; } - - // Step 2: Check if this is their first time (enable web search, set flag, show popup) - const hasSeenWebSearchInfo = - localStorage.getItem("hasSeenWebSearchInfo") === "true"; - if (!hasSeenWebSearchInfo) { - localStorage.setItem("hasSeenWebSearchInfo", "true"); - setIsWebSearchEnabled(true); - setWebSearchInfoDialogOpen(true); - return; - } - - // Step 3: Toggle web search directly setIsWebSearchEnabled(!isWebSearchEnabled); }} aria-label={ @@ -3404,19 +3383,6 @@ export function UnifiedChat() { hasDocument={!!documentName} /> - {/* Web search info dialog for first-time paid users */} - { - // When dialog is closed via X or backdrop, just dismiss - web search already enabled on click - setWebSearchInfoDialogOpen(open); - }} - onConfirm={() => { - // "Got it" button - just close (web search already enabled on click) - setWebSearchInfoDialogOpen(false); - }} - /> - {/* TTS setup dialog */} diff --git a/frontend/src/components/WebSearchInfoDialog.tsx b/frontend/src/components/WebSearchInfoDialog.tsx deleted file mode 100644 index d7d8b0b9..00000000 --- a/frontend/src/components/WebSearchInfoDialog.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle -} from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Globe, Check } from "lucide-react"; - -interface WebSearchInfoDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onConfirm: () => void; -} - -export function WebSearchInfoDialog({ open, onOpenChange, onConfirm }: WebSearchInfoDialogProps) { - return ( - - - -
-
- -
- Live Web Search -
- - When toggled on, Maple will automatically search the web when your question requires - current or real-time information. - -
- -
-
-

What you get:

-
    -
  • - - Live web search powered by Brave -
  • -
  • - - Get up-to-date information from the internet -
  • -
  • - - Search queries are sent to Brave but not linked to your identity -
  • -
  • - - Results are processed privately and securely -
  • -
  • - - Perfect for current events, research, and fact-checking -
  • -
-
- -
-

- Click the globe icon anytime to toggle web search on or off for your messages. -

-
-
- - - - -
-
- ); -} diff --git a/frontend/src/state/LocalStateContext.tsx b/frontend/src/state/LocalStateContext.tsx index 49a4d20f..3c43787a 100644 --- a/frontend/src/state/LocalStateContext.tsx +++ b/frontend/src/state/LocalStateContext.tsx @@ -12,6 +12,47 @@ export { } from "./LocalStateContextDef"; export const DEFAULT_MODEL_ID = "gpt-oss-120b"; +export const PAID_DEFAULT_MODEL_ID = "kimi-k2-5"; + +// Check if a plan name corresponds to a pro/max/team plan +function isProMaxOrTeamPlan(planName: string): boolean { + return planName.includes("pro") || planName.includes("max") || planName.includes("team"); +} + +// Check if paid defaults have already been applied for this user. +// The value is an ISO date string indicating when defaults were last applied. +function hasPaidDefaultsBeenApplied(): boolean { + return localStorage.getItem("paidDefaultsApplied") !== null; +} + +// Helper to get the initial web search state from localStorage, billing-aware +export function getInitialWebSearchEnabled(): boolean { + try { + // If user has explicitly toggled web search before, respect that + const webSearchSetting = localStorage.getItem("webSearchEnabled"); + if (webSearchSetting !== null) { + return webSearchSetting === "true"; + } + + // No explicit setting — check if paid defaults were applied (web search would be on) + if (hasPaidDefaultsBeenApplied()) { + return true; + } + + // Check cached billing for fast initial load (before billing fetch completes) + const cachedBillingStr = localStorage.getItem("cachedBillingStatus"); + if (cachedBillingStr) { + const cachedBilling = JSON.parse(cachedBillingStr) as BillingStatus; + const planName = cachedBilling.product_name?.toLowerCase() || ""; + if (isProMaxOrTeamPlan(planName)) { + return true; + } + } + } catch (error) { + console.error("Failed to get initial web search state:", error); + } + return false; +} // Helper to get default model based on cached billing status function getInitialModel(): string { @@ -21,28 +62,35 @@ function getInitialModel(): string { } try { - // Priority 1: Check local storage for last used model + // Priority 1: Check local storage for user's explicit model choice const selectedModel = localStorage.getItem("selectedModel"); if (selectedModel) { return aliasModelName(selectedModel); } - // Priority 2: Check cached billing status for pro/max/team users + // Priority 2: Check if paid defaults were already applied + // (user is returning paid user who got the one-time flip but then + // cleared selectedModel somehow — unlikely but safe fallback) + if (hasPaidDefaultsBeenApplied()) { + return PAID_DEFAULT_MODEL_ID; + } + + // Priority 3: Check cached billing status for pro/max/team users const cachedBillingStr = localStorage.getItem("cachedBillingStatus"); if (cachedBillingStr) { const cachedBilling = JSON.parse(cachedBillingStr) as BillingStatus; const planName = cachedBilling.product_name?.toLowerCase() || ""; - // Pro, Max, or Team users get default model - if (planName.includes("pro") || planName.includes("max") || planName.includes("team")) { - return DEFAULT_MODEL_ID; + // Pro, Max, or Team users get the powerful reasoning model + if (isProMaxOrTeamPlan(planName)) { + return PAID_DEFAULT_MODEL_ID; } } } catch (error) { console.error("Failed to load initial model:", error); } - // Priority 3: Default to free model + // Priority 4: Default to free model return DEFAULT_MODEL_ID; } @@ -162,8 +210,7 @@ export const LocalStateProvider = ({ children }: { children: React.ReactNode }) planName.includes("team") || planName.includes("starter"); - const isProMaxOrTeam = - planName.includes("pro") || planName.includes("max") || planName.includes("team"); + const isProMaxOrTeam = isProMaxOrTeamPlan(planName); // Check if billing plan changed from cached version let billingChanged = false; @@ -190,17 +237,43 @@ export const LocalStateProvider = ({ children }: { children: React.ReactNode }) console.error("Failed to cache billing status:", error); } - // Update model if: 1) no custom selectedModel OR 2) billing plan changed + // One-time paid defaults: when a user is on pro/max/team and we haven't + // applied paid defaults yet, flip model to "Powerful" and web search ON. + // This handles both new signups and free-to-paid upgrades. try { - const selectedModel = localStorage.getItem("selectedModel"); - const shouldUpdateModel = !selectedModel || billingChanged; + if (isProMaxOrTeam && !hasPaidDefaultsBeenApplied()) { + // Apply paid defaults — set model to Powerful reasoning model + setModelInternal(PAID_DEFAULT_MODEL_ID); + + // Enable web search + localStorage.setItem("webSearchEnabled", "true"); + + // Mark when we applied paid defaults (ISO date) so we never override again. + // Future defaults can check this date to decide whether to re-apply newer defaults. + localStorage.setItem("paidDefaultsApplied", new Date().toISOString()); + + return; + } + } catch (error) { + console.error("Failed to apply paid defaults:", error); + } - if (shouldUpdateModel) { + // For users who already had defaults applied: handle plan changes + try { + if (billingChanged) { if (isProMaxOrTeam) { - setModel(DEFAULT_MODEL_ID); - } else if (billingChanged) { - // User downgraded, switch back to free model - setModel(DEFAULT_MODEL_ID); + // Plan changed but still pro-tier — only update model if user + // hasn't manually chosen one (selectedModel not in localStorage) + const selectedModel = localStorage.getItem("selectedModel"); + if (!selectedModel) { + setModelInternal(PAID_DEFAULT_MODEL_ID); + } + } else { + // User downgraded to free/starter — switch back to free model + // and clear paid defaults so they get re-applied if they upgrade again + setModelInternal(DEFAULT_MODEL_ID); + localStorage.removeItem("paidDefaultsApplied"); + localStorage.removeItem("selectedModel"); } } } catch (error) { @@ -375,12 +448,29 @@ export const LocalStateProvider = ({ children }: { children: React.ReactNode }) }); } + // Internal model setter — updates state and localStorage but does NOT mark as + // a user's explicit choice. Used by billing/system logic. + function setModelInternal(modelId: string) { + const aliasedModel = aliasModelName(modelId); + setLocalState((prev) => { + if (prev.model === aliasedModel) return prev; + return { ...prev, model: aliasedModel }; + }); + try { + localStorage.setItem("selectedModel", aliasedModel); + } catch (error) { + console.error("Failed to save model to localStorage:", error); + } + } + + // Public model setter — records the choice as a user-initiated selection. + // After this, we won't auto-override their model choice. function setModel(model: string) { const aliasedModel = aliasModelName(model); setLocalState((prev) => { if (prev.model === aliasedModel) return prev; - // Save to localStorage when model changes + // Save to localStorage as user's explicit choice try { localStorage.setItem("selectedModel", aliasedModel); } catch (error) {