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:
pablodanswer
2024-12-02 14:43:53 -08:00
committed by GitHub
parent 2783fa08a3
commit 03e2789392
17 changed files with 660 additions and 121 deletions

View File

@@ -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

View File

@@ -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>
);

View File

@@ -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(

View File

@@ -644,7 +644,6 @@ export async function useScrollonStream({
}: {
chatState: ChatState;
scrollableDivRef: RefObject<HTMLDivElement>;
waitForScrollRef: RefObject<boolean>;
scrollDist: MutableRefObject<number>;
endDivRef: RefObject<HTMLDivElement>;
debounceNumber: number;

View File

@@ -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(

View File

@@ -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]
);

View File

@@ -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>
</>
);
}

View 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>
);
}

View File

@@ -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

View File

@@ -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">

View 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,
};