Newer
Older
cortex-hub / ui / client-app / src / components / ChatArea.js
import React, { useState, useRef, useEffect } from "react";
import ChatWindow from "./ChatWindow";
import './ChatArea.css';

const ChatArea = ({
  chatHistory,
  onSendMessage,
  isProcessing,
  featureName = "default",
  workspaceId = null,
  syncConfig = null,
  isSourceDisconnected = false
}) => {
  const [inputValue, setInputValue] = useState("");
  const inputRef = useRef(null);
  const chatScrollRef = useRef(null);

  const handleSendMessage = (e) => {
    e.preventDefault();
    if (inputValue.trim() !== "" && !isSourceDisconnected) {
      onSendMessage(inputValue);
      setInputValue("");
    }
  };

  const handleKeyDown = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleSendMessage(e);
    }
  };

  // Scroll chat to bottom on new message
  useEffect(() => {
    if (chatScrollRef.current) {
      chatScrollRef.current.scrollTop = chatScrollRef.current.scrollHeight;
    }
  }, [chatHistory]);

  return (
    <div className="flex flex-col h-full">
      {/* Scrollable ChatWindow */}
      <div ref={chatScrollRef} className="flex-grow overflow-y-auto">
        <ChatWindow chatHistory={chatHistory} featureName={featureName} />
      </div>

      {/* Sticky Input */}
      <div className="p-2 bg-transparent sticky bottom-0 left-0 z-10">
        {featureName === "coding_assistant" && workspaceId && (
          <div className={`mx-2 mb-2 px-3 py-1.5 border rounded-lg flex flex-col gap-1 animate-in fade-in slide-in-from-bottom-2 duration-300 ${isSourceDisconnected
            ? 'bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800'
            : 'bg-emerald-50 dark:bg-emerald-900/30 border-emerald-100 dark:border-emerald-800'
            }`}>
            <div className="flex items-center justify-between">
              <div className="flex items-center gap-2">
                <div className="relative">
                  <div className={`w-2 h-2 rounded-full ${isSourceDisconnected ? 'bg-red-500' : 'bg-emerald-500'}`}></div>
                  <div className={`absolute inset-0 w-2 h-2 rounded-full animate-ping ${isSourceDisconnected ? 'bg-red-400' : 'bg-emerald-400'}`}></div>
                </div>
                <span className={`text-[10px] font-black uppercase tracking-widest ${isSourceDisconnected ? 'text-red-700 dark:text-red-400' : 'text-emerald-700 dark:text-emerald-400'}`}>
                  {isSourceDisconnected ? 'Source Node Disconnected' : 'Workspace Sync Active'}
                </span>
              </div>
              <span className={`text-[9px] font-mono ${isSourceDisconnected ? 'text-red-600/70 dark:text-red-500/50' : 'text-emerald-600/70 dark:text-emerald-500/50'}`}>
                {workspaceId}
              </span>
            </div>

            {syncConfig && (
              <div className={`flex items-center gap-3 text-[9px] font-medium overflow-hidden whitespace-nowrap ${isSourceDisconnected ? 'text-red-600/60 dark:text-red-400/40' : 'text-emerald-600/60 dark:text-emerald-400/40'
                }`}>
                <div className="flex items-center gap-1 shrink-0">
                  <span className="uppercase opacity-50">Source:</span>
                  <span className="font-bold">{syncConfig.source === 'node_local' ? 'Node Local' : syncConfig.source === 'server' ? 'Hub' : 'Empty'}</span>
                </div>
                {syncConfig.source === 'node_local' && (
                  <>
                    <div className={`w-px h-2 shrink-0 ${isSourceDisconnected ? 'bg-red-200 dark:bg-red-800' : 'bg-emerald-200 dark:bg-emerald-800'}`}></div>
                    <div className="flex items-center gap-1 shrink-0">
                      <span className="uppercase opacity-50 text-red-500/80 font-black tracking-widest">⚠️ SOURCE NODE:</span>
                      <span className="font-bold">{syncConfig.source_node_id}</span>
                    </div>
                    <div className={`w-px h-2 shrink-0 ${isSourceDisconnected ? 'bg-red-200 dark:bg-red-800' : 'bg-emerald-200 dark:bg-emerald-800'}`}></div>
                    <div className="flex items-center gap-1 truncate">
                      <span className="uppercase opacity-50 shrink-0">Path:</span>
                      <span className="font-mono truncate" title={syncConfig.path}>{syncConfig.path}</span>
                    </div>
                  </>
                )}
              </div>
            )}
          </div>
        )}
        <form onSubmit={handleSendMessage} className="flex items-center space-x-2">
          <textarea
            ref={inputRef}
            value={inputValue}
            onChange={(e) => setInputValue(e.target.value)}
            onKeyDown={handleKeyDown}
            disabled={isProcessing || isSourceDisconnected}
            className={`flex-grow p-3 rounded-lg border bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 ${isSourceDisconnected ? 'border-red-300 dark:border-red-900 cursor-not-allowed grayscale' : 'border-gray-300 dark:border-gray-600'
              }`}
            placeholder={
              isProcessing
                ? "AI is thinking..."
                : isSourceDisconnected
                  ? "Chat locked: Source node is offline. Reconnect the node to continue."
                  : "Type a message..."
            }
          ></textarea>
          <button
            type="submit"
            disabled={isProcessing || isSourceDisconnected}
            className={`p-3 rounded-lg text-white font-bold transition-colors flex-shrink-0 ${isProcessing || isSourceDisconnected
              ? "bg-gray-400 dark:bg-gray-600 cursor-not-allowed"
              : "bg-indigo-600 hover:bg-indigo-700"
              }`}
          >
            Send
          </button>
        </form>
      </div>
    </div>
  );
};

export default ChatArea;