Newer
Older
cortex-hub / ui / client-app / src / hooks / useCodeAssistant.js
import { useState, useEffect, useRef, useCallback } from "react";
import { connectToWebSocket } from "../services/websocket";
import { v4 as uuidv4 } from 'uuid';

const useCodeAssistant = ({ pageContainerRef }) => {
  const [chatHistory, setChatHistory] = useState([]);
  const [thinkingProcess, setThinkingProcess] = useState([]);
  const [selectedFolder, setSelectedFolder] = useState(null);
  const [connectionStatus, setConnectionStatus] = useState("disconnected");
  const [isProcessing, setIsProcessing] = useState(false);
  const [isPaused, setIsPaused] = useState(false);
  const [errorMessage, setErrorMessage] = useState("");
  const [showErrorModal, setShowErrorModal] = useState(false);
  const [sessionId, setSessionId] = useState(null);

  const sessionIdRef = useRef(null); // ✅ Always current sessionId
  const ws = useRef(null);
  const initialized = useRef(false);
  const dirHandleRef = useRef(null);

  const handleChatMessage = useCallback((message) => {
    console.log("Received chat message:", message);
    setChatHistory((prev) => [...prev, {
      isUser: false,
      isPureAnswer: true,
      text: message.content,
      dicision: message.dicision,
      reasoning: message.reasoning
    }]);
    setIsProcessing(false);
  }, []);

  const handleCodeChange = useCallback((message) => {
    console.log("Received code change:", message);
    setChatHistory((prev) => [...prev, {
      isUser: false,
      isPureAnswer: false,
      text: message.content,
      code_changes: message.code_changes,
      steps: message.steps,
      reasoning: message.reasoning
    }]);
    if (message.done === true){
      setThinkingProcess((prev) => [...prev,{
        type: "system",
        message: "AI processing is complete"
      }])
       setIsProcessing(false);
    } else{
      setIsProcessing(true);
    }
  }, []);


  const handleThinkingLog = useCallback((message) => {
    setThinkingProcess((prev) => [...prev, {
      type: "remote",
      message: message.content,
    }]);
  }, []);

  const handleError = useCallback((message) => {
    setErrorMessage(message.content);
    setShowErrorModal(true);
    setIsProcessing(false);
  }, []);

  const handleStatusUpdate = useCallback((message) => {
    setIsProcessing(message.processing);
    setIsPaused(message.paused);
    setConnectionStatus(message.status);
  }, []);

  const handleListDirectoryRequest = useCallback(async (message) => {
    const { request_id } = message;
    console.log("Received list directory request:", message);
    const dirHandle = dirHandleRef.current;

    if (!dirHandle) {
      const errorMsg = "No folder selected by user.";
      console.warn(errorMsg);
      ws.current.send(JSON.stringify({ type: "error", content: errorMsg, request_id }));
      return;
    }

    try {
      const files = [];
      setThinkingProcess((prev) => [...prev, {
        type: "local",
        message: `Iterating the directory: ${dirHandle.name} to list files...`,
      }]);
      async function walkDirectory(handle, path = '') {
        for await (const entry of handle.values()) {
          const entryPath = `${path}/${entry.name}`;
          if (entry.kind === "file") {
            const file = await entry.getFile();
            files.push({
              name: file.name,
              path: entryPath,
              size: file.size,
              lastModified: file.lastModified,
              created: file.created,
            });
          } else if (entry.kind === "directory") {
            await walkDirectory(entry, entryPath);
          }
        }
      }

      await walkDirectory(dirHandle);

      ws.current.send(JSON.stringify({
        type: "list_directory_response",
        files,
        request_id,
        session_id: sessionIdRef.current, // ✅ Always current
      }));

      setThinkingProcess((prev) => [...prev, {
        type: "local",
        message: `Sent ${files.length} files metadata information to server.`,
      }]);
    } catch (error) {
      console.error("Failed to list directory:", error);
      ws.current.send(JSON.stringify({
        type: "error",
        content: "Failed to access folder contents.",
        request_id,
        session_id: sessionIdRef.current,
      }));
    }
  }, []);

  const getFileHandleFromPath = async (dirHandle, filePath) => {
    const pathParts = filePath.split('/').filter(Boolean);
    let currentHandle = dirHandle;

    for (let i = 0; i < pathParts.length; i++) {
      const part = pathParts[i];
      try {
        if (i === pathParts.length - 1) {
          return await currentHandle.getFileHandle(part);
        } else {
          currentHandle = await currentHandle.getDirectoryHandle(part);
        }
      } catch (error) {
        console.warn(`Path not found: ${filePath}`, error);
        return null;
      }
    }
    return null;
  };

  const handleReadFilesRequest = useCallback(async (message) => {
    const { filepaths, request_id } = message;
    console.log("Received read files request:", message);
    const dirHandle = dirHandleRef.current;

    if (!dirHandle) {
      ws.current.send(JSON.stringify({
        type: "error",
        content: "No folder selected.",
        request_id,
        session_id: sessionIdRef.current,
      }));
      return;
    }

    const filesData = [];
    const readFiles = [];

    for (const filepath of filepaths) {
      try {
        const fileHandle = await getFileHandleFromPath(dirHandle, filepath);
        if (!fileHandle) {
          filesData.push({ filepath, content: null });
        }
        const file = await fileHandle.getFile();
        const content = await file.text();
        filesData.push({ filepath, content });
        readFiles.push(filepath);
      } catch (error) {
        console.warn(`Failed to read file: ${filepath}`, error);
        // ws.current.send(JSON.stringify({
        //   type: "error",
        //   content: `Could not read file: ${filepath}`,
        //   request_id,
        //   session_id: sessionIdRef.current,
        // }));
      }
    }

    ws.current.send(JSON.stringify({
      type: "file_content_response",
      files: filesData,
      request_id,
      session_id: sessionIdRef.current,
    }));

    setThinkingProcess((prev) => {
      const newMessages = [];

      if (readFiles.length > 0) {
        const displayMessage = readFiles.length > 10
          ? `Read ${readFiles.length} files successfully.`
          : `Read files and send successfully: [${readFiles.map(f => `"${f}"`).join(', ')}]`;
        newMessages.push({ type: "local", message: displayMessage });
      }
      return [...prev, ...newMessages];
    });
  }, []);

  const handleExecuteCommandRequest = useCallback((message) => {
    const { command, request_id } = message;
    const output = `Simulated output for command: '${command}'`;

    ws.current.send(JSON.stringify({
      type: "execute_command_response",
      command,
      output,
      request_id,
      session_id: sessionIdRef.current,
    }));

    setThinkingProcess((prev) => [...prev, {
      type: "system",
      message: `Simulated execution of command: '${command}'`
    }]);
  }, []);

  const handleIncomingMessage = useCallback((message) => {
    switch (message.type) {
      case "chat_message":
        handleChatMessage(message);
        break;
      case "code_change":
        handleCodeChange(message);
        break;
      case "thinking_log":
        handleThinkingLog(message);
        break;
      case "error":
        handleError(message);
        break;
      case "status_update":
        handleStatusUpdate(message);
        break;
      case "list_directory":
        handleListDirectoryRequest(message);
        break;
      case "get_file_content":
        handleReadFilesRequest(message);
        break;
      case "execute_command":
        handleExecuteCommandRequest(message);
        break;
      default:
        console.log("Unknown message type:", message);
    }
  }, [
    handleChatMessage,
    handleCodeChange,
    handleThinkingLog,
    handleError,
    handleStatusUpdate,
    handleListDirectoryRequest,
    handleReadFilesRequest,
    handleExecuteCommandRequest
  ]);

  // WebSocket Setup
  useEffect(() => {
    if (initialized.current) return;
    initialized.current = true;

    const setupConnection = async () => {
      try {
        const { ws: newWs, sessionId: newSessionId } = await connectToWebSocket(
          handleIncomingMessage,
          () => setConnectionStatus("connected"),
          () => {
            setConnectionStatus("disconnected");
            setIsProcessing(false);
          },
          (error) => {
            setConnectionStatus("error");
            setErrorMessage(`Failed to connect: ${error.message}`);
            setShowErrorModal(true);
          }
        );
        ws.current = newWs;
        setSessionId(newSessionId);
        sessionIdRef.current = newSessionId; // ✅ Keep ref in sync
      } catch (error) {
        console.error("Setup failed:", error);
      }
    };

    setupConnection();

    return () => {
      if (ws.current) {
        ws.current.close();
      }
    };
  }, [handleIncomingMessage]);

  const handleSendChat = useCallback(async (text) => {
    if (ws.current && ws.current.readyState === WebSocket.OPEN) {
      setChatHistory((prev) => [...prev, { isUser: true, text }]);
      setIsProcessing(true);
      ws.current.send(JSON.stringify({
        type: "chat_message",
        content: text,
        session_id: sessionIdRef.current,
        path: dirHandleRef.current ? dirHandleRef.current.name : null
      }));
    }
  }, []);

  const handleSelectFolder = useCallback(async (directoryHandle) => {
    if (!window.showDirectoryPicker) return;

    try {
      dirHandleRef.current = directoryHandle;
      setSelectedFolder(directoryHandle.name);

      setThinkingProcess((prev) => [...prev, {
        type: "user",
        message: `Selected local folder: ${directoryHandle.name}`
      }]);
    } catch (error) {
      console.error("Folder selection canceled or failed:", error);
    }
  }, []);

  const handlePause = useCallback(() => {
    if (ws.current && ws.current.readyState === WebSocket.OPEN) {
      ws.current.send(JSON.stringify({
        type: "control",
        command: "pause",
        session_id: sessionIdRef.current
      }));
    }
  }, []);

  const handleStop = useCallback(() => {
    if (ws.current && ws.current.readyState === WebSocket.OPEN) {
      ws.current.send(JSON.stringify({
        type: "control",
        command: "stop",
        session_id: sessionIdRef.current
      }));
    }
  }, []);

  return {
    chatHistory,
    thinkingProcess,
    selectedFolder,
    connectionStatus,
    isProcessing,
    isPaused,
    errorMessage,
    showErrorModal,
    handleSendChat,
    handleSelectFolder,
    handlePause,
    handleStop,
    setShowErrorModal,
  };
};

export default useCodeAssistant;