Newer
Older
navi-1 / webclient / src / utils / contentTypes.js
// One table for every viewer decision in the client.
//
// It mirrors navi/content_types.py: the backend detects a published file's
// type (extension, then signature) and this list is the fallback for content
// the client has to render on its own — a tool result replayed after a reload,
// or a file listed in the Files tab. Keeping the two in step is what stopped
// audio, markdown and glTF from falling through to the "unknown" card.

const TEXT_EXTENSIONS = [
  'txt', 'text', 'log', 'md', 'markdown', 'csv', 'tsv', 'json', 'jsonl', 'ndjson',
  'yaml', 'yml', 'xml', 'ini', 'cfg', 'conf', 'toml', 'py', 'js', 'mjs', 'cjs',
  'ts', 'tsx', 'jsx', 'css', 'scss', 'sass', 'less', 'sh', 'bash', 'zsh', 'sql',
  'scad', 'srt', 'vtt', 'diff', 'patch', 'tex', 'c', 'h', 'cpp', 'hpp', 'java',
  'go', 'rs', 'rb', 'php', 'kt', 'swift',
]

const EXTENSION_TYPES = {
  stl: 'stl',
  glb: 'model',
  gltf: 'model',
  html: 'html',
  htm: 'html',
  svg: 'svg',
  pdf: 'pdf',
  png: 'image',
  jpg: 'image',
  jpeg: 'image',
  gif: 'image',
  webp: 'image',
  bmp: 'image',
  avif: 'image',
  ico: 'image',
  mp4: 'video',
  webm: 'video',
  mov: 'video',
  mkv: 'video',
  m4v: 'video',
  ogv: 'video',
  mp3: 'audio',
  wav: 'audio',
  ogg: 'audio',
  oga: 'audio',
  opus: 'audio',
  m4a: 'audio',
  flac: 'audio',
  aac: 'audio',
  weba: 'audio',
}

for (const ext of TEXT_EXTENSIONS) EXTENSION_TYPES[ext] = 'text'

export const CONTENT_TYPE_ICONS = {
  stl: 'ph ph-cube',
  model: 'ph ph-cube',
  html: 'ph ph-globe',
  svg: 'ph ph-paint-brush',
  pdf: 'ph ph-file-pdf',
  image: 'ph ph-image',
  video: 'ph ph-film-strip',
  audio: 'ph ph-waveform',
  text: 'ph ph-file-text',
  unknown: 'ph ph-paperclip',
}

export const BADGE_VARIANTS = {
  stl: 'info',
  model: 'info',
  html: 'secondary',
  svg: 'success',
  pdf: 'primary',
  image: 'primary',
  video: 'danger',
  audio: 'success',
  text: 'secondary',
  unknown: 'primary',
}

// Types the browser renders itself rather than an iframe viewer page.
export const MEDIA_TYPES = ['image', 'video', 'audio']
// Types that live inside /content-viewers/<name>.html. `stl` and `model`
// share model.html — the file's extension picks the three.js loader.
export const FRAME_TYPES = ['stl', 'model', 'html', 'svg', 'pdf']
// Types the content lightbox can show on the whole screen: media the browser
// renders itself, a source view for text, and the iframe viewer pages.
export const FULLSCREEN_TYPES = [
  'stl', 'model', 'html', 'svg', 'pdf', 'image', 'video', 'audio', 'text',
]

export function extensionOf(name) {
  const clean = String(name || '').split('?')[0].split('#')[0]
  if (!clean.includes('.')) return ''
  return clean.split('.').pop().toLowerCase()
}

export function contentTypeFromName(name) {
  return EXTENSION_TYPES[extensionOf(name)] || 'unknown'
}

// An explicit type from a tool result is trusted only if a viewer exists for it.
export function normalizeContentType(value) {
  const type = String(value || '').trim().toLowerCase()
  if (!type || type === 'unknown') return null
  return Object.hasOwn(CONTENT_TYPE_ICONS, type) ? type : null
}

export function isFrameType(type) {
  return FRAME_TYPES.includes(type)
}

export function isFullscreenType(type) {
  return FULLSCREEN_TYPES.includes(type)
}

// The viewer page to load: model.html serves both STL and glTF.
export function viewerNameFor(type) {
  return type === 'stl' ? 'model' : type
}

// Types whose raw URL a browser shows in place. STL and glTF need the viewer
// page, so they are not on this list: their link is a download.
export const BROWSER_INLINE_TYPES = ['image', 'video', 'audio', 'text', 'html', 'svg', 'pdf']

export function isInlineType(type) {
  return BROWSER_INLINE_TYPES.includes(type)
}