Sasquatch: restore the pulse, hide by dragging left, cleaner Go Pro
- Restore the always-on blink/pulse (still motion-reduce:hidden for a11y). - Drag the bubble off the left edge to hide it; a tiny left-edge tab brings it back. (The ✕/panel/Settings Hide paths remain.) - Replaced the cramped inline "Go Pro" header pill with a clean full-width strip at the top of the panel body.
This commit is contained in:
parent
0e405eaa00
commit
d82ecf4adf
3 changed files with 54 additions and 25 deletions
|
|
@ -1136,7 +1136,6 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
<LazySasquatchGuide
|
||||
message={currentMessage}
|
||||
onDismiss={dismissCurrentMessage}
|
||||
isFullBleed={isFullBleed}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
|
|
|
|||
|
|
@ -117,8 +117,6 @@ export interface GuideMessage {
|
|||
interface SasquatchGuideProps {
|
||||
message?: GuideMessage;
|
||||
onDismiss?: () => void;
|
||||
/** True on full-bleed routes (the map) — the bubble runs compact/no-pulse. */
|
||||
isFullBleed?: boolean;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
|
|
@ -661,7 +659,7 @@ function ChatText({ text }: { text: string }) {
|
|||
|
||||
// ─── Main component ───────────────────────────────────────────────────────────
|
||||
|
||||
export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGuideProps) {
|
||||
export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
|
||||
const { user } = useCurrentUser();
|
||||
const { t, language } = useTranslation();
|
||||
const cvm = useCVMClient();
|
||||
|
|
@ -693,6 +691,9 @@ export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGui
|
|||
// Draggable position state for minimized bubble
|
||||
const [dragPos, setDragPos] = useLocalStorage<{ x: number; y: number } | null>('sasquatch-pos', null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
// Mirror of dragPos for the pointer-up handler (state may be stale in closures).
|
||||
const dragPosRef = useRef(dragPos);
|
||||
useEffect(() => { dragPosRef.current = dragPos; }, [dragPos]);
|
||||
const dragRef = useRef<HTMLDivElement>(null);
|
||||
const dragStart = useRef<{ mx: number; my: number; bx: number; by: number } | null>(null);
|
||||
const dragMoved = useRef(false);
|
||||
|
|
@ -741,6 +742,15 @@ export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGui
|
|||
dragStart.current = null;
|
||||
dragMoved.current = false;
|
||||
setTimeout(() => setIsDragging(false), 50);
|
||||
// Drag it off the left edge to hide it — a tiny tab brings it back.
|
||||
if (wasDrag) {
|
||||
const x = dragPosRef.current?.x;
|
||||
if (x != null && x <= 28) {
|
||||
setHidden(true);
|
||||
setDragPos(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If this pointer sequence was a genuine tap (no real drag), open here.
|
||||
// A parallel onClick handler also opens as a reliable fallback for mobile
|
||||
// WebViews where the pointer-capture tap can be flaky on first interaction;
|
||||
|
|
@ -748,7 +758,7 @@ export function SasquatchGuide({ message, onDismiss, isFullBleed }: SasquatchGui
|
|||
if (!wasDrag) {
|
||||
openPanel();
|
||||
}
|
||||
}, [openPanel]);
|
||||
}, [openPanel, setHidden, setDragPos]);
|
||||
|
||||
// Reliable open path: plain click. On mobile WebViews the pointer-capture tap
|
||||
// detection can miss the very first interaction after mount (the "1st tap does
|
||||
|
|
@ -1549,10 +1559,8 @@ Instructions:
|
|||
// opt-in auto-show message (e.g. onboarding) — never automatically on / or /lab.
|
||||
const shouldShowPanel = isVisible;
|
||||
|
||||
// A proactive tip is waiting (drives the pulse/dot), and the map route runs
|
||||
// the bubble in a quieter, compact form.
|
||||
// A proactive tip is waiting (drives the unread dot).
|
||||
const hasUnread = !!message;
|
||||
const compact = !!isFullBleed;
|
||||
|
||||
// Notify the rest of the app (e.g. MapPage FAB) when the panel opens or closes
|
||||
useEffect(() => {
|
||||
|
|
@ -1583,12 +1591,26 @@ Instructions:
|
|||
enableChat: true,
|
||||
};
|
||||
|
||||
if (hidden) return null;
|
||||
// Hidden by dragging to the left edge (or via the ✕ / panel Hide button). A
|
||||
// tiny edge tab brings it back onto the screen.
|
||||
if (hidden) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('sasquatchUi.showAria')}
|
||||
title={t('sasquatchUi.showAria')}
|
||||
onClick={() => setHidden(false)}
|
||||
className="fixed left-0 top-1/2 z-50 -translate-y-1/2 rounded-r-full border border-l-0 border-[#ccff00]/40 bg-black/70 py-3 pl-0.5 pr-1.5 transition-colors hover:bg-black/90"
|
||||
>
|
||||
<span className="block h-5 w-1.5 rounded-full bg-[#ccff00]/70" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Minimized bubble — visible unless the user hid the assistant ── */}
|
||||
{!hidden && (!shouldShowPanel || isMinimized) && (
|
||||
{/* ── Minimized bubble — always visible, draggable ── */}
|
||||
{(!shouldShowPanel || isMinimized) && (
|
||||
<div
|
||||
ref={dragRef}
|
||||
className={cn(
|
||||
|
|
@ -1621,11 +1643,9 @@ Instructions:
|
|||
alt="The Sasquatch"
|
||||
className="w-8 h-8 md:w-9 md:h-9 rounded-full object-cover pointer-events-none"
|
||||
/>
|
||||
{/* Pulse only when there's an unread tip, never on the map, and never
|
||||
under prefers-reduced-motion (the motion is the nuisance). */}
|
||||
{hasUnread && !compact && (
|
||||
<span className="absolute inset-0 rounded-full animate-ping opacity-20 border-2 border-[#ccff00] motion-reduce:hidden" />
|
||||
)}
|
||||
{/* Pulse like the original — always on, except under
|
||||
prefers-reduced-motion. */}
|
||||
<span className="absolute inset-0 rounded-full animate-ping opacity-20 border-2 border-[#ccff00] motion-reduce:hidden" />
|
||||
{hasUnread && (
|
||||
<span className="absolute -top-0.5 -right-0.5 w-3 h-3 bg-[#ccff00] rounded-full border-2 border-black shadow-[0_0_6px_#ccff00]" />
|
||||
)}
|
||||
|
|
@ -1702,15 +1722,6 @@ Instructions:
|
|||
>
|
||||
⚡ Pro{proDaysLeft !== null ? ` · ${proDaysLeft}d` : ''}
|
||||
</span>
|
||||
) : user ? (
|
||||
<button
|
||||
onClick={() => setProDialogOpen(true)}
|
||||
className="ml-0.5 inline-flex items-center gap-1 rounded-full border border-[#ccff00]/40 bg-[#ccff00]/10 px-2 py-0.5 text-[9px] font-black uppercase tracking-wider text-[#ccff00] transition-colors hover:border-[#ccff00]/70 hover:bg-[#ccff00]/20"
|
||||
title={t('sasquatchUi.upgradeTitle')}
|
||||
>
|
||||
<span aria-hidden>⚡</span>
|
||||
{t('sasquatchUi.goPro')}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{/* Context + (owner-only) AI balance on one tidy line */}
|
||||
|
|
@ -1765,6 +1776,21 @@ Instructions:
|
|||
|
||||
{/* Body — scrolls internally so the card never outgrows the viewport */}
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain p-3">
|
||||
{/* Go Pro — a clean full-width strip (the inline header pill looked
|
||||
cramped). Only for signed-in non-Pro users. */}
|
||||
{!isPro && user && (
|
||||
<button
|
||||
onClick={() => setProDialogOpen(true)}
|
||||
className="mb-3 flex w-full items-center gap-2 rounded-xl border border-[#ccff00]/30 bg-[#ccff00]/10 px-3 py-2 text-left transition-colors hover:border-[#ccff00]/60 hover:bg-[#ccff00]/15"
|
||||
title={t('sasquatchUi.upgradeTitle')}
|
||||
>
|
||||
<span aria-hidden className="text-sm">⚡</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-[11px] font-black uppercase tracking-wider text-[#ccff00]">{t('sasquatchUi.goPro')}</span>
|
||||
<span className="block truncate text-[10px] text-[#ccff00]/60">{t('sasquatchUi.goProBlurb')}</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{/* Greeting — only shown before the user starts chatting, so the
|
||||
same wall of welcome text doesn't reappear on every open once
|
||||
a conversation is underway. */}
|
||||
|
|
|
|||
|
|
@ -31,10 +31,12 @@ const sasquatchUi = {
|
|||
openGuideAria: 'Open Sasquatch guide',
|
||||
hideAria: 'Hide the Sasquatch assistant',
|
||||
hide: 'Hide assistant',
|
||||
showAria: 'Show the Sasquatch assistant',
|
||||
activeUntil: 'Active until {{date}}',
|
||||
platformOwner: 'Platform owner',
|
||||
upgradeTitle: 'Upgrade to Sasquatch Pro',
|
||||
goPro: 'Go Pro',
|
||||
goProBlurb: 'Personal AI · from 2k sats',
|
||||
aiBalanceTitle: 'Sasquatch AI credit balance (owner-only) — top up at {{provider}}',
|
||||
aiChip: '⚡ AI {{balance}}',
|
||||
minimizeToBubbleAria: 'Minimize to bubble',
|
||||
|
|
@ -164,10 +166,12 @@ const sasquatchUi = {
|
|||
openGuideAria: 'Abrir la guía del Sasquatch',
|
||||
hideAria: 'Ocultar el asistente Sasquatch',
|
||||
hide: 'Ocultar asistente',
|
||||
showAria: 'Mostrar el asistente Sasquatch',
|
||||
activeUntil: 'Activo hasta el {{date}}',
|
||||
platformOwner: 'Propietario de la plataforma',
|
||||
upgradeTitle: 'Mejora a Sasquatch Pro',
|
||||
goPro: 'Hazte Pro',
|
||||
goProBlurb: 'IA personal · desde 2k sats',
|
||||
aiBalanceTitle: 'Saldo de crédito de la IA del Sasquatch (solo propietario) — recarga en {{provider}}',
|
||||
aiChip: '⚡ IA {{balance}}',
|
||||
minimizeToBubbleAria: 'Minimizar a la burbuja',
|
||||
|
|
|
|||
Loading…
Reference in a new issue