Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Excel viewer #87

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions examples/rag-ui/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 37 additions & 14 deletions examples/rag-ui/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ApplicationLayout from "./components/ApplicationLayout.tsx";
import ApplicationMainContent from "./components/ApplicationMainContent.tsx";
import {FC, useEffect, useState} from "react"
import {ActionHandlerResponse, createTheme, ErrorDisplay, Message, RAGProvider, Source, UserAction} from "lexio";
import {ActionHandlerResponse, createTheme, ErrorDisplay, Message, LexioProvider, Source, UserAction} from "lexio";
import {v4 as uuid} from "uuid";

const customTheme = createTheme({
Expand All @@ -13,17 +13,18 @@ const customTheme = createTheme({

const App: FC = () => {
const { sources: mockSources } = useMockData();

const mockActionResult: ActionHandlerResponse = {
response: Promise.resolve("This is what jarvis is about"),
sources: Promise.resolve<Source[]>(mockSources),
}
// @ts-ignore
const handleAction = (actionHandlerFunction: UserAction, messages: Message[], sources: Source[], activeSources: Source[], selectedSource: (Source | null)): ActionHandlerResponse | undefined => {
return {
response: Promise.resolve("This is what jarvis is about"),
messages: [""],
sources: Promise.resolve<Source[]>(mockSources),
};
return mockActionResult;
}

return (
<RAGProvider
<LexioProvider
onAction={handleAction}
config={undefined}
theme={customTheme}
Expand All @@ -32,34 +33,42 @@ const App: FC = () => {
<ApplicationMainContent />
<ErrorDisplay />
</ApplicationLayout>
</RAGProvider>
</LexioProvider>
);
}

type MockData = {
sources: Source[];
}
const useMockData = (): MockData => {
const [jarvisPdfBufferData, setJarvisPdfBufferData] = useState<ArrayBuffer | undefined>(undefined);
const [dummyPdfBufferData, setDummyPdfBufferData] = useState<ArrayBuffer | undefined>(undefined);
const [jarvisPdfBuffer, setJarvisPdfBuffer] = useState<ArrayBuffer | undefined>(undefined);
const [dummyPdfBuffer, setDummyPdfBuffer] = useState<ArrayBuffer | undefined>(undefined);
const [excelSampleBuffer, setExcelSampleBuffer] = useState<ArrayBuffer | undefined>(undefined);

// Pdf content mocks
useEffect(() => {
// Jarvis pdf
const getJarvisPdfSource = async () => {
const response = await fetch("http://localhost:5173/Current_State_Of_LLM-based_Assistants_For_Engineering.pdf");
const data = await response.arrayBuffer() as Uint8Array<ArrayBufferLike>;
setJarvisPdfBufferData(data);
setJarvisPdfBuffer(data);
}
// Dummy pdf
const getDummyPdfSource = async () => {
const response = await fetch("http://localhost:5173/dummy.pdf");
const data = await response.arrayBuffer() as Uint8Array<ArrayBufferLike>;
setDummyPdfBufferData(data);
setDummyPdfBuffer(data);
}
// Excel file
const getExcelSampleSource = async () => {
const response = await fetch("http://localhost:5173/excel_sample.xlsx");
const data = await response.arrayBuffer();
setExcelSampleBuffer(data);
}

getJarvisPdfSource();
getDummyPdfSource();
getExcelSampleSource();
}, []);

const sources: Source[] = [
Expand All @@ -69,7 +78,7 @@ const useMockData = (): MockData => {
title: "Current_State_Of_LLM-based_Assistants_For_Engineering.pdf",
type: "pdf",
relevance: 40,
data: new Uint8Array(jarvisPdfBufferData as ArrayBuffer),
data: new Uint8Array(jarvisPdfBuffer as ArrayBuffer),
metadata: {
page: 4,
}
Expand All @@ -79,11 +88,25 @@ const useMockData = (): MockData => {
title: "dummy.pdf",
type: "pdf",
relevance: 30,
data: new Uint8Array(dummyPdfBufferData as ArrayBuffer),
data: new Uint8Array(dummyPdfBuffer as ArrayBuffer),
metadata: {
page: 1,
}
},
// Excel Sources
{
id: uuid(),
title: "excel_sample.xlsx",
type: "xlsx",
relevance: 30,
data: excelSampleBuffer,
rangesHighlights: [
{
sheetName: "Equipment List",
ranges: ["A1:B8", "B9:C15", "C18:E20", "D5:F15", "F9:H15", "B42:E50"],
}
],
},
] as Source[];

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ const ApplicationMainContent: FunctionComponent = () => {
const gridColumns = "grid-cols-[1fr_max-content_1fr]";

return (
<div className={`grid gap-4 ${gridColumns} h-full max-w-[1700px]`}>
<div
className={`grid gap-4 ${gridColumns} h-full`}
style={{
maxWidth: "var(--app-main-content-max-width)",
}}
>
<CardContainer className={"p-1 border-none overflow-hidden gap-0"}>
<div className={"overflow-auto pt-4"}>
<ChatWindow />
Expand Down
111 changes: 76 additions & 35 deletions lexio/lib/components/ContentDisplay/ContentDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useContext } from 'react';
import React, {FC, useContext} from "react";
import { PdfViewer } from "../Viewers/PdfViewer";
import { HtmlViewer } from "../Viewers/HtmlViewer";
import { MarkdownViewer } from "../Viewers/MarkdownViewer";
import { useSources } from "../../hooks";
import { ThemeContext, removeUndefined } from "../../theme/ThemeContext";
import {SpreadsheetViewer} from "../Viewers/SpreadsheetViewer";
import {Source} from "../../types.ts";

export interface ContentDisplayStyles extends React.CSSProperties {
backgroundColor?: string;
Expand All @@ -19,22 +21,23 @@ interface ContentDisplayProps {

/**
* A component for displaying various content types from selected sources.
*
*
* ContentDisplay renders the content of the currently selected source, automatically
* choosing the appropriate viewer based on the source type (PDF, HTML, or Markdown).
*
* choosing the appropriate viewer based on the source type (PDF, HTML, Markdown or Excel spreadsheet).
*
* @component
*
*
* Features:
* - Automatic content type detection
* - PDF viewer with navigation, zoom, and highlight support
* - HTML viewer with sanitization and styling
* - Markdown viewer with formatting
* - Excel spreadsheet viewer with range highlighting
* - Loading state indication
* - Responsive design
*
*
* @example
*
*
* ```tsx
* <ContentDisplay
* componentKey="main-content"
Expand All @@ -51,8 +54,8 @@ const ContentDisplay: React.FC<ContentDisplayProps> = ({
styleOverrides = {},
componentKey = undefined,
}) => {
const { selectedSource } = useSources(componentKey ? `ContentDisplay-${componentKey}` : 'ContentDisplay');
const { selectedSource, sources, selectedSourceId } = useSources(componentKey ? `ContentDisplay-${componentKey}` : 'ContentDisplay');

// use theme
const theme = useContext(ThemeContext);
if (!theme) {
Expand All @@ -69,44 +72,82 @@ const ContentDisplay: React.FC<ContentDisplayProps> = ({
...removeUndefined(styleOverrides),
};

const source = selectedSourceId ? (sources.find((s) => s.id === selectedSourceId) ?? undefined) : undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the selectedSource directly? no need to look it up in sources

const filename = source?.title ?? "";

if (!selectedSource) {
return null;
}

const renderContent = () => {
if (selectedSource.type === 'pdf' && selectedSource.data && selectedSource.data instanceof Uint8Array) {
// Prefer 'page' over '_page' if both are defined
const page = selectedSource.metadata?.page ?? selectedSource.metadata?._page;

return (
<PdfViewer
data={selectedSource.data}
page={page}
highlights={selectedSource.highlights}
return (
<div className="w-full h-full overflow-hidden" style={style}>
<FileViewerRenderer
fileName={filename}
selectedSource={selectedSource}
/>
</div>
);
};

type PropsFileViewerRenderer = {
fileName: string;
selectedSource: Source;
}
const FileViewerRenderer: FC<PropsFileViewerRenderer> = (props) => {
const {
fileName,
selectedSource,
} = props;

if (selectedSource.type === 'pdf' && selectedSource.data && selectedSource.data instanceof Uint8Array) {
// Prefer 'page' over '_page' if both are defined
const page = selectedSource.metadata?.page ?? selectedSource.metadata?._page;

return (
<PdfViewer
data={selectedSource.data}
page={page}
highlights={selectedSource.highlights}
/>
);
}
);
}

if (selectedSource.type === 'html' && selectedSource.data && typeof selectedSource.data === 'string') {
return <HtmlViewer htmlContent={selectedSource.data} />;
}
if (selectedSource.type === 'html' && selectedSource.data && typeof selectedSource.data === 'string') {
return <HtmlViewer htmlContent={selectedSource.data} />;
}

if (selectedSource.type === 'markdown' && selectedSource.data && typeof selectedSource.data === 'string') {
return <MarkdownViewer markdownContent={selectedSource.data} />;
}

if (selectedSource.type === 'markdown' && selectedSource.data && typeof selectedSource.data === 'string') {
return <MarkdownViewer markdownContent={selectedSource.data} />;
}
if (selectedSource.type === 'text' && selectedSource.data && typeof selectedSource.data === 'string') {
return <MarkdownViewer markdownContent={selectedSource.data} />;
}

if (selectedSource.type === 'text' && selectedSource.data && typeof selectedSource.data === 'string') {
return <MarkdownViewer markdownContent={selectedSource.data} />;
}
if (
selectedSource.type === "xlsx" &&
selectedSource.data &&
selectedSource.data instanceof ArrayBuffer
) {
const {data, rangesHighlights} = selectedSource;
const ranges = rangesHighlights?.map((h) => h.ranges).flat();
const defaultSheetName = rangesHighlights ? rangesHighlights[0].sheetName : undefined;

return (
<SpreadsheetViewer
fileName={fileName}
fileBufferArray={data as ArrayBuffer}
rangesToHighlight={ranges}
defaultSelectedSheet={defaultSheetName}
/>
)
}

return (
<div className="flex justify-center items-center w-full h-full">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
);
};

return <div className="w-full h-full" style={style}>{renderContent()}</div>;
};
);
}

export { ContentDisplay };
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {FC, ReactNode, useEffect, useRef, useState} from "react";

export type ContainerSize = {
width: number;
height: number;
}

const DEFAULT_PARENT_SIZE: ContainerSize = {
width: 0,
height: 0,
};

type Props = {
className?: string;
children?: ((parentSize: ContainerSize) => ReactNode) | undefined;
}
const ParentSizeObserver: FC<Props> = (props) => {

const [parentSize, setParentSize] = useState<ContainerSize>(DEFAULT_PARENT_SIZE);

const parentRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const observeParent = () => {
if (!parentRef.current) return DEFAULT_PARENT_SIZE;

const resizeObserver = new ResizeObserver(entries => {
entries.forEach((entry) => {
const { width, height } = entry.contentRect;
setParentSize({ width, height });
})
});

resizeObserver.observe(parentRef.current);

return () => {
resizeObserver.disconnect();
};
};

return () => {
observeParent();
}
}, []);

return (
<div ref={parentRef} className={props.className}>
{props.children ? props.children(parentSize) : null}
</div>
);
}
ParentSizeObserver.displayName = "ParentSizeObserver";

export default ParentSizeObserver;
Loading