mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 13:19:16 +00:00
Merge branch 'master' of github.com:khoj-ai/khoj into features/add-a-knowledge-base-page
This commit is contained in:
@@ -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?`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.");
|
||||
|
||||
173
src/interface/web/app/components/mermaid/mermaid.tsx
Normal file
173
src/interface/web/app/components/mermaid/mermaid.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user