mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-04 05:39:06 +00:00
Make most major changes for an updated chat UI (#843)
- Updated references panel - Use subtle coloring for chat cards - Chat streaming with train of thought - Side panel with limited sessions, expandable - Manage conversation file filters easily from the side panel - Updated nav menu, easily go to agents/automations/profile - Upload data from the chat UI (on click attachment icon) - Slash command pop-up menu, scrollable and selectable - Dark mode-enabled - Mostly mobile friendly
This commit is contained in:
@@ -6,73 +6,193 @@ import React, { Suspense, useEffect, useState } from 'react';
|
||||
import SuggestionCard from '../components/suggestions/suggestionCard';
|
||||
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
||||
import ChatHistory from '../components/chatHistory/chatHistory';
|
||||
import { SingleChatMessage } from '../components/chatMessage/chatMessage';
|
||||
import NavMenu from '../components/navMenu/navMenu';
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import ReferencePanel, { hasValidReferences } from '../components/referencePanel/referencePanel';
|
||||
import Loading from '../components/loading/loading';
|
||||
|
||||
import { handleCompiledReferences, handleImageResponse, setupWebSocket } from '../common/chatFunctions';
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
interface ChatOptions {
|
||||
[key: string]: string
|
||||
}
|
||||
import { StreamMessage } from '../components/chatMessage/chatMessage';
|
||||
import { welcomeConsole } from '../common/utils';
|
||||
import ChatInputArea, { ChatOptions } from '../components/chatInputArea/chatInputArea';
|
||||
import { useAuthenticatedData } from '../common/auth';
|
||||
|
||||
|
||||
const styleClassOptions = ['pink', 'blue', 'green', 'yellow', 'purple'];
|
||||
|
||||
interface ChatBodyDataProps {
|
||||
chatOptionsData: ChatOptions | null;
|
||||
setTitle: (title: string) => void;
|
||||
onConversationIdChange?: (conversationId: string) => void;
|
||||
setQueryToProcess: (query: string) => void;
|
||||
streamedMessages: StreamMessage[];
|
||||
setUploadedFiles: (files: string[]) => void;
|
||||
isMobileWidth?: boolean;
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
function ChatBodyData({ chatOptionsData }: { chatOptionsData: ChatOptions | null }) {
|
||||
const searchParams = useSearchParams();
|
||||
const conversationId = searchParams.get('conversationId');
|
||||
const [showReferencePanel, setShowReferencePanel] = useState(true);
|
||||
const [referencePanelData, setReferencePanelData] = useState<SingleChatMessage | null>(null);
|
||||
|
||||
function ChatBodyData(props: ChatBodyDataProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const conversationId = searchParams.get('conversationId');
|
||||
const [message, setMessage] = useState('');
|
||||
const [processingMessage, setProcessingMessage] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversationId) {
|
||||
props.onConversationIdChange?.(conversationId);
|
||||
}
|
||||
}, [conversationId, props.onConversationIdChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (message) {
|
||||
setProcessingMessage(true);
|
||||
props.setQueryToProcess(message);
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.streamedMessages &&
|
||||
props.streamedMessages.length > 0 &&
|
||||
props.streamedMessages[props.streamedMessages.length - 1].completed) {
|
||||
|
||||
setProcessingMessage(false);
|
||||
} else {
|
||||
setMessage('');
|
||||
}
|
||||
}, [props.streamedMessages]);
|
||||
|
||||
if (!conversationId) {
|
||||
return (
|
||||
<div className={styles.suggestions}>
|
||||
{chatOptionsData && Object.entries(chatOptionsData).map(([key, value]) => (
|
||||
{props.chatOptionsData && Object.entries(props.chatOptionsData).map(([key, value]) => (
|
||||
<SuggestionCard
|
||||
key={key}
|
||||
title={`/${key}`}
|
||||
body={value}
|
||||
link='#' // replace with actual link if available
|
||||
styleClass={styleClassOptions[Math.floor(Math.random() * styleClassOptions.length)]}
|
||||
/>
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return(
|
||||
<div className={(hasValidReferences(referencePanelData) && showReferencePanel) ? styles.chatBody : styles.chatBodyFull}>
|
||||
<ChatHistory conversationId={conversationId} setReferencePanelData={setReferencePanelData} setShowReferencePanel={setShowReferencePanel} />
|
||||
{
|
||||
(hasValidReferences(referencePanelData) && showReferencePanel) &&
|
||||
<ReferencePanel referencePanelData={referencePanelData} setShowReferencePanel={setShowReferencePanel} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return <h2>🌀 Loading...</h2>;
|
||||
}
|
||||
|
||||
function handleChatInput(e: React.FormEvent<HTMLInputElement>) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
console.log(target.value);
|
||||
return (
|
||||
<>
|
||||
<div className={false ? styles.chatBody : styles.chatBodyFull}>
|
||||
<ChatHistory
|
||||
conversationId={conversationId}
|
||||
setTitle={props.setTitle}
|
||||
pendingMessage={processingMessage ? message : ''}
|
||||
incomingMessages={props.streamedMessages} />
|
||||
</div>
|
||||
<div className={`${styles.inputBox} bg-background align-middle items-center justify-center px-3`}>
|
||||
<ChatInputArea
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
sendMessage={(message) => setMessage(message)}
|
||||
sendDisabled={processingMessage}
|
||||
chatOptionsData={props.chatOptionsData}
|
||||
conversationId={conversationId}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
setUploadedFiles={props.setUploadedFiles} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Chat() {
|
||||
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [title, setTitle] = useState('Khoj AI - Chat');
|
||||
const [conversationId, setConversationID] = useState<string | null>(null);
|
||||
const [chatWS, setChatWS] = useState<WebSocket | null>(null);
|
||||
const [messages, setMessages] = useState<StreamMessage[]>([]);
|
||||
const [queryToProcess, setQueryToProcess] = useState<string>('');
|
||||
const [processQuerySignal, setProcessQuerySignal] = useState(false);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
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())
|
||||
.then((data: ChatOptions) => {
|
||||
setLoading(false);
|
||||
// Render chat options, if any
|
||||
if (data) {
|
||||
console.log(data);
|
||||
setChatOptionsData(data);
|
||||
}
|
||||
})
|
||||
@@ -80,28 +200,92 @@ export default function Chat() {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
}, []);
|
||||
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatWS && queryToProcess) {
|
||||
// Add a new object to the state
|
||||
const newStreamMessage: StreamMessage = {
|
||||
rawResponse: "",
|
||||
trainOfThought: [],
|
||||
context: [],
|
||||
onlineContext: {},
|
||||
completed: false,
|
||||
timestamp: (new Date()).toISOString(),
|
||||
rawQuery: queryToProcess || "",
|
||||
}
|
||||
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);
|
||||
}
|
||||
}, [processQuerySignal]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (conversationId) {
|
||||
const newWS = await setupWebSocket(conversationId);
|
||||
setChatWS(newWS);
|
||||
}
|
||||
})();
|
||||
}, [conversationId]);
|
||||
|
||||
const handleConversationIdChange = (newConversationId: string) => {
|
||||
setConversationID(newConversationId);
|
||||
};
|
||||
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.main + " " + styles.chatLayout}>
|
||||
<title>
|
||||
{title}
|
||||
</title>
|
||||
<div className={styles.sidePanel}>
|
||||
<SidePanel />
|
||||
<SidePanel
|
||||
webSocketConnected={chatWS !== null}
|
||||
conversationId={conversationId}
|
||||
uploadedFiles={uploadedFiles} />
|
||||
</div>
|
||||
<div className={styles.chatBox}>
|
||||
<title>
|
||||
Khoj AI - Chat
|
||||
</title>
|
||||
<NavMenu selected="Chat" />
|
||||
<div>
|
||||
<NavMenu selected="Chat" title={title} />
|
||||
<div className={styles.chatBoxBody}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<ChatBodyData chatOptionsData={chatOptionsData} />
|
||||
<ChatBodyData
|
||||
isLoggedIn={authenticatedData !== null}
|
||||
streamedMessages={messages}
|
||||
chatOptionsData={chatOptionsData}
|
||||
setTitle={setTitle}
|
||||
setQueryToProcess={setQueryToProcess}
|
||||
setUploadedFiles={setUploadedFiles}
|
||||
isMobileWidth={isMobileWidth}
|
||||
onConversationIdChange={handleConversationIdChange} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className={styles.inputBox}>
|
||||
<input className={styles.inputBox} type="text" placeholder="Type here..." onInput={(e) => handleChatInput(e)} />
|
||||
<button className={styles.inputBox}>Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user