mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-27 04:18:35 +02:00
Text embedding (PDF, TXT) (#3113)
* add text embedding * post rebase cleanup * fully functional post rebase * rm logs * rm ' * quick clean up * k
This commit is contained in:
@@ -106,6 +106,7 @@ import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import AssistantBanner from "../../components/assistants/AssistantBanner";
|
||||
import TextView from "@/components/chat_search/TextView";
|
||||
import AssistantSelector from "@/components/chat_search/AssistantSelector";
|
||||
import { Modal } from "@/components/Modal";
|
||||
|
||||
@@ -279,6 +280,9 @@ export function ChatPage({
|
||||
const [alternativeAssistant, setAlternativeAssistant] =
|
||||
useState<Persona | null>(null);
|
||||
|
||||
const [presentingDocument, setPresentingDocument] =
|
||||
useState<DanswerDocument | null>(null);
|
||||
|
||||
const {
|
||||
visibleAssistants: assistants,
|
||||
recentAssistants,
|
||||
@@ -490,6 +494,7 @@ export function ChatPage({
|
||||
clientScrollToBottom(true);
|
||||
}
|
||||
}
|
||||
|
||||
setIsFetchingChatMessages(false);
|
||||
|
||||
// if this is a seeded chat, then kick off the AI message generation
|
||||
@@ -1649,7 +1654,6 @@ export function ChatPage({
|
||||
scrollDist,
|
||||
endDivRef,
|
||||
debounceNumber,
|
||||
waitForScrollRef,
|
||||
mobile: settings?.isMobile,
|
||||
enableAutoScroll: autoScrollEnabled,
|
||||
});
|
||||
@@ -1946,6 +1950,7 @@ export function ChatPage({
|
||||
{popup}
|
||||
|
||||
<ChatPopup />
|
||||
|
||||
{currentFeedback && (
|
||||
<FeedbackModal
|
||||
feedbackType={currentFeedback[0]}
|
||||
@@ -1979,6 +1984,7 @@ export function ChatPage({
|
||||
<div className="md:hidden">
|
||||
<Modal noPadding noScroll>
|
||||
<ChatFilters
|
||||
setPresentingDocument={setPresentingDocument}
|
||||
modal={true}
|
||||
filterManager={filterManager}
|
||||
ccPairs={ccPairs}
|
||||
@@ -2024,6 +2030,13 @@ export function ChatPage({
|
||||
/>
|
||||
)}
|
||||
|
||||
{presentingDocument && (
|
||||
<TextView
|
||||
presentingDocument={presentingDocument}
|
||||
onClose={() => setPresentingDocument(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{stackTraceModalContent && (
|
||||
<ExceptionTraceModal
|
||||
onOutsideClick={() => setStackTraceModalContent(null)}
|
||||
@@ -2127,6 +2140,7 @@ export function ChatPage({
|
||||
`}
|
||||
>
|
||||
<ChatFilters
|
||||
setPresentingDocument={setPresentingDocument}
|
||||
modal={false}
|
||||
filterManager={filterManager}
|
||||
ccPairs={ccPairs}
|
||||
@@ -2424,6 +2438,9 @@ export function ChatPage({
|
||||
}
|
||||
>
|
||||
<AIMessage
|
||||
setPresentingDocument={
|
||||
setPresentingDocument
|
||||
}
|
||||
index={i}
|
||||
selectedMessageForDocDisplay={
|
||||
selectedMessageForDocDisplay
|
||||
|
@@ -6,13 +6,16 @@ import { buildDocumentSummaryDisplay } from "@/components/search/DocumentDisplay
|
||||
import { DocumentUpdatedAtBadge } from "@/components/search/DocumentUpdatedAtBadge";
|
||||
import { MetadataBadge } from "@/components/MetadataBadge";
|
||||
import { WebResultIcon } from "@/components/WebResultIcon";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface DocumentDisplayProps {
|
||||
closeSidebar: () => void;
|
||||
document: DanswerDocument;
|
||||
modal?: boolean;
|
||||
isSelected: boolean;
|
||||
handleSelect: (documentId: string) => void;
|
||||
tokenLimitReached: boolean;
|
||||
setPresentingDocument: Dispatch<SetStateAction<DanswerDocument | null>>;
|
||||
}
|
||||
|
||||
export function DocumentMetadataBlock({
|
||||
@@ -55,11 +58,13 @@ export function DocumentMetadataBlock({
|
||||
}
|
||||
|
||||
export function ChatDocumentDisplay({
|
||||
closeSidebar,
|
||||
document,
|
||||
modal,
|
||||
isSelected,
|
||||
handleSelect,
|
||||
tokenLimitReached,
|
||||
setPresentingDocument,
|
||||
}: DocumentDisplayProps) {
|
||||
const isInternet = document.is_internet;
|
||||
|
||||
@@ -67,6 +72,18 @@ export function ChatDocumentDisplay({
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleViewFile = async () => {
|
||||
if (document.link) {
|
||||
window.open(document.link, "_blank");
|
||||
} else {
|
||||
closeSidebar();
|
||||
|
||||
setTimeout(async () => {
|
||||
setPresentingDocument(document);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`opacity-100 ${modal ? "w-[90vw]" : "w-full"}`}>
|
||||
<div
|
||||
@@ -74,11 +91,9 @@ export function ChatDocumentDisplay({
|
||||
isSelected ? "bg-gray-200" : "hover:bg-background-125"
|
||||
}`}
|
||||
>
|
||||
<a
|
||||
href={document.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="cursor-pointer flex flex-col px-2 py-1.5"
|
||||
<button
|
||||
onClick={handleViewFile}
|
||||
className="cursor-pointer text-left flex flex-col px-2 py-1.5"
|
||||
>
|
||||
<div className="line-clamp-1 mb-1 flex h-6 items-center gap-2 text-xs">
|
||||
{document.is_internet || document.source_type === "web" ? (
|
||||
@@ -111,7 +126,7 @@ export function ChatDocumentDisplay({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -3,7 +3,14 @@ import { ChatDocumentDisplay } from "./ChatDocumentDisplay";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { removeDuplicateDocs } from "@/lib/documentUtils";
|
||||
import { Message } from "../interfaces";
|
||||
import { ForwardedRef, forwardRef, useEffect, useState } from "react";
|
||||
import {
|
||||
Dispatch,
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FilterManager } from "@/lib/hooks";
|
||||
import { CCPairBasicInfo, DocumentSet, Tag } from "@/lib/types";
|
||||
import { SourceSelector } from "../shared_chat_search/SearchFilters";
|
||||
@@ -25,6 +32,7 @@ interface ChatFiltersProps {
|
||||
tags: Tag[];
|
||||
documentSets: DocumentSet[];
|
||||
showFilters: boolean;
|
||||
setPresentingDocument: Dispatch<SetStateAction<DanswerDocument | null>>;
|
||||
}
|
||||
|
||||
export const ChatFilters = forwardRef<HTMLDivElement, ChatFiltersProps>(
|
||||
@@ -43,6 +51,7 @@ export const ChatFilters = forwardRef<HTMLDivElement, ChatFiltersProps>(
|
||||
isOpen,
|
||||
ccPairs,
|
||||
tags,
|
||||
setPresentingDocument,
|
||||
documentSets,
|
||||
showFilters,
|
||||
},
|
||||
@@ -134,6 +143,8 @@ export const ChatFilters = forwardRef<HTMLDivElement, ChatFiltersProps>(
|
||||
}`}
|
||||
>
|
||||
<ChatDocumentDisplay
|
||||
setPresentingDocument={setPresentingDocument}
|
||||
closeSidebar={closeSidebar}
|
||||
modal={modal}
|
||||
document={document}
|
||||
isSelected={selectedDocumentIds.includes(
|
||||
|
@@ -644,7 +644,6 @@ export async function useScrollonStream({
|
||||
}: {
|
||||
chatState: ChatState;
|
||||
scrollableDivRef: RefObject<HTMLDivElement>;
|
||||
waitForScrollRef: RefObject<boolean>;
|
||||
scrollDist: MutableRefObject<number>;
|
||||
endDivRef: RefObject<HTMLDivElement>;
|
||||
debounceNumber: number;
|
||||
|
@@ -6,45 +6,53 @@ import { ValidSources } from "@/lib/types";
|
||||
import React, { memo } from "react";
|
||||
import isEqual from "lodash/isEqual";
|
||||
|
||||
export const MemoizedAnchor = memo(({ docs, children }: any) => {
|
||||
console.log(children);
|
||||
const value = children?.toString();
|
||||
if (value?.startsWith("[") && value?.endsWith("]")) {
|
||||
const match = value.match(/\[(\d+)\]/);
|
||||
if (match) {
|
||||
const index = parseInt(match[1], 10) - 1;
|
||||
const associatedDoc = docs && docs[index];
|
||||
export const MemoizedAnchor = memo(
|
||||
({ docs, updatePresentingDocument, children }: any) => {
|
||||
const value = children?.toString();
|
||||
if (value?.startsWith("[") && value?.endsWith("]")) {
|
||||
const match = value.match(/\[(\d+)\]/);
|
||||
if (match) {
|
||||
const index = parseInt(match[1], 10) - 1;
|
||||
const associatedDoc = docs && docs[index];
|
||||
|
||||
const url = associatedDoc?.link
|
||||
? new URL(associatedDoc.link).origin + "/favicon.ico"
|
||||
: "";
|
||||
const url = associatedDoc?.link
|
||||
? new URL(associatedDoc.link).origin + "/favicon.ico"
|
||||
: "";
|
||||
|
||||
const getIcon = (sourceType: ValidSources, link: string) => {
|
||||
return getSourceMetadata(sourceType).icon({ size: 18 });
|
||||
};
|
||||
const getIcon = (sourceType: ValidSources, link: string) => {
|
||||
return getSourceMetadata(sourceType).icon({ size: 18 });
|
||||
};
|
||||
|
||||
const icon =
|
||||
associatedDoc?.source_type === "web" ? (
|
||||
<WebResultIcon url={associatedDoc.link} />
|
||||
) : (
|
||||
getIcon(
|
||||
associatedDoc?.source_type || "web",
|
||||
associatedDoc?.link || ""
|
||||
)
|
||||
const icon =
|
||||
associatedDoc?.source_type === "web" ? (
|
||||
<WebResultIcon url={associatedDoc.link} />
|
||||
) : (
|
||||
getIcon(
|
||||
associatedDoc?.source_type || "web",
|
||||
associatedDoc?.link || ""
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<MemoizedLink
|
||||
updatePresentingDocument={updatePresentingDocument}
|
||||
document={{ ...associatedDoc, icon, url }}
|
||||
>
|
||||
{children}
|
||||
</MemoizedLink>
|
||||
);
|
||||
|
||||
return (
|
||||
<MemoizedLink document={{ ...associatedDoc, icon, url }}>
|
||||
{children}
|
||||
</MemoizedLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<MemoizedLink updatePresentingDocument={updatePresentingDocument}>
|
||||
{children}
|
||||
</MemoizedLink>
|
||||
);
|
||||
}
|
||||
return <MemoizedLink>{children}</MemoizedLink>;
|
||||
});
|
||||
);
|
||||
|
||||
export const MemoizedLink = memo((props: any) => {
|
||||
const { node, document, ...rest } = props;
|
||||
const { node, document, updatePresentingDocument, ...rest } = props;
|
||||
const value = rest.children;
|
||||
|
||||
if (value?.toString().startsWith("*")) {
|
||||
@@ -58,22 +66,21 @@ export const MemoizedLink = memo((props: any) => {
|
||||
icon={document?.icon as React.ReactNode}
|
||||
link={rest?.href}
|
||||
document={document as LoadedDanswerDocument}
|
||||
updatePresentingDocument={updatePresentingDocument}
|
||||
>
|
||||
{rest.children}
|
||||
</Citation>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a
|
||||
onMouseDown={() =>
|
||||
rest.href ? window.open(rest.href, "_blank") : undefined
|
||||
}
|
||||
className="cursor-pointer text-link hover:text-link-hover"
|
||||
>
|
||||
{rest.children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
onMouseDown={() => rest.href && window.open(rest.href, "_blank")}
|
||||
className="cursor-pointer text-link hover:text-link-hover"
|
||||
>
|
||||
{rest.children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
export const MemoizedParagraph = memo(
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
import { FeedbackType } from "../types";
|
||||
import React, {
|
||||
memo,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
@@ -21,6 +22,7 @@ import ReactMarkdown from "react-markdown";
|
||||
import {
|
||||
DanswerDocument,
|
||||
FilteredDanswerDocument,
|
||||
LoadedDanswerDocument,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { SearchSummary } from "./SearchSummary";
|
||||
|
||||
@@ -188,6 +190,7 @@ export const AIMessage = ({
|
||||
currentPersona,
|
||||
otherMessagesCanSwitchTo,
|
||||
onMessageSelection,
|
||||
setPresentingDocument,
|
||||
index,
|
||||
}: {
|
||||
index?: number;
|
||||
@@ -218,6 +221,7 @@ export const AIMessage = ({
|
||||
retrievalDisabled?: boolean;
|
||||
overriddenModel?: string;
|
||||
regenerate?: (modelOverRide: LlmOverride) => Promise<void>;
|
||||
setPresentingDocument?: (document: DanswerDocument) => void;
|
||||
}) => {
|
||||
const toolCallGenerating = toolCall && !toolCall.tool_result;
|
||||
const processContent = (content: string | JSX.Element) => {
|
||||
@@ -308,7 +312,12 @@ export const AIMessage = ({
|
||||
|
||||
const anchorCallback = useCallback(
|
||||
(props: any) => (
|
||||
<MemoizedAnchor docs={docs}>{props.children}</MemoizedAnchor>
|
||||
<MemoizedAnchor
|
||||
updatePresentingDocument={setPresentingDocument}
|
||||
docs={docs}
|
||||
>
|
||||
{props.children}
|
||||
</MemoizedAnchor>
|
||||
),
|
||||
[docs]
|
||||
);
|
||||
|
@@ -17,6 +17,8 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoader";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DanswerDocument } from "@/lib/search/interfaces";
|
||||
import TextView from "@/components/chat_search/TextView";
|
||||
|
||||
function BackToDanswerButton() {
|
||||
const router = useRouter();
|
||||
@@ -41,6 +43,9 @@ export function SharedChatDisplay({
|
||||
persona: Persona;
|
||||
}) {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [presentingDocument, setPresentingDocument] =
|
||||
useState<DanswerDocument | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Prism.highlightAll();
|
||||
setIsReady(true);
|
||||
@@ -63,61 +68,70 @@ export function SharedChatDisplay({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full h-[100dvh] overflow-hidden">
|
||||
<div className="flex max-h-full overflow-hidden pb-[72px]">
|
||||
<div className="flex w-full overflow-hidden overflow-y-scroll">
|
||||
<div className="w-full h-full flex-col flex max-w-message-max mx-auto">
|
||||
<div className="px-5 pt-8">
|
||||
<h1 className="text-3xl text-strong font-bold">
|
||||
{chatSession.description ||
|
||||
`Chat ${chatSession.chat_session_id}`}
|
||||
</h1>
|
||||
<p className="text-emphasis">
|
||||
{humanReadableFormat(chatSession.time_created)}
|
||||
</p>
|
||||
<>
|
||||
{presentingDocument && (
|
||||
<TextView
|
||||
presentingDocument={presentingDocument}
|
||||
onClose={() => setPresentingDocument(null)}
|
||||
/>
|
||||
)}
|
||||
<div className="w-full h-[100dvh] overflow-hidden">
|
||||
<div className="flex max-h-full overflow-hidden pb-[72px]">
|
||||
<div className="flex w-full overflow-hidden overflow-y-scroll">
|
||||
<div className="w-full h-full flex-col flex max-w-message-max mx-auto">
|
||||
<div className="px-5 pt-8">
|
||||
<h1 className="text-3xl text-strong font-bold">
|
||||
{chatSession.description ||
|
||||
`Chat ${chatSession.chat_session_id}`}
|
||||
</h1>
|
||||
<p className="text-emphasis">
|
||||
{humanReadableFormat(chatSession.time_created)}
|
||||
</p>
|
||||
|
||||
<Separator />
|
||||
</div>
|
||||
{isReady ? (
|
||||
<div className="w-full pb-16">
|
||||
{messages.map((message) => {
|
||||
if (message.type === "user") {
|
||||
return (
|
||||
<HumanMessage
|
||||
shared
|
||||
key={message.messageId}
|
||||
content={message.message}
|
||||
files={message.files}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<AIMessage
|
||||
shared
|
||||
currentPersona={persona}
|
||||
key={message.messageId}
|
||||
messageId={message.messageId}
|
||||
content={message.message}
|
||||
files={message.files || []}
|
||||
citedDocuments={getCitedDocumentsFromMessage(message)}
|
||||
isComplete
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
<Separator />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grow flex-0 h-screen w-full flex items-center justify-center">
|
||||
<div className="mb-[33vh]">
|
||||
<DanswerInitializingLoader />
|
||||
{isReady ? (
|
||||
<div className="w-full pb-16">
|
||||
{messages.map((message) => {
|
||||
if (message.type === "user") {
|
||||
return (
|
||||
<HumanMessage
|
||||
shared
|
||||
key={message.messageId}
|
||||
content={message.message}
|
||||
files={message.files}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<AIMessage
|
||||
shared
|
||||
setPresentingDocument={setPresentingDocument}
|
||||
currentPersona={persona}
|
||||
key={message.messageId}
|
||||
messageId={message.messageId}
|
||||
content={message.message}
|
||||
files={message.files || []}
|
||||
citedDocuments={getCitedDocumentsFromMessage(message)}
|
||||
isComplete
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="grow flex-0 h-screen w-full flex items-center justify-center">
|
||||
<div className="mb-[33vh]">
|
||||
<DanswerInitializingLoader />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackToDanswerButton />
|
||||
</div>
|
||||
<BackToDanswerButton />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
173
web/src/components/chat_search/TextView.tsx
Normal file
173
web/src/components/chat_search/TextView.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Download, XIcon, ZoomIn, ZoomOut } from "lucide-react";
|
||||
import { DanswerDocument } from "@/lib/search/interfaces";
|
||||
import { MinimalMarkdown } from "./MinimalMarkdown";
|
||||
|
||||
interface TextViewProps {
|
||||
presentingDocument: DanswerDocument;
|
||||
onClose: () => void;
|
||||
}
|
||||
export default function TextView({
|
||||
presentingDocument,
|
||||
onClose,
|
||||
}: TextViewProps) {
|
||||
const [zoom, setZoom] = useState(100);
|
||||
const [fileContent, setFileContent] = useState<string>("");
|
||||
const [fileUrl, setFileUrl] = useState<string>("");
|
||||
const [fileName, setFileName] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [fileType, setFileType] = useState<string>("application/octet-stream");
|
||||
|
||||
const isMarkdownFormat = (mimeType: string): boolean => {
|
||||
const markdownFormats = [
|
||||
"text/markdown",
|
||||
"text/x-markdown",
|
||||
"text/plain",
|
||||
"text/x-rst",
|
||||
"text/x-org",
|
||||
];
|
||||
return markdownFormats.some((format) => mimeType.startsWith(format));
|
||||
};
|
||||
|
||||
const isSupportedIframeFormat = (mimeType: string): boolean => {
|
||||
const supportedFormats = [
|
||||
"application/pdf",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/svg+xml",
|
||||
];
|
||||
return supportedFormats.some((format) => mimeType.startsWith(format));
|
||||
};
|
||||
|
||||
const fetchFile = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
const fileId = presentingDocument.document_id.split("__")[1];
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/chat/file/${encodeURIComponent(fileId)}`,
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
setFileUrl(url);
|
||||
setFileName(presentingDocument.semantic_identifier || "document");
|
||||
const contentType =
|
||||
response.headers.get("Content-Type") || "application/octet-stream";
|
||||
setFileType(contentType);
|
||||
|
||||
if (isMarkdownFormat(blob.type)) {
|
||||
const text = await blob.text();
|
||||
setFileContent(text);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching file:", error);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
}
|
||||
}, [presentingDocument]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchFile();
|
||||
}, [fetchFile]);
|
||||
|
||||
const handleDownload = () => {
|
||||
const link = document.createElement("a");
|
||||
link.href = fileUrl;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const handleZoomIn = () => setZoom((prev) => Math.min(prev + 25, 200));
|
||||
const handleZoomOut = () => setZoom((prev) => Math.max(prev - 25, 100));
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
hideCloseIcon
|
||||
className="max-w-5xl w-[90vw] flex flex-col justify-between gap-y-0 h-full max-h-[80vh] p-0"
|
||||
>
|
||||
<DialogHeader className="px-4 mb-0 pt-2 pb-3 flex flex-row items-center justify-between border-b">
|
||||
<DialogTitle className="text-lg font-medium truncate">
|
||||
{fileName}
|
||||
</DialogTitle>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="ghost" size="icon" onClick={handleZoomOut}>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
<span className="sr-only">Zoom Out</span>
|
||||
</Button>
|
||||
<span className="text-sm">{zoom}%</span>
|
||||
<Button variant="ghost" size="icon" onClick={handleZoomIn}>
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
<span className="sr-only">Zoom In</span>
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={handleDownload}>
|
||||
<Download className="h-4 w-4" />
|
||||
<span className="sr-only">Download</span>
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => onClose()}>
|
||||
<XIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="mt-0 rounded-b-lg flex-1 overflow-hidden">
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-primary"></div>
|
||||
<p className="mt-6 text-lg font-medium text-muted-foreground">
|
||||
Loading document...
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`w-full h-full transform origin-center transition-transform duration-300 ease-in-out`}
|
||||
style={{ transform: `scale(${zoom / 100})` }}
|
||||
>
|
||||
{isSupportedIframeFormat(fileType) ? (
|
||||
<iframe
|
||||
src={`${fileUrl}#toolbar=0`}
|
||||
className="w-full h-full border-none"
|
||||
title="File Viewer"
|
||||
/>
|
||||
) : isMarkdownFormat(fileType) ? (
|
||||
<div className="w-full h-full p-6 overflow-y-scroll overflow-x-hidden">
|
||||
<MinimalMarkdown
|
||||
content={fileContent}
|
||||
className="w-full pb-4 h-full text-lg text-wrap break-words"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-lg font-medium text-muted-foreground">
|
||||
This file format is not supported for preview.
|
||||
</p>
|
||||
<Button className="mt-4" onClick={handleDownload}>
|
||||
Download File
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@@ -19,6 +19,7 @@ import { FiTag } from "react-icons/fi";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
import { CustomTooltip, TooltipGroup } from "../tooltip/CustomTooltip";
|
||||
import { WarningCircle } from "@phosphor-icons/react";
|
||||
import TextView from "../chat_search/TextView";
|
||||
import { SearchResultIcon } from "../SearchResultIcon";
|
||||
|
||||
export const buildDocumentSummaryDisplay = (
|
||||
@@ -188,6 +189,12 @@ export const DocumentDisplay = ({
|
||||
const relevance_explanation =
|
||||
document.relevance_explanation ?? additional_relevance?.content;
|
||||
const settings = useContext(SettingsContext);
|
||||
const [presentingDocument, setPresentingDocument] =
|
||||
useState<DanswerDocument | null>(null);
|
||||
|
||||
const handleViewFile = async () => {
|
||||
setPresentingDocument(document);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -219,19 +226,22 @@ export const DocumentDisplay = ({
|
||||
}`}
|
||||
>
|
||||
<div className="flex relative">
|
||||
<a
|
||||
className={`rounded-lg flex font-bold text-link max-w-full ${
|
||||
document.link ? "" : "pointer-events-none"
|
||||
}`}
|
||||
href={document.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded-lg flex font-bold text-link max-w-full`}
|
||||
onClick={() => {
|
||||
if (document.link) {
|
||||
window.open(document.link, "_blank");
|
||||
} else {
|
||||
handleViewFile();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SourceIcon sourceType={document.source_type} iconSize={22} />
|
||||
<p className="truncate text-wrap break-all ml-2 my-auto line-clamp-1 text-base max-w-full">
|
||||
{document.semantic_identifier || document.document_id}
|
||||
</p>
|
||||
</a>
|
||||
</button>
|
||||
<div className="ml-auto flex items-center">
|
||||
<TooltipGroup>
|
||||
{isHovered && messageId && (
|
||||
@@ -270,6 +280,13 @@ export const DocumentDisplay = ({
|
||||
<DocumentMetadataBlock document={document} />
|
||||
</div>
|
||||
|
||||
{presentingDocument && (
|
||||
<TextView
|
||||
presentingDocument={presentingDocument}
|
||||
onClose={() => setPresentingDocument(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<p
|
||||
style={{ transition: "height 0.30s ease-in-out" }}
|
||||
className="pl-1 pt-2 pb-3 break-words text-wrap"
|
||||
@@ -297,11 +314,14 @@ export const AgenticDocumentDisplay = ({
|
||||
setPopup,
|
||||
}: DocumentDisplayProps) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [presentingDocument, setPresentingDocument] =
|
||||
useState<DanswerDocument | null>(null);
|
||||
|
||||
const [alternativeToggled, setAlternativeToggled] = useState(false);
|
||||
|
||||
const relevance_explanation =
|
||||
document.relevance_explanation ?? additional_relevance?.content;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={document.semantic_identifier}
|
||||
@@ -320,19 +340,24 @@ export const AgenticDocumentDisplay = ({
|
||||
}`}
|
||||
>
|
||||
<div className="flex relative">
|
||||
<a
|
||||
<button
|
||||
type="button"
|
||||
className={`rounded-lg flex font-bold text-link max-w-full ${
|
||||
document.link ? "" : "pointer-events-none"
|
||||
}`}
|
||||
href={document.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => {
|
||||
if (document.link) {
|
||||
window.open(document.link, "_blank");
|
||||
} else {
|
||||
setPresentingDocument(document);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SourceIcon sourceType={document.source_type} iconSize={22} />
|
||||
<p className="truncate text-wrap break-all ml-2 my-auto line-clamp-1 text-base max-w-full">
|
||||
{document.semantic_identifier || document.document_id}
|
||||
</p>
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<div className="ml-auto items-center flex">
|
||||
<TooltipGroup>
|
||||
@@ -365,6 +390,12 @@ export const AgenticDocumentDisplay = ({
|
||||
<div className="mt-1">
|
||||
<DocumentMetadataBlock document={document} />
|
||||
</div>
|
||||
{presentingDocument && (
|
||||
<TextView
|
||||
presentingDocument={presentingDocument}
|
||||
onClose={() => setPresentingDocument(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="pt-2 break-words flex gap-x-2">
|
||||
<p
|
||||
|
@@ -13,12 +13,14 @@ export function Citation({
|
||||
link,
|
||||
document,
|
||||
index,
|
||||
updatePresentingDocument,
|
||||
icon,
|
||||
url,
|
||||
}: {
|
||||
link?: string;
|
||||
children?: JSX.Element | string | null | ReactNode;
|
||||
index?: number;
|
||||
updatePresentingDocument: (documentIndex: LoadedDanswerDocument) => void;
|
||||
document: LoadedDanswerDocument;
|
||||
icon?: React.ReactNode;
|
||||
url?: string;
|
||||
@@ -33,7 +35,13 @@ export function Citation({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
onMouseDown={() => window.open(link, "_blank")}
|
||||
onMouseDown={() => {
|
||||
if (!link) {
|
||||
updatePresentingDocument(document);
|
||||
} else {
|
||||
window.open(link, "_blank");
|
||||
}
|
||||
}}
|
||||
className="inline-flex items-center ml-1 cursor-pointer transition-all duration-200 ease-in-out"
|
||||
>
|
||||
<span className="relative min-w-[1.4rem] text-center no-underline -top-0.5 px-1.5 py-0.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-full border border-gray-300 hover:bg-gray-200 hover:text-gray-900 shadow-sm no-underline">
|
||||
@@ -53,7 +61,13 @@ export function Citation({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
onMouseDown={() => window.open(link, "_blank")}
|
||||
onMouseDown={() => {
|
||||
if (!link) {
|
||||
updatePresentingDocument(document);
|
||||
} else {
|
||||
window.open(link, "_blank");
|
||||
}
|
||||
}}
|
||||
className="inline-flex items-center ml-1 cursor-pointer transition-all duration-200 ease-in-out"
|
||||
>
|
||||
<span className="relative min-w-[1.4rem] pchatno-underline -top-0.5 px-1.5 py-0.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-full border border-gray-300 hover:bg-gray-200 hover:text-gray-900 shadow-sm no-underline">
|
||||
|
125
web/src/components/ui/dialog.tsx
Normal file
125
web/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||
hideCloseIcon?: boolean;
|
||||
}
|
||||
>(({ className, children, hideCloseIcon = false, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-neutral-800 dark:bg-neutral-950",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{!hideCloseIcon && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
Reference in New Issue
Block a user