diff --git a/.gitignore b/.gitignore index c8106682..a2d17ea5 100644 --- a/.gitignore +++ b/.gitignore @@ -205,7 +205,7 @@ next-env.d.ts !.vscode .vscode/* !.vscode/settings.json -!.vscode/tasks.json +!.vscode/agentic_operations.json !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets @@ -229,6 +229,7 @@ results checkpoints chroma_db uploads +outputs # Custom files .env* @@ -239,8 +240,11 @@ uploads token.pickle chatsDb.json userProfileDb.json -tasks.json +agentic_operations.json context.json memory_operations.json +notificationsDb.json ruff_cache -.db \ No newline at end of file +memory.db +token.json +.wav diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 23b6fd19..00000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Run Agents Server", - "type": "shell", - "command": "sudo -E -m uvicorn agents:app --host 0.0.0.0 --port 5001", - "options": { - "cwd": "${workspaceFolder}/client/model/agents" - }, - "group": "build", - "presentation": { - "panel": "new" - } - }, - { - "label": "Run App Server", - "type": "shell", - "command": "sudo -E -m uvicorn app:app --host 0.0.0.0 --port 5000", - "options": { - "cwd": "${workspaceFolder}/client/model/app" - }, - "group": "build", - "presentation": { - "panel": "new" - } - }, - { - "label": "Run Chat Server", - "type": "shell", - "command": "sudo -E -m uvicorn chat:app --host 0.0.0.0 --port 5003", - "options": { - "cwd": "${workspaceFolder}/client/model/chat" - }, - "group": "build", - "presentation": { - "panel": "new" - } - }, - { - "label": "Run Common Server", - "type": "shell", - "command": "sudo -E -m uvicorn common:app --host 0.0.0.0 --port 5006", - "options": { - "cwd": "${workspaceFolder}/client/model/common" - }, - "group": "build", - "presentation": { - "panel": "new" - } - }, - { - "label": "Run Memory Server", - "type": "shell", - "command": "sudo -E -m uvicorn memory:app --host 0.0.0.0 --port 5002", - "options": { - "cwd": "${workspaceFolder}/client/model/memory" - }, - "group": "build", - "presentation": { - "panel": "new" - } - }, - { - "label": "Run Scraper Server", - "type": "shell", - "command": "sudo -E -m uvicorn scraper:app --host 0.0.0.0 --port 5004", - "options": { - "cwd": "${workspaceFolder}/client/model/scraper" - }, - "group": "build", - "presentation": { - "panel": "new" - } - }, - { - "label": "Run Utils Server", - "type": "shell", - "command": "sudo -E -m uvicorn utils:app --host 0.0.0.0 --port 5005", - "options": { - "cwd": "${workspaceFolder}/client/model/utils" - }, - "group": "build", - "presentation": { - "panel": "new" - } - }, - { - "label": "Run Auth Server", - "type": "shell", - "command": "sudo -E -m uvicorn auth:app --host 0.0.0.0 --port 5007", - "options": { - "cwd": "${workspaceFolder}/client/model/auth" - }, - "group": "build", - "presentation": { - "panel": "new" - } - }, - { - "label": "Run All Servers", - "dependsOn": [ - "Run Agents Server", - "Run App Server", - "Run Chat Server", - "Run Common Server", - "Run Memory Server", - "Run Scraper Server", - "Run Utils Server", - "Run Auth Server" - ], - "runOptions": { - "runOn": "default" - }, - "problemMatcher": [] - } - ] -} diff --git a/README.md b/README.md index 06030fe2..f907b51e 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ We at [Existence](https://existence.technology) believe that AI won't simply die ### :dart: Features -- Local-first, with support for Ollama +- Local-first, with support for Ollama - Multi-Model Support (Switch between multiple Ollama/Cloud models) - MBTI Personality Test (Used to collect initial information about the user and their personality to personalize responses) - LinkedIn, Reddit and X Integration for Personal Context diff --git a/src/interface/.env.template b/src/client/.env.template similarity index 100% rename from src/interface/.env.template rename to src/client/.env.template diff --git a/src/interface/.prettierrc b/src/client/.prettierrc similarity index 100% rename from src/interface/.prettierrc rename to src/client/.prettierrc diff --git a/src/interface/README.md b/src/client/README.md similarity index 100% rename from src/interface/README.md rename to src/client/README.md diff --git a/src/client/app/chat/page.js b/src/client/app/chat/page.js new file mode 100644 index 00000000..34793649 --- /dev/null +++ b/src/client/app/chat/page.js @@ -0,0 +1,689 @@ +"use client" +import React, { + useState, + useEffect, + useRef, + useCallback + // No longer need forwardRef here +} from "react" +import ChatBubble from "@components/ChatBubble" +import ToolResultBubble from "@components/ToolResultBubble" +import Sidebar from "@components/Sidebar" +import TopControlBar from "@components/TopControlBar" +import { + IconSend, + IconRefresh, + IconLoader, + IconPhone, + IconPhoneOff + // REMOVED: Mute/unmute icons are no longer needed here + // IconMicrophone, + // IconMicrophoneOff, +} from "@tabler/icons-react" +import toast from "react-hot-toast" + +// Import the DEFAULT export (forwardRef-wrapped component) +import BackgroundCircleProvider from "@components/voice-test/background-circle-provider" + +const Chat = () => { + // --- State Variables --- + const [messages, setMessages] = useState([]) + const [input, setInput] = useState("") + const [userDetails, setUserDetails] = useState("") + const [thinking, setThinking] = useState(false) + const [serverStatus, setServerStatus] = useState(true) + const [isSidebarVisible, setSidebarVisible] = useState(false) + const [currentModel, setCurrentModel] = useState("") + const [chatMode, setChatMode] = useState("voice") + const [isLoading, setIsLoading] = useState(() => chatMode === "text") + const [connectionStatus, setConnectionStatus] = useState("disconnected") + // REMOVED: isMuted state + // REMOVED: callDuration state + const [audioInputDevices, setAudioInputDevices] = useState([]) + const [selectedAudioInputDevice, setSelectedAudioInputDevice] = useState("") + + // --- Refs --- + const textareaRef = useRef(null) + const chatEndRef = useRef(null) + const eventListenersAdded = useRef(false) + const backgroundCircleProviderRef = useRef(null) + const ringtoneAudioRef = useRef(null) + const connectedAudioRef = useRef(null) + // REMOVED: timerIntervalRef + + // --- Handlers --- + const handleInputChange = (e) => { + const value = e.target.value + setInput(value) + if (textareaRef.current) { + textareaRef.current.style.height = "auto" + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px` + } + } + + const handleToggleMode = (targetMode) => { + if (targetMode === chatMode) return + setChatMode(targetMode) + if (targetMode === "text" && connectionStatus !== "disconnected") { + handleStopVoice() + } + } + + // REMOVED: formatDuration helper + + // --- Connection Status and Timer Handling --- + const handleStatusChange = useCallback((status) => { + console.log("Connection status changed:", status) + setConnectionStatus(status) + + // Stop ringing sound + if (status !== "connecting" && ringtoneAudioRef.current) { + ringtoneAudioRef.current.pause() + ringtoneAudioRef.current.currentTime = 0 + } + + // Play connected sound + if (status === "connected") { + // REMOVED: Reset duration + if (connectedAudioRef.current) { + connectedAudioRef.current.volume = 0.4 // Keep volume adjustment + connectedAudioRef.current + .play() + .catch((e) => + console.error("Error playing connected sound:", e) + ) + } + // REMOVED: Timer starting logic + } else { + // REMOVED: Timer clearing logic + // REMOVED: Reset duration logic + } + }, []) + + // --- Voice Control Handlers --- + const handleStartVoice = async () => { + // MODIFIED: No longer needs to pass deviceId to connect + if ( + connectionStatus !== "disconnected" || + !backgroundCircleProviderRef.current + ) + return + console.log("ChatPage: handleStartVoice called") + setConnectionStatus("connecting") + if (ringtoneAudioRef.current) { + ringtoneAudioRef.current.volume = 0.3 + ringtoneAudioRef.current.loop = true + ringtoneAudioRef.current + .play() + .catch((e) => console.error("Error playing ringtone:", e)) + } + try { + // Call connect without arguments + await backgroundCircleProviderRef.current?.connect() + } catch (error) { + console.error("ChatPage: Error starting voice connection:", error) + toast.error( + `Failed to connect: ${error.message || "Unknown error"}` + ) + handleStatusChange("disconnected") + } + } + + const handleStopVoice = () => { + if ( + connectionStatus === "disconnected" || + !backgroundCircleProviderRef.current + ) + return + console.log("ChatPage: handleStopVoice called") + backgroundCircleProviderRef.current?.disconnect() + } + + // REMOVED: handleToggleMute handler + + // MODIFIED: Device change handler - only updates state + const handleDeviceChange = (event) => { + const deviceId = event.target.value + console.log("ChatPage: Selected audio input device changed:", deviceId) + setSelectedAudioInputDevice(deviceId) + // Inform user that a reconnect is needed for the change to take effect + toast.success( + "Microphone selection changed. Please restart the call to use the new device.", + { duration: 4000 } + ) + // REMOVED: Automatic reconnect logic + } + + // --- Data Fetching and IPC --- + const fetchChatHistory = async () => { + try { + const response = await window.electron?.invoke("fetch-chat-history") + if (response?.status === 200) { + console.log( + "fetchChatHistory: Received history", + response.messages?.length + ) + setMessages(response.messages || []) + } else { + console.error( + "fetchChatHistory: Error status from IPC:", + response?.status + ) + toast.error("Error fetching chat history.") + setMessages([]) + } + } catch (error) { + console.error("fetchChatHistory: Exception caught:", error) + toast.error("Error fetching chat history.") + setMessages([]) + } finally { + console.log( + "fetchChatHistory: Setting isLoading to false in finally block." + ) + setIsLoading(false) + } + } + + const fetchUserDetails = async () => { + try { + const response = await window.electron?.invoke("get-profile") + setUserDetails(response) + } catch (error) { + toast.error("Error fetching user details.") + } + } + + const fetchCurrentModel = async () => { + setCurrentModel("llama3.2:3b") + } + + const setupIpcListeners = () => { + if (!eventListenersAdded.current && window.electron) { + const handleMessageStream = ({ messageId, token }) => { + setMessages((prev) => { + const messageIndex = prev.findIndex( + (msg) => msg.id === messageId + ) + if (messageIndex === -1) { + return [ + ...prev, + { + id: messageId, + message: token, + isUser: false, + memoryUsed: false, + agentsUsed: false, + internetUsed: false, + type: "text" + } + ] + } + return prev.map((msg, index) => + index === messageIndex + ? { ...msg, message: msg.message + token } + : msg + ) + }) + } + window.electron.onMessageStream(handleMessageStream) + eventListenersAdded.current = true + } + } + + const sendMessage = async () => { + if (input.trim() === "" || chatMode !== "text") return + + const newMessage = { + message: input, + isUser: true, + id: Date.now(), + type: "text" + } + setMessages((prev) => [...prev, newMessage]) + setInput("") + if (textareaRef.current) { + textareaRef.current.style.height = "auto" + } + setThinking(true) + setupIpcListeners() + + try { + const response = await window.electron?.invoke("send-message", { + input: newMessage.message + }) + if (response.status === 200) { + console.log( + "Message send invoked, waiting for stream/completion." + ) + } else { + toast.error("Failed to send message via IPC.") + setThinking(false) + } + } catch (error) { + toast.error("Error sending message.") + setThinking(false) + } finally { + setThinking(false) + fetchChatHistory() // Fetch history after sending + } + } + + const clearChatHistory = async () => { + try { + const response = await window.electron?.invoke("clear-chat-history") + if (response.status === 200) { + setMessages([]) + if (chatMode === "text") setInput("") + toast.success("Chat history cleared.") + } else { + toast.error("Failed to clear chat history via IPC.") + } + } catch (error) { + toast.error("Error clearing chat history.") + } + } + + const reinitiateServer = async () => { + setServerStatus(false) + toast.loading("Restarting server...") + try { + const response = await window.electron?.invoke("reinitiate-server") + if (response.status === 200) { + toast.dismiss() + toast.success("Server restarted. Fetching history...") + if (chatMode === "text") { + await fetchChatHistory() + } + } else { + toast.dismiss() + toast.error("Failed to restart server.") + } + } catch (error) { + toast.dismiss() + toast.error("Error restarting the server.") + } finally { + setServerStatus(true) + } + } + + // --- Effects --- + // Initial setup effect + useEffect(() => { + console.log("ChatPage: Initial Mount Effect - chatMode:", chatMode) + fetchUserDetails() + fetchCurrentModel() + + // ADDED: Fetch audio input devices on mount directly using navigator + const getDevices = async () => { + try { + if ( + !navigator.mediaDevices || + !navigator.mediaDevices.enumerateDevices + ) { + console.warn("ChatPage: enumerateDevices() not supported.") + setAudioInputDevices([]) + return + } + const devices = await navigator.mediaDevices.enumerateDevices() + const audioInputDevices = devices.filter( + (device) => device.kind === "audioinput" + ) + + if (audioInputDevices.length > 0) { + console.log( + "ChatPage: Fetched audio devices:", + audioInputDevices + ) + setAudioInputDevices( + audioInputDevices.map((d) => ({ + // Store only needed info + deviceId: d.deviceId, + label: + d.label || + `Microphone ${audioInputDevices.indexOf(d) + 1}` + })) + ) + // Set default selected device only if not already set + if (!selectedAudioInputDevice) { + const defaultDevice = + audioInputDevices.find( + (d) => d.deviceId === "default" + ) || audioInputDevices[0] + if (defaultDevice) { + setSelectedAudioInputDevice(defaultDevice.deviceId) + console.log( + "ChatPage: Set default audio input device:", + defaultDevice.deviceId + ) + } + } + } else { + console.log("ChatPage: No audio input devices found.") + setAudioInputDevices([]) + setSelectedAudioInputDevice("") + } + } catch (error) { + console.error("ChatPage: Error fetching audio devices:", error) + toast.error("Could not get microphone list.") + } + } + getDevices() // Call the function + + // Fetch history only if starting in text mode + if (chatMode === "text") { + console.log( + "ChatPage: Initial Mount - Fetching history (text mode)." + ) + fetchChatHistory() + } else { + console.log( + "ChatPage: Initial Mount - Setting isLoading false (voice mode)." + ) + setIsLoading(false) // Ensure loader is off if starting in voice mode + } + setupIpcListeners() + + // Cleanup + return () => { + console.log("ChatPage: Unmount Cleanup") + eventListenersAdded.current = false + // REMOVED: Timer cleanup + if ( + backgroundCircleProviderRef.current && + connectionStatus !== "disconnected" + ) { + console.log("ChatPage: Disconnecting voice on unmount") + backgroundCircleProviderRef.current.disconnect() + } + if (ringtoneAudioRef.current) { + ringtoneAudioRef.current.pause() + ringtoneAudioRef.current.currentTime = 0 + } + if (connectedAudioRef.current) { + connectedAudioRef.current.pause() + connectedAudioRef.current.currentTime = 0 + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // Effect for scrolling and fetching history on mode switch + useEffect(() => { + console.log( + "ChatPage: Mode/Messages Effect - chatMode:", + chatMode, + "isLoading:", + isLoading + ) + if (chatMode === "text") { + if (chatEndRef.current) { + chatEndRef.current.scrollIntoView({ behavior: "smooth" }) + } + // Fetch logic + if (!isLoading) { + console.log( + "ChatPage: Switched to text mode, fetching history." + ) + fetchChatHistory() + } + } + }, [chatMode]) // Correct dependencies + + // Effect for textarea resize + useEffect(() => { + if (chatMode === "text" && textareaRef.current) { + handleInputChange({ target: textareaRef.current }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [chatMode, input]) + + // --- Component Return (JSX) --- + return ( +
+ + + + + {/* Main Content Area */} +
+ {/* Top Right Buttons */} +
+ +
+ + {/* Conditional Content Container */} +
+ {isLoading ? ( // Simple loading check +
+ +
+ ) : chatMode === "text" ? ( + // --- Text Chat UI --- +
+ {/* Message Display */} +
+ {messages.length === 0 && !thinking ? ( +
+ {" "} +

+ {" "} + Send a message to start{" "} +

{" "} +
+ ) : ( + messages.map((msg) => ( +
+ {" "} + {msg.type === "tool_result" ? ( + + ) : ( + + )}{" "} +
+ )) + )} + {thinking && ( +
+ {" "} +
+ {" "} +
{" "} +
{" "} +
{" "} +
{" "} +
+ )} +
+
+ {/* Input Area */} +
+

+ Check out our{" "} + + docs + {" "} + to see what Sentient can do. +

+
+