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="contentType === 'stl'"
        :src="viewerUrl('stl')"
        class="content-iframe"
        sandbox="allow-scripts"
        loading="lazy"
      />

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

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

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

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

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

      <!-- Unknown -->
      <div v-else class="content-unknown">
        <p>Cannot preview this file type.</p>
        <a :href="url" target="_blank" rel="noopener noreferrer" class="content-link">
          Open in new tab
        </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
  if (props.tool.metadata) {
    return props.tool.metadata
  }
  // Fallback: try to extract from output text
  const match = props.tool.result?.match?.(/URL: (.+)/)
  if (match) {
    return { url: match[1].trim() }
  }
  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 CONTENT_ICONS = {
  stl: '🧊',
  html: '🌐',
  svg: '🎨',
  pdf: '📄',
  image: '🖼️',
  video: '🎬',
  unknown: '📎'
}

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

const contentTypeClass = computed(() => `is-${contentType.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;
  border-radius: 10px;
  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;
  border-radius: 4px;
  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);
}

.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: 20px;
  text-align: center;
  color: var(--text-muted, #6c7086);
}

.content-link {
  color: var(--accent, #4ec9b0);
  text-decoration: none;
}

.content-link:hover {
  text-decoration: underline;
}

/* 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>