mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
feat(web): add inline PDF preview support for skill file viewer
Enable PDF files to be previewed directly in the file content panel instead of showing as unsupported files requiring download. Uses the existing react-pdf-highlighter library with zoom controls and keyboard shortcuts (up/down arrows).
This commit is contained in:
@ -3,6 +3,7 @@ import {
|
||||
getFileExtension,
|
||||
isImageFile,
|
||||
isMarkdownFile,
|
||||
isPdfFile,
|
||||
isSQLiteFile,
|
||||
isTextLikeFile,
|
||||
isVideoFile,
|
||||
@ -13,6 +14,7 @@ export type FileTypeInfo = {
|
||||
isCodeOrText: boolean
|
||||
isImage: boolean
|
||||
isVideo: boolean
|
||||
isPdf: boolean
|
||||
isSQLite: boolean
|
||||
isEditable: boolean
|
||||
isMediaFile: boolean
|
||||
@ -27,6 +29,7 @@ export function useFileTypeInfo(fileNode: { name: string, extension?: string | n
|
||||
isCodeOrText: false,
|
||||
isImage: false,
|
||||
isVideo: false,
|
||||
isPdf: false,
|
||||
isSQLite: false,
|
||||
isEditable: false,
|
||||
isMediaFile: false,
|
||||
@ -38,6 +41,7 @@ export function useFileTypeInfo(fileNode: { name: string, extension?: string | n
|
||||
const markdown = isMarkdownFile(ext)
|
||||
const image = isImageFile(ext)
|
||||
const video = isVideoFile(ext)
|
||||
const pdf = isPdfFile(ext)
|
||||
const sqlite = isSQLiteFile(ext)
|
||||
const editable = isTextLikeFile(ext)
|
||||
const codeOrText = editable && !markdown
|
||||
@ -47,10 +51,11 @@ export function useFileTypeInfo(fileNode: { name: string, extension?: string | n
|
||||
isCodeOrText: codeOrText,
|
||||
isImage: image,
|
||||
isVideo: video,
|
||||
isPdf: pdf,
|
||||
isSQLite: sqlite,
|
||||
isEditable: editable,
|
||||
isMediaFile: image || video,
|
||||
isPreviewable: editable || image || video || sqlite,
|
||||
isPreviewable: editable || image || video || pdf || sqlite,
|
||||
}
|
||||
}, [fileNode?.name, fileNode?.extension])
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ const CODE_EXTENSIONS = new Set(['json', 'yaml', 'yml', 'toml', 'js', 'jsx', 'ts
|
||||
const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff', 'psd', 'heic', 'heif', 'avif'])
|
||||
const VIDEO_EXTENSIONS = new Set(['mp4', 'mov', 'webm', 'mpeg', 'mpg', 'm4v', 'avi', 'mkv', 'flv', 'wmv', '3gp'])
|
||||
const SQLITE_EXTENSIONS = new Set(['db', 'sqlite', 'sqlite3'])
|
||||
const PDF_EXTENSIONS_SET = new Set(['pdf'])
|
||||
|
||||
const BINARY_EXTENSIONS = new Set([
|
||||
'mp3',
|
||||
@ -46,7 +47,6 @@ const BINARY_EXTENSIONS = new Set([
|
||||
'msi',
|
||||
'deb',
|
||||
'rpm',
|
||||
'pdf',
|
||||
'doc',
|
||||
'docx',
|
||||
'xls',
|
||||
@ -87,7 +87,6 @@ export function getFileExtension(name?: string, extension?: string): string {
|
||||
}
|
||||
|
||||
const AUDIO_EXTENSIONS = ['mp3', 'm4a', 'wav', 'amr', 'mpga', 'ogg', 'flac', 'aac', 'wma', 'aiff', 'opus']
|
||||
const PDF_EXTENSIONS = ['pdf']
|
||||
const EXCEL_EXTENSIONS = ['xlsx', 'xls', 'csv']
|
||||
const WORD_EXTENSIONS = ['doc', 'docx']
|
||||
const PPT_EXTENSIONS = ['ppt', 'pptx']
|
||||
@ -98,7 +97,7 @@ const EXTENSION_TO_ICON_TYPE = new Map<string, FileAppearanceTypeEnum>(
|
||||
[IMAGE_EXTENSIONS, FileAppearanceTypeEnum.image],
|
||||
[VIDEO_EXTENSIONS, FileAppearanceTypeEnum.video],
|
||||
[AUDIO_EXTENSIONS, FileAppearanceTypeEnum.audio],
|
||||
[PDF_EXTENSIONS, FileAppearanceTypeEnum.pdf],
|
||||
[PDF_EXTENSIONS_SET, FileAppearanceTypeEnum.pdf],
|
||||
[MARKDOWN_EXTENSIONS, FileAppearanceTypeEnum.markdown],
|
||||
[EXCEL_EXTENSIONS, FileAppearanceTypeEnum.excel],
|
||||
[WORD_EXTENSIONS, FileAppearanceTypeEnum.word],
|
||||
@ -139,6 +138,10 @@ export function isSQLiteFile(extension: string): boolean {
|
||||
return SQLITE_EXTENSIONS.has(extension)
|
||||
}
|
||||
|
||||
export function isPdfFile(extension: string): boolean {
|
||||
return PDF_EXTENSIONS_SET.has(extension)
|
||||
}
|
||||
|
||||
export function getFileLanguage(name: string): string {
|
||||
const extension = name.split('.').pop()?.toLowerCase() ?? ''
|
||||
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
|
||||
import { RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { PdfHighlighter, PdfLoader } from 'react-pdf-highlighter'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import 'react-pdf-highlighter/dist/style.css'
|
||||
|
||||
type PdfFilePreviewProps = {
|
||||
downloadUrl: string
|
||||
}
|
||||
|
||||
const PdfFilePreview = ({ downloadUrl }: PdfFilePreviewProps) => {
|
||||
const [scale, setScale] = useState(1)
|
||||
|
||||
const zoomIn = () => {
|
||||
setScale(prevScale => Math.min(prevScale * 1.2, 3))
|
||||
}
|
||||
|
||||
const zoomOut = () => {
|
||||
setScale(prevScale => Math.max(prevScale / 1.2, 0.5))
|
||||
}
|
||||
|
||||
useHotkeys('up', zoomIn)
|
||||
useHotkeys('down', zoomOut)
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<div className="absolute right-4 top-4 z-10 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={zoomOut}
|
||||
className="flex size-8 cursor-pointer items-center justify-center rounded-lg bg-components-panel-bg shadow-md hover:bg-state-base-hover"
|
||||
aria-label="Zoom out"
|
||||
>
|
||||
<RiZoomOutLine className="size-4 text-text-tertiary" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={zoomIn}
|
||||
className="flex size-8 cursor-pointer items-center justify-center rounded-lg bg-components-panel-bg shadow-md hover:bg-state-base-hover"
|
||||
aria-label="Zoom in"
|
||||
>
|
||||
<RiZoomInLine className="size-4 text-text-tertiary" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full overflow-auto">
|
||||
<div
|
||||
className="min-h-full p-6"
|
||||
style={{ transform: `scale(${scale})`, transformOrigin: 'top center' }}
|
||||
>
|
||||
<PdfLoader
|
||||
workerSrc="/pdf.worker.min.mjs"
|
||||
url={downloadUrl}
|
||||
beforeLoad={(
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loading type="app" />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{pdfDocument => (
|
||||
<PdfHighlighter
|
||||
pdfDocument={pdfDocument}
|
||||
enableAreaSelection={() => false}
|
||||
scrollRef={noop}
|
||||
onScrollChange={noop}
|
||||
onSelectionFinished={() => null}
|
||||
highlightTransform={() => <div />}
|
||||
highlights={[]}
|
||||
/>
|
||||
)}
|
||||
</PdfLoader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(PdfFilePreview)
|
||||
@ -24,6 +24,11 @@ const SQLiteFilePreview = dynamic(
|
||||
{ ssr: false, loading: () => <Loading type="area" /> },
|
||||
)
|
||||
|
||||
const PdfFilePreview = dynamic(
|
||||
() => import('./pdf-file-preview'),
|
||||
{ ssr: false, loading: () => <Loading type="area" /> },
|
||||
)
|
||||
|
||||
type ReadOnlyFilePreviewProps = {
|
||||
downloadUrl: string
|
||||
fileName: string
|
||||
@ -41,7 +46,7 @@ const ReadOnlyFilePreview = ({
|
||||
() => ({ name: fileName, extension }),
|
||||
[fileName, extension],
|
||||
)
|
||||
const { isMarkdown, isCodeOrText, isImage, isVideo, isSQLite, isPreviewable } = useFileTypeInfo(fileNode)
|
||||
const { isMarkdown, isCodeOrText, isImage, isVideo, isPdf, isSQLite, isPreviewable } = useFileTypeInfo(fileNode)
|
||||
const isTextFile = isPreviewable && (isMarkdown || isCodeOrText)
|
||||
const { data: textContent, isLoading: isTextLoading } = useFetchTextContent(
|
||||
isTextFile ? downloadUrl : undefined,
|
||||
@ -78,6 +83,9 @@ const ReadOnlyFilePreview = ({
|
||||
if (isSQLite)
|
||||
return <SQLiteFilePreview downloadUrl={downloadUrl} />
|
||||
|
||||
if (isPdf)
|
||||
return <PdfFilePreview downloadUrl={downloadUrl} />
|
||||
|
||||
return (
|
||||
<UnsupportedFileDownload
|
||||
name={fileName}
|
||||
|
||||
Reference in New Issue
Block a user