diff --git a/src/interface/web/app/agents/page.tsx b/src/interface/web/app/agents/page.tsx index e2e16071..5e68a0b9 100644 --- a/src/interface/web/app/agents/page.tsx +++ b/src/interface/web/app/agents/page.tsx @@ -278,7 +278,6 @@ export default function Agents() {
{ const storedMessage = localStorage.getItem("message"); if (storedMessage) { - setMessage(storedMessage); + setProcessingMessage(true); + props.setQueryToProcess(storedMessage); } }, []); @@ -97,82 +98,19 @@ function ChatBodyData(props: ChatBodyDataProps) { ); } + export default function Chat() { const [chatOptionsData, setChatOptionsData] = useState(null); const [isLoading, setLoading] = useState(true); const [title, setTitle] = useState('Khoj AI - Chat'); const [conversationId, setConversationID] = useState(null); - const [chatWS, setChatWS] = useState(null); const [messages, setMessages] = useState([]); const [queryToProcess, setQueryToProcess] = useState(''); const [processQuerySignal, setProcessQuerySignal] = useState(false); const [uploadedFiles, setUploadedFiles] = useState([]); const [isMobileWidth, setIsMobileWidth] = useState(false); - + const locationData = useIPLocationData(); const authenticatedData = useAuthenticatedData(); - welcomeConsole(); - - const handleWebSocketMessage = (event: MessageEvent) => { - let chunk = event.data; - let currentMessage = messages.find(message => !message.completed); - if (!currentMessage) { - console.error("No current message found"); - return; - } - - // Process WebSocket streamed data - if (chunk === "start_llm_response") { - console.log("Started streaming", new Date()); - } else if (chunk === "end_llm_response") { - currentMessage.completed = true; - } else { - // Get the current message - // Process and update state with the new message - if (chunk.includes("application/json")) { - chunk = JSON.parse(chunk); - } - - const contentType = chunk["content-type"]; - if (contentType === "application/json") { - try { - if (chunk.image || chunk.detail) { - let responseWithReference = handleImageResponse(chunk); - console.log("Image response", responseWithReference); - if (responseWithReference.response) currentMessage.rawResponse = responseWithReference.response; - if (responseWithReference.online) currentMessage.onlineContext = responseWithReference.online; - if (responseWithReference.context) currentMessage.context = responseWithReference.context; - } else if (chunk.type == "status") { - currentMessage.trainOfThought.push(chunk.message); - } else if (chunk.type == "rate_limit") { - console.log("Rate limit message", chunk); - currentMessage.rawResponse = chunk.message; - } else { - console.log("any message", chunk); - } - } catch (error) { - console.error("Error processing message", error); - currentMessage.completed = true; - } finally { - // no-op - } - } else { - // Update the current message with the new chunk - if (chunk && chunk.includes("### compiled references:")) { - let responseWithReference = handleCompiledReferences(chunk, ""); - currentMessage.rawResponse += responseWithReference.response; - - if (responseWithReference.response) currentMessage.rawResponse = responseWithReference.response; - if (responseWithReference.online) currentMessage.onlineContext = responseWithReference.online; - if (responseWithReference.context) currentMessage.context = responseWithReference.context; - } else { - // If the chunk is not a JSON object, just display it as is - currentMessage.rawResponse += chunk; - } - } - }; - // Update the state with the new message, currentMessage - setMessages([...messages]); - } useEffect(() => { fetch('/api/chat/options') @@ -189,6 +127,8 @@ export default function Chat() { return; }); + welcomeConsole(); + setIsMobileWidth(window.innerWidth < 786); window.addEventListener('resize', () => { @@ -198,19 +138,7 @@ export default function Chat() { }, []); useEffect(() => { - if (chatWS) { - chatWS.onmessage = handleWebSocketMessage; - } - }, [chatWS, messages]); - - //same as ChatBodyData for local storage message - useEffect(() => { - const storedMessage = localStorage.getItem("message"); - setQueryToProcess(storedMessage || ''); - }, []); - - useEffect(() => { - if (chatWS && queryToProcess) { + if (queryToProcess) { const newStreamMessage: StreamMessage = { rawResponse: "", trainOfThought: [], @@ -221,53 +149,82 @@ export default function Chat() { rawQuery: queryToProcess || "", }; setMessages(prevMessages => [...prevMessages, newStreamMessage]); - - if (chatWS.readyState === WebSocket.OPEN) { - chatWS.send(queryToProcess); - setProcessQuerySignal(true); - } - else { - console.error("WebSocket is not open. ReadyState:", chatWS.readyState); - } - - setQueryToProcess(''); + setProcessQuerySignal(true); } - }, [queryToProcess, chatWS]); + }, [queryToProcess]); useEffect(() => { - if (processQuerySignal && chatWS && chatWS.readyState === WebSocket.OPEN) { - setProcessQuerySignal(false); - chatWS.onmessage = handleWebSocketMessage; - chatWS.send(queryToProcess); - localStorage.removeItem("message"); + if (processQuerySignal) { + chat(); } - }, [processQuerySignal, chatWS]); + }, [processQuerySignal]); - useEffect(() => { - const setupWebSocketConnection = async () => { - if (conversationId && (!chatWS || chatWS.readyState === WebSocket.CLOSED)) { - if (queryToProcess) { - const newWS = await setupWebSocket(conversationId, queryToProcess); - localStorage.removeItem("message"); - setChatWS(newWS); - } - else { - const newWS = await setupWebSocket(conversationId); - setChatWS(newWS); + async function readChatStream(response: Response) { + if (!response.ok) throw new Error(response.statusText); + if (!response.body) throw new Error("Response body is null"); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + const eventDelimiter = '␃🔚␗'; + let buffer = ""; + + // Track context used for chat response + let context: Context[] = []; + let onlineContext: OnlineContext = {}; + + while (true) { + const { done, value } = await reader.read(); + if (done) { + setQueryToProcess(''); + setProcessQuerySignal(false); + break; + } + + const chunk = decoder.decode(value, { stream: true }); + buffer += chunk; + + let newEventIndex; + while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) { + const event = buffer.slice(0, newEventIndex); + buffer = buffer.slice(newEventIndex + eventDelimiter.length); + if (event) { + const currentMessage = messages.find(message => !message.completed); + + if (!currentMessage) { + console.error("No current message found"); + return; + } + + // Track context used for chat response. References are rendered at the end of the chat + ({context, onlineContext} = processMessageChunk(event, currentMessage, context, onlineContext)); + + setMessages([...messages]); } } - }; - setupWebSocketConnection(); - }, [conversationId]); + } + } + + async function chat() { + localStorage.removeItem("message"); + if (!queryToProcess || !conversationId) return; + let chatAPI = `/api/chat?q=${encodeURIComponent(queryToProcess)}&conversation_id=${conversationId}&stream=true&client=web`; + if (locationData) { + chatAPI += `®ion=${locationData.region}&country=${locationData.country}&city=${locationData.city}&timezone=${locationData.timezone}`; + } + + const response = await fetch(chatAPI); + try { + await readChatStream(response); + } catch (err) { + console.log(err); + } + } const handleConversationIdChange = (newConversationId: string) => { setConversationID(newConversationId); }; - if (isLoading) { - return ; - } - + if (isLoading) return ; return (
@@ -276,7 +233,6 @@ export default function Chat() {
void, - setInitialResponse: (response: string) => void, - setInitialReferences: (references: ResponseWithReferences) => void) { - setIsLoading(true); - // Send a message to the chat server to verify the fact - const chatURL = "/api/chat"; - const apiURL = `${chatURL}?q=${encodeURIComponent(message)}&client=web&stream=true&conversation_id=${conversationId}`; - try { - const response = await fetch(apiURL); - if (!response.body) throw new Error("No response body found"); - - const reader = response.body?.getReader(); - let decoder = new TextDecoder(); - let result = ""; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - let chunk = decoder.decode(value, { stream: true }); - - if (chunk.includes("### compiled references:")) { - const references = handleCompiledReferences(chunk, result); - if (references.response) { - result = references.response; - setInitialResponse(references.response); - setInitialReferences(references); - } - } else { - result += chunk; - setInitialResponse(result); +export function convertMessageChunkToJson(chunk: string): MessageChunk { + if (chunk.startsWith("{") && chunk.endsWith("}")) { + try { + const jsonChunk = JSON.parse(chunk); + if (!jsonChunk.type) { + return { + type: "message", + data: jsonChunk + }; } + return jsonChunk; + } catch (error) { + return { + type: "message", + data: chunk + }; } - } catch (error) { - console.error("Error verifying statement: ", error); - } finally { - setIsLoading(false); + } else if (chunk.length > 0) { + return { + type: "message", + data: chunk + }; + } else { + return { + type: "message", + data: "" + }; } } -export const setupWebSocket = async (conversationId: string, initialMessage?: string) => { - const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - - const host = process.env.NODE_ENV === 'production' ? window.location.host : 'localhost:42110'; - - let webSocketUrl = `${wsProtocol}//${host}/api/chat/ws`; - - if (conversationId === null) { - return null; +function handleJsonResponse(chunkData: any) { + const jsonData = chunkData as any; + if (jsonData.image || jsonData.detail) { + let responseWithReference = handleImageResponse(chunkData, true); + if (responseWithReference.response) return responseWithReference.response; + } else if (jsonData.response) { + return jsonData.response; + } else { + throw new Error("Invalid JSON response"); } +} - if (conversationId) { - webSocketUrl += `?conversation_id=${conversationId}`; - } +export function processMessageChunk( + rawChunk: string, + currentMessage: StreamMessage, + context: Context[] = [], + onlineContext: OnlineContext = {}): { context: Context[], onlineContext: OnlineContext } { - const chatWS = new WebSocket(webSocketUrl); + const chunk = convertMessageChunkToJson(rawChunk); - chatWS.onopen = () => { - console.log('WebSocket connection established'); - if (initialMessage) { - chatWS.send(initialMessage); + if (!currentMessage || !chunk || !chunk.type) return {context, onlineContext}; + + if (chunk.type === "status") { + console.log(`status: ${chunk.data}`); + const statusMessage = chunk.data as string; + currentMessage.trainOfThought.push(statusMessage); + } else if (chunk.type === "references") { + const references = chunk.data as RawReferenceData; + + if (references.context) context = references.context; + if (references.onlineContext) onlineContext = references.onlineContext; + return {context, onlineContext} + } else if (chunk.type === "message") { + const chunkData = chunk.data; + if (chunkData !== null && typeof chunkData === 'object') { + currentMessage.rawResponse += handleJsonResponse(chunkData); + } else if (typeof chunkData === 'string' && chunkData.trim()?.startsWith("{") && chunkData.trim()?.endsWith("}")) { + try { + const jsonData = JSON.parse(chunkData.trim()); + currentMessage.rawResponse += handleJsonResponse(jsonData); + } catch (e) { + currentMessage.rawResponse += JSON.stringify(chunkData); + } + } else { + currentMessage.rawResponse += chunkData; } - }; + } else if (chunk.type === "start_llm_response") { + console.log(`Started streaming: ${new Date()}`); + } else if (chunk.type === "end_llm_response") { + console.log(`Completed streaming: ${new Date()}`); - chatWS.onmessage = (event) => { - console.log(event.data); - }; + // Append any references after all the data has been streamed + if (onlineContext) currentMessage.onlineContext = onlineContext; + if (context) currentMessage.context = context; - chatWS.onerror = (error) => { - console.error('WebSocket error: ', error); - }; + // Mark current message streaming as completed + currentMessage.completed = true; + } + return {context, onlineContext}; +} - chatWS.onclose = () => { - console.log('WebSocket connection closed'); - }; - - return chatWS; -}; - -export function handleImageResponse(imageJson: any) { +export function handleImageResponse(imageJson: any, liveStream: boolean): ResponseWithReferences { let rawResponse = ""; @@ -123,7 +122,7 @@ export function handleImageResponse(imageJson: any) { } else if (imageJson.intentType === "text-to-image-v3") { rawResponse = `![](data:image/webp;base64,${imageJson.image})`; } - if (inferredQuery) { + if (inferredQuery && !liveStream) { rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`; } } diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index a6fde5c9..2505dbf7 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -17,6 +17,7 @@ import { Lightbulb } from "@phosphor-icons/react"; import ProfileCard from '../profileCard/profileCard'; import { getIconFromIconName } from '@/app/common/iconUtils'; import { AgentData } from '@/app/agents/page'; +import React from 'react'; interface ChatResponse { status: string; @@ -120,7 +121,6 @@ export default function ChatHistory(props: ChatHistoryProps) { }, [props.conversationId]); useEffect(() => { - console.log(props.incomingMessages); if (props.incomingMessages) { const lastMessage = props.incomingMessages[props.incomingMessages.length - 1]; if (lastMessage && !lastMessage.completed) { @@ -195,7 +195,7 @@ export default function ChatHistory(props: ChatHistoryProps) { setFetchingData(false); } else { if (chatData.response.agent && chatData.response.conversation_id) { - const chatMetadata ={ + const chatMetadata = { chat: [], agent: chatData.response.agent, conversation_id: chatData.response.conversation_id, @@ -256,7 +256,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
- {fetchingData && } + {fetchingData && }
{(data && data.chat) && data.chat.map((chatMessage, index) => ( { return ( - <> + + - + ) }) } diff --git a/src/interface/web/app/components/chatMessage/chatMessage.module.css b/src/interface/web/app/components/chatMessage/chatMessage.module.css index 592e30af..809acc2e 100644 --- a/src/interface/web/app/components/chatMessage/chatMessage.module.css +++ b/src/interface/web/app/components/chatMessage/chatMessage.module.css @@ -123,6 +123,11 @@ div.trainOfThought.primary p { color: inherit; } +div.trainOfThoughtElement { + display: grid; + grid-template-columns: auto 1fr; +} + @media screen and (max-width: 768px) { div.youfullHistory { max-width: 90%; diff --git a/src/interface/web/app/components/chatMessage/chatMessage.tsx b/src/interface/web/app/components/chatMessage/chatMessage.tsx index 6b05fdf9..71a5b29e 100644 --- a/src/interface/web/app/components/chatMessage/chatMessage.tsx +++ b/src/interface/web/app/components/chatMessage/chatMessage.tsx @@ -10,9 +10,9 @@ import 'katex/dist/katex.min.css'; import { TeaserReferencesSection, constructAllReferences } from '../referencePanel/referencePanel'; -import { ThumbsUp, ThumbsDown, Copy, Brain, Cloud, Folder, Book, Aperture, SpeakerHigh, MagnifyingGlass, Pause } from '@phosphor-icons/react'; +import { ThumbsUp, ThumbsDown, Copy, Brain, Cloud, Folder, Book, Aperture, SpeakerHigh, MagnifyingGlass, Pause, Palette } from '@phosphor-icons/react'; -import * as DomPurify from 'dompurify'; +import DOMPurify from 'dompurify'; import { InlineLoading } from '../loading/loading'; import { convertColorToTextClass } from '@/app/common/colorUtils'; import { AgentData } from '@/app/agents/page'; @@ -33,6 +33,10 @@ export interface Context { file: string; } +export interface OnlineContext { + [key: string]: OnlineContextData; +} + export interface WebPage { link: string; query: string; @@ -85,11 +89,9 @@ export interface SingleChatMessage { automationId: string; by: string; message: string; - context: Context[]; created: string; - onlineContext: { - [key: string]: OnlineContextData - } + context: Context[]; + onlineContext: OnlineContext; rawQuery?: string; intent?: Intent; agent?: AgentData; @@ -99,9 +101,7 @@ export interface StreamMessage { rawResponse: string; trainOfThought: string[]; context: Context[]; - onlineContext: { - [key: string]: OnlineContextData - } + onlineContext: OnlineContext; completed: boolean; rawQuery: string; timestamp: string; @@ -180,10 +180,14 @@ function chooseIconFromHeader(header: string, iconColor: string) { return ; } - if (compareHeader.includes("summary") || compareHeader.includes("summarize")) { + if (compareHeader.includes("summary") || compareHeader.includes("summarize") || compareHeader.includes("enhanc")) { return ; } + if (compareHeader.includes("paint")) { + return ; + } + return ; } @@ -193,9 +197,9 @@ export function TrainOfThought(props: TrainOfThoughtProps) { let header = extractedHeader ? extractedHeader[1] : ""; const iconColor = props.primary ? convertColorToTextClass(props.agentColor) : 'text-gray-500'; const icon = chooseIconFromHeader(header, iconColor); - let markdownRendered = DomPurify.sanitize(md.render(props.message)); + let markdownRendered = DOMPurify.sanitize(md.render(props.message)); return ( -
+
{icon}
@@ -241,7 +245,7 @@ export default function ChatMessage(props: ChatMessageProps) { .replace(/LEFTBRACKET/g, '\\[').replace(/RIGHTBRACKET/g, '\\]'); // Sanitize and set the rendered markdown - setMarkdownRendered(DomPurify.sanitize(markdownRendered)); + setMarkdownRendered(DOMPurify.sanitize(markdownRendered)); }, [props.chatMessage.message]); useEffect(() => { diff --git a/src/interface/web/app/components/logo/khogLogo.tsx b/src/interface/web/app/components/logo/khogLogo.tsx index a53e122a..c2cdc8c1 100644 --- a/src/interface/web/app/components/logo/khogLogo.tsx +++ b/src/interface/web/app/components/logo/khogLogo.tsx @@ -1,4 +1,4 @@ -export function KhojLogo() { +export function KhojLogoType() { return ( @@ -29,3 +29,31 @@ export function KhojLogo() { ); } + +export function KhojLogo() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/interface/web/app/components/navMenu/navMenu.tsx b/src/interface/web/app/components/navMenu/navMenu.tsx index 0f8d78a8..386a3afc 100644 --- a/src/interface/web/app/components/navMenu/navMenu.tsx +++ b/src/interface/web/app/components/navMenu/navMenu.tsx @@ -23,7 +23,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Moon, Sun, UserCircle, User, Robot, MagnifyingGlass, Question, GearFine, ArrowRight } from '@phosphor-icons/react'; -import { KhojLogo } from '../logo/khogLogo'; +import { KhojLogoType } from '../logo/khogLogo'; interface NavMenuProps { @@ -99,7 +99,7 @@ export default function NavMenu(props: NavMenuProps) { { !displayTitle && props.showLogo && - + }
diff --git a/src/interface/web/app/components/referencePanel/referencePanel.tsx b/src/interface/web/app/components/referencePanel/referencePanel.tsx index 17632bea..5fc70406 100644 --- a/src/interface/web/app/components/referencePanel/referencePanel.tsx +++ b/src/interface/web/app/components/referencePanel/referencePanel.tsx @@ -11,7 +11,7 @@ const md = new markdownIt({ typographer: true }); -import { Context, WebPage, OnlineContextData } from "../chatMessage/chatMessage"; +import { Context, WebPage, OnlineContext } from "../chatMessage/chatMessage"; import { Card } from "@/components/ui/card"; import { @@ -23,7 +23,7 @@ import { SheetTrigger, } from "@/components/ui/sheet"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import * as DomPurify from 'dompurify'; +import DOMPurify from 'dompurify'; interface NotesContextReferenceData { title: string; @@ -36,7 +36,7 @@ interface NotesContextReferenceCardProps extends NotesContextReferenceData { function NotesContextReferenceCard(props: NotesContextReferenceCardProps) { - const snippet = props.showFullContent ? DomPurify.sanitize(md.render(props.content)) : DomPurify.sanitize(props.content); + const snippet = props.showFullContent ? DOMPurify.sanitize(md.render(props.content)) : DOMPurify.sanitize(props.content); const [isHovering, setIsHovering] = useState(false); return ( @@ -161,7 +161,7 @@ function GenericOnlineReferenceCard(props: OnlineReferenceCardProps) { ) } -export function constructAllReferences(contextData: Context[], onlineData: { [key: string]: OnlineContextData }) { +export function constructAllReferences(contextData: Context[], onlineData: OnlineContext) { const onlineReferences: OnlineReferenceData[] = []; const contextReferences: NotesContextReferenceData[] = []; diff --git a/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx b/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx index cd0f8bea..b02ea754 100644 --- a/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx +++ b/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx @@ -5,10 +5,8 @@ import styles from "./sidePanel.module.css"; import { useEffect, useState } from "react"; import { UserProfile, useAuthenticatedData } from "@/app/common/auth"; -import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import Link from "next/link"; import useSWR from "swr"; -import Image from "next/image"; import { Command, @@ -72,13 +70,12 @@ import { import { Pencil, Trash, Share } from "@phosphor-icons/react"; -import { Button, buttonVariants } from "@/components/ui/button"; +import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; import { modifyFileFilterForConversation } from "@/app/common/chatFunctions"; import { ScrollAreaScrollbar } from "@radix-ui/react-scroll-area"; -import { KhojLogo } from "../logo/khogLogo"; +import { KhojLogo, KhojLogoType } from "@/app/components/logo/khogLogo"; // Define a fetcher function const fetcher = (url: string) => fetch(url).then((res) => res.json()); @@ -320,7 +317,6 @@ function FilesMenu(props: FilesMenuProps) { } interface SessionsAndFilesProps { - webSocketConnected?: boolean; setEnabled: (enabled: boolean) => void; subsetOrganizedData: GroupedChatHistory | null; organizedData: GroupedChatHistory | null; @@ -591,12 +587,6 @@ function ChatSessionsModal({ data, showSidePanel }: ChatSessionsModalProps) { ); } -interface UserProfileProps { - userProfile: UserProfile; - webSocketConnected?: boolean; - collapsed: boolean; -} - const fetchChatHistory = async (url: string) => { const response = await fetch(url, { method: 'GET', @@ -618,7 +608,6 @@ export const useChatSessionsFetchRequest = (url: string) => { }; interface SidePanelProps { - webSocketConnected?: boolean; conversationId: string | null; uploadedFiles: string[]; isMobileWidth: boolean; @@ -674,7 +663,7 @@ export default function SidePanel(props: SidePanelProps) {
- + {props.isMobileWidth && || } { authenticatedData && props.isMobileWidth ? @@ -691,7 +680,6 @@ export default function SidePanel(props: SidePanelProps) {
([]); const [selectedAgent, setSelectedAgent] = useState("khoj"); + const [agentIcons, setAgentIcons] = useState([]); + const [agents, setAgents] = useState([]); const agentsFetcher = () => window.fetch('/api/agents').then(res => res.json()).catch(err => console.log(err)); - const { data, error } = useSWR('agents', agentsFetcher, { revalidateOnFocus: false }); + const { data: agentsData, error } = useSWR('agents', agentsFetcher, { revalidateOnFocus: false }); function shuffleAndSetOptions() { const shuffled = [...suggestionsData].sort(() => 0.5 - Math.random()); @@ -82,6 +84,28 @@ function ChatBodyData(props: ChatBodyDataProps) { } }, [props.chatOptionsData]); + useEffect(() => { + const nSlice = props.isMobileWidth ? 3 : 4; + + const shuffledAgents = agentsData ? [...agentsData].sort(() => 0.5 - Math.random()) : []; + + const agents = agentsData ? [agentsData[0]] : []; // Always add the first/default agent. + + shuffledAgents.slice(0, nSlice - 1).forEach(agent => { + if (!agents.find(a => a.slug === agent.slug)) { + agents.push(agent); + } + }); + + setAgents(agents); + + //generate colored icons for the selected agents + const agentIcons = agents.map( + agent => getIconFromIconName(agent.icon, agent.color) || {agent.name} + ); + setAgentIcons(agentIcons); + }, [agentsData]); + function shuffleSuggestionsCards() { shuffleAndSetOptions(); } @@ -109,24 +133,6 @@ function ChatBodyData(props: ChatBodyDataProps) { }; }, [selectedAgent, message]); - const nSlice = props.isMobileWidth ? 3 : 4; - - const shuffledAgents = data ? [...data].sort(() => 0.5 - Math.random()) : []; - - const agents = data ? [data[0]] : []; // Always add the first/default agent. - - shuffledAgents.slice(0, nSlice - 1).forEach(agent => { - if (!agents.find(a => a.slug === agent.slug)) { - agents.push(agent); - } - }); - - - //generate colored icons for the selected agents - const agentIcons = agents.map( - agent => getIconFromIconName(agent.icon, agent.color) || {agent.name} - ); - function fillArea(link: string, type: string, prompt: string) { if (!link) { let message_str = ""; @@ -298,7 +304,6 @@ export default function Home() {
{ +const useApiKeys = () => { const [apiKeys, setApiKeys] = useState([]); const { toast } = useToast(); @@ -649,7 +649,6 @@ export default function SettingsView() {
(undefined); - const [chatWS, setChatWS] = useState(null); const [messages, setMessages] = useState([]); const [queryToProcess, setQueryToProcess] = useState(''); const [processQuerySignal, setProcessQuerySignal] = useState(false); @@ -103,77 +102,9 @@ export default function SharedChat() { const [isMobileWidth, setIsMobileWidth] = useState(false); const [paramSlug, setParamSlug] = useState(undefined); + const locationData = useIPLocationData(); const authenticatedData = useAuthenticatedData(); - welcomeConsole(); - - const handleWebSocketMessage = (event: MessageEvent) => { - let chunk = event.data; - - let currentMessage = messages.find(message => !message.completed); - - if (!currentMessage) { - console.error("No current message found"); - return; - } - - // Process WebSocket streamed data - if (chunk === "start_llm_response") { - console.log("Started streaming", new Date()); - } else if (chunk === "end_llm_response") { - currentMessage.completed = true; - } else { - // Get the current message - // Process and update state with the new message - if (chunk.includes("application/json")) { - chunk = JSON.parse(chunk); - } - - const contentType = chunk["content-type"]; - if (contentType === "application/json") { - try { - if (chunk.image || chunk.detail) { - let responseWithReference = handleImageResponse(chunk); - console.log("Image response", responseWithReference); - if (responseWithReference.response) currentMessage.rawResponse = responseWithReference.response; - if (responseWithReference.online) currentMessage.onlineContext = responseWithReference.online; - if (responseWithReference.context) currentMessage.context = responseWithReference.context; - } else if (chunk.type == "status") { - currentMessage.trainOfThought.push(chunk.message); - } else if (chunk.type == "rate_limit") { - console.log("Rate limit message", chunk); - currentMessage.rawResponse = chunk.message; - } else { - console.log("any message", chunk); - } - } catch (error) { - console.error("Error processing message", error); - currentMessage.completed = true; - } finally { - // no-op - } - - } else { - // Update the current message with the new chunk - if (chunk && chunk.includes("### compiled references:")) { - let responseWithReference = handleCompiledReferences(chunk, ""); - currentMessage.rawResponse += responseWithReference.response; - - if (responseWithReference.response) currentMessage.rawResponse = responseWithReference.response; - if (responseWithReference.online) currentMessage.onlineContext = responseWithReference.online; - if (responseWithReference.context) currentMessage.context = responseWithReference.context; - } else { - // If the chunk is not a JSON object, just display it as is - currentMessage.rawResponse += chunk; - } - - } - }; - // Update the state with the new message, currentMessage - setMessages([...messages]); - } - - useEffect(() => { fetch('/api/chat/options') .then(response => response.json()) @@ -189,6 +120,8 @@ export default function SharedChat() { return; }); + welcomeConsole(); + setIsMobileWidth(window.innerWidth < 786); window.addEventListener('resize', () => { @@ -201,6 +134,7 @@ export default function SharedChat() { useEffect(() => { if (queryToProcess && !conversationId) { + // If the user has not yet started conversing in the chat, create a new conversation fetch(`/api/chat/share/fork?public_conversation_slug=${paramSlug}`, { method: 'POST', headers: { @@ -219,7 +153,7 @@ export default function SharedChat() { } - if (chatWS && queryToProcess) { + if (queryToProcess) { // Add a new object to the state const newStreamMessage: StreamMessage = { rawResponse: "", @@ -232,40 +166,77 @@ export default function SharedChat() { } setMessages(prevMessages => [...prevMessages, newStreamMessage]); setProcessQuerySignal(true); - } else { - if (!chatWS) { - console.error("No WebSocket connection available"); - } - if (!queryToProcess) { - console.error("No query to process"); - } } }, [queryToProcess]); useEffect(() => { - if (processQuerySignal && chatWS) { - setProcessQuerySignal(false); - chatWS.onmessage = handleWebSocketMessage; - chatWS?.send(queryToProcess); + if (processQuerySignal) { + chat(); } }, [processQuerySignal]); - useEffect(() => { - if (chatWS) { - chatWS.onmessage = handleWebSocketMessage; + + async function readChatStream(response: Response) { + if (!response.ok) throw new Error(response.statusText); + if (!response.body) throw new Error("Response body is null"); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + const eventDelimiter = '␃🔚␗'; + let buffer = ""; + + while (true) { + + const { done, value } = await reader.read(); + if (done) { + setQueryToProcess(''); + setProcessQuerySignal(false); + break; + } + + const chunk = decoder.decode(value, { stream: true }); + + buffer += chunk; + + let newEventIndex; + while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) { + const event = buffer.slice(0, newEventIndex); + buffer = buffer.slice(newEventIndex + eventDelimiter.length); + if (event) { + const currentMessage = messages.find(message => !message.completed); + + if (!currentMessage) { + console.error("No current message found"); + return; + } + + processMessageChunk(event, currentMessage); + + setMessages([...messages]); + } + } + } - }, [chatWS]); + } + + async function chat() { + if (!queryToProcess || !conversationId) return; + let chatAPI = `/api/chat?q=${encodeURIComponent(queryToProcess)}&conversation_id=${conversationId}&stream=true&client=web`; + if (locationData) { + chatAPI += `®ion=${locationData.region}&country=${locationData.country}&city=${locationData.city}&timezone=${locationData.timezone}`; + } + + const response = await fetch(chatAPI); + try { + await readChatStream(response); + } catch (err) { + console.log(err); + } + } useEffect(() => { (async () => { if (conversationId) { - const newWS = await setupWebSocket(conversationId, queryToProcess); - if (!newWS) { - console.error("No WebSocket connection available"); - return; - } - setChatWS(newWS); - // Add a new object to the state const newStreamMessage: StreamMessage = { rawResponse: "", @@ -276,6 +247,7 @@ export default function SharedChat() { timestamp: (new Date()).toISOString(), rawQuery: queryToProcess || "", } + setProcessQuerySignal(true); setMessages(prevMessages => [...prevMessages, newStreamMessage]); } })(); @@ -301,7 +273,6 @@ export default function SharedChat() {
--> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/khoj/processor/tools/online_search.py b/src/khoj/processor/tools/online_search.py index c087de70..caf800b0 100644 --- a/src/khoj/processor/tools/online_search.py +++ b/src/khoj/processor/tools/online_search.py @@ -68,7 +68,7 @@ async def search_online( logger.info(f"🌐 Searching the Internet for {list(subqueries)}") if send_status_func: subqueries_str = "\n- " + "\n- ".join(list(subqueries)) - async for event in send_status_func(f"**🌐 Searching the Internet for**: {subqueries_str}"): + async for event in send_status_func(f"**Searching the Internet for**: {subqueries_str}"): yield {ChatEvent.STATUS: event} with timer(f"Internet searches for {list(subqueries)} took", logger): @@ -92,7 +92,7 @@ async def search_online( logger.info(f"🌐👀 Reading web pages at: {list(webpage_links)}") if send_status_func: webpage_links_str = "\n- " + "\n- ".join(list(webpage_links)) - async for event in send_status_func(f"**📖 Reading web pages**: {webpage_links_str}"): + async for event in send_status_func(f"**Reading web pages**: {webpage_links_str}"): yield {ChatEvent.STATUS: event} tasks = [read_webpage_and_extract_content(subquery, link, content) for link, subquery, content in webpages] results = await asyncio.gather(*tasks) @@ -131,14 +131,14 @@ async def read_webpages( "Infer web pages to read from the query and extract relevant information from them" logger.info(f"Inferring web pages to read") if send_status_func: - async for event in send_status_func(f"**🧐 Inferring web pages to read**"): + async for event in send_status_func(f"**Inferring web pages to read**"): yield {ChatEvent.STATUS: event} urls = await infer_webpage_urls(query, conversation_history, location) logger.info(f"Reading web pages at: {urls}") if send_status_func: webpage_links_str = "\n- " + "\n- ".join(list(urls)) - async for event in send_status_func(f"**📖 Reading web pages**: {webpage_links_str}"): + async for event in send_status_func(f"**Reading web pages**: {webpage_links_str}"): yield {ChatEvent.STATUS: event} tasks = [read_webpage_and_extract_content(query, url) for url in urls] results = await asyncio.gather(*tasks) diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index fa37e1f5..edb1c99a 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -603,6 +603,8 @@ async def chat( metadata=chat_metadata, ) + conversation_commands = [get_conversation_command(query=q, any_references=True)] + conversation = await ConversationAdapters.aget_conversation_by_user( user, client_application=request.user.client_app, conversation_id=conversation_id, title=title ) @@ -624,10 +626,6 @@ async def chat( return user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - conversation_commands = [get_conversation_command(query=q, any_references=True)] - - async for result in send_event(ChatEvent.STATUS, f"**Understanding Query**: {q}"): - yield result meta_log = conversation.conversation_log is_automated_task = conversation_commands == [ConversationCommand.AutomatedTask] diff --git a/src/khoj/routers/helpers.py b/src/khoj/routers/helpers.py index 0223ab8c..412113a6 100644 --- a/src/khoj/routers/helpers.py +++ b/src/khoj/routers/helpers.py @@ -816,7 +816,7 @@ async def text_to_image( ) if send_status_func: - async for event in send_status_func(f"**🖼️ Painting using Enhanced Prompt**:\n{improved_image_prompt}"): + async for event in send_status_func(f"**Painting to Imagine**:\n{improved_image_prompt}"): yield {ChatEvent.STATUS: event} if text_to_image_config.model_type == TextToImageModelConfig.ModelType.OPENAI: