mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-27 17:35:07 +01:00
Add DOMPurify for rendering md text. Add a easter egg in the console
This commit is contained in:
parent
e358723baa
commit
e1a5c17775
7 changed files with 47 additions and 172 deletions
|
@ -48,6 +48,7 @@ import { StreamMessage } from '../components/chatMessage/chatMessage';
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
||||||
import { Popover, PopoverContent } from '@/components/ui/popover';
|
import { Popover, PopoverContent } from '@/components/ui/popover';
|
||||||
import { PopoverTrigger } from '@radix-ui/react-popover';
|
import { PopoverTrigger } from '@radix-ui/react-popover';
|
||||||
|
import { welcomeConsole } from '../common/utils';
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
sendMessage: (message: string) => void;
|
sendMessage: (message: string) => void;
|
||||||
|
@ -392,6 +393,9 @@ export default function Chat() {
|
||||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
welcomeConsole();
|
||||||
|
|
||||||
|
|
||||||
const handleWebSocketMessage = (event: MessageEvent) => {
|
const handleWebSocketMessage = (event: MessageEvent) => {
|
||||||
let chunk = event.data;
|
let chunk = event.data;
|
||||||
|
|
||||||
|
|
|
@ -82,8 +82,6 @@ export const setupWebSocket = async (conversationId: string) => {
|
||||||
webSocketUrl += `?conversation_id=${conversationId}`;
|
webSocketUrl += `?conversation_id=${conversationId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("WebSocket URL: ", webSocketUrl);
|
|
||||||
|
|
||||||
const chatWS = new WebSocket(webSocketUrl);
|
const chatWS = new WebSocket(webSocketUrl);
|
||||||
|
|
||||||
chatWS.onopen = () => {
|
chatWS.onopen = () => {
|
||||||
|
|
17
src/interface/web/app/common/utils.ts
Normal file
17
src/interface/web/app/common/utils.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export function welcomeConsole() {
|
||||||
|
console.log(`%c %s`, "font-family:monospace", `
|
||||||
|
__ __ __ __ ______ __ _____ __
|
||||||
|
/\\ \\/ / /\\ \\_\\ \\ /\\ __ \\ /\\ \\ /\\ __ \\ /\\ \\
|
||||||
|
\\ \\ _"-. \\ \\ __ \\ \\ \\ \\/\\ \\ _\\_\\ \\ \\ \\ __ \\ \\ \\ \\
|
||||||
|
\\ \\_\\ \\_\\ \\ \\_\\ \\_\\ \\ \\_____\\ /\\_____\\ \\ \\_\\ \\_\\ \\ \\_\\
|
||||||
|
\\/_/\\/_/ \\/_/\\/_/ \\/_____/ \\/_____/ \\/_/\\/_/ \\/_/
|
||||||
|
|
||||||
|
|
||||||
|
Greetings traveller,
|
||||||
|
|
||||||
|
I am ✨Khoj✨, your open-source, personal AI copilot.
|
||||||
|
|
||||||
|
See my source code at https://github.com/khoj-ai/khoj
|
||||||
|
Read my operating manual at https://docs.khoj.dev
|
||||||
|
`);
|
||||||
|
}
|
|
@ -14,6 +14,8 @@ import { TeaserReferencesSection, constructAllReferences } from '../referencePan
|
||||||
import { ThumbsUp, ThumbsDown, Copy, Brain, Cloud, Folder, Book, Aperture, ArrowRight, SpeakerHifi } from '@phosphor-icons/react';
|
import { ThumbsUp, ThumbsDown, Copy, Brain, Cloud, Folder, Book, Aperture, ArrowRight, SpeakerHifi } from '@phosphor-icons/react';
|
||||||
import { MagnifyingGlass } from '@phosphor-icons/react/dist/ssr';
|
import { MagnifyingGlass } from '@phosphor-icons/react/dist/ssr';
|
||||||
|
|
||||||
|
import * as DomPurify from 'dompurify';
|
||||||
|
|
||||||
const md = new markdownIt({
|
const md = new markdownIt({
|
||||||
html: true,
|
html: true,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
|
@ -192,7 +194,7 @@ export function TrainOfThought(props: TrainOfThoughtProps) {
|
||||||
let header = extractedHeader ? extractedHeader[1] : "";
|
let header = extractedHeader ? extractedHeader[1] : "";
|
||||||
const iconColor = props.primary ? 'text-orange-400' : 'text-gray-500';
|
const iconColor = props.primary ? 'text-orange-400' : 'text-gray-500';
|
||||||
const icon = chooseIconFromHeader(header, iconColor);
|
const icon = chooseIconFromHeader(header, iconColor);
|
||||||
let markdownRendered = md.render(props.message);
|
let markdownRendered = DomPurify.sanitize(md.render(props.message));
|
||||||
return (
|
return (
|
||||||
<div className={`flex items-center ${props.primary ? 'text-gray-400' : 'text-gray-300'} ${styles.trainOfThought} ${props.primary ? styles.primary : ''}`} >
|
<div className={`flex items-center ${props.primary ? 'text-gray-400' : 'text-gray-300'} ${styles.trainOfThought} ${props.primary ? styles.primary : ''}`} >
|
||||||
{icon}
|
{icon}
|
||||||
|
@ -223,7 +225,7 @@ export default function ChatMessage(props: ChatMessageProps) {
|
||||||
// Replace placeholders with LaTeX delimiters
|
// Replace placeholders with LaTeX delimiters
|
||||||
markdownRendered = markdownRendered.replace(/LEFTPAREN/g, '\\(').replace(/RIGHTPAREN/g, '\\)')
|
markdownRendered = markdownRendered.replace(/LEFTPAREN/g, '\\(').replace(/RIGHTPAREN/g, '\\)')
|
||||||
.replace(/LEFTBRACKET/g, '\\[').replace(/RIGHTBRACKET/g, '\\]');
|
.replace(/LEFTBRACKET/g, '\\[').replace(/RIGHTBRACKET/g, '\\]');
|
||||||
setMarkdownRendered(markdownRendered);
|
setMarkdownRendered(DomPurify.sanitize(markdownRendered));
|
||||||
}, [props.chatMessage.message]);
|
}, [props.chatMessage.message]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import styles from "./referencePanel.module.css";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { ArrowRight, File } from "@phosphor-icons/react";
|
import { ArrowRight, File } from "@phosphor-icons/react";
|
||||||
|
@ -13,9 +11,8 @@ const md = new markdownIt({
|
||||||
typographer: true
|
typographer: true
|
||||||
});
|
});
|
||||||
|
|
||||||
import { SingleChatMessage, Context, WebPage, OnlineContextData } from "../chatMessage/chatMessage";
|
import { Context, WebPage, OnlineContextData } from "../chatMessage/chatMessage";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
|
@ -26,13 +23,7 @@ import {
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import * as DomPurify from 'dompurify';
|
||||||
|
|
||||||
interface ReferencePanelProps {
|
|
||||||
referencePanelData: SingleChatMessage | null;
|
|
||||||
setShowReferencePanel: (showReferencePanel: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface NotesContextReferenceData {
|
interface NotesContextReferenceData {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -45,7 +36,7 @@ interface NotesContextReferenceCardProps extends NotesContextReferenceData {
|
||||||
|
|
||||||
|
|
||||||
function NotesContextReferenceCard(props: NotesContextReferenceCardProps) {
|
function NotesContextReferenceCard(props: NotesContextReferenceCardProps) {
|
||||||
const snippet = md.render(props.content);
|
const snippet = props.showFullContent ? DomPurify.sanitize(md.render(props.content)) : DomPurify.sanitize(props.content);
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -109,12 +100,10 @@ function GenericOnlineReferenceCard(props: OnlineReferenceCardProps) {
|
||||||
const favicon = `https://www.google.com/s2/favicons?domain=${domain}`;
|
const favicon = `https://www.google.com/s2/favicons?domain=${domain}`;
|
||||||
|
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
console.log("mouse entered card");
|
|
||||||
setIsHovering(true);
|
setIsHovering(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => {
|
||||||
console.log("mouse left card");
|
|
||||||
setIsHovering(false);
|
setIsHovering(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +111,6 @@ function GenericOnlineReferenceCard(props: OnlineReferenceCardProps) {
|
||||||
<>
|
<>
|
||||||
<Popover
|
<Popover
|
||||||
open={isHovering && !props.showFullContent}
|
open={isHovering && !props.showFullContent}
|
||||||
// open={true}
|
|
||||||
onOpenChange={setIsHovering}
|
onOpenChange={setIsHovering}
|
||||||
>
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
@ -349,156 +337,3 @@ export default function ReferencePanel(props: ReferencePanelDataProps) {
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CompiledReference(props: { context: (Context | string) }) {
|
|
||||||
|
|
||||||
let snippet = "";
|
|
||||||
let file = "";
|
|
||||||
if (typeof props.context === "string") {
|
|
||||||
// Treat context as a string and get the first line for the file name
|
|
||||||
const lines = props.context.split("\n");
|
|
||||||
file = lines[0];
|
|
||||||
snippet = lines.slice(1).join("\n");
|
|
||||||
} else {
|
|
||||||
const context = props.context as Context;
|
|
||||||
snippet = context.compiled;
|
|
||||||
file = context.file;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [showSnippet, setShowSnippet] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.singleReference}>
|
|
||||||
<div className={styles.contextReference} onClick={() => setShowSnippet(!showSnippet)}>
|
|
||||||
<div className={styles.referencePanelTitle}>
|
|
||||||
{file}
|
|
||||||
</div>
|
|
||||||
<div className={styles.referencePanelContent} style={{ display: showSnippet ? "block" : "none" }}>
|
|
||||||
<div>
|
|
||||||
{snippet}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function WebPageReference(props: { webpages: WebPage, query: string | null }) {
|
|
||||||
|
|
||||||
let snippet = md.render(props.webpages.snippet);
|
|
||||||
|
|
||||||
const [showSnippet, setShowSnippet] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.onlineReference} onClick={() => setShowSnippet(!showSnippet)}>
|
|
||||||
<div className={styles.onlineReferenceTitle}>
|
|
||||||
<a href={props.webpages.link} target="_blank" rel="noreferrer">
|
|
||||||
{
|
|
||||||
props.query ? (
|
|
||||||
<span>
|
|
||||||
{props.query}
|
|
||||||
</span>
|
|
||||||
) : <span>
|
|
||||||
{props.webpages.query}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className={styles.onlineReferenceContent} style={{ display: showSnippet ? "block" : "none" }}>
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: snippet }}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function OnlineReferences(props: { onlineContext: OnlineContextData, query: string }) {
|
|
||||||
|
|
||||||
const webpages = props.onlineContext.webpages;
|
|
||||||
const answerBox = props.onlineContext.answerBox;
|
|
||||||
const peopleAlsoAsk = props.onlineContext.peopleAlsoAsk;
|
|
||||||
const knowledgeGraph = props.onlineContext.knowledgeGraph;
|
|
||||||
const organic = props.onlineContext.organic;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.singleReference}>
|
|
||||||
{
|
|
||||||
webpages && (
|
|
||||||
!Array.isArray(webpages) ? (
|
|
||||||
<WebPageReference webpages={webpages} query={props.query} />
|
|
||||||
) : (
|
|
||||||
webpages.map((webpage, index) => {
|
|
||||||
return <WebPageReference webpages={webpage} key={index} query={null} />
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
answerBox && (
|
|
||||||
<div className={styles.onlineReference}>
|
|
||||||
<div className={styles.onlineReferenceTitle}>
|
|
||||||
{answerBox.title}
|
|
||||||
</div>
|
|
||||||
<div className={styles.onlineReferenceContent}>
|
|
||||||
<div>
|
|
||||||
{answerBox.answer}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
organic && organic.map((organicData, index) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.onlineReference} key={index}>
|
|
||||||
<div className={styles.onlineReferenceTitle}>
|
|
||||||
<a href={organicData.link} target="_blank" rel="noreferrer">
|
|
||||||
{organicData.title}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className={styles.onlineReferenceContent}>
|
|
||||||
<div>
|
|
||||||
{organicData.snippet}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{
|
|
||||||
peopleAlsoAsk && peopleAlsoAsk.map((people, index) => {
|
|
||||||
return (
|
|
||||||
<div className={styles.onlineReference} key={index}>
|
|
||||||
<div className={styles.onlineReferenceTitle}>
|
|
||||||
<a href={people.link} target="_blank" rel="noreferrer">
|
|
||||||
{people.question}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className={styles.onlineReferenceContent}>
|
|
||||||
<div>
|
|
||||||
{people.snippet}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{
|
|
||||||
knowledgeGraph && (
|
|
||||||
<div className={styles.onlineReference}>
|
|
||||||
<div className={styles.onlineReferenceTitle}>
|
|
||||||
<a href={knowledgeGraph.descriptionLink} target="_blank" rel="noreferrer">
|
|
||||||
{knowledgeGraph.title}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className={styles.onlineReferenceContent}>
|
|
||||||
<div>
|
|
||||||
{knowledgeGraph.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -31,12 +31,14 @@
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.7",
|
||||||
"@types/markdown-it": "^14.1.1",
|
"@types/markdown-it": "^14.1.1",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
|
"dompurify": "^3.1.6",
|
||||||
"katex": "^0.16.10",
|
"katex": "^0.16.10",
|
||||||
"lucide-react": "^0.397.0",
|
"lucide-react": "^0.397.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
|
|
|
@ -1045,6 +1045,13 @@
|
||||||
mkdirp "^2.1.6"
|
mkdirp "^2.1.6"
|
||||||
path-browserify "^1.0.1"
|
path-browserify "^1.0.1"
|
||||||
|
|
||||||
|
"@types/dompurify@^3.0.5":
|
||||||
|
version "3.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7"
|
||||||
|
integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==
|
||||||
|
dependencies:
|
||||||
|
"@types/trusted-types" "*"
|
||||||
|
|
||||||
"@types/json5@^0.0.29":
|
"@types/json5@^0.0.29":
|
||||||
version "0.0.29"
|
version "0.0.29"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
|
@ -1100,6 +1107,11 @@
|
||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
|
"@types/trusted-types@*":
|
||||||
|
version "2.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||||
|
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||||
|
|
||||||
"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0":
|
"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || 7.0.0 - 7.2.0":
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a"
|
||||||
|
@ -1792,6 +1804,11 @@ doctrine@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
|
|
||||||
|
dompurify@^3.1.6:
|
||||||
|
version "3.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2"
|
||||||
|
integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==
|
||||||
|
|
||||||
eastasianwidth@^0.2.0:
|
eastasianwidth@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
|
||||||
|
|
Loading…
Reference in a new issue