import { computed, ref, watch } from 'vue'
import { useLightbox } from '@/composables/useLightbox.js'
import { locale, t } from '@/i18n/index.js'
import { itemContentType } from '@/utils/artifactContent.js'
import {
absoluteContentUrl,
contentFileUrl,
versionedContentUrl,
viewerContentUrl,
} from '@/utils/contentLinks.js'
import {
CONTENT_TYPE_ICONS,
isFrameType,
isFullscreenType,
viewerNameFor,
} from '@/utils/contentTypes.js'
import { highlightSource, sourceLanguage } from '@/utils/sourceHighlight.js'
/**
* The Artifacts tab: which published artifact is selected, its source or text
* preview, and every URL the pane offers (preview, raw, download, share).
*/
const TEXT_PREVIEW_LIMIT = 1_000_000
export function useArtifactsView(chat) {
const { open: openLightbox, openContent } = useLightbox()
const selectedKey = ref(null)
const copiedKey = ref(null)
const showSource = ref(false)
const sourceText = ref('')
const sourceLoading = ref(false)
const sourceError = ref('')
// Text files are fetched into the preview rather than offered as a download.
const textContent = ref('')
const textLoading = ref(false)
const textError = ref('')
const textTruncated = ref(false)
const textPreviewLimitLabel = `${Math.round(TEXT_PREVIEW_LIMIT / 1024)} KB`
const artifacts = computed(() => chat.artifacts)
const selected = computed(() => artifacts.value.find(item => artifactKey(item) === selectedKey.value) || artifacts.value[0] || null)
const selectedSourceLanguage = computed(() => selected.value ? sourceLanguage(selected.value) : 'plaintext')
const highlightedSource = computed(() => highlightSource(sourceText.value, selectedSourceLanguage.value))
const highlightedText = computed(() => highlightSource(textContent.value, selectedSourceLanguage.value))
function openViewedImage(src) {
openLightbox(src)
}
async function loadText(item) {
if (!item || textLoading.value || textContent.value) return
textLoading.value = true
textError.value = ''
try {
const res = await fetch(versionedUrl(item))
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const raw = await res.text()
textTruncated.value = raw.length > TEXT_PREVIEW_LIMIT
textContent.value = textTruncated.value ? raw.slice(0, TEXT_PREVIEW_LIMIT) : raw
} catch (err) {
textError.value = t('artifacts.sourceFailed', { error: err?.message || err })
} finally {
textLoading.value = false
}
}
// A different artifact selected → start its text over, and fetch it right
// away (the detail pane is always visible, unlike the card's collapsed body).
watch(selected, (item) => {
textContent.value = ''
textError.value = ''
textTruncated.value = false
if (item && !showSource.value && contentType(item) === 'text') loadText(item)
}, { immediate: true })
watch(
artifacts,
(items) => {
if (!items.length) {
selectedKey.value = null
return
}
if (!selectedKey.value || !items.some(item => artifactKey(item) === selectedKey.value)) {
selectedKey.value = artifactKey(items[0])
}
},
{ immediate: true }
)
function artifactKey(item) {
return item.id || item.filename
}
function selectArtifact(item) {
selectedKey.value = artifactKey(item)
showSource.value = false
sourceText.value = ''
sourceError.value = ''
}
// The backend detects the type; the name is the fallback for entries that
// arrive without one. Both cards read the same table now.
function contentType(item) {
return itemContentType(item)
}
function iconClass(item) {
return CONTENT_TYPE_ICONS[contentType(item)] || CONTENT_TYPE_ICONS.unknown
}
function labelFor(item) {
const type = contentType(item).toUpperCase()
const date = item.updated_at || item.created_at
if (!date) return type
return `${type} · ${formatDate(date)}`
}
function formatDate(value) {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value
return date.toLocaleTimeString(locale.value, { hour: '2-digit', minute: '2-digit' })
}
function versionedUrl(item) {
return versionedContentUrl(item.url, item.updated_at || item.created_at || '')
}
function viewerUrl(item, viewerType) {
return viewerContentUrl(viewerType, versionedUrl(item))
}
function previewUrl(item) {
const type = contentType(item)
return isFrameType(type)
? viewerUrl(item, viewerNameFor(type))
: absoluteContentUrl(versionedUrl(item))
}
function canFullscreen(item) {
return isFullscreenType(contentType(item))
}
function openFullscreen(item) {
if (!item || !canFullscreen(item)) return
openContent({
url: absoluteContentUrl(versionedUrl(item)),
type: contentType(item),
title: item.title || item.filename,
})
}
function rawUrl(item) {
return contentFileUrl(item.url)
}
function sourceUrl(item) {
const type = contentType(item)
if (['html', 'svg'].includes(type)) return rawUrl(item)
if (['stl', 'model'].includes(type) && item.source_url) return contentFileUrl(item.source_url)
return ''
}
function hasSource(item) {
return Boolean(sourceUrl(item))
}
async function toggleSource(item) {
showSource.value = !showSource.value
if (showSource.value) await loadSource(item)
}
async function loadSource(item) {
const url = sourceUrl(item)
if (!url || sourceText.value || sourceLoading.value) return
sourceLoading.value = true
sourceError.value = ''
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
sourceText.value = await res.text()
} catch (err) {
sourceError.value = t('artifacts.sourceFailed', { error: err?.message || err })
} finally {
sourceLoading.value = false
}
}
function shareUrl(item) {
if (['stl', 'model'].includes(contentType(item))) return previewUrl(item)
return absoluteContentUrl(versionedUrl(item))
}
function downloadUrl(item) {
const url = contentFileUrl(item.download_url || item.url)
if (item.download_url || !url) return url
return `${url}${url.includes('?') ? '&' : '?'}download=1`
}
function isFramePreview(item) {
return isFrameType(contentType(item))
}
async function copyLink(item) {
try {
await navigator.clipboard.writeText(shareUrl(item))
copiedKey.value = artifactKey(item)
setTimeout(() => {
if (copiedKey.value === artifactKey(item)) copiedKey.value = null
}, 1500)
} catch {
// Clipboard can fail on non-secure origins; ignore.
}
}
function copyIcon(item) {
return copiedKey.value === artifactKey(item) ? 'ph ph-check' : 'ph ph-copy'
}
return {
selectedKey,
copiedKey,
showSource,
sourceText,
sourceLoading,
sourceError,
textContent,
textLoading,
textError,
textTruncated,
textPreviewLimitLabel,
artifacts,
selected,
selectedSourceLanguage,
highlightedSource,
highlightedText,
openViewedImage,
artifactKey,
selectArtifact,
contentType,
iconClass,
labelFor,
versionedUrl,
previewUrl,
canFullscreen,
openFullscreen,
rawUrl,
hasSource,
toggleSource,
downloadUrl,
isFramePreview,
copyLink,
copyIcon,
}
}