Ctrl K
ComunidadGuías…

components/action-buttons.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
'use client'

import { useEffect, useRef, useState } from 'react'

import {
  IconBulb as Bulb,
  IconPencil as Pencil,
  IconScale as Scale,
  IconSearch as Search,
  IconSettings as Settings,
  IconTool as Tool,
  type TablerIcon
} from '@tabler/icons-react'

import { captureClient } from '@/lib/analytics/posthog-client'
import { cn } from '@/lib/utils'

import { Button } from './ui/button'

// Constants for timing delays
const FOCUS_OUT_DELAY_MS = 100 // Delay to ensure focus has actually moved

interface ActionCategory {
  icon: TablerIcon
  label: string
  key: string
}

const actionCategories: ActionCategory[] = [
  {
    icon: Scale,
    label: 'Decide',
    key: 'decide'
  },
  {
    icon: Tool,
    label: 'Troubleshoot',
    key: 'troubleshoot'
  },
  {
    icon: Settings,
    label: 'How-to',
    key: 'howto'
  },
  {
    icon: Bulb,
    label: 'Understand',
    key: 'understand'
  },
  {
    icon: Pencil,
    label: 'Create',
    key: 'create'
  }
]

// Onboarding examples are tuned to showcase grounded, GenUI-rich answers
// (images, comparison tables, structured depth) for concrete, self-contained
// tasks — the patterns that correlate with follow-up in real usage. Create
// mirrors what people write in drafts. Keep each example self-contained (no
// "my notes"/"this file" referencing absent context).
const promptSamples: Record<string, string[]> = {
  troubleshoot: [
    'My car starts then immediately stalls, but the electronics still work',
    'Wi-Fi keeps dropping on one laptop but not my phone — how do I fix it?',
    "My sourdough starter isn't rising after a week — what's wrong?",
    'Next.js build fails with "Module not found" only in production'
  ],
  howto: [
    'Move my photos off Google Photos without losing albums',
    'Set up a Proxmox home server for self-hosting',
    'Convert a folder of .txt files to clean HTML',
    'Set up a Plex media server to stream my movies'
  ],
  decide: [
    'Tesla vs Rivian — which should I buy?',
    'Standing vs sitting desk for lower-back pain — which and why?',
    'A budget mirrorless camera for travel under $1,000',
    'Notion vs Obsidian for a personal knowledge base'
  ],
  understand: [
    'What causes the northern lights?',
    'Why did the dinosaurs really go extinct?',
    'How does a nuclear reactor actually generate electricity?',
    'How do noise-cancelling headphones actually work?'
  ],
  create: [
    'Write a polite email asking my landlord to fix the broken heater this week',
    'Write an Instagram post announcing a bakery’s new autumn menu',
    'Write a short cover letter for a part-time barista job at a local café',
    'Write a text-to-video prompt for a 10-second clip of a cat surfing at sunset'
  ]
}

interface ActionButtonsProps {
  onSelectPrompt: (prompt: string) => void
  onCategoryClick: (category: string) => void
  inputRef?: React.RefObject<HTMLTextAreaElement>
  className?: string
}

export function ActionButtons({
  onSelectPrompt,
  onCategoryClick,
  inputRef,
  className
}: ActionButtonsProps) {
  const [activeCategory, setActiveCategory] = useState<string | null>(null)
  const containerRef = useRef<HTMLDivElement>(null)

  const handleCategoryClick = (category: ActionCategory) => {
    setActiveCategory(category.key)
    onCategoryClick(category.label)
    captureClient('example_category_opened', { category: category.key })
  }

  const handlePromptClick = (prompt: string) => {
    captureClient('example_prompt_clicked', {
      category: activeCategory,
      prompt
    })
    setActiveCategory(null)
    onSelectPrompt(prompt)
  }

  const resetToButtons = () => {
    setActiveCategory(null)
  }

  // Handle Escape key and clicks outside (including focus loss)
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape' && activeCategory) {
        resetToButtons()
      }
    }

    const handleClickOutside = (e: MouseEvent) => {
      if (
        containerRef.current &&
        !containerRef.current.contains(e.target as Node)
      ) {
        if (activeCategory) {
          // Check if click is not on the input field
          if (!inputRef?.current?.contains(e.target as Node)) {
            resetToButtons()
          }
        }
      }
    }

    const handleFocusOut = () => {
      // Check if focus is moving outside both the container and input
      setTimeout(() => {
        const activeElement = document.activeElement
        if (
          activeCategory &&
          !containerRef.current?.contains(activeElement) &&
          activeElement !== inputRef?.current
        ) {
          resetToButtons()
        }
      }, FOCUS_OUT_DELAY_MS)
    }

    document.addEventListener('keydown', handleEscape)
    document.addEventListener('mousedown', handleClickOutside)
    document.addEventListener('focusout', handleFocusOut)

    return () => {
      document.removeEventListener('keydown', handleEscape)
      document.removeEventListener('mousedown', handleClickOutside)
      document.removeEventListener('focusout', handleFocusOut)
    }
  }, [activeCategory, inputRef])

  // Max height for samples (4 items up to 2 lines each + padding); overflow scrolls
  const containerHeight = 'h-[232px]'

  return (
    <div
      ref={containerRef}
      className={cn('relative', containerHeight, className)}
    >
      <div className="relative h-full">
        {/* Action buttons */}
        <div
          className={cn(
            'absolute inset-0 flex items-start justify-center pt-2 transition-opacity duration-[180ms] ease-[var(--motion-ease-out)]',
            activeCategory ? 'opacity-0 pointer-events-none' : 'opacity-100'
          )}
        >
          <div className="flex flex-wrap justify-center gap-2 px-2">
            {actionCategories.map(category => {
              const Icon = category.icon
              return (
                <Button
                  key={category.key}
                  type="button"
                  variant="outline"
                  size="sm"
                  className={cn(
                    'flex items-center gap-2 whitespace-nowrap rounded-full',
                    'text-xs sm:text-sm px-3 sm:px-4'
                  )}
                  onClick={() => handleCategoryClick(category)}
                >
                  <Icon className="h-3 w-3 sm:h-4 sm:w-4" />
                  <span>{category.label}</span>
                </Button>
              )
            })}
          </div>
        </div>

        {/* Prompt samples */}
        <div
          className={cn(
            'absolute inset-0 space-y-1 overflow-y-auto py-1 transition-opacity duration-[180ms] ease-[var(--motion-ease-out)]',
            !activeCategory ? 'opacity-0 pointer-events-none' : 'opacity-100'
          )}
        >
          {activeCategory &&
            promptSamples[activeCategory]?.map((prompt, index) => (
              <button
                key={index}
                type="button"
                className={cn(
                  'w-full rounded-md px-3 py-2 text-left text-sm',
                  'transition-colors duration-[140ms] ease-[var(--motion-ease-out)] hover:bg-muted',
                  'flex items-center gap-2 group'
                )}
                onClick={() => handlePromptClick(prompt)}
              >
                <Search className="h-3 w-3 text-muted-foreground flex-shrink-0 group-hover:text-foreground" />
                <span className="line-clamp-2">{prompt}</span>
              </button>
            ))}
        </div>
      </div>
    </div>
  )
}