bookwiz.io / components / chat / ChatMessages.tsx
ChatMessages.tsx
Raw
import React, { useRef, useEffect, useCallback } from 'react'
import { IoSearchOutline, IoDocumentTextOutline } from 'react-icons/io5'
import Message from '../Message'
import { MessageUI } from '@/lib/types/database'
import { ToolResult } from '@/lib/services/tool-executor'

interface ContextInfo {
  filesFound: number
  fileNames: string[]
}

interface ChatMessagesProps {
  // Messages and display
  allDisplayMessages: (MessageUI & { _isStreaming?: boolean })[]
  isStreaming: boolean
  currentChat: any
  bookId?: string
  
  // Context and tool results
  contextInfo: ContextInfo | null
  toolResults: ToolResult[]
  
  // Handlers
  onViewDiff?: (filePath: string, oldContent: string, newContent: string) => void
  getAvatarUrl: () => string | null
  getInitials: () => string
  onFileClick: (fileId: string, fileName: string) => Promise<void>
  onDeleteMessage?: (messageId: string) => Promise<void>
}

export default function ChatMessages({
  allDisplayMessages,
  isStreaming,
  currentChat,
  bookId,
  contextInfo,
  toolResults,
  onViewDiff,
  getAvatarUrl,
  getInitials,
  onFileClick,
  onDeleteMessage
}: ChatMessagesProps) {
  const messagesEndRef = useRef<HTMLDivElement>(null)

  const scrollToBottom = useCallback(() => {
    if (messagesEndRef.current) {
      messagesEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' })
    }
  }, [])

  // Debounced scroll to bottom for better performance
  const debouncedScrollToBottom = useCallback(() => {
    const timeoutId = setTimeout(() => {
      requestAnimationFrame(() => {
        scrollToBottom()
      })
    }, 50) // Small delay to batch multiple updates
    return () => clearTimeout(timeoutId)
  }, [scrollToBottom])

  useEffect(() => {
    const cleanup = debouncedScrollToBottom()
    return cleanup
  }, [allDisplayMessages.length, debouncedScrollToBottom])

  const renderEmptyState = () => (
    <div className="flex items-center justify-center h-full text-slate-400">
      <div className="text-center">
        <div className="text-2xl mb-2">๐Ÿ’ฌ</div>
        <h3 className="text-sm font-medium mb-0.5">
          {currentChat ? 'Welcome to Bookwiz Chat' : 'Ready to start chatting?'}
        </h3>
        <div className="text-xs space-y-0.5">
          <p>
            {currentChat 
              ? 'Ask me anything about writing, editing, or creative feedback!' 
              : 'Type a message below to start a new conversation'
            }
          </p>
          {bookId && (
            <div className="mt-2 p-3 bg-gradient-to-br from-slate-800/50 to-slate-900/50 rounded-lg border border-slate-700/50 text-[10px]">
              <p className="font-medium text-teal-400 mb-1">Smart Mode is active! Try asking:</p>
              <ul className="space-y-1 text-slate-300">
                <li className="flex items-center gap-1.5">
                  <span className="w-1 h-1 bg-teal-500 rounded-full"></span>
                  "Tell me about [character name]"
                </li>
                <li className="flex items-center gap-1.5">
                  <span className="w-1 h-1 bg-teal-500 rounded-full"></span>
                  "What happens in @chapter-1?"
                </li>
                <li className="flex items-center gap-1.5">
                  <span className="w-1 h-1 bg-teal-500 rounded-full"></span>
                  "How does the magic system work?"
                </li>
                <li className="flex items-center gap-1.5">
                  <span className="w-1 h-1 bg-teal-500 rounded-full"></span>
                  "Improve the dialogue in chapter 2"
                </li>
                <li className="flex items-center gap-1.5 text-blue-300 font-medium">
                  <span className="w-1 h-1 bg-blue-500 rounded-full"></span>
                  "Improve the main idea file with more details"
                </li>
                <li className="flex items-center gap-1.5 text-blue-300 font-medium">
                  <span className="w-1 h-1 bg-blue-500 rounded-full"></span>
                  "Add more backstory to Kris's character file"
                </li>
                <li className="flex items-center gap-1.5 text-blue-300 font-medium">
                  <span className="w-1 h-1 bg-blue-500 rounded-full"></span>
                  "Edit the outline to include new plot points"
                </li>
              </ul>
            </div>
          )}
        </div>
      </div>
    </div>
  )

  return (
    <>
      {/* Context Info Display */}
      {contextInfo && (
        <div className="px-3 py-1.5 bg-gradient-to-r from-blue-500/10 to-purple-500/10 border-b border-slate-700/50">
          <div className="flex items-center gap-1.5 text-[10px] text-slate-300">
            <IoSearchOutline className="w-2.5 h-2.5" />
            <span>
              Found {contextInfo.filesFound} relevant file{contextInfo.filesFound !== 1 ? 's' : ''}
              {contextInfo.fileNames.length > 0 && (
                <>: {contextInfo.fileNames.slice(0, 3).join(', ')}
                  {contextInfo.fileNames.length > 3 && ' and more...'}
                </>
              )}
            </span>
          </div>
        </div>
      )}

      {/* Tool Results Display */}
      {toolResults.length > 0 && (
        <div className="px-3 py-1.5 bg-gradient-to-r from-emerald-500/10 to-teal-500/10 border-b border-slate-700/50">
          <div className="flex flex-wrap gap-1">
            {toolResults.map((result, index) => (
              <div key={index} className="flex items-center gap-1 text-[10px] bg-slate-800/50 text-slate-300 px-1.5 py-0.5 rounded-md border border-slate-700/50">
                {result.name === 'search_files' && '๐Ÿ”'}
                {result.name === 'read_file' && '๐Ÿ“–'}
                {result.name === 'update_file' && 'โœ๏ธ'}
                {result.name === 'create_file' && '๐Ÿ“'}
                {result.name === 'delete_file' && '๐Ÿ—‘๏ธ'}
                {result.name === 'list_files' && '๐Ÿ“'}
                <span>{result.name}</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Scrollable Messages Area */}
      <div className="flex-1 overflow-y-auto px-3 py-2 space-y-3 chat-scroll min-h-0">
        {allDisplayMessages.length === 0 ? renderEmptyState() : (
          allDisplayMessages.map((message: any, index: number) => (
            <Message
              key={message.id}
              message={message}
              index={index}
              isStreaming={isStreaming}
              isLastMessage={index === allDisplayMessages.length - 1}
              onViewDiff={onViewDiff ? () => onViewDiff('', '', '') : undefined}
              getAvatarUrl={getAvatarUrl}
              getInitials={getInitials}
              onFileClick={onFileClick}
              onDeleteMessage={onDeleteMessage}
            />
          ))
        )}
        <div ref={messagesEndRef} />
      </div>
    </>
  )
}