Merge branch 'master' of github.com:khoj-ai/khoj into HEAD

This commit is contained in:
sabaimran
2025-01-20 15:35:19 -08:00
46 changed files with 1876 additions and 259 deletions

View File

@@ -229,7 +229,7 @@ function generateImageMarkdown(message, intentType, inferredQueries=null) { //sa
} else if (intentType === "text-to-image2") {
imageMarkdown = `![](${message})`;
} else if (intentType === "text-to-image-v3") {
imageMarkdown = `![](data:image/webp;base64,${message})`;
imageMarkdown = `![](${message})`;
}
const inferredQuery = inferredQueries?.[0];
if (inferredQuery) {
@@ -423,7 +423,7 @@ function handleImageResponse(imageJson, rawResponse) {
} else if (imageJson.intentType === "text-to-image2") {
rawResponse += `![generated_image](${imageJson.image})`;
} else if (imageJson.intentType === "text-to-image-v3") {
rawResponse = `![](data:image/webp;base64,${imageJson.image})`;
rawResponse = `![](${imageJson.image})`;
} else if (imageJson.intentType === "excalidraw") {
const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in the web app`;
rawResponse += redirectMessage;

View File

@@ -1,6 +1,6 @@
{
"name": "Khoj",
"version": "1.33.2",
"version": "1.34.0",
"description": "Your Second Brain",
"author": "Khoj Inc. <team@khoj.dev>",
"license": "GPL-3.0-or-later",

View File

@@ -6,7 +6,7 @@
;; Saba Imran <saba@khoj.dev>
;; Description: Your Second Brain
;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image
;; Version: 1.33.2
;; Version: 1.34.0
;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1"))
;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs

View File

@@ -1,7 +1,7 @@
{
"id": "khoj",
"name": "Khoj",
"version": "1.33.2",
"version": "1.34.0",
"minAppVersion": "0.15.0",
"description": "Your Second Brain",
"author": "Khoj Inc.",

View File

@@ -1,6 +1,6 @@
{
"name": "Khoj",
"version": "1.33.2",
"version": "1.34.0",
"description": "Your Second Brain",
"author": "Debanjum Singh Solanky, Saba Imran <team@khoj.dev>",
"license": "GPL-3.0-or-later",

View File

@@ -501,6 +501,7 @@ export class KhojChatView extends KhojPaneView {
conversationId?: string,
images?: string[],
excalidrawDiagram?: string,
mermaidjsDiagram?: string
) {
if (!message) return;
@@ -509,8 +510,9 @@ export class KhojChatView extends KhojPaneView {
intentType?.includes("text-to-image") ||
intentType === "excalidraw" ||
(images && images.length > 0) ||
mermaidjsDiagram ||
excalidrawDiagram) {
let imageMarkdown = this.generateImageMarkdown(message, intentType ?? "", inferredQueries, conversationId, images, excalidrawDiagram);
let imageMarkdown = this.generateImageMarkdown(message, intentType ?? "", inferredQueries, conversationId, images, excalidrawDiagram, mermaidjsDiagram);
chatMessageEl = this.renderMessage({
chatBodyEl: chatEl,
message: imageMarkdown,
@@ -542,28 +544,23 @@ export class KhojChatView extends KhojPaneView {
chatMessageBodyEl.appendChild(this.createReferenceSection(references));
}
generateImageMarkdown(message: string, intentType: string, inferredQueries?: string[], conversationId?: string, images?: string[], excalidrawDiagram?: string): string {
generateImageMarkdown(message: string, intentType: string, inferredQueries?: string[], conversationId?: string, images?: string[], excalidrawDiagram?: string, mermaidjsDiagram?: string): string {
let imageMarkdown = "";
if (intentType === "text-to-image") {
imageMarkdown = `![](data:image/png;base64,${message})`;
} else if (intentType === "text-to-image2") {
imageMarkdown = `![](${message})`;
} else if (intentType === "text-to-image-v3") {
imageMarkdown = `![](data:image/webp;base64,${message})`;
imageMarkdown = `![](${message})`;
} else if (intentType === "excalidraw" || excalidrawDiagram) {
const domain = this.setting.khojUrl.endsWith("/") ? this.setting.khojUrl : `${this.setting.khojUrl}/`;
const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in ${domain}chat?conversationId=${conversationId}`;
imageMarkdown = redirectMessage;
} else if (mermaidjsDiagram) {
imageMarkdown = "```mermaid\n" + mermaidjsDiagram + "\n```";
} else if (images && images.length > 0) {
for (let image of images) {
if (image.startsWith("https://")) {
imageMarkdown += `![](${image})\n\n`;
} else {
imageMarkdown += `![](data:image/png;base64,${image})\n\n`;
}
}
imageMarkdown += `${message}`;
imageMarkdown += images.map(image => `![](${image})`).join('\n\n');
imageMarkdown += message;
}
if (images?.length === 0 && inferredQueries) {
@@ -961,6 +958,7 @@ export class KhojChatView extends KhojPaneView {
chatBodyEl.dataset.conversationId ?? "",
chatLog.images,
chatLog.excalidrawDiagram,
chatLog.mermaidjsDiagram,
);
// push the user messages to the chat history
if (chatLog.by === "you") {
@@ -1077,7 +1075,7 @@ export class KhojChatView extends KhojPaneView {
}
handleJsonResponse(jsonData: any): void {
if (jsonData.image || jsonData.detail || jsonData.images || jsonData.excalidrawDiagram) {
if (jsonData.image || jsonData.detail || jsonData.images || jsonData.mermaidjsDiagram) {
this.chatMessageState.rawResponse = this.handleImageResponse(jsonData, this.chatMessageState.rawResponse);
} else if (jsonData.response) {
this.chatMessageState.rawResponse = jsonData.response;
@@ -1450,7 +1448,7 @@ export class KhojChatView extends KhojPaneView {
} else if (imageJson.intentType === "text-to-image2") {
rawResponse += `![generated_image](${imageJson.image})`;
} else if (imageJson.intentType === "text-to-image-v3") {
rawResponse = `![](data:image/webp;base64,${imageJson.image})`;
rawResponse = `![generated_image](${imageJson.image})`;
} else if (imageJson.intentType === "excalidraw") {
const domain = this.setting.khojUrl.endsWith("/") ? this.setting.khojUrl : `${this.setting.khojUrl}/`;
const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in ${domain}`;
@@ -1462,17 +1460,14 @@ export class KhojChatView extends KhojPaneView {
} else if (imageJson.images) {
// If response has images field, response is a list of generated images.
imageJson.images.forEach((image: any) => {
if (image.startsWith("http")) {
rawResponse += `![generated_image](${image})\n\n`;
} else {
rawResponse += `![generated_image](data:image/png;base64,${image})\n\n`;
}
rawResponse += `![generated_image](${image})\n\n`;
});
} else if (imageJson.excalidrawDiagram) {
const domain = this.setting.khojUrl.endsWith("/") ? this.setting.khojUrl : `${this.setting.khojUrl}/`;
const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in ${domain}`;
rawResponse += redirectMessage;
} else if (imageJson.mermaidjsDiagram) {
rawResponse += imageJson.mermaidjsDiagram;
}
// If response has detail field, response is an error message.

View File

@@ -108,5 +108,6 @@
"1.32.2": "0.15.0",
"1.33.0": "0.15.0",
"1.33.1": "0.15.0",
"1.33.2": "0.15.0"
"1.33.2": "0.15.0",
"1.34.0": "0.15.0"
}

View File

@@ -354,7 +354,15 @@ export default function Chat() {
try {
await readChatStream(response);
} catch (err) {
const apiError = await response.json();
let apiError;
try {
apiError = await response.json();
} catch (err) {
// Error reading API error response
apiError = {
streamError: "Error reading API error response stream. Expected JSON response.",
};
}
console.error(apiError);
// Retrieve latest message being processed
const currentMessage = messages.find((message) => !message.completed);
@@ -365,7 +373,9 @@ export default function Chat() {
const errorName = (err as Error).name;
if (errorMessage.includes("Error in input stream"))
currentMessage.rawResponse = `Woops! The connection broke while I was writing my thoughts down. Maybe try again in a bit or dislike this message if the issue persists?`;
else if (response.status === 429) {
else if (apiError.streamError) {
currentMessage.rawResponse = `Umm, not sure what just happened but I lost my train of thought. Could you try again or ask my developers to look into this if the issue persists? They can be contacted at the Khoj Github, Discord or team@khoj.dev.`;
} else if (response.status === 429) {
"detail" in apiError
? (currentMessage.rawResponse = `${apiError.detail}`)
: (currentMessage.rawResponse = `I'm a bit overwhelmed at the moment. Could you try again in a bit or dislike this message if the issue persists?`);

View File

@@ -19,7 +19,7 @@ export interface MessageMetadata {
export interface GeneratedAssetsData {
images: string[];
excalidrawDiagram: string;
mermaidjsDiagram: string;
files: AttachedFileText[];
}
@@ -114,8 +114,8 @@ export function processMessageChunk(
currentMessage.generatedImages = generatedAssets.images;
}
if (generatedAssets.excalidrawDiagram) {
currentMessage.generatedExcalidrawDiagram = generatedAssets.excalidrawDiagram;
if (generatedAssets.mermaidjsDiagram) {
currentMessage.generatedMermaidjsDiagram = generatedAssets.mermaidjsDiagram;
}
if (generatedAssets.files) {

View File

@@ -418,7 +418,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
conversationId: props.conversationId,
images: message.generatedImages,
queryFiles: message.generatedFiles,
excalidrawDiagram: message.generatedExcalidrawDiagram,
mermaidjsDiagram: message.generatedMermaidjsDiagram,
turnId: messageTurnId,
}}
conversationId={props.conversationId}

View File

@@ -53,6 +53,7 @@ import { DialogTitle } from "@radix-ui/react-dialog";
import { convertBytesToText } from "@/app/common/utils";
import { ScrollArea } from "@/components/ui/scroll-area";
import { getIconFromFilename } from "@/app/common/iconUtils";
import Mermaid from "../mermaid/mermaid";
const md = new markdownIt({
html: true,
@@ -164,6 +165,7 @@ export interface SingleChatMessage {
turnId?: string;
queryFiles?: AttachedFileText[];
excalidrawDiagram?: string;
mermaidjsDiagram?: string;
}
export interface StreamMessage {
@@ -182,9 +184,11 @@ export interface StreamMessage {
turnId?: string;
queryFiles?: AttachedFileText[];
excalidrawDiagram?: string;
mermaidjsDiagram?: string;
generatedFiles?: AttachedFileText[];
generatedImages?: string[];
generatedExcalidrawDiagram?: string;
generatedMermaidjsDiagram?: string;
}
export interface ChatHistoryData {
@@ -271,6 +275,7 @@ interface ChatMessageProps {
turnId?: string;
generatedImage?: string;
excalidrawDiagram?: string;
mermaidjsDiagram?: string;
generatedFiles?: AttachedFileText[];
}
@@ -358,6 +363,7 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [interrupted, setInterrupted] = useState<boolean>(false);
const [excalidrawData, setExcalidrawData] = useState<string>("");
const [mermaidjsData, setMermaidjsData] = useState<string>("");
const interruptedRef = useRef<boolean>(false);
const messageRef = useRef<HTMLDivElement>(null);
@@ -401,6 +407,10 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
setExcalidrawData(props.chatMessage.excalidrawDiagram);
}
if (props.chatMessage.mermaidjsDiagram) {
setMermaidjsData(props.chatMessage.mermaidjsDiagram);
}
// Replace LaTeX delimiters with placeholders
message = message
.replace(/\\\(/g, "LEFTPAREN")
@@ -718,6 +728,7 @@ const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>((props, ref) =>
dangerouslySetInnerHTML={{ __html: markdownRendered }}
/>
{excalidrawData && <ExcalidrawComponent data={excalidrawData} />}
{mermaidjsData && <Mermaid chart={mermaidjsData} />}
</div>
<div className={styles.teaserReferencesContainer}>
<TeaserReferencesSection

View File

@@ -1,4 +1,9 @@
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { CircleNotch } from "@phosphor-icons/react";
import { AppSidebar } from "../appSidebar/appSidebar";
import { Separator } from "@/components/ui/separator";
import { useIsMobileWidth } from "@/app/common/utils";
import { KhojLogoType } from "../logo/khojLogo";
interface LoadingProps {
className?: string;
@@ -7,21 +12,39 @@ interface LoadingProps {
}
export default function Loading(props: LoadingProps) {
const isMobileWidth = useIsMobileWidth();
return (
// NOTE: We can display usage tips here for casual learning moments.
<div
className={
props.className ||
"bg-background opacity-50 flex items-center justify-center h-screen"
}
>
<div>
{props.message || "Loading"}{" "}
<span>
<CircleNotch className="inline animate-spin h-5 w-5" />
</span>
<SidebarProvider>
<AppSidebar conversationId={""} />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
{isMobileWidth ? (
<a className="p-0 no-underline" href="/">
<KhojLogoType className="h-auto w-16" />
</a>
) : (
<h2 className="text-lg">Ask Anything</h2>
)}
</header>
</SidebarInset>
<div
className={
props.className ||
"bg-background opacity-50 flex items-center justify-center h-full w-full fixed top-0 left-0 z-50"
}
>
<div>
{props.message || "Loading"}{" "}
<span>
<CircleNotch className="inline animate-spin h-5 w-5" />
</span>
</div>
</div>
</div>
</SidebarProvider>
);
}

View File

@@ -2,7 +2,7 @@
import styles from "./loginPrompt.module.css";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import Autoplay from "embla-carousel-autoplay";
import {
@@ -27,6 +27,7 @@ import {
} from "@/components/ui/carousel";
import { Card, CardContent } from "@/components/ui/card";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
export interface LoginPromptProps {
onOpenChange: (open: boolean) => void;
@@ -181,6 +182,9 @@ export default function LoginPrompt(props: LoginPromptProps) {
<DialogContent
className={`flex flex-col gap-4 ${!useEmailSignIn ? "p-0 pb-4 m-0 max-w-xl" : "w-fit"}`}
>
<VisuallyHidden.Root>
<DialogTitle>Login Dialog</DialogTitle>
</VisuallyHidden.Root>
<div>
{useEmailSignIn ? (
<EmailSignInContext
@@ -232,7 +236,7 @@ function EmailSignInContext({
const [numFailures, setNumFailures] = useState(0);
function checkOTPAndRedirect() {
const verifyUrl = `/auth/magic?code=${otp}&email=${email}`;
const verifyUrl = `/auth/magic?code=${encodeURIComponent(otp)}&email=${encodeURIComponent(email)}`;
if (numFailures >= ALLOWED_OTP_ATTEMPTS) {
setOTPError("Too many failed attempts. Please try again tomorrow.");

View File

@@ -0,0 +1,173 @@
import React, { useEffect, useState, useRef } from "react";
import mermaid from "mermaid";
import { Download, Info } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";
interface MermaidProps {
chart: string;
}
const Mermaid: React.FC<MermaidProps> = ({ chart }) => {
const [mermaidError, setMermaidError] = useState<string | null>(null);
const [mermaidId] = useState(`mermaid-chart-${Math.random().toString(12).substring(7)}`);
const elementRef = useRef<HTMLDivElement>(null);
useEffect(() => {
mermaid.initialize({
startOnLoad: false,
});
mermaid.parseError = (error) => {
console.error("Mermaid errors:", error);
// Extract error message from error object
// Parse error message safely
let errorMessage;
try {
errorMessage = typeof error === "string" ? JSON.parse(error) : error;
} catch (e) {
errorMessage = error?.toString() || "Unknown error";
}
console.log("Mermaid error message:", errorMessage);
if (errorMessage.str !== "element is null") {
setMermaidError(
"Something went wrong while rendering the diagram. Please try again later or downvote the message if the issue persists.",
);
} else {
setMermaidError(null);
}
};
mermaid.contentLoaded();
}, []);
const handleExport = async () => {
if (!elementRef.current) return;
try {
// Get SVG element
const svgElement = elementRef.current.querySelector("svg");
if (!svgElement) throw new Error("No SVG found");
// Get SVG viewBox dimensions
const viewBox = svgElement.getAttribute("viewBox")?.split(" ").map(Number) || [
0, 0, 0, 0,
];
const [, , viewBoxWidth, viewBoxHeight] = viewBox;
// Create canvas with viewBox dimensions
const canvas = document.createElement("canvas");
const scale = 2; // For better resolution
canvas.width = viewBoxWidth * scale;
canvas.height = viewBoxHeight * scale;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Failed to get canvas context");
// Convert SVG to data URL
const svgData = new XMLSerializer().serializeToString(svgElement);
const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
const svgUrl = URL.createObjectURL(svgBlob);
// Create and load image
const img = new Image();
img.src = svgUrl;
await new Promise((resolve, reject) => {
img.onload = () => {
// Scale context for better resolution
ctx.scale(scale, scale);
ctx.drawImage(img, 0, 0, viewBoxWidth, viewBoxHeight);
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error("Failed to create blob"));
return;
}
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `mermaid-diagram-${Date.now()}.png`;
a.click();
// Cleanup
URL.revokeObjectURL(url);
URL.revokeObjectURL(svgUrl);
resolve(true);
}, "image/png");
};
img.onerror = () => reject(new Error("Failed to load SVG"));
});
} catch (error) {
console.error("Error exporting diagram:", error);
setMermaidError("Failed to export diagram");
}
};
useEffect(() => {
if (elementRef.current) {
elementRef.current.removeAttribute("data-processed");
mermaid
.run({
nodes: [elementRef.current],
})
.then(() => {
setMermaidError(null);
})
.catch((error) => {
let errorMessage;
try {
errorMessage = typeof error === "string" ? JSON.parse(error) : error;
} catch (e) {
errorMessage = error?.toString() || "Unknown error";
}
console.log("Mermaid error message:", errorMessage);
if (errorMessage.str !== "element is null") {
setMermaidError(
"Something went wrong while rendering the diagram. Please try again later or downvote the message if the issue persists.",
);
} else {
setMermaidError(null);
}
});
}
}, [chart]);
return (
<div>
{mermaidError ? (
<div className="flex items-center gap-2 bg-red-100 border border-red-500 rounded-md p-3 mt-3 text-red-900 text-sm">
<Info className="w-12 h-12" />
<span>Error rendering diagram: {mermaidError}</span>
</div>
) : (
<div
id={mermaidId}
ref={elementRef}
className="mermaid"
style={{
width: "auto",
height: "auto",
boxSizing: "border-box",
overflow: "auto",
}}
>
{chart}
</div>
)}
{!mermaidError && (
<Button onClick={handleExport} variant={"secondary"} className="mt-3">
<Download className="w-5 h-5" />
Export as PNG
</Button>
)}
</div>
);
};
export default Mermaid;

View File

@@ -173,8 +173,10 @@ export default function Search() {
const [searchResultsLoading, setSearchResultsLoading] = useState(false);
const [focusSearchResult, setFocusSearchResult] = useState<SearchResult | null>(null);
const [exampleQuery, setExampleQuery] = useState("");
const [fileSuggestions, setFileSuggestions] = useState<string[]>([]);
const [allFiles, setAllFiles] = useState<string[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isMobileWidth = useIsMobileWidth();
useEffect(() => {
@@ -183,8 +185,68 @@ export default function Search() {
Math.floor(Math.random() * naturalLanguageSearchQueryExamples.length)
],
);
// Load all files once on page load
fetch('/api/content/computer', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
setAllFiles(data);
})
.catch(error => {
console.error('Error loading files:', error);
});
}, []);
function getFileSuggestions(query: string) {
const fileFilterMatch = query.match(/file:([^"\s]*|"[^"]*")?/);
if (!fileFilterMatch) {
setFileSuggestions([]);
setShowSuggestions(false);
return;
}
const filePrefix = fileFilterMatch[1]?.replace(/^"|"$/g, '').trim() || '';
const filteredSuggestions = allFiles
.filter(file => file.toLowerCase().includes(filePrefix.toLowerCase()))
.sort()
.slice(0, 10);
setFileSuggestions(filteredSuggestions);
setShowSuggestions(true);
}
function handleSearchInputChange(value: string) {
setSearchQuery(value);
// Clear previous search timeout
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
// Get file suggestions immediately
getFileSuggestions(value);
// Debounce search
if (value.trim()) {
searchTimeoutRef.current = setTimeout(() => {
search();
}, 750);
}
}
function applySuggestion(suggestion: string) {
// Replace the file: filter with the selected suggestion
const newQuery = searchQuery.replace(/file:([^"\s]*|"[^"]*")?/, `file:"${suggestion}"`);
setSearchQuery(newQuery);
setShowSuggestions(false);
search();
}
function search() {
if (searchResultsLoading || !searchQuery.trim()) return;
@@ -205,30 +267,6 @@ export default function Search() {
});
}
useEffect(() => {
if (!searchQuery.trim()) {
return;
}
setFocusSearchResult(null);
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
if (searchQuery.trim()) {
searchTimeoutRef.current = setTimeout(() => {
search();
}, 750); // 1000 milliseconds = 1 second
}
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
};
}, [searchQuery]);
return (
<SidebarProvider>
<AppSidebar conversationId={""} />
@@ -249,14 +287,38 @@ export default function Search() {
<div className="md:w-3/4 sm:w-full mx-auto pt-6 md:pt-8">
<div className="p-4 md:w-3/4 sm:w-full mx-auto">
<div className="flex justify-between items-center border-2 border-muted p-1 gap-1 rounded-lg">
<Input
autoFocus={true}
className="border-none pl-4"
onChange={(e) => setSearchQuery(e.currentTarget.value)}
onKeyDown={(e) => e.key === "Enter" && search()}
type="search"
placeholder="Search Documents"
/>
<div className="relative flex-1">
<Input
autoFocus={true}
className="border-none pl-4"
onChange={(e) => handleSearchInputChange(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (showSuggestions && fileSuggestions.length > 0) {
applySuggestion(fileSuggestions[0]);
} else {
search();
}
}
}}
type="search"
placeholder="Search Documents (type 'file:' for file suggestions)"
value={searchQuery}
/>
{showSuggestions && fileSuggestions.length > 0 && (
<div className="absolute z-10 w-full mt-1 bg-background border rounded-md shadow-lg">
{fileSuggestions.map((suggestion, index) => (
<div
key={index}
className="px-4 py-2 hover:bg-muted cursor-pointer"
onClick={() => applySuggestion(suggestion)}
>
{suggestion}
</div>
))}
</div>
)}
</div>
<button
className="px-2 gap-2 inline-flex items-center rounded border-l border-gray-300 hover:text-gray-500"
onClick={() => search()}

View File

@@ -1,6 +1,6 @@
{
"name": "khoj-ai",
"version": "1.33.2",
"version": "1.34.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -53,12 +53,13 @@
"eslint-config-next": "14.2.3",
"input-otp": "^1.2.4",
"intl-tel-input": "^23.8.1",
"katex": "^0.16.10",
"katex": "^0.16.21",
"libphonenumber-js": "^1.11.4",
"lucide-react": "^0.468.0",
"markdown-it": "^14.1.0",
"markdown-it-highlightjs": "^4.1.0",
"next": "14.2.15",
"mermaid": "^11.4.1",
"next": "14.2.21",
"nodemon": "^3.1.3",
"postcss": "^8.4.38",
"react": "^18",

File diff suppressed because it is too large Load Diff