2026-04-22 02:09:58 +02:00
|
|
|
<script setup lang="ts">
|
2026-05-17 12:20:53 +08:00
|
|
|
import { computed, ref, onUnmounted } from 'vue'
|
2026-05-20 18:26:01 +08:00
|
|
|
import { NPopconfirm, NCheckbox, NTooltip } from 'naive-ui'
|
2026-04-22 02:09:58 +02:00
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
import type { Session } from '@/stores/hermes/chat'
|
2026-05-17 12:20:53 +08:00
|
|
|
import { useAppStore } from '@/stores/hermes/app'
|
2026-05-20 14:15:01 +08:00
|
|
|
import { useProfilesStore } from '@/stores/hermes/profiles'
|
|
|
|
|
import ProfileAvatar from '@/components/hermes/profiles/ProfileAvatar.vue'
|
2026-04-22 02:09:58 +02:00
|
|
|
import { formatTimestampMs } from '@/shared/session-display'
|
|
|
|
|
|
2026-05-19 16:09:59 +08:00
|
|
|
const props = withDefaults(defineProps<{
|
2026-04-22 02:09:58 +02:00
|
|
|
session: Session
|
|
|
|
|
active: boolean
|
|
|
|
|
pinned: boolean
|
|
|
|
|
canDelete: boolean
|
2026-04-29 16:26:24 +08:00
|
|
|
streaming?: boolean
|
2026-05-06 16:15:42 +08:00
|
|
|
selectable?: boolean
|
|
|
|
|
selected?: boolean
|
2026-05-19 16:09:59 +08:00
|
|
|
showProfile?: boolean
|
2026-05-24 14:13:42 +03:00
|
|
|
to?: string
|
2026-05-19 16:09:59 +08:00
|
|
|
}>(), {
|
|
|
|
|
showProfile: true,
|
|
|
|
|
})
|
2026-04-22 02:09:58 +02:00
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
select: []
|
|
|
|
|
contextmenu: [event: MouseEvent]
|
|
|
|
|
delete: []
|
2026-05-06 16:15:42 +08:00
|
|
|
'toggle-select': []
|
2026-04-22 02:09:58 +02:00
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n()
|
2026-05-17 12:20:53 +08:00
|
|
|
const appStore = useAppStore()
|
2026-05-20 14:15:01 +08:00
|
|
|
const profilesStore = useProfilesStore()
|
2026-05-17 12:20:53 +08:00
|
|
|
const sessionModelName = computed(() =>
|
|
|
|
|
props.session.model
|
|
|
|
|
? appStore.displayModelName(props.session.model, props.session.provider)
|
|
|
|
|
: '',
|
|
|
|
|
)
|
2026-05-19 16:09:59 +08:00
|
|
|
const profileName = computed(() => props.session.profile || 'default')
|
2026-05-20 14:15:01 +08:00
|
|
|
const profileAvatar = computed(() => profilesStore.profiles.find(profile => profile.name === profileName.value)?.avatar)
|
2026-05-20 18:26:01 +08:00
|
|
|
const profileHasModels = computed(() => {
|
|
|
|
|
const profileModels = appStore.profileModelGroups.find(profile => profile.profile === profileName.value)
|
|
|
|
|
return !!profileModels?.groups?.some(group => group.models.length > 0)
|
|
|
|
|
})
|
|
|
|
|
const profileModelsMissing = computed(() =>
|
|
|
|
|
appStore.profileModelGroups.length > 0 && !profileHasModels.value,
|
|
|
|
|
)
|
2026-05-07 13:49:57 +08:00
|
|
|
|
|
|
|
|
let longPressTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
const longPressTriggered = ref(false)
|
|
|
|
|
|
|
|
|
|
function onTouchStart(e: TouchEvent) {
|
|
|
|
|
longPressTriggered.value = false
|
|
|
|
|
longPressTimer = setTimeout(() => {
|
|
|
|
|
longPressTriggered.value = true
|
|
|
|
|
const touch = e.touches[0]
|
|
|
|
|
const syntheticEvent = new MouseEvent('contextmenu', {
|
|
|
|
|
clientX: touch.clientX,
|
|
|
|
|
clientY: touch.clientY,
|
|
|
|
|
bubbles: true,
|
|
|
|
|
})
|
|
|
|
|
emit('contextmenu', syntheticEvent)
|
|
|
|
|
}, 500)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onTouchEnd() {
|
|
|
|
|
if (longPressTimer) {
|
|
|
|
|
clearTimeout(longPressTimer)
|
|
|
|
|
longPressTimer = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onTouchMove() {
|
|
|
|
|
if (longPressTimer) {
|
|
|
|
|
clearTimeout(longPressTimer)
|
|
|
|
|
longPressTimer = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 14:13:42 +03:00
|
|
|
function isModifiedNavigation(event?: MouseEvent) {
|
|
|
|
|
return !!event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onClick(event?: MouseEvent) {
|
2026-05-07 13:49:57 +08:00
|
|
|
if (longPressTriggered.value) {
|
|
|
|
|
longPressTriggered.value = false
|
2026-05-24 14:13:42 +03:00
|
|
|
event?.preventDefault()
|
2026-05-07 13:49:57 +08:00
|
|
|
return
|
|
|
|
|
}
|
2026-05-24 14:13:42 +03:00
|
|
|
if (isModifiedNavigation(event)) return
|
|
|
|
|
if (props.to && !props.selectable) event?.preventDefault()
|
2026-05-07 13:49:57 +08:00
|
|
|
emit('select')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (longPressTimer) clearTimeout(longPressTimer)
|
|
|
|
|
})
|
2026-04-22 02:09:58 +02:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-05-24 14:13:42 +03:00
|
|
|
<component
|
|
|
|
|
:is="selectable || !to ? 'button' : 'a'"
|
2026-04-22 02:09:58 +02:00
|
|
|
class="session-item"
|
2026-05-20 18:26:01 +08:00
|
|
|
:class="{ active, 'batch-mode': selectable, 'missing-models': profileModelsMissing }"
|
2026-04-22 02:09:58 +02:00
|
|
|
:aria-current="active ? 'page' : undefined"
|
2026-05-24 14:13:42 +03:00
|
|
|
:href="!selectable ? to : undefined"
|
|
|
|
|
:type="selectable || !to ? 'button' : undefined"
|
2026-05-07 13:49:57 +08:00
|
|
|
@click="onClick"
|
2026-04-22 02:09:58 +02:00
|
|
|
@contextmenu="emit('contextmenu', $event)"
|
2026-05-07 13:49:57 +08:00
|
|
|
@touchstart="onTouchStart"
|
|
|
|
|
@touchend="onTouchEnd"
|
|
|
|
|
@touchmove="onTouchMove"
|
2026-04-22 02:09:58 +02:00
|
|
|
>
|
2026-05-06 16:15:42 +08:00
|
|
|
<div v-if="selectable" class="session-item-checkbox">
|
|
|
|
|
<NCheckbox :checked="selected" @click.stop="emit('toggle-select')" />
|
|
|
|
|
</div>
|
2026-04-22 02:09:58 +02:00
|
|
|
<div class="session-item-content">
|
|
|
|
|
<span class="session-item-title-row">
|
|
|
|
|
<span v-if="pinned" class="session-item-pin" aria-hidden="true">
|
|
|
|
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
|
|
<path d="M12 17v5" />
|
|
|
|
|
<path d="M5 8l14 0" />
|
|
|
|
|
<path d="M8 3l8 0 0 5 3 5-14 0 3-5z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</span>
|
2026-04-29 16:26:24 +08:00
|
|
|
<span class="session-item-title">
|
|
|
|
|
<svg v-if="streaming" class="session-item-streaming" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
|
|
|
|
|
{{ session.title }}
|
2026-04-23 11:18:56 +08:00
|
|
|
</span>
|
2026-05-20 18:26:01 +08:00
|
|
|
<NTooltip v-if="profileModelsMissing" trigger="click" placement="top">
|
|
|
|
|
<template #trigger>
|
2026-05-24 14:13:42 +03:00
|
|
|
<button class="session-item-warning" type="button" @click.stop.prevent>
|
2026-05-20 18:26:01 +08:00
|
|
|
!
|
|
|
|
|
</button>
|
|
|
|
|
</template>
|
|
|
|
|
{{ t('chat.profileMissingModelsTip', { profile: profileName }) }}
|
|
|
|
|
</NTooltip>
|
2026-04-22 02:09:58 +02:00
|
|
|
</span>
|
|
|
|
|
<span class="session-item-meta">
|
2026-05-17 12:20:53 +08:00
|
|
|
<span v-if="sessionModelName" class="session-item-model" :title="session.model">{{ sessionModelName }}</span>
|
2026-04-22 02:09:58 +02:00
|
|
|
<span class="session-item-time">{{ formatTimestampMs(session.createdAt) }}</span>
|
|
|
|
|
</span>
|
2026-05-19 16:09:59 +08:00
|
|
|
<span v-if="props.showProfile" class="session-item-profile">
|
2026-05-20 14:15:01 +08:00
|
|
|
<ProfileAvatar class="session-item-profile-avatar" :name="profileName" :avatar="profileAvatar" :size="16" />
|
2026-05-19 16:09:59 +08:00
|
|
|
<span class="session-item-profile-name">{{ profileName }}</span>
|
|
|
|
|
</span>
|
2026-04-22 02:09:58 +02:00
|
|
|
</div>
|
2026-05-06 16:15:42 +08:00
|
|
|
<NPopconfirm v-if="canDelete && !selectable" @positive-click="emit('delete')">
|
2026-04-22 02:09:58 +02:00
|
|
|
<template #trigger>
|
2026-05-24 14:13:42 +03:00
|
|
|
<button class="session-item-delete" @click.stop.prevent>
|
2026-04-22 02:09:58 +02:00
|
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
</template>
|
|
|
|
|
{{ t('chat.deleteSession') }}
|
|
|
|
|
</NPopconfirm>
|
2026-05-24 14:13:42 +03:00
|
|
|
</component>
|
2026-04-22 02:09:58 +02:00
|
|
|
</template>
|
2026-05-19 16:09:59 +08:00
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.session-item-profile {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 5px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.session-item-profile-avatar {
|
2026-05-20 14:15:01 +08:00
|
|
|
background: var(--bg-secondary);
|
2026-05-19 16:09:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.session-item-profile-name {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
line-height: 16px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
2026-05-20 18:26:01 +08:00
|
|
|
|
|
|
|
|
.session-item-warning {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
border: 1px solid rgba(180, 35, 24, 0.35);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: rgba(220, 38, 38, 0.1);
|
|
|
|
|
color: #b42318;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
line-height: 14px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
2026-05-19 16:09:59 +08:00
|
|
|
</style>
|