Newer
Older
navi-1 / webclient / src / components / messages / ContentCard.vue
<template>
  <details
    ref="detailsEl"
    class="content-card"
    :class="contentTypeClass"
    open
  >
    <summary>
      <span class="content-icon">{{ icon }}</span>
      <span class="content-title">{{ title }}</span>
      <span class="content-badge">{{ contentType }}</span>
      <i class="ph ph-caret-down content-chevron"></i>
    </summary>

    <div class="content-body">
      <!-- STL -->
      <iframe
        v-if="detectedContentType === 'stl'"
        :src="viewerUrl('stl')"
        class="content-iframe"
        sandbox="allow-scripts"
        loading="lazy"
      />

      <!-- HTML -->
      <iframe
        v-else-if="detectedContentType === 'html'"
        :src="viewerUrl('html')"
        class="content-iframe"
        sandbox="allow-scripts allow-same-origin"
        loading="lazy"
      />

      <!-- SVG -->
      <iframe
        v-else-if="detectedContentType === 'svg'"
        :src="viewerUrl('svg')"
        class="content-iframe"
        sandbox="allow-scripts allow-same-origin"
        loading="lazy"
      />

      <!-- PDF -->
      <iframe
        v-else-if="detectedContentType === 'pdf'"
        :src="viewerUrl('pdf')"
        class="content-iframe"
        loading="lazy"
      />

      <!-- Image -->
      <img
        v-else-if="detectedContentType === 'image'"
        :src="url"
        class="content-image"
        :alt="title"
        loading="lazy"
        @click="openLightbox"
      />

      <!-- Video -->
      <video
        v-else-if="detectedContentType === 'video'"
        :src="url"
        class="content-video"
        controls
        playsinline
      />

      <!-- Unknown -->
      <div v-else class="content-unknown">
        <div class="unknown-icon">{{ icon }}</div>
        <div class="unknown-name">{{ title }}</div>
        <a :href="url" target="_blank" rel="noopener noreferrer" class="unknown-open">
          Open file
        </a>
      </div>
    </div>
  </details>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useLightbox } from '@/composables/useLightbox.js'

const props = defineProps({
  tool: { type: Object, required: true }
})

const detailsEl = ref(null)
const { open } = useLightbox()

// content_publish tool stores metadata in the tool card
const metadata = computed(() => {
  // metadata is set by onToolCall from WS event (live stream)
  if (props.tool.metadata) {
    return props.tool.metadata
  }
  // Fallback: parse from persisted text output (used on page reload / reconnect)
  const result = String(props.tool.result ?? '')
  const urlMatch = result.match(/URL: (.+)/)
  if (urlMatch) {
    const typeMatch = result.match(/Published: .+ \(([^)]+)\)/)
    const titleMatch = result.match(/Published: (.+) \(/)
    return {
      url: urlMatch[1].trim(),
      content_type: typeMatch ? typeMatch[1].trim() : 'unknown',
      title: titleMatch ? titleMatch[1].trim() : 'Content',
    }
  }
  return {}
})

const url = computed(() => metadata.value.url || '')
const contentType = computed(() => metadata.value.content_type || 'unknown')
const title = computed(() => metadata.value.title || metadata.value.filename || 'Content')

const detectedContentType = computed(() => {
  if (contentType.value !== 'unknown') return contentType.value
  const ext = url.value.split('.').pop()?.toLowerCase()
  const map = {
    svg: 'svg', html: 'html', htm: 'html', pdf: 'pdf', stl: 'stl',
    png: 'image', jpg: 'image', jpeg: 'image', gif: 'image', webp: 'image', bmp: 'image',
    mp4: 'video', webm: 'video', mov: 'video', mkv: 'video',
  }
  return map[ext] || 'unknown'
})

const CONTENT_ICONS = {
  stl: '🧊',
  html: '🌐',
  svg: '🎨',
  pdf: '📄',
  image: '🖼️',
  video: '🎬',
  unknown: '📎'
}

const icon = computed(() => CONTENT_ICONS[contentType.value] || CONTENT_ICONS.unknown)

const contentTypeClass = computed(() => `is-${detectedContentType.value}`)

function viewerUrl(viewerType) {
  return `/content-viewers/${viewerType}.html?url=${encodeURIComponent(url.value)}`
}

function openLightbox() {
  if (contentType.value === 'image') {
    open(url.value)
  }
}
</script>

<style scoped>
.content-card {
  margin: 8px 0;
  background: var(--surface, #1e1e2e);
  border: 1px solid var(--border, #2a2a3e);
  overflow: hidden;
}

.content-card summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  cursor: pointer;
  user-select: none;
  list-style: none;
}

.content-card summary::-webkit-details-marker {
  display: none;
}

.content-icon {
  font-size: 1.2em;
  line-height: 1;
}

.content-title {
  flex: 1;
  font-size: 0.9em;
  font-weight: 500;
  color: var(--text, #cdd6f4);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.content-badge {
  font-size: 0.7em;
  text-transform: uppercase;
  padding: 2px 8px;
  background: var(--accent-muted, #313244);
  color: var(--accent, #4ec9b0);
}

.content-chevron {
  transition: transform 0.2s;
  color: var(--text-muted, #6c7086);
}

.content-card[open] .content-chevron {
  transform: rotate(180deg);
}

.content-body {
  border-top: 1px solid var(--border, #2a2a3e);
  overflow: auto;

  &::-webkit-scrollbar { width: 10px; }
  &::-webkit-scrollbar-track { width: 10px; background: #16161e; cursor: pointer; }
  &::-webkit-scrollbar-thumb { width: 10px; background: #414868; cursor: default; }
  &::-webkit-scrollbar-corner { background: transparent; height: 1px; }
  &::-webkit-scrollbar-button { display: none; }
}

.content-iframe {
  width: 100%;
  height: 300px;
  border: none;
  display: block;
}

@media (min-width: 768px) {
  .content-iframe {
    height: 500px;
  }
}

.content-image {
  width: 100%;
  max-height: 60vh;
  object-fit: contain;
  cursor: zoom-in;
  display: block;
  background: var(--surface, #1e1e2e);
}

.content-video {
  width: 100%;
  max-height: 60vh;
  display: block;
  background: var(--surface, #1e1e2e);
}

.content-unknown {
  padding: 24px;
  text-align: center;
  color: var(--text-muted, #6c7086);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

.unknown-icon {
  font-size: 2em;
  line-height: 1;
}

.unknown-name {
  font-size: 0.95em;
  font-weight: 500;
  color: var(--text, #cdd6f4);
  word-break: break-word;
  max-width: 100%;
}

.unknown-open {
  display: inline-block;
  margin-top: 4px;
  padding: 6px 14px;
  border-radius: 0;
  background: var(--accent-muted, #313244);
  color: var(--accent, #4ec9b0);
  text-decoration: none;
  font-size: 0.85em;
  font-weight: 500;
  transition: background 0.15s;
}

.unknown-open:hover {
  background: var(--border, #2a2a3e);
}

/* Type-specific accents */
.content-card.is-stl .content-badge { color: #f5c2e7; }
.content-card.is-html .content-badge { color: #89b4fa; }
.content-card.is-svg .content-badge { color: #a6e3a1; }
.content-card.is-pdf .content-badge { color: #fab387; }
.content-card.is-image .content-badge { color: #89dceb; }
.content-card.is-video .content-badge { color: #f38ba8; }
</style>