diff --git a/smoosense-gui/src/lib/utils/cellRenderers/BboxCellRenderer.tsx b/smoosense-gui/src/lib/utils/cellRenderers/BboxCellRenderer.tsx
index 6095f48..df8ad30 100644
--- a/smoosense-gui/src/lib/utils/cellRenderers/BboxCellRenderer.tsx
+++ b/smoosense-gui/src/lib/utils/cellRenderers/BboxCellRenderer.tsx
@@ -17,6 +17,7 @@ const BboxCellRenderer = memo(function BboxCellRenderer({
value,
nodeData
}: BboxCellRendererProps) {
+ const tablePath = useAppSelector((state) => state.ui.tablePath)
const baseUrl = useAppSelector((state) => state.ui.baseUrl)
// Handle empty or invalid values
@@ -38,12 +39,12 @@ const BboxCellRenderer = memo(function BboxCellRenderer({
return
}
- // Construct the viz-bbox.html URL with image URL and baseUrl
- if (!baseUrl) {
+ // Construct the viz-bbox.html URL with image URL, tablePath, and baseUrl
+ if (!baseUrl || !tablePath) {
return
}
- const vizUrl = buildBboxVizUrl(imageUrl, [bbox], baseUrl)
+ const vizUrl = buildBboxVizUrl(imageUrl, [bbox], tablePath, baseUrl)
// Use IFrameCellRenderer to display the viz
return
diff --git a/smoosense-gui/src/lib/utils/cellRenderers/ImageCellRenderer.tsx b/smoosense-gui/src/lib/utils/cellRenderers/ImageCellRenderer.tsx
index b623baa..0cc3afc 100644
--- a/smoosense-gui/src/lib/utils/cellRenderers/ImageCellRenderer.tsx
+++ b/smoosense-gui/src/lib/utils/cellRenderers/ImageCellRenderer.tsx
@@ -4,6 +4,8 @@ import { memo } from 'react'
import CellPopover from '@/components/ui/CellPopover'
import ImageBlock from '@/components/common/ImageBlock'
import { isNil } from 'lodash'
+import { mayResolveUrl } from '../mediaUrlUtils'
+import { useAppSelector } from '@/lib/hooks'
interface ImageCellRendererProps {
value: unknown
@@ -12,7 +14,12 @@ interface ImageCellRendererProps {
const ImageCellRenderer = memo(function ImageCellRenderer({
value
}: ImageCellRendererProps) {
+ const tablePath = useAppSelector((state) => state.ui.tablePath)
+ const baseUrl = useAppSelector((state) => state.ui.baseUrl)
const originalUrl = String(value).trim()
+ console.log('Image original url', originalUrl)
+
+ const resolvedUrl = mayResolveUrl({ value, tablePath, baseUrl })
// Handle empty or invalid values
if (isNil(value) || value === '' || !originalUrl) {
@@ -25,7 +32,7 @@ const ImageCellRenderer = memo(function ImageCellRenderer({
const cellContent = (
@@ -34,7 +41,7 @@ const ImageCellRenderer = memo(function ImageCellRenderer({
const popoverContent = (
state.ui.tablePath)
+ const baseUrl = useAppSelector((state) => state.ui.baseUrl)
+ const originalUrl = String(value).trim()
+
+ const resolvedUrl = mayResolveUrl({ value, tablePath, baseUrl })
// Extract filename from URL
const filename = pathBasename(originalUrl)
@@ -26,7 +30,7 @@ const PdfCellRenderer = memo(function PdfCellRenderer({ value }: PdfCellRenderer
const popoverContent = (
@@ -36,8 +40,8 @@ const PdfCellRenderer = memo(function PdfCellRenderer({ value }: PdfCellRenderer
)
diff --git a/smoosense-gui/src/lib/utils/cellRenderers/VideoCellRenderer.tsx b/smoosense-gui/src/lib/utils/cellRenderers/VideoCellRenderer.tsx
index adaa7aa..1e72cb2 100644
--- a/smoosense-gui/src/lib/utils/cellRenderers/VideoCellRenderer.tsx
+++ b/smoosense-gui/src/lib/utils/cellRenderers/VideoCellRenderer.tsx
@@ -4,6 +4,8 @@ import { memo } from 'react'
import CellPopover from '@/components/ui/CellPopover'
import VideoPlayer from '@/components/common/VideoPlayer'
import { isNil } from 'lodash'
+import { mayResolveUrl } from '../mediaUrlUtils'
+import { useAppSelector } from '@/lib/hooks'
interface VideoCellRendererProps {
value: unknown
@@ -12,8 +14,12 @@ interface VideoCellRendererProps {
const VideoCellRenderer = memo(function VideoCellRenderer({
value
}: VideoCellRendererProps) {
+ const tablePath = useAppSelector((state) => state.ui.tablePath)
+ const baseUrl = useAppSelector((state) => state.ui.baseUrl)
const originalUrl = String(value).trim()
+ const resolvedUrl = mayResolveUrl({ value, tablePath, baseUrl })
+
// Handle empty or invalid values
if (isNil(value) || value === '' || !originalUrl) {
return (
@@ -28,7 +34,7 @@ const VideoCellRenderer = memo(function VideoCellRenderer({
className="relative rounded overflow-hidden bg-muted w-full h-full"
>
@@ -37,7 +43,7 @@ const VideoCellRenderer = memo(function VideoCellRenderer({
const popoverContent = (
@@ -49,7 +55,7 @@ const VideoCellRenderer = memo(function VideoCellRenderer({
cellContent={cellContent}
cellContentClassName="items-center justify-center"
popoverContent={popoverContent}
- url={originalUrl}
+ url={resolvedUrl}
copyValue={originalUrl}
/>
)
diff --git a/smoosense-gui/src/lib/utils/mediaUrlUtils.ts b/smoosense-gui/src/lib/utils/mediaUrlUtils.ts
index 57e7370..2aae645 100644
--- a/smoosense-gui/src/lib/utils/mediaUrlUtils.ts
+++ b/smoosense-gui/src/lib/utils/mediaUrlUtils.ts
@@ -7,7 +7,7 @@ import {getFileType, FileType} from './fileTypes'
* Returns true if:
* - Value is a string
* - AND starts with ./, /, ~/, or s3://
- * - AND has a media file extension (image, video, or audio)
+ * - AND has a media file extension (image, video, audio, or pdf)
*/
export const needToResolveMediaUrl = (value: unknown): boolean => {
// Must be a string
@@ -26,9 +26,9 @@ export const needToResolveMediaUrl = (value: unknown): boolean => {
return false
}
- // Must be a media file
+ // Must be a media file (image, video, audio, or pdf)
const fileType = getFileType(value)
- return fileType === FileType.Image || fileType === FileType.Video || fileType === FileType.Audio
+ return [FileType.Image, FileType.Video, FileType.Audio, FileType.Pdf].includes(fileType)
}
/**
@@ -82,6 +82,31 @@ function resolveRelativePath(tablePath: string, relativePath: string): string {
return pathJoin(dirPath, cleanRelative)
}
+/**
+ * Conditionally resolve a media URL if needed
+ * @param params - Object containing value, tablePath, and baseUrl
+ * @param params.value - The value to check and potentially resolve
+ * @param params.tablePath - The current table path (for resolving relative paths like ./)
+ * @param params.baseUrl - The base URL to prepend (for absolute paths like / or ~/)
+ * @returns The resolved URL if resolution was needed, otherwise the original value as string
+ */
+export const mayResolveUrl = ({
+ value,
+ tablePath,
+ baseUrl
+}: {
+ value: unknown
+ tablePath: string | null
+ baseUrl: string | null
+}): string => {
+ const originalUrl = String(value).trim()
+
+ // Resolve URL if needed
+ return (tablePath && baseUrl && needToResolveMediaUrl(value))
+ ? resolveAssetUrl(originalUrl, tablePath, baseUrl)
+ : originalUrl
+}
+
/**
* Resolve asset URLs to full URLs, handling relative paths, absolute paths, S3 URLs, and remote URLs
* @param url - The URL or file path to resolve
diff --git a/smoosense-gui/src/lib/utils/urlUtils.ts b/smoosense-gui/src/lib/utils/urlUtils.ts
index 7717b56..cb0833e 100644
--- a/smoosense-gui/src/lib/utils/urlUtils.ts
+++ b/smoosense-gui/src/lib/utils/urlUtils.ts
@@ -23,27 +23,9 @@ export function isUrl(str: string): boolean {
str.startsWith('s3://') ||
str.startsWith('ftp://') ||
str.startsWith('file://') ||
- str.startsWith('./')
-}
-
-export const needProxy = (url: string): boolean => {
- const scheme = getScheme(url);
- return !['http', 'https', ''].includes(scheme);
-}
-
-export const proxyedUrl = (url: string): string => {
- // Assert that relative URLs should have been handled before this stage
- if (url.startsWith('./') || url.startsWith('/') || url.startsWith('~/')) {
- return url
- }
-
- // At this point, url should be an absolute URL (http://, https://, s3://, etc.)
- if (!needProxy(url)) {
- return url
- } else {
- // Proxy cloud storage URLs (s3://, etc.)
- return `${API_PREFIX}/s3-proxy?url=${encodeURIComponent(url)}`
- }
+ str.startsWith('./') ||
+ str.startsWith('~/') ||
+ str.startsWith('/')
}
export const isOnCloud = (fullPath: string): string => {
diff --git a/smoosense-py/smoosense/handlers/fs.py b/smoosense-py/smoosense/handlers/fs.py
index 85b08b1..6a5a92e 100644
--- a/smoosense-py/smoosense/handlers/fs.py
+++ b/smoosense-py/smoosense/handlers/fs.py
@@ -12,6 +12,7 @@
from smoosense.exceptions import AccessDeniedException, InvalidInputException
from smoosense.utils.api import handle_api_errors, require_arg
from smoosense.utils.local_fs import LocalFileSystem
+from smoosense.utils.mime_types import get_mime_type
from smoosense.utils.s3_fs import S3FileSystem
logger = logging.getLogger(__name__)
@@ -26,6 +27,7 @@ def create_streaming_response(
flask_response.headers["Access-Control-Allow-Origin"] = "*"
flask_response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
flask_response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
+ flask_response.headers["Content-Disposition"] = "inline"
return flask_response
@@ -49,10 +51,6 @@ def get_file() -> Response:
path = require_arg("path")
redirect_param = request.args.get("redirect", "false").lower() == "true"
ext = os.path.splitext(path)[1].lower()
- mime_type = {
- ".json": "application/json",
- ".txt": "text/plain",
- }
if path.startswith("http://"):
logger.info(f"Proxying HTTP URL {path}")
@@ -60,9 +58,7 @@ def get_file() -> Response:
response = requests.get(path, stream=True, timeout=30)
response.raise_for_status()
- content_type = response.headers.get(
- "content-type", mime_type.get(ext, "application/octet-stream")
- )
+ content_type = response.headers.get("content-type", get_mime_type(ext))
def generate() -> Generator[bytes, None, None]:
for chunk in response.iter_content(chunk_size=8192):
@@ -104,7 +100,7 @@ def generate() -> Generator[bytes, None, None]:
break
yield chunk
- content_type = mime_type.get(ext, "application/octet-stream")
+ content_type = get_mime_type(ext)
return create_streaming_response(generate(), content_type)
except Exception as e:
logger.error(f"Failed to proxy S3 file {path}: {e}")
@@ -114,11 +110,13 @@ def generate() -> Generator[bytes, None, None]:
if path.startswith("~"):
path = os.path.expanduser(path)
logger.info(f"Sending file {path}")
- file_response = send_file(path, mimetype=mime_type.get(ext, "application/octet-stream"))
+ file_response = send_file(path, mimetype=get_mime_type(ext))
# Add CORS headers for local file responses
file_response.headers["Access-Control-Allow-Origin"] = "*"
file_response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS"
file_response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
+ file_response.headers["Content-Disposition"] = "inline"
+
return file_response
diff --git a/smoosense-py/smoosense/utils/mime_types.py b/smoosense-py/smoosense/utils/mime_types.py
new file mode 100644
index 0000000..d4162b1
--- /dev/null
+++ b/smoosense-py/smoosense/utils/mime_types.py
@@ -0,0 +1,66 @@
+"""Utility functions for MIME type detection."""
+
+# Comprehensive MIME type mapping for common file extensions
+MIME_TYPES = {
+ # Text formats
+ ".txt": "text/plain",
+ ".json": "application/json",
+ ".csv": "text/csv",
+ ".xml": "application/xml",
+ ".html": "text/html",
+ ".css": "text/css",
+ ".js": "application/javascript",
+ # Image formats
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".png": "image/png",
+ ".gif": "image/gif",
+ ".svg": "image/svg+xml",
+ ".webp": "image/webp",
+ ".bmp": "image/bmp",
+ ".ico": "image/x-icon",
+ # Video formats
+ ".mp4": "video/mp4",
+ ".webm": "video/webm",
+ ".avi": "video/x-msvideo",
+ ".mov": "video/quicktime",
+ ".wmv": "video/x-ms-wmv",
+ ".flv": "video/x-flv",
+ ".mkv": "video/x-matroska",
+ # Audio formats
+ ".mp3": "audio/mpeg",
+ ".wav": "audio/wav",
+ ".ogg": "audio/ogg",
+ ".m4a": "audio/mp4",
+ ".flac": "audio/flac",
+ ".aac": "audio/aac",
+ # Document formats
+ ".pdf": "application/pdf",
+ ".doc": "application/msword",
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ ".xls": "application/vnd.ms-excel",
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ ".ppt": "application/vnd.ms-powerpoint",
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ # Archive formats
+ ".zip": "application/zip",
+ ".tar": "application/x-tar",
+ ".gz": "application/gzip",
+ ".7z": "application/x-7z-compressed",
+ # Data formats
+ ".parquet": "application/octet-stream",
+}
+
+
+def get_mime_type(file_extension: str, default: str = "application/octet-stream") -> str:
+ """
+ Get the MIME type for a given file extension.
+
+ Args:
+ file_extension: The file extension (e.g., '.pdf', '.jpg')
+ default: Default MIME type if extension is not recognized
+
+ Returns:
+ The MIME type string
+ """
+ return MIME_TYPES.get(file_extension.lower(), default)