diff --git a/web/app/components/workflow/skill/file-content-panel.tsx b/web/app/components/workflow/skill/file-content-panel.tsx
index 0d619b95a3..cb465090dc 100644
--- a/web/app/components/workflow/skill/file-content-panel.tsx
+++ b/web/app/components/workflow/skill/file-content-panel.tsx
@@ -3,6 +3,7 @@
import type { OnMount } from '@monaco-editor/react'
import type { FC } from 'react'
import { loader } from '@monaco-editor/react'
+import dynamic from 'next/dynamic'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -22,6 +23,11 @@ import { getFileLanguage } from './utils/file-utils'
import MediaFilePreview from './viewer/media-file-preview'
import UnsupportedFileDownload from './viewer/unsupported-file-download'
+const SQLiteFilePreview = dynamic(
+ () => import('./viewer/sqlite-file-preview'),
+ { ssr: false, loading: () => },
+)
+
if (typeof window !== 'undefined')
loader.config({ paths: { vs: `${window.location.origin}${basePath}/vs` } })
@@ -43,7 +49,7 @@ const FileContentPanel: FC = () => {
const currentFileNode = activeTabId ? nodeMap?.get(activeTabId) : undefined
- const { isMarkdown, isCodeOrText, isImage, isVideo, isEditable } = useFileTypeInfo(currentFileNode)
+ const { isMarkdown, isCodeOrText, isImage, isVideo, isSQLite, isEditable } = useFileTypeInfo(currentFileNode)
const { fileContent, downloadUrlData, isLoading, error } = useSkillFileData(appId, activeTabId, isEditable)
@@ -149,11 +155,11 @@ const FileContentPanel: FC = () => {
)
}
- // For non-editable files (media, unsupported), use download URL
+ // For non-editable files (media, sqlite, unsupported), use download URL
const downloadUrl = downloadUrlData?.download_url || ''
const fileName = currentFileNode?.name || ''
const fileSize = currentFileNode?.size
- const isUnsupportedFile = !isMarkdown && !isCodeOrText && !isImage && !isVideo
+ const isUnsupportedFile = !isMarkdown && !isCodeOrText && !isImage && !isVideo && !isSQLite
return (
@@ -186,6 +192,14 @@ const FileContentPanel: FC = () => {
/>
)
: null}
+ {isSQLite
+ ? (
+
+ )
+ : null}
{isUnsupportedFile
? (
Promise
+}
+
+type SQLiteModuleType = typeof import('wa-sqlite')
+type SQLiteAPI = ReturnType
+type SQLiteVFS = Parameters[0]
+
+type SQLiteClient = {
+ sqlite3: ReturnType
+ sqlite: SQLiteModuleType
+ vfs: MemoryVFS
+}
+
+type SQLiteState = {
+ tables: string[]
+ isLoading: boolean
+ error: Error | null
+}
+
+type SQLiteAction
+ = | { type: 'reset' }
+ | { type: 'loading' }
+ | { type: 'success', tables: string[] }
+ | { type: 'error', error: Error }
+
+const TABLES_QUERY = 'SELECT name FROM sqlite_master WHERE type=\'table\' AND name NOT LIKE \'sqlite_%\' ORDER BY name'
+const DEFAULT_ROW_LIMIT = 200
+
+let sqliteClientPromise: Promise | null = null
+
+function createTempFileName(): string {
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto)
+ return `preview-${crypto.randomUUID()}.db`
+ return `preview-${Date.now()}-${Math.random().toString(16).slice(2)}.db`
+}
+
+async function getSQLiteClient(): Promise {
+ if (!sqliteClientPromise) {
+ sqliteClientPromise = (async () => {
+ const [{ default: SQLiteESMFactory }, sqlite, { MemoryVFS }] = await Promise.all([
+ import('wa-sqlite/dist/wa-sqlite.mjs'),
+ import('wa-sqlite'),
+ import('wa-sqlite/src/examples/MemoryVFS.js'),
+ ])
+ const sqliteModule = await SQLiteESMFactory()
+ const sqlite3 = sqlite.Factory(sqliteModule)
+ const vfs = new MemoryVFS()
+ sqlite3.vfs_register(vfs as unknown as SQLiteVFS, false)
+ return {
+ sqlite3,
+ sqlite,
+ vfs,
+ }
+ })()
+ }
+ return sqliteClientPromise
+}
+
+export function useSQLiteDatabase(downloadUrl: string | undefined): UseSQLiteDatabaseResult {
+ const [state, dispatch] = useReducer((current: SQLiteState, action: SQLiteAction): SQLiteState => {
+ switch (action.type) {
+ case 'reset':
+ return {
+ tables: [],
+ isLoading: false,
+ error: null,
+ }
+ case 'loading':
+ return {
+ ...current,
+ isLoading: true,
+ error: null,
+ tables: [],
+ }
+ case 'success':
+ return {
+ tables: action.tables,
+ isLoading: false,
+ error: null,
+ }
+ case 'error':
+ return {
+ tables: [],
+ isLoading: false,
+ error: action.error,
+ }
+ default:
+ return current
+ }
+ }, {
+ tables: [],
+ isLoading: false,
+ error: null,
+ })
+ const dbRef = useRef(null)
+ const fileRef = useRef(null)
+ const clientRef = useRef(null)
+ const cacheRef = useRef