@gymmymac/bob-widget 3.2.18 → 3.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,12 +5,44 @@ All notable changes to the `@gymmymac/bob-widget` package will be documented in
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [v3.2.21] - 2026-04-01
9
+
10
+ ### Changed
11
+
12
+ - **Product tile visual refinements** — Inverted product tiles to dark navy theme (`#0F172A`) with light text, distinguishing individual parts from white service package cards. Hero image zone uses a white background so product image edges blend seamlessly. Price font increased to 28px bold for prominence. Brand fallback text enlarged to 22px for better readability when no product image is available.
13
+ - **Heart/wishlist button on product tiles** — Added a red heart (♡) icon button alongside the green "Add" button, allowing customers to save individual products to their wishlist, matching the service package tier card pattern.
14
+ - **Green gradient Add button** — Product tile CTA now uses a vibrant green gradient (`#22C55E → #16A34A`) with drop shadow for high visibility on the dark card background.
15
+
16
+ ---
17
+
18
+ ## [v3.2.20] - 2026-03-30
19
+
20
+ ### Fixed
21
+
22
+ - **Mobile height gap between chat drawer and bottom nav** — Replaced CSS percentage height inheritance (`absolute inset-0`) with explicit pixel heights via a new `useContainerHeight` hook backed by `ResizeObserver`. On mobile Safari, `dvh` recalculations during URL-bar show/hide and virtual keyboard transitions caused the container height to fluctuate, detaching the chat drawer from the bottom of the screen. The widget now observes its parent's actual pixel height and applies it directly, immune to `dvh` drift.
23
+ - **Chat drawer GPU layer stability** — Added `will-change: transform, height` to `ContainedChatDrawer` to prevent compositor layer detachment during height transitions.
24
+ - **AskBob demo page updated** — Reference implementation now uses `visualViewport` listeners to set explicit pixel height on the Bob container, serving as a guide for CARFIX production integration.
25
+
26
+ ---
27
+
28
+ ## [v3.2.19] - 2026-03-26
29
+
30
+ ### Fixed
31
+
32
+ - **Premature product shelf spinner fixed** — Decoupled `isResearching` from general message sends. The product shelf loading spinner now only appears when parts are actually being fetched (after vehicle identification or when the backend emits a `researching_parts` SSE event). Previously, the spinner showed on every message send, even during conversational REGO lookup. Added `onPartsResearchStart` callback to separate animation state (thinking) from shelf visibility.
33
+
34
+ ---
35
+
8
36
  ## [v3.2.18] - 2026-03-26
9
37
 
10
38
  ### Fixed
11
39
 
12
40
  - **iOS TTS now works on ALL iOS browsers (Safari, Chrome, Firefox)** — Replaced the silent-WAV `HTMLAudioElement` unlock with a shared **Web Audio API `AudioContext`** singleton. The context is `.resume()`'d on the first user gesture and stays unlocked for the page lifetime. All TTS playback now routes through `decodeAudioData()` → `AudioBufferSourceNode`, which succeeds even after async `fetch()` calls. This fixes the root cause: iOS WebKit does not transfer an audio "unlock" between different `Audio` elements.
13
41
 
42
+ ### Removed
43
+
44
+ - **Canned speech / pre-recorded audio clips fully stripped** — Removed all `audio_hint`, `bob_searching` audio, clip pattern matching, `AudioController` priority system, `getSearchingClip()`, `checkCannedResponse()`, and `fetchAudioClip()`. All responses now go through AI → ElevenLabs TTS exclusively. The `AUDIO DISABLED` rule was also removed from the LLM system prompt (`rules_and_guardrails`).
45
+
14
46
  ---
15
47
 
16
48
  ## [v3.2.17] - 2026-03-26
@@ -7,13 +7,5 @@ interface ProductTileProps {
7
7
  onProductClick?: (product: Product) => void;
8
8
  onAddToCart?: (product: Product) => void;
9
9
  }
10
- /**
11
- * ProductTile - Premium Glassmorphism Product Cards
12
- *
13
- * Image fallback chain:
14
- * 1. product.image_url (product photo)
15
- * 2. product.brandImageUrl (brand logo)
16
- * 3. "No Image" placeholder
17
- */
18
10
  export declare const ProductTile: React.FC<ProductTileProps>;
19
11
  export {};
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { VariantCard } from './MobileProductColumn';
2
+ import { VariantCard } from '../shelf';
3
3
  import { Message, HighlightedProduct } from '../../types/message';
4
4
  import { Product, ServicePackage } from '../../types';
5
5
  import { Vehicle } from '../../types/vehicle';
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { VariantCard } from './MobileProductColumn';
2
+ import { VariantCard } from '../shelf';
3
3
  import { Message, HighlightedProduct } from '../../types/message';
4
4
  import { Product, ServicePackage } from '../../types';
5
5
  import { Vehicle } from '../../types/vehicle';
@@ -1,23 +1,11 @@
1
1
  import { default as React } from 'react';
2
2
  import { BobPosition } from './MobileBobCharacter';
3
+ import { VariantCard } from '../shelf';
3
4
  import { Product, ServicePackage } from '../../types';
4
5
  import { HighlightedProduct } from '../../types/message';
5
6
  import { Vehicle } from '../../types/vehicle';
6
7
 
7
- export interface VariantCard {
8
- vehicle_id: number;
9
- optionNumber: number;
10
- displayTitle: string;
11
- displaySubtitle: string;
12
- characterization: string;
13
- kw?: number | null;
14
- cc?: number | null;
15
- ccDisplay?: string | null;
16
- fuelType?: string | null;
17
- engineCode?: string | null;
18
- make: string;
19
- model: string;
20
- }
8
+ export type { VariantCard };
21
9
  interface MobileBobLayoutCoreProps {
22
10
  currentImage: string;
23
11
  animationState: string;
@@ -54,4 +42,3 @@ interface MobileBobLayoutCoreProps {
54
42
  * - hidden: User swiped, full product view
55
43
  */
56
44
  export declare const MobileBobLayoutCore: React.FC<MobileBobLayoutCoreProps>;
57
- export {};
@@ -1,41 +1,5 @@
1
- import { default as React } from 'react';
2
- import { Product, ServicePackage } from '../../types';
3
- import { HighlightedProduct } from '../../types/message';
4
-
5
- export interface VariantCard {
6
- vehicle_id: number;
7
- optionNumber: number;
8
- displayTitle: string;
9
- displaySubtitle: string;
10
- characterization: string;
11
- kw?: number | null;
12
- cc?: number | null;
13
- ccDisplay?: string | null;
14
- fuelType?: string | null;
15
- engineCode?: string | null;
16
- make: string;
17
- model: string;
18
- }
19
- interface MobileProductColumnProps {
20
- products: Product[];
21
- servicePackages: ServicePackage[];
22
- highlightedPartType?: string | null;
23
- highlightedProduct?: HighlightedProduct | null;
24
- scrollToCategory?: string | null;
25
- onScrollToCategoryComplete?: () => void;
26
- onProductClick?: (product: Product) => void;
27
- onPackageSelect?: (pkg: ServicePackage) => void;
28
- isResearching?: boolean;
29
- visible?: boolean;
30
- counterHeightPercent?: number;
31
- hasVehicle?: boolean;
32
- onAddToCart?: (product: Product | Product[]) => void;
33
- /** Vehicle make and model for header display */
34
- vehicleMakeModel?: string;
35
- pendingVariants?: VariantCard[];
36
- pendingVariantMake?: string;
37
- pendingVariantModel?: string;
38
- onVariantSelect?: (variant: VariantCard) => void;
39
- }
40
- export declare const MobileProductColumn: React.FC<MobileProductColumnProps>;
41
- export {};
1
+ /**
2
+ * Legacy re-export MobileProductColumn is now ShelfColumn.
3
+ * Kept for backward-compatible imports.
4
+ */
5
+ export { ShelfColumn as MobileProductColumn } from '../shelf/ShelfColumn';
@@ -1,7 +1,8 @@
1
1
  export { MobileBobCharacter } from './MobileBobCharacter';
2
2
  export { MobileBobLayoutCore } from './MobileBobLayoutCore';
3
3
  export { MobileChatDrawer } from './MobileChatDrawer';
4
- export { MobileProductColumn } from './MobileProductColumn';
4
+ export { ShelfColumn as MobileProductColumn } from '../shelf';
5
+ export type { VariantCard } from '../shelf';
5
6
  export { MobileBobLayout } from './MobileBobLayout';
6
7
  export { ContainedMobileBobLayout } from './ContainedMobileBobLayout';
7
8
  export { ContainedChatDrawer } from './ContainedChatDrawer';
@@ -0,0 +1,19 @@
1
+ import { default as React } from 'react';
2
+ import { ViewportSize } from '../../hooks/useViewportSize';
3
+
4
+ interface HorizontalRowProps {
5
+ children: React.ReactNode;
6
+ viewportSize: ViewportSize;
7
+ /** Extra className for the scroll track */
8
+ className?: string;
9
+ /** Extra style for the scroll track */
10
+ style?: React.CSSProperties;
11
+ }
12
+ /**
13
+ * HorizontalRow — The single scroll primitive for all horizontal rows.
14
+ *
15
+ * Mobile/Tablet: snap-x mandatory, touch-action: pan-x, overscroll-behavior: contain, hidden scrollbar
16
+ * Desktop: no snap, orange arrow buttons, thin scrollbar, fade masks
17
+ */
18
+ export declare const HorizontalRow: React.FC<HorizontalRowProps>;
19
+ export {};
@@ -0,0 +1,16 @@
1
+ import { default as React } from 'react';
2
+ import { Product } from '../../types';
3
+ import { HighlightedProduct } from '../../types/message';
4
+ import { ViewportSize } from '../../hooks/useViewportSize';
5
+
6
+ interface PartslotSectionProps {
7
+ name: string;
8
+ products: Product[];
9
+ viewportSize: ViewportSize;
10
+ isHighlighted: boolean;
11
+ highlightedProduct?: HighlightedProduct | null;
12
+ onProductClick?: (product: Product) => void;
13
+ onAddToCart?: (product: Product) => void;
14
+ }
15
+ export declare const PartslotSection: React.FC<PartslotSectionProps>;
16
+ export {};
@@ -0,0 +1,11 @@
1
+ import { default as React } from 'react';
2
+ import { ServicePackage, Product } from '../../types';
3
+ import { ViewportSize } from '../../hooks/useViewportSize';
4
+
5
+ interface ServicePackageCardProps {
6
+ pkg: ServicePackage;
7
+ viewportSize: ViewportSize;
8
+ onAddToCart?: (products: Product | Product[]) => void;
9
+ }
10
+ export declare const ServicePackageCard: React.FC<ServicePackageCardProps>;
11
+ export {};
@@ -0,0 +1,40 @@
1
+ import { default as React } from 'react';
2
+ import { Product, ServicePackage } from '../../types';
3
+ import { HighlightedProduct } from '../../types/message';
4
+
5
+ export interface VariantCard {
6
+ vehicle_id: number;
7
+ optionNumber: number;
8
+ displayTitle: string;
9
+ displaySubtitle: string;
10
+ characterization: string;
11
+ kw?: number | null;
12
+ cc?: number | null;
13
+ ccDisplay?: string | null;
14
+ fuelType?: string | null;
15
+ engineCode?: string | null;
16
+ make: string;
17
+ model: string;
18
+ }
19
+ interface ShelfColumnProps {
20
+ products: Product[];
21
+ servicePackages: ServicePackage[];
22
+ highlightedPartType?: string | null;
23
+ highlightedProduct?: HighlightedProduct | null;
24
+ scrollToCategory?: string | null;
25
+ onScrollToCategoryComplete?: () => void;
26
+ onProductClick?: (product: Product) => void;
27
+ onPackageSelect?: (pkg: ServicePackage) => void;
28
+ isResearching?: boolean;
29
+ visible?: boolean;
30
+ counterHeightPercent?: number;
31
+ hasVehicle?: boolean;
32
+ onAddToCart?: (product: Product | Product[]) => void;
33
+ vehicleMakeModel?: string;
34
+ pendingVariants?: VariantCard[];
35
+ pendingVariantMake?: string;
36
+ pendingVariantModel?: string;
37
+ onVariantSelect?: (variant: VariantCard) => void;
38
+ }
39
+ export declare const ShelfColumn: React.FC<ShelfColumnProps>;
40
+ export {};
@@ -0,0 +1,9 @@
1
+ import { default as React } from 'react';
2
+
3
+ interface ShelfHeaderProps {
4
+ vehicleDisplayName: string;
5
+ itemCount: number;
6
+ isUpdating: boolean;
7
+ }
8
+ export declare const ShelfHeader: React.FC<ShelfHeaderProps>;
9
+ export {};
@@ -0,0 +1,7 @@
1
+ import { default as React } from 'react';
2
+
3
+ export declare const ShelfLoading: React.FC;
4
+ export declare const ShelfEmpty: React.FC<{
5
+ productCount: number;
6
+ packageCount: number;
7
+ }>;
@@ -0,0 +1,15 @@
1
+ import { default as React } from 'react';
2
+ import { PreparedTier, Product } from '../../types';
3
+ import { ViewportSize } from '../../hooks/useViewportSize';
4
+
5
+ interface TierCardProps {
6
+ tier: PreparedTier;
7
+ isSelected: boolean;
8
+ viewportSize: ViewportSize;
9
+ packageTitle: string;
10
+ packageId: string;
11
+ onSelect: () => void;
12
+ onAddToCart?: (products: Product[]) => void;
13
+ }
14
+ export declare const TierCard: React.FC<TierCardProps>;
15
+ export {};
@@ -0,0 +1,8 @@
1
+ import { default as React } from 'react';
2
+ import { PreparedTier } from '../../types';
3
+
4
+ interface TierProductListProps {
5
+ tier: PreparedTier;
6
+ }
7
+ export declare const TierProductList: React.FC<TierProductListProps>;
8
+ export {};
@@ -0,0 +1,11 @@
1
+ import { default as React } from 'react';
2
+ import { VariantCard } from './ShelfColumn';
3
+
4
+ interface VariantSelectorProps {
5
+ variants: VariantCard[];
6
+ make?: string;
7
+ model?: string;
8
+ onSelect: (variant: VariantCard) => void;
9
+ }
10
+ export declare const VariantSelector: React.FC<VariantSelectorProps>;
11
+ export {};
@@ -0,0 +1,10 @@
1
+ export { ShelfColumn } from './ShelfColumn';
2
+ export type { VariantCard } from './ShelfColumn';
3
+ export { HorizontalRow } from './HorizontalRow';
4
+ export { ShelfHeader } from './ShelfHeader';
5
+ export { ShelfLoading, ShelfEmpty } from './ShelfLoadingState';
6
+ export { VariantSelector } from './VariantSelector';
7
+ export { ServicePackageCard } from './ServicePackageCard';
8
+ export { TierCard } from './TierCard';
9
+ export { TierProductList } from './TierProductList';
10
+ export { PartslotSection } from './PartslotSection';
@@ -1,5 +1,5 @@
1
1
  import { Vehicle } from '../types/vehicle';
2
- import { VariantCard } from '../components/mobile/MobileProductColumn';
2
+ import { VariantCard } from '../components/shelf';
3
3
  import { Message, HighlightedProduct } from '../types/message';
4
4
 
5
5
  export type AnimationState = string;
@@ -15,6 +15,8 @@ interface UseBobChatProps {
15
15
  onStreamComplete?: () => void;
16
16
  onShowingProduct?: () => void;
17
17
  onResearchStart?: () => void;
18
+ /** v3.2.18: Fires only when parts/packages are actually being fetched (not on every message) */
19
+ onPartsResearchStart?: () => void;
18
20
  onReadyToSpeak?: () => void;
19
21
  onHighlightPart?: (partType: string) => void;
20
22
  onHighlightProduct?: (product: HighlightedProduct) => void;
@@ -23,7 +25,7 @@ interface UseBobChatProps {
23
25
  /** When backend requires user to pick a vehicle variant, provide UI-ready cards for the shelf */
24
26
  onVariantSelectionRequired?: (variants: VariantCard[], make: string, model: string) => void;
25
27
  /** Ref containing current shelf category names for post-stream scroll matching */
26
- shelfCategoriesRef?: React.RefObject<Set<string>>;
28
+ shelfCategoriesRef?: React.RefObject<Map<string, 'package' | 'partslot'>>;
27
29
  /** Optional pre-identified vehicle — skips REGO lookup when no session exists */
28
30
  initialVehicle?: {
29
31
  vehicle_id: string | number;
@@ -33,7 +35,7 @@ interface UseBobChatProps {
33
35
  [key: string]: unknown;
34
36
  };
35
37
  }
36
- export declare const useBobChat: ({ setAnimationState, manualMode, talkingState, thinkingState, completeState, idleState, listenState, onStreamStart, onStreamComplete, onShowingProduct, onResearchStart, onReadyToSpeak, onHighlightPart, onHighlightProduct, onNoPartsFound, onAutoFetchComplete, onVariantSelectionRequired, shelfCategoriesRef, initialVehicle }: UseBobChatProps) => {
38
+ export declare const useBobChat: ({ setAnimationState, manualMode, talkingState, thinkingState, completeState, idleState, listenState, onStreamStart, onStreamComplete, onShowingProduct, onResearchStart, onPartsResearchStart, onReadyToSpeak, onHighlightPart, onHighlightProduct, onNoPartsFound, onAutoFetchComplete, onVariantSelectionRequired, shelfCategoriesRef, initialVehicle }: UseBobChatProps) => {
37
39
  messages: Message[];
38
40
  input: string;
39
41
  setInput: import('react').Dispatch<import('react').SetStateAction<string>>;
@@ -51,5 +53,6 @@ export declare const useBobChat: ({ setAnimationState, manualMode, talkingState,
51
53
  clearVehicle: () => void;
52
54
  sendDirectMessage: (content: string) => void;
53
55
  stopAllAudio: () => void;
56
+ refetchPartsForVehicle: () => Promise<void>;
54
57
  };
55
58
  export {};
@@ -0,0 +1,13 @@
1
+ import { RefObject } from 'react';
2
+
3
+ /**
4
+ * useContainerHeight — ResizeObserver-based hook that returns the actual
5
+ * pixel height of the referenced element. Falls back to `null` when
6
+ * ResizeObserver is unavailable (the caller should use `height: 100%`).
7
+ *
8
+ * Why: On mobile Safari, `100dvh` / percentage heights fluctuate during
9
+ * URL-bar show/hide and virtual-keyboard transitions. An explicit pixel
10
+ * height keeps the widget layout stable and prevents the "gap" between
11
+ * the chat drawer and the host's bottom navigation.
12
+ */
13
+ export declare function useContainerHeight(ref: RefObject<HTMLElement | null>): number | null;