Newer
Older
navi-1 / webclient / src / components / sidebar / SessionItem.vue
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 15 May 3 KB Add self-recall (scheduled callback) system
<template>
  <div
    class="session-item"
    :class="{ 'is-active': active, 'is-pinned': session.pinned }"
    @click="emit('select')"
  >
    <i class="ph ph-push-pin session-pin-icon"></i>

    <div class="session-info">
      <div class="session-name-row">
        <span v-html="highlightedName"></span>
        <i v-if="session.has_pending_recall" class="ph ph-clock recall-badge" title="Scheduled recall pending"></i>
      </div>
      <div v-if="displayPreview" class="session-preview" v-html="highlightedPreview"></div>
      <div v-if="sessionTime" class="session-time">{{ sessionTime }}</div>
    </div>

    <div class="session-actions" @click.stop>
      <button
        v-if="session.has_pending_recall"
        class="btn-icon"
        title="Cancel recall"
        @click="emit('cancel-recall')"
      >
        <i class="ph ph-clock-slash"></i>
      </button>
      <button
        v-if="session.has_pending_recall"
        class="btn-icon"
        title="Skip next"
        @click="emit('skip-recall')"
      >
        <i class="ph ph-skip-forward"></i>
      </button>
      <button
        class="btn-icon"
        :title="session.pinned ? 'Unpin' : 'Pin'"
        @click="emit('pin')"
      >
        <i :class="session.pinned ? 'ph ph-push-pin-slash' : 'ph ph-push-pin'"></i>
      </button>
      <button
        class="btn-icon"
        title="Delete"
        @click="onDelete"
      >
        <i class="ph ph-trash"></i>
      </button>
    </div>
  </div>
</template>

<script setup>
import { computed } from 'vue'
import { useTimeLabel } from '@/composables/useTime.js'
import { useConfirm } from '@/composables/useConfirm.js'

const props = defineProps({
  session: { type: Object, required: true },
  active: { type: Boolean, default: false },
  searchQuery: { type: String, default: '' }
})

const emit = defineEmits(['select', 'delete', 'pin', 'cancel-recall', 'skip-recall'])

const confirm = useConfirm()

const displayName = computed(() => {
  const id = props.session.session_id ?? ''
  return props.session.name || id.slice(0, 8)
})

const displayPreview = computed(() => {
  if (props.searchQuery && props.session.match_preview) {
    return props.session.match_preview
  }
  return props.session.preview
})

const tsRef = computed(() => props.session.last_active)
const sessionTime = useTimeLabel(tsRef)

function escapeHtml(str) {
  if (typeof str !== 'string') return String(str ?? '')
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
}

function highlightText(text, query) {
  if (!query || !text) return escapeHtml(text)
  const q = escapeHtml(query.toLowerCase())
  const t = escapeHtml(text)
  const parts = []
  let lastIndex = 0
  const lower = t.toLowerCase()
  while (true) {
    const idx = lower.indexOf(q, lastIndex)
    if (idx === -1) break
    if (idx > lastIndex) parts.push(t.slice(lastIndex, idx))
    parts.push(`<mark class="search-highlight">${t.slice(idx, idx + query.length)}</mark>`)
    lastIndex = idx + query.length
  }
  if (lastIndex < t.length) parts.push(t.slice(lastIndex))
  return parts.join('')
}

const highlightedName = computed(() => highlightText(displayName.value, props.searchQuery))
const highlightedPreview = computed(() => highlightText(displayPreview.value, props.searchQuery))

async function onDelete() {
  const ok = await confirm('Delete this conversation?')
  if (ok) emit('delete')
}
</script>

<style scoped lang="scss">
@use 'kit-deps' as *;

.search-highlight {
  background: rgba($color-secondary, 0.25);
  color: $color-secondary;
  font-weight: 600;
  padding: 0 2px;
  border-radius: 2px;
}

.session-name-row {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 500;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.recall-badge {
  color: var(--color-accent, #f59e0b);
  font-size: 0.85em;
  flex-shrink: 0;
}
</style>