// 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)
}