Support natural interrupt and send query behavior from web app

- Just send your new query. If a query was running previously it'd
be interrupted and new query would start processing. This improves on
the previous 2 click interrupt and send ux.

- Utilizes partial research for interrupted query, so you can now
redirect khoj's research direction. This is useful if you need to
share more details, change khoj's research direction in anyway or
complete research. Khoj's train of thought can be helpful for this.
This commit is contained in:
Debanjum
2025-05-21 13:28:12 -07:00
parent 2b7dd7401b
commit 6cb512d9cf
2 changed files with 24 additions and 8 deletions

View File

@@ -49,6 +49,7 @@ interface ChatBodyDataProps {
isChatSideBarOpen: boolean;
setIsChatSideBarOpen: (open: boolean) => void;
isActive?: boolean;
isParentProcessing?: boolean;
}
function ChatBodyData(props: ChatBodyDataProps) {
@@ -166,7 +167,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
isLoggedIn={props.isLoggedIn}
sendMessage={(message) => setMessage(message)}
sendImage={(image) => setImages((prevImages) => [...prevImages, image])}
sendDisabled={processingMessage}
sendDisabled={props.isParentProcessing || false}
chatOptionsData={props.chatOptionsData}
conversationId={conversationId}
isMobileWidth={props.isMobileWidth}
@@ -203,6 +204,7 @@ export default function Chat() {
const [abortMessageStreamController, setAbortMessageStreamController] =
useState<AbortController | null>(null);
const [triggeredAbort, setTriggeredAbort] = useState(false);
const [shouldSendWithInterrupt, setShouldSendWithInterrupt] = useState(false);
const { locationData, locationDataError, locationDataLoading } = useIPLocationData() || {
locationData: {
@@ -239,6 +241,7 @@ export default function Chat() {
if (triggeredAbort) {
abortMessageStreamController?.abort();
handleAbortedMessage();
setShouldSendWithInterrupt(true);
setTriggeredAbort(false);
}
}, [triggeredAbort]);
@@ -335,18 +338,21 @@ export default function Chat() {
currentMessage.completed = true;
setMessages([...messages]);
setQueryToProcess("");
setProcessQuerySignal(false);
}
async function chat() {
localStorage.removeItem("message");
if (!queryToProcess || !conversationId) return;
if (!queryToProcess || !conversationId) {
setProcessQuerySignal(false);
return;
}
const chatAPI = "/api/chat?client=web";
const chatAPIBody = {
q: queryToProcess,
conversation_id: conversationId,
stream: true,
interrupt: shouldSendWithInterrupt,
...(locationData && {
city: locationData.city,
region: locationData.region,
@@ -358,6 +364,9 @@ export default function Chat() {
...(uploadedFiles && { files: uploadedFiles }),
};
// Reset the flag after using it
setShouldSendWithInterrupt(false);
const response = await fetch(chatAPI, {
method: "POST",
headers: {
@@ -481,6 +490,7 @@ export default function Chat() {
isChatSideBarOpen={isChatSideBarOpen}
setIsChatSideBarOpen={setIsChatSideBarOpen}
isActive={authenticatedData?.is_active}
isParentProcessing={processQuerySignal}
/>
</Suspense>
</div>

View File

@@ -195,6 +195,11 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
return;
}
// If currently processing, trigger abort first
if (props.sendDisabled) {
props.setTriggeredAbort(true);
}
let messageToSend = message.trim();
// Check if message starts with an explicit slash command
const startsWithSlashCommand =
@@ -657,7 +662,7 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
<Button
variant={"ghost"}
className="!bg-none p-0 m-2 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
disabled={props.sendDisabled || !props.isLoggedIn}
disabled={!props.isLoggedIn}
onClick={handleFileButtonClick}
ref={fileInputButtonRef}
>
@@ -686,7 +691,8 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
e.key === "Enter" &&
!e.shiftKey &&
!props.isMobileWidth &&
!props.sendDisabled
!recording &&
message
) {
setImageUploaded(false);
setImagePaths([]);
@@ -725,7 +731,7 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
{props.sendDisabled ? (
{props.sendDisabled && !message ? (
<Button
variant="default"
className={`${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
@@ -758,8 +764,8 @@ export const ChatInputArea = forwardRef<HTMLTextAreaElement, ChatInputProps>((pr
</TooltipProvider>
)}
<Button
className={`${(!message || recording || props.sendDisabled) && "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
disabled={props.sendDisabled || !props.isLoggedIn}
className={`${(!message || recording) && "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
disabled={!message || recording || !props.isLoggedIn}
onClick={onSendMessage}
>
<ArrowUp className="w-6 h-6" weight="bold" />