import React, { useEffect, useRef, type ReactNode } from 'react'; import { Animated, Pressable, ScrollView, Text, View, type StyleProp, type ViewStyle } from 'react-native'; import { RefreshCw, Sparkles, X } from 'lucide-react-native'; import { composerSuggestionStyles as s } from './composerSuggestionStyles'; import { ConnectorBrandIcon, hasConnectorBrandIcon } from '../connectors/connectorBrandIcons'; import { styles } from '../../styles'; import { themedColor } from '../../theme'; import type { SuperagentPromptSuggestion } from '../../types'; const ICON_COLOR = '#A1A1AA'; const SKELETON_WIDTHS = [110, 140, 120]; /** * Prompt-suggestion chips shown above the composer input — the native mirror of the web * builder's `ChatInputSuggestionsAbove` in its new design-system variant (DS `Chip * skin="light"` + ghost `IconButton`s, sparkles restore). Pure UI: the parent * (`ConversationChat`) owns the suggestion state, fills the draft on select, and reports * analytics. Renders the chip row when `visible`, a collapsed restore button when * `showRestore`, and nothing otherwise. * * Native intentionally omits two desktop-only web affordances — auto-shortening chip titles * to fit the row, and the hover-to-preview placeholder — matching the web builder's own * mobile behavior (both are gated off on touch widths there). */ export function ComposerSuggestions({ suggestions, visible, showRestore, refreshing, onSelect, onRefresh, onDismiss, onRestore, }: { suggestions: SuperagentPromptSuggestion[]; visible: boolean; showRestore: boolean; refreshing: boolean; onSelect: (suggestion: SuperagentPromptSuggestion) => void; onRefresh: () => void; onDismiss: () => void; onRestore: () => void; }) { if (visible) { return ( {refreshing ? SKELETON_WIDTHS.map((width, index) => ) : suggestions.map((suggestion, index) => ( ))} ); } if (showRestore) { return ( ); } return null; } function SuggestionChip({ suggestion, onSelect, }: { suggestion: SuperagentPromptSuggestion; onSelect: (suggestion: SuperagentPromptSuggestion) => void; }) { const connectorTypes = Array.from(new Set(suggestion.connector_types ?? [])).filter(hasConnectorBrandIcon); return ( onSelect(suggestion)} style={({ pressed }) => [s.chip, pressed && styles.pressed]} > {connectorTypes.map((type) => ( ))} {suggestion.title} ); } function SkeletonChip({ width }: { width: number }) { const opacity = useRef(new Animated.Value(0.4)).current; useEffect(() => { const loop = Animated.loop( Animated.sequence([ Animated.timing(opacity, { toValue: 0.85, duration: 600, useNativeDriver: true }), Animated.timing(opacity, { toValue: 0.4, duration: 600, useNativeDriver: true }), ]), ); loop.start(); return () => loop.stop(); }, [opacity]); return ; } function ActionButton({ accessibilityLabel, boxed, children, disabled, onPress, }: { accessibilityLabel: string; /** Bordered-box style (the restore affordance); refresh/dismiss default to the ghost look. */ boxed?: boolean; children: ReactNode; disabled?: boolean; onPress: () => void; }) { return ( [ boxed ? s.restoreButton : s.actionButton, disabled && s.actionButtonDisabled, pressed && styles.pressed, ]} > {children} ); } function FadeIn({ children, style }: { children: ReactNode; style?: StyleProp }) { const opacity = useRef(new Animated.Value(0)).current; useEffect(() => { Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true }).start(); }, [opacity]); return {children}; }