feat: new tab tips section (#304)

* fix: tips

* fix: show tips only 1/5 times

* fix: guard against empty tips array in getRandomTip

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: biome exhaustive deps in SurveyChat voice effect

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nikhil
2026-02-05 15:04:08 -08:00
committed by GitHub
parent d242adde26
commit 9f3562eb85
5 changed files with 145 additions and 1 deletions

View File

@@ -89,7 +89,7 @@ export const Chat: FC<Props> = ({
})
voice.clearTranscript()
}
}, [voice.transcript, voice.isTranscribing, voice.clearTranscript])
}, [voice])
const handleSubmit = (e: FormEvent) => {
e.preventDefault()

View File

@@ -53,6 +53,7 @@ import {
useSuggestions,
} from './lib/suggestions/useSuggestions'
import { NewTabBranding } from './NewTabBranding'
import { NewTabTip } from './NewTabTip'
import { ScheduleResults } from './ScheduleResults'
import { SearchSuggestions } from './SearchSuggestions'
import { ShortcutsDialog } from './ShortcutsDialog'
@@ -597,6 +598,8 @@ export const NewTab = () => {
</div>
</div>
{mounted && !isSuggestionsVisible && <NewTabTip />}
{/* Top sites */}
{!isSuggestionsVisible && <TopSites />}

View File

@@ -0,0 +1,48 @@
import { Lightbulb, X } from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import { type FC, useMemo, useState } from 'react'
import { NEWTAB_TIP_DISMISSED_EVENT } from '@/lib/constants/analyticsEvents'
import { track } from '@/lib/metrics/track'
import { dismissTip, getRandomTip, shouldShowTip } from './tips'
export const NewTabTip: FC = () => {
const tip = useMemo(() => getRandomTip(), [])
const [visible, setVisible] = useState(() => tip !== null && shouldShowTip())
const handleDismiss = () => {
setVisible(false)
dismissTip()
if (tip) track(NEWTAB_TIP_DISMISSED_EVENT, { tip_id: tip.id })
}
return (
<AnimatePresence>
{visible && tip && (
<motion.div
className="flex items-center justify-center"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
>
<div className="group flex max-w-lg items-center gap-2.5 rounded-lg border border-border/50 px-3.5 py-2">
<Lightbulb className="h-3.5 w-3.5 flex-shrink-0 text-[var(--accent-orange)]" />
<p className="text-muted-foreground text-xs leading-relaxed">
<span className="font-semibold text-[var(--accent-orange)]">
Tip:
</span>{' '}
{tip.text}
</p>
<button
type="button"
onClick={handleDismiss}
className="flex-shrink-0 rounded-sm p-0.5 text-muted-foreground/50 opacity-0 transition-all hover:text-muted-foreground group-hover:opacity-100"
>
<X className="h-3 w-3" />
</button>
</div>
</motion.div>
)}
</AnimatePresence>
)
}

View File

@@ -0,0 +1,90 @@
export interface Tip {
id: string
text: string
shortcut?: string
}
export const TIP_SHOW_PROBABILITY = 0.2
const TIP_DISMISSED_KEY = 'tip-dismissed-session'
export const TIPS: Tip[] = [
{
id: 'chat-any-page',
text: 'Press Option+K to open the AI Chat panel on any webpage — it includes full page context.',
shortcut: '⌥K',
},
{
id: 'compare-models',
text: 'Press Cmd+Shift+U to open LLM Hub and query multiple AI models side-by-side.',
shortcut: '⌘⇧U',
},
{
id: 'switch-models',
text: 'Press Option+L to cycle between AI providers without closing the chat panel.',
shortcut: '⌥L',
},
{
id: 'screenshot-chat',
text: 'Use the Image button in Chat to capture the visible page and ask visual questions about it.',
},
{
id: 'copy-page-content',
text: 'Click the Copy button in Chat to grab all webpage text and paste it into your prompt.',
},
{
id: 'cowork-mode',
text: 'Enable Cowork and select a folder to let the agent browse the web AND create files in a single task.',
},
{
id: 'scheduled-tasks',
text: 'Set up Scheduled Tasks to run the agent on a timer — results appear right here on your New Tab.',
},
{
id: 'background-tasks',
text: 'Scheduled tasks run in a hidden window so they never interrupt your browsing.',
},
{
id: 'claude-code-mcp',
text: 'Connect BrowserOS to Claude Code with the MCP integration for full browser control from your terminal.',
},
{
id: 'mcp-servers',
text: 'Add MCP servers for Google Calendar, Gmail, Notion, and more to build multi-service workflows.',
},
{
id: 'import-chrome',
text: 'Go to chrome://settings/importData to import bookmarks, passwords, and history from Chrome in one click.',
},
{
id: 'ad-blocking',
text: 'BrowserOS comes with uBlock Origin pre-enabled — blocking 10x more ads than Chrome out of the box.',
},
{
id: 'at-mention-tabs',
text: 'Type @ in the search bar to mention and attach open tabs as context for your AI queries.',
},
{
id: 'workflows',
text: 'For complex repeatable tasks, build visual Workflows instead of one-off prompts for consistent results.',
},
{
id: 'model-selection',
text: 'Use Gemini Flash for quick questions and Claude Opus for complex multi-step agent tasks.',
},
]
export const shouldShowTip = (): boolean => {
const dismissed = sessionStorage.getItem(TIP_DISMISSED_KEY)
if (dismissed) return false
return Math.random() < TIP_SHOW_PROBABILITY
}
export const dismissTip = () => {
sessionStorage.setItem(TIP_DISMISSED_KEY, Date.now().toString())
}
export const getRandomTip = (): Tip | null => {
if (TIPS.length === 0) return null
return TIPS[Math.floor(Math.random() * TIPS.length)]
}

View File

@@ -95,6 +95,9 @@ export const NEWTAB_TAB_REMOVED_EVENT = 'newtab.tab.removed'
/** @public */
export const NEWTAB_APPS_OPENED_EVENT = 'newtab.apps.opened'
/** @public */
export const NEWTAB_TIP_DISMISSED_EVENT = 'newtab.tip.dismissed'
/** @public */
export const WORKFLOW_DELETED_EVENT = 'settings.workflow.deleted'