bookwiz.io / app / dashboard / chat / page.tsx
page.tsx
Raw
'use client'

import React, { useState, useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useAuth } from '@/components/AuthProvider'
import { MessageUI } from '@/lib/types/database'
import { DEFAULT_MODEL } from '@/lib/config/models'
import ChatMessages from '@/components/chat/ChatMessages'
import ModelSelector from '@/components/ModelSelector'
import { IoSendOutline, IoStopOutline, IoSparklesOutline, IoBulbOutline, IoRocketOutline } from 'react-icons/io5'
import { useChatContext } from '@/lib/contexts/ChatContext'
import { useUsageLimit } from '@/lib/hooks/useUsageLimit'
import { deleteMessage } from '@/lib/utils/chatMessageUtils'

export default function NewChatPage() {
  const router = useRouter()
  const { setCurrentChatId, addNewChatToList } = useChatContext()
  const { user } = useAuth()
  const [messages, setMessages] = useState<MessageUI[]>([])
  const [inputValue, setInputValue] = useState('')
  const [isStreaming, setIsStreaming] = useState(false)
  const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL.name)
  
  const abortControllerRef = useRef<AbortController | null>(null)
  const { usageInfo } = useUsageLimit(user?.id || null)

  // Rotating tips for inspiration
  const tips = [
    {
      icon: IoSparklesOutline,
      title: "Brainstorm Ideas",
      description: "Help me develop plot twists, character arcs, or world-building elements for your story."
    },
    {
      icon: IoBulbOutline,
      title: "Writing Feedback",
      description: "Get constructive feedback on your writing style, pacing, or dialogue."
    },
    {
      icon: IoRocketOutline,
      title: "Creative Prompts",
      description: "Ask for writing prompts, scene suggestions, or creative challenges to overcome writer's block."
    },
    {
      icon: IoSparklesOutline,
      title: "Research Assistant",
      description: "Help me research historical facts, scientific concepts, or cultural details for your writing."
    },
    {
      icon: IoBulbOutline,
      title: "Character Development",
      description: "Create detailed character profiles, backstories, or personality traits."
    },
    {
      icon: IoRocketOutline,
      title: "Plot Structure",
      description: "Help me outline chapters, plan story arcs, or organize plot points."
    }
  ]

  const [currentTipIndex, setCurrentTipIndex] = useState(0)

  // Rotate tips every 4 seconds
  useEffect(() => {
    const interval = setInterval(() => {
      setCurrentTipIndex((prev) => (prev + 1) % tips.length)
    }, 4000)

    return () => clearInterval(interval)
  }, [tips.length])

  // Clear current chat when on new chat page
  useEffect(() => {
    setCurrentChatId(null)
  }, [setCurrentChatId])

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault()
    
    if (!inputValue.trim() || !user?.id || isStreaming) return

    const userMessage = inputValue.trim()
    setInputValue('')
    setIsStreaming(true)

    // Add user message to UI immediately
    const tempUserMessage: MessageUI = {
      id: `temp-${Date.now()}`,
      type: 'user',
      content: userMessage,
      timestamp: new Date()
    }

    setMessages([tempUserMessage])

    try {
      abortControllerRef.current = new AbortController()

      const response = await fetch('/api/standalone-chat', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          messages: [tempUserMessage],
          model: selectedModel,
          userId: user?.id,
          chatId: null // New chat
        }),
        signal: abortControllerRef.current.signal
      })

      if (!response.ok) {
        const errorData = await response.json()
        throw new Error(errorData.error || 'Failed to send message')
      }

      // Handle streaming response
      const reader = response.body?.getReader()
      if (!reader) throw new Error('No response body')

      const decoder = new TextDecoder()
      let aiResponse = ''
      let newChatId: string | null = null

      const tempAiMessage: MessageUI & { _isStreaming?: boolean } = {
        id: `temp-ai-${Date.now()}`,
        type: 'ai',
        content: '',
        timestamp: new Date(),
        model: selectedModel,
        _isStreaming: true
      }

      setMessages(prev => [...prev, tempAiMessage])

      while (true) {
        const { done, value } = await reader.read()
        if (done) break

        const chunk = decoder.decode(value, { stream: true })
        const lines = chunk.split('\n')

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            try {
              const data = JSON.parse(line.slice(6))
              
              if (data.type === 'content') {
                aiResponse += data.content
                setMessages(prev => prev.map(msg => 
                  msg.id === tempAiMessage.id 
                    ? { ...msg, content: aiResponse }
                    : msg
                ))
              } else if (data.type === 'done') {
                newChatId = data.chatId
                // Fetch the new chat data and add it to sidebar before redirecting
                if (newChatId && addNewChatToList) {
                  try {
                    const { supabase } = await import('@/lib/supabase')
                    const { data: newChatData, error } = await supabase
                      .from('chats')
                      .select('*')
                      .eq('id', newChatId)
                      .single()
                    
                    if (!error && newChatData) {
                      // Add the new chat to the sidebar
                      addNewChatToList(newChatData)
                    }
                  } catch (error) {
                    console.error('Error fetching new chat data:', error)
                  }
                }
                
                // Redirect to the new chat page
                if (newChatId) {
                  router.push(`/dashboard/chat/${newChatId}`)
                }
              } else if (data.type === 'error') {
                throw new Error(data.error)
              }
            } catch (e) {
              // Skip invalid JSON
            }
          }
        }
      }

      // Remove streaming flag
      setMessages(prev => prev.map(msg => 
        msg.id === tempAiMessage.id 
          ? { ...msg, _isStreaming: false }
          : msg
      ))

    } catch (error: any) {
      console.error('Error sending message:', error)
      
      if (error.name === 'AbortError') {
        // Request was cancelled
        setMessages([])
      } else {
        // Show error message
        const errorMessage: MessageUI = {
          id: `error-${Date.now()}`,
          type: 'ai',
          content: `Sorry, I encountered an error: ${error.message}`,
          timestamp: new Date()
        }
        setMessages(prev => [...prev.slice(0, -1), errorMessage])
      }
    } finally {
      setIsStreaming(false)
      abortControllerRef.current = null
    }
  }

  const stopExecution = () => {
    if (abortControllerRef.current) {
      abortControllerRef.current.abort()
    }
  }

  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault()
      handleSubmit(e as any)
    }
  }

  const getAvatarUrl = () => user?.user_metadata?.avatar_url || null
  const getInitials = () => {
    const fullName = user?.user_metadata?.full_name
    if (fullName) {
      return fullName.split(' ').map((n: string) => n[0]).join('').toUpperCase()
    }
    return user?.email?.[0]?.toUpperCase() || 'U'
  }

  const getPlaceholder = () => {
    if (isStreaming) return "AI is thinking..."
    return "Start a new conversation..."
  }

  const handleDeleteMessage = async (messageId: string) => {
    try {
      await deleteMessage(messageId)
      
      // Remove the message from the local state
      setMessages(prev => prev.filter(msg => msg.id.toString() !== messageId))
    } catch (error: any) {
      console.error('Error deleting message:', error)
      // You could show a toast notification here
      throw error // Re-throw to let the Message component handle the error state
    }
  }

  return (
    <div className="flex flex-col h-full">
      {/* Header */}
      <div className="border-b border-white/10 bg-black/40 backdrop-blur-sm px-4 py-2 md:pl-4 pl-4">
        <h1 className="text-sm font-medium text-slate-300">New Chat</h1>
      </div>

      {/* Messages Area */}
      <div className="flex-1 flex flex-col min-h-0">
        <div className="flex-1 overflow-y-auto">
          <div className="max-w-4xl mx-auto px-4">
            {messages.length === 0 ? (
              <div className="flex-1 flex items-center justify-center py-8">
                <div className="text-center max-w-2xl mx-auto px-4">
                  {/* Main heading with gradient */}
                  <div className="mb-8">
                    <h2 className="text-3xl font-bold bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400 bg-clip-text text-transparent mb-4">
                      Your AI Writing Partner
                    </h2>
                    <p className="text-slate-300 text-lg leading-relaxed">
                      Ready to bring your stories to life? Let's create something amazing together.
                    </p>
                  </div>

                  {/* Rotating tip card */}
                  <div className="mb-8">
                    <div className="bg-gradient-to-br from-slate-800/50 to-slate-900/50 backdrop-blur-sm border border-slate-700/50 rounded-2xl p-6 transition-all duration-500 h-48 flex flex-col justify-center">
                      <div className="flex items-center justify-center mb-4">
                        <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-purple-500/20 to-pink-500/20 flex items-center justify-center border border-purple-500/30">
                          {React.createElement(tips[currentTipIndex].icon, { className: "w-6 h-6 text-purple-400" })}
                        </div>
                      </div>
                      <h3 className="text-lg font-semibold text-white mb-2 text-center">
                        {tips[currentTipIndex].title}
                      </h3>
                      <p className="text-slate-300 text-sm leading-relaxed text-center line-clamp-3">
                        {tips[currentTipIndex].description}
                      </p>
                    </div>
                  </div>

                  {/* Quick prompt suggestions */}
                  <div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-8">
                    <button
                      onClick={() => setInputValue("Help me brainstorm a unique plot twist for my fantasy novel about a time-traveling librarian.")}
                      className="p-3 text-left bg-slate-800/30 hover:bg-slate-700/40 border border-slate-700/50 hover:border-slate-600/50 rounded-xl transition-all duration-200 group"
                    >
                      <div className="text-xs text-purple-400 font-medium mb-1">💡 Plot Twist</div>
                      <div className="text-sm text-slate-300 group-hover:text-white transition-colors">
                        Help me brainstorm a unique plot twist...
                      </div>
                    </button>
                    
                    <button
                      onClick={() => setInputValue("Create a detailed character profile for a morally ambiguous anti-hero who was once a respected judge.")}
                      className="p-3 text-left bg-slate-800/30 hover:bg-slate-700/40 border border-slate-700/50 hover:border-slate-600/50 rounded-xl transition-all duration-200 group"
                    >
                      <div className="text-xs text-blue-400 font-medium mb-1">👤 Character</div>
                      <div className="text-sm text-slate-300 group-hover:text-white transition-colors">
                        Create a detailed character profile...
                      </div>
                    </button>
                    
                    <button
                      onClick={() => setInputValue("Give me feedback on this dialogue: 'I never asked for this,' she whispered, her voice barely audible over the storm.'")}
                      className="p-3 text-left bg-slate-800/30 hover:bg-slate-700/40 border border-slate-700/50 hover:border-slate-600/50 rounded-xl transition-all duration-200 group"
                    >
                      <div className="text-xs text-emerald-400 font-medium mb-1">✍️ Feedback</div>
                      <div className="text-sm text-slate-300 group-hover:text-white transition-colors">
                        Give me feedback on this dialogue...
                      </div>
                    </button>
                    
                    <button
                      onClick={() => {
                        setInputValue("Write an explicit sex scene between two characters. Include graphic descriptions, vulgar language, and intimate details. Make it hot and erotic.")
                        setSelectedModel("Mistral Nemo")
                      }}
                      className="p-3 text-left bg-slate-800/30 hover:bg-slate-700/40 border border-slate-700/50 hover:border-slate-600/50 rounded-xl transition-all duration-200 group"
                    >
                      <div className="text-xs text-red-400 font-medium mb-1">🔥 NSFW</div>
                      <div className="text-sm text-slate-300 group-hover:text-white transition-colors">
                        Write an explicit sex scene...
                      </div>
                    </button>
                  </div>

                  {/* Inspiration footer */}
                  <div className="text-slate-500 text-sm">
                    <span className="inline-flex items-center gap-1">
                      <IoSparklesOutline className="w-4 h-4" />
                      Your creativity, amplified by AI
                    </span>
                  </div>
                </div>
              </div>
            ) : (
              <ChatMessages
                allDisplayMessages={messages}
                isStreaming={isStreaming}
                currentChat={null}
                contextInfo={null}
                toolResults={[]}
                getAvatarUrl={getAvatarUrl}
                getInitials={getInitials}
                onFileClick={async () => {}}
                onDeleteMessage={handleDeleteMessage}
              />
            )}
          </div>
        </div>
      </div>

      {/* Input Area */}
      <div className="bg-black/40 backdrop-blur-sm px-4 py-3">
        <div className="max-w-4xl mx-auto">
          <form onSubmit={handleSubmit}>
            <div className="relative bg-white/5 backdrop-blur-sm border border-white/10 rounded-lg transition-all">
              <textarea
                value={inputValue}
                onChange={(e) => {
                  setInputValue(e.target.value)
                  e.target.style.height = 'auto'
                  e.target.style.height = `${Math.min(e.target.scrollHeight, 120)}px`
                }}
                onKeyDown={handleKeyDown}
                placeholder={getPlaceholder()}
                className="w-full bg-transparent text-slate-200 placeholder-slate-400 border-0 outline-none resize-none px-4 py-3 pr-12 pb-12 min-h-[3rem] max-h-[7.5rem]"
                disabled={isStreaming}
                autoFocus
              />
              
              <div className="absolute left-4 bottom-3 flex items-center gap-2">
                <ModelSelector
                  selectedModel={selectedModel}
                  onModelChange={setSelectedModel}
                  disabled={isStreaming}
                  variant="minimal"
                  className="min-w-0"
                  usageInfo={usageInfo}
                />
              </div>
              
              <div className="absolute right-2 bottom-3 flex items-center gap-2">
                {isStreaming ? (
                  <button
                    type="button"
                    onClick={stopExecution}
                    className="p-2 text-slate-400 hover:text-slate-200 hover:bg-white/10 rounded-md transition-colors"
                    title="Stop generation"
                  >
                    <IoStopOutline className="h-4 w-4" />
                  </button>
                ) : (
                  <button
                    type="submit"
                    disabled={!inputValue.trim() || isStreaming}
                    className="p-2 text-slate-400 hover:text-slate-200 hover:bg-white/10 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                    title="Send message"
                  >
                    <IoSendOutline className="h-4 w-4" />
                  </button>
                )}
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}