<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" v-html="highlightedName"></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
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'])
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
}
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;
}
</style>