Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 102 additions & 49 deletions frontend/src/components/UnifiedChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ 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 { Alert, AlertDescription } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
import {
Expand Down Expand Up @@ -647,10 +648,11 @@ export function UnifiedChat() {
const [attachmentError, setAttachmentError] = useState<string | null>(null);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
const [upgradeFeature, setUpgradeFeature] = useState<
"image" | "document" | "voice" | "usage" | "tokens"
"image" | "document" | "voice" | "usage" | "tokens" | "websearch"
>("image");
const [documentPlatformDialogOpen, setDocumentPlatformDialogOpen] = useState(false);
const [contextLimitDialogOpen, setContextLimitDialogOpen] = useState(false);
const [webSearchInfoDialogOpen, setWebSearchInfoDialogOpen] = useState(false);

// Audio recording states
const [isRecording, setIsRecording] = useState(false);
Expand All @@ -660,9 +662,8 @@ export function UnifiedChat() {

// Web search toggle state
const [isWebSearchEnabled, setIsWebSearchEnabled] = useState(false);
const [isWebSearchUnlocked, setIsWebSearchUnlocked] = useState(() => {
return localStorage.getItem("webSearchUnlocked") === "true";
});

// Easter egg state (for future features)
const [logoTapCount, setLogoTapCount] = useState(0);
const tapTimeoutRef = useRef<number | null>(null);

Expand Down Expand Up @@ -1287,7 +1288,7 @@ export function UnifiedChat() {
// Toggle sidebar
const toggleSidebar = useCallback(() => setIsSidebarOpen((prev) => !prev), []);

// Handle logo tap for easter egg unlock
// Handle logo tap for easter egg (reserved for future features)
const handleLogoTap = useCallback(() => {
const newCount = logoTapCount + 1;
setLogoTapCount(newCount);
Expand All @@ -1302,14 +1303,14 @@ export function UnifiedChat() {
setLogoTapCount(0);
}, 2000);

// Unlock web search after 7 taps
// Easter egg trigger at 7 taps (currently unused, reserved for future features)
if (newCount >= 7) {
localStorage.setItem("webSearchUnlocked", "true");
setIsWebSearchUnlocked(true);
console.log("Easter egg activated! πŸ₯š");
setLogoTapCount(0);
if (tapTimeoutRef.current !== null) {
window.clearTimeout(tapTimeoutRef.current);
}
// TODO: Add easter egg feature here
}
}, [logoTapCount]);

Expand All @@ -1324,6 +1325,7 @@ export function UnifiedChat() {
const canUseImages = hasProAccess;
const canUseDocuments = hasProAccess;
const canUseVoice = hasProAccess && localState.hasWhisperModel;
const canUseWebSearch = hasProAccess;

const handleAddImages = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -2491,6 +2493,46 @@ export function UnifiedChat() {
</Button>
)}

{/* Web search toggle button - always visible */}
<Button
type="button"
variant="ghost"
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
const hasSeenWebSearchInfo =
localStorage.getItem("hasSeenWebSearchInfo") === "true";
if (!hasSeenWebSearchInfo) {
// Immediately enable web search and set flag
localStorage.setItem("hasSeenWebSearchInfo", "true");
setIsWebSearchEnabled(true);
// Show informational dialog
setWebSearchInfoDialogOpen(true);
return;
}

// Step 3: Toggle web search directly
setIsWebSearchEnabled(!isWebSearchEnabled);
}}
aria-label={
isWebSearchEnabled ? "Disable web search" : "Enable web search"
}
>
<Globe
className={`h-4 w-4 ${
isWebSearchEnabled ? "text-blue-500" : "text-muted-foreground"
}`}
/>
</Button>

{/* Attachment dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand Down Expand Up @@ -2535,26 +2577,6 @@ export function UnifiedChat() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

{/* Web search toggle button - hidden until unlocked */}
{isWebSearchUnlocked && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => setIsWebSearchEnabled(!isWebSearchEnabled)}
aria-label={
isWebSearchEnabled ? "Disable web search" : "Enable web search"
}
>
<Globe
className={`h-4 w-4 ${
isWebSearchEnabled ? "text-blue-500" : "text-muted-foreground"
}`}
/>
</Button>
)}
</div>

<div className="flex items-center gap-2">
Expand Down Expand Up @@ -2732,6 +2754,46 @@ export function UnifiedChat() {
</Button>
)}

{/* Web search toggle button - always visible */}
<Button
type="button"
variant="ghost"
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
const hasSeenWebSearchInfo =
localStorage.getItem("hasSeenWebSearchInfo") === "true";
if (!hasSeenWebSearchInfo) {
// Immediately enable web search and set flag
localStorage.setItem("hasSeenWebSearchInfo", "true");
setIsWebSearchEnabled(true);
// Show informational dialog
setWebSearchInfoDialogOpen(true);
return;
}

// Step 3: Toggle web search directly
setIsWebSearchEnabled(!isWebSearchEnabled);
}}
aria-label={
isWebSearchEnabled ? "Disable web search" : "Enable web search"
}
>
<Globe
className={`h-4 w-4 ${
isWebSearchEnabled ? "text-blue-500" : "text-muted-foreground"
}`}
/>
</Button>

{/* Attachment dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand Down Expand Up @@ -2776,26 +2838,6 @@ export function UnifiedChat() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

{/* Web search toggle button - hidden until unlocked */}
{isWebSearchUnlocked && (
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => setIsWebSearchEnabled(!isWebSearchEnabled)}
aria-label={
isWebSearchEnabled ? "Disable web search" : "Enable web search"
}
>
<Globe
className={`h-4 w-4 ${
isWebSearchEnabled ? "text-blue-500" : "text-muted-foreground"
}`}
/>
</Button>
)}
</div>

<div className="flex items-center gap-2">
Expand Down Expand Up @@ -2868,7 +2910,9 @@ export function UnifiedChat() {
? "usage"
: upgradeFeature === "tokens"
? "tokens"
: "image"
: upgradeFeature === "websearch"
? "websearch"
: "image"
}
/>

Expand All @@ -2887,6 +2931,15 @@ export function UnifiedChat() {
hasDocument={!!documentName}
/>

{/* Web search info dialog for first-time paid users */}
<WebSearchInfoDialog
open={webSearchInfoDialogOpen}
onOpenChange={setWebSearchInfoDialogOpen}
onConfirm={() => {
setWebSearchInfoDialogOpen(false);
}}
/>

{/* Hidden file inputs - must be outside conditional rendering to work in both views */}
<input
type="file"
Expand Down
22 changes: 19 additions & 3 deletions frontend/src/components/UpgradePromptDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import {
Volume2,
FileText,
Gauge,
MessageCircle
MessageCircle,
Globe
} from "lucide-react";
import { useNavigate } from "@tanstack/react-router";
import { useLocalState } from "@/state/useLocalState";

interface UpgradePromptDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
feature: "image" | "voice" | "model" | "tts" | "document" | "usage" | "tokens";
feature: "image" | "voice" | "model" | "tts" | "document" | "usage" | "tokens" | "websearch";
modelName?: string;
}

Expand Down Expand Up @@ -65,7 +66,22 @@ export function UpgradePromptDialog({
};

const getFeatureInfo = () => {
if (feature === "image") {
if (feature === "websearch") {
return {
icon: <Globe className="h-8 w-8" />,
title: "Live Web Search",
description: "Search the web in real-time with AI-powered results",
requiredPlan: "Pro",
benefits: [
"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",
"Seamlessly integrated into your chat experience"
]
};
} else if (feature === "image") {
return {
icon: <Image className="h-8 w-8" />,
title: "Image Upload",
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/components/WebSearchInfoDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
<Globe className="h-8 w-8" />
</div>
<DialogTitle>Live Web Search</DialogTitle>
</div>
<DialogDescription className="text-base">
When toggled on, Maple will automatically search the web when your question requires
current or real-time information.
</DialogDescription>
</DialogHeader>

<div className="space-y-4 py-4">
<div className="space-y-2">
<p className="text-sm font-medium">What you get:</p>
<ul className="space-y-2">
<li className="flex items-start gap-2 text-sm">
<Check className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
<span>Live web search powered by Brave</span>
</li>
<li className="flex items-start gap-2 text-sm">
<Check className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
<span>Get up-to-date information from the internet</span>
</li>
<li className="flex items-start gap-2 text-sm">
<Check className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
<span>Search queries are sent to Brave but not linked to your identity</span>
</li>
<li className="flex items-start gap-2 text-sm">
<Check className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
<span>Results are processed privately and securely</span>
</li>
<li className="flex items-start gap-2 text-sm">
<Check className="h-4 w-4 text-blue-500 mt-0.5 shrink-0" />
<span>Perfect for current events, research, and fact-checking</span>
</li>
</ul>
</div>

<div className="pt-2 border-t">
<p className="text-sm text-muted-foreground">
Click the globe icon anytime to toggle web search on or off for your messages.
</p>
</div>
</div>

<DialogFooter>
<Button onClick={onConfirm} className="w-full">
Got it
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Loading