Newer
Older
cortex-hub / ui / client-app / src / pages / CodingAssistantPage.js
import React, { useState, useRef, useEffect, useCallback } from "react";
// Import the components you'll create for each section
import ChatArea from "../components/ChatArea";
import CodeFolderAccess from "../components/CodeFolderAccess";
import InteractionLog from "../components/InteractionLog";
// import Controls from "../components/Controls";
import SessionSidebar from "../components/SessionSidebar";

// A custom hook to manage WebSocket connection and state
import useCodeAssistant from "../hooks/useCodeAssistant";

const CodeAssistantPage = () => {
  // Reference for the main container to manage scrolling
  const pageContainerRef = useRef(null);

  // Use a custom hook to handle the core logic
  const {
    chatHistory,
    thinkingProcess,
    connectionStatus,
    selectedFolder,
    isProcessing,
    isPaused,
    errorMessage,
    showErrorModal,
    tokenUsage,
    handleSendChat,
    handleSelectFolder,
    handlePause,
    handleStop,
    setShowErrorModal,
    handleSwitchSession,
    sessionId,
    userConfigData,
    localActiveLLM,
    setLocalActiveLLM,
    isConfigured,
    missingConfigs
  } = useCodeAssistant({ pageContainerRef });

  const [isPanelExpanded, setIsPanelExpanded] = useState(false);
  const [showConfigModal, setShowConfigModal] = useState(false);

  // Scroll to the bottom of the page when new content is added
  useEffect(() => {
    if (pageContainerRef.current) {
      pageContainerRef.current.scrollTop = pageContainerRef.current.scrollHeight;
    }
  }, [chatHistory, thinkingProcess]);

  return (
    <div className="flex flex-col flex-grow h-full min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 relative">
      <SessionSidebar
        featureName="coding_assistant"
        currentSessionId={sessionId}
        onSwitchSession={handleSwitchSession}
        onNewSession={() => handleSendChat("/new")}
      />

      {/* Main content area */}
      <div className="flex-grow px-4 overflow-hidden" ref={pageContainerRef}>
        {/* Adjusted grid for narrow right panel */}
        <div className={`grid grid-cols-1 ${isPanelExpanded ? 'md:grid-cols-3' : 'md:grid-cols-1'} gap-4 h-full`}>
          {/* Left Column: Chat Area */}
          <div className="md:col-span-2 flex flex-col">
            {/* Area 1: Chat with LLM */}
            <div className="flex-grow p-4 bg-white dark:bg-gray-800 rounded-lg shadow">
              <h2 className="text-xl font-bold mb-2 flex justify-between items-center">
                <div className="flex items-center gap-2">
                  <span>Chat with the LLM</span>
                  {userConfigData?.effective?.llm?.providers && Object.keys(userConfigData.effective.llm.providers).length > 0 && (
                    <div className="flex items-center gap-1.5">
                      {!isConfigured && (
                        <div className="group relative flex items-center">
                          <svg className="w-5 h-5 text-yellow-500 cursor-pointer" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
                          </svg>
                          <div className="absolute left-1/2 -translate-x-1/2 top-full mt-2 w-56 bg-gray-900 text-white text-[11px] rounded shadow-lg p-2.5 opacity-0 group-hover:opacity-100 transition-opacity z-50 pointer-events-none text-left">
                            <p className="font-bold mb-1 text-red-400">Missing Key</p>
                            <ul className="list-disc pl-3 space-y-0.5">
                              {missingConfigs?.map((m, i) => <li key={i}>{m}</li>)}
                            </ul>
                          </div>
                        </div>
                      )}
                      <button
                        onClick={() => setShowConfigModal(true)}
                        className="text-gray-400 hover:text-indigo-400 dark:text-gray-500 dark:hover:text-indigo-300 transition-colors"
                        title="Quick Session Settings"
                      >
                        <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
                      </button>
                    </div>
                  )}
                </div>
                <div className="flex items-center space-x-4">
                  <div className="text-sm font-mono text-gray-500 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
                    {tokenUsage?.token_count || 0} / {tokenUsage?.token_limit || 100000} tokens ({tokenUsage?.percentage || 0}%)
                  </div>
                  <button
                    onClick={() => handleSendChat("/new")}
                    className="text-sm bg-indigo-100 hover:bg-indigo-200 text-indigo-700 dark:bg-indigo-900 dark:hover:bg-indigo-800 dark:text-indigo-200 px-3 py-1 rounded font-semibold transition-colors"
                  >
                    + New Session
                  </button>
                  <span className="text-sm text-gray-500 dark:text-gray-400 font-semibold italic ml-2 hidden md:inline-block">
                    Unlock powerful coding assistance with RAG!
                  </span>
                  <button
                    onClick={() => setIsPanelExpanded(!isPanelExpanded)}
                    className="relative inline-flex items-center h-6 rounded-full w-11 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                    role="switch"
                    aria-checked={isPanelExpanded}
                    aria-label={isPanelExpanded ? "Collapse panel" : "Expand panel"}
                  >
                    <span
                      className={`${isPanelExpanded ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-600"
                        } absolute inset-0 rounded-full transition-colors duration-200 ease-in-out`}
                    />
                    <span
                      className={`${isPanelExpanded
                        ? "translate-x-6 bg-white"
                        : "translate-x-1 bg-gray-400 dark:bg-gray-300"
                        } inline-block w-4 h-4 transform rounded-full transition-transform duration-200 ease-in-out`}
                    />
                  </button>
                </div>
              </h2>
              {/* Note: ChatArea component needs to be implemented with a <textarea> */}
              <ChatArea chatHistory={chatHistory} onSendMessage={handleSendChat} isProcessing={isProcessing} />
            </div>
          </div>

          {/* Right Column: Code Folder Access & AI Thinking Process */}
          {isPanelExpanded && (
            <div className="md:col-span-1 flex flex-col space-y-4">
              {/* Area 2: Code Folder Lookup - Moved to the right */}
              <div className="p-4 bg-white dark:bg-gray-800 rounded-lg shadow">
                <h2 className="text-xl font-bold mb-2">Code Folder Access</h2>
                <CodeFolderAccess selectedFolder={selectedFolder} onSelectFolder={handleSelectFolder} />
              </div>

              {/* Area 3: Thinking Process */}
              <div className="flex-grow p-4 bg-white dark:bg-gray-800 rounded-lg shadow">
                <h2 className="text-xl font-bold mb-2">AI Thinking Process</h2>
                <InteractionLog logs={thinkingProcess} />
              </div>
            </div>
          )}
        </div>
      </div>

      {/* Controls at the bottom
      <Controls
        connectionStatus={connectionStatus}
        isProcessing={isProcessing}
        isPaused={isPaused}
        onPause={handlePause}
        onStop={handleStop}
      /> */}

      {/* Error Modal */}
      {showErrorModal && (
        <div className="fixed inset-0 bg-gray-900 bg-opacity-75 flex justify-center items-center z-50">
          <div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl text-center">
            <h2 className="text-xl font-bold mb-4 text-red-500">Error</h2>
            <p className="mb-4">{errorMessage}</p>
            <button
              onClick={() => setShowErrorModal(false)}
              className="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
            >
              Close
            </button>
          </div>
        </div>
      )}

      {showConfigModal && (
        <div className="fixed inset-0 bg-gray-900/40 backdrop-blur-sm flex justify-center items-center z-50">
          <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-md overflow-hidden animate-fade-in">
            <div className="px-6 py-4 border-b border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 flex justify-between items-center">
              <h3 className="text-lg font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
                <svg className="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
                Session Engine
              </h3>
              <button onClick={() => setShowConfigModal(false)} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
                <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
              </button>
            </div>
            <div className="p-6 space-y-5">
              <div>
                <label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Active LLM Provider</label>
                <select
                  value={localActiveLLM}
                  onChange={(e) => setLocalActiveLLM(e.target.value)}
                  className="w-full border border-gray-300 dark:border-gray-600 rounded-lg p-2.5 bg-white dark:bg-gray-800 text-sm focus:ring-2 focus:ring-indigo-500"
                >
                  <option value="">-- Choose Provider --</option>
                  {userConfigData?.effective?.llm?.providers && Object.keys(userConfigData.effective.llm.providers).map(pid => {
                    const modelName = userConfigData.effective.llm.providers[pid].model;
                    return (
                      <option key={pid} value={pid}>
                        {pid} {modelName ? `(${modelName})` : ''}
                      </option>
                    );
                  })}
                </select>
              </div>
            </div>
            <div className="px-6 py-4 bg-gray-50 dark:bg-gray-800/50 border-t border-gray-100 dark:border-gray-700 flex justify-end gap-3">
              <button
                onClick={() => setShowConfigModal(false)}
                className="px-4 py-2 text-sm font-semibold text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
              >
                Cancel
              </button>
              <button
                onClick={() => setShowConfigModal(false)}
                className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded font-bold text-sm shadow-sm transition-all"
              >
                Apply to Current Session
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

export default CodeAssistantPage;