mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
feat(contacts): show AI memories in contact details
This commit is contained in:
1 parent
db582d852e
commit
2a4ab0d2ae
5 files changed
+102
-3
No files matched your search
@@ -57,6 +57,8 @@
|
||||
"scoreHelp": "The score is used to sort contacts. A higher score means stronger interaction signals.",
|
||||
"lastInteraction": "Last interaction",
|
||||
"sources": "Source chats",
|
||||
"memories": "Memory",
|
||||
"memoryLoadFailed": "Failed to load memory",
|
||||
"sourceType": {
|
||||
"private": "Private",
|
||||
"group": "Group"
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
"scoreHelp": "総合スコアは連絡先の並び順に使われます。スコアが高いほど、やり取りのシグナルが強いことを示します。",
|
||||
"lastInteraction": "最終インタラクション",
|
||||
"sources": "元チャット",
|
||||
"memories": "メモリ",
|
||||
"memoryLoadFailed": "メモリを読み込めませんでした",
|
||||
"sourceType": {
|
||||
"private": "個人",
|
||||
"group": "グループ"
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
"scoreHelp": "综合分用于联系人排序。分数越高,代表互动信号越强。",
|
||||
"lastInteraction": "最近互动",
|
||||
"sources": "来源会话",
|
||||
"memories": "记忆",
|
||||
"memoryLoadFailed": "加载记忆失败",
|
||||
"sourceType": {
|
||||
"private": "私聊",
|
||||
"group": "群聊"
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
"scoreHelp": "綜合分用於聯絡人排序。分數越高,代表互動訊號越強。",
|
||||
"lastInteraction": "最近互動",
|
||||
"sources": "來源會話",
|
||||
"memories": "記憶",
|
||||
"memoryLoadFailed": "載入記憶失敗",
|
||||
"sourceType": {
|
||||
"private": "私聊",
|
||||
"group": "群聊"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { ContactItem } from '@openchatlab/shared-types'
|
||||
import type { AIMemoryEntry, ContactItem } from '@openchatlab/shared-types'
|
||||
import { useAIService } from '@/services'
|
||||
|
||||
const props = defineProps<{
|
||||
selectedKey: string | null
|
||||
@@ -18,7 +19,11 @@ const emit = defineEmits<{
|
||||
unmarkFriend: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t, locale } = useI18n()
|
||||
const aiService = useAIService()
|
||||
const memories = ref<AIMemoryEntry[]>([])
|
||||
const isMemoryLoading = ref(false)
|
||||
const memoryLoadFailed = ref(false)
|
||||
|
||||
const visibleAliases = computed(() => {
|
||||
const contact = props.contact
|
||||
@@ -34,6 +39,45 @@ const showFriendActions = computed(() => canMarkFriend.value || canUnmarkFriend.
|
||||
function avatarText(contact: ContactItem): string {
|
||||
return contact.displayName.trim().slice(0, 1).toUpperCase() || '?'
|
||||
}
|
||||
|
||||
function formatMemoryUpdatedAt(timestamp: number): string {
|
||||
return new Intl.DateTimeFormat(locale.value, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}).format(timestamp)
|
||||
}
|
||||
|
||||
async function loadContactMemories(key: string): Promise<void> {
|
||||
memories.value = []
|
||||
memoryLoadFailed.value = false
|
||||
isMemoryLoading.value = true
|
||||
try {
|
||||
const loaded = await aiService.getAIMemories({ scopeType: 'contact', scopeId: key })
|
||||
if (props.selectedKey === key) memories.value = loaded
|
||||
} catch {
|
||||
if (props.selectedKey === key) memoryLoadFailed.value = true
|
||||
} finally {
|
||||
if (props.selectedKey === key) isMemoryLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function retryLoadMemories(): void {
|
||||
if (props.selectedKey) void loadContactMemories(props.selectedKey)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.selectedKey,
|
||||
(key) => {
|
||||
if (key) void loadContactMemories(key)
|
||||
else {
|
||||
memories.value = []
|
||||
memoryLoadFailed.value = false
|
||||
isMemoryLoading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -180,6 +224,53 @@ function avatarText(contact: ContactItem): string {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="border-t border-gray-100 px-5 py-4 dark:border-white/5">
|
||||
<div class="mb-3 flex items-center justify-between gap-3">
|
||||
<h3
|
||||
class="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-gray-400 dark:text-gray-500"
|
||||
>
|
||||
<UIcon name="i-lucide-brain" class="h-3.5 w-3.5" />
|
||||
{{ t('contacts.detail.memories') }}
|
||||
</h3>
|
||||
<span v-if="!isMemoryLoading && !memoryLoadFailed" class="text-[10px] text-gray-400 dark:text-gray-500">
|
||||
{{ t('ai.global.memory.count', { count: memories.length }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isMemoryLoading" class="flex items-center gap-2 py-3 text-xs text-gray-400 dark:text-gray-500">
|
||||
<UIcon name="i-lucide-loader-2" class="h-3.5 w-3.5 animate-spin" />
|
||||
<span>{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="memoryLoadFailed"
|
||||
class="flex items-center justify-between gap-3 rounded-xl bg-gray-50 px-3.5 py-3 dark:bg-white/5"
|
||||
>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('contacts.detail.memoryLoadFailed') }}</span>
|
||||
<UButton color="neutral" variant="ghost" size="xs" @click="retryLoadMemories">
|
||||
{{ t('common.retry') }}
|
||||
</UButton>
|
||||
</div>
|
||||
<p v-else-if="memories.length === 0" class="py-2 text-xs leading-5 text-gray-400 dark:text-gray-500">
|
||||
{{ t('ai.global.memory.empty') }}
|
||||
</p>
|
||||
<div v-else class="space-y-2">
|
||||
<article
|
||||
v-for="memory in memories"
|
||||
:key="memory.id"
|
||||
class="rounded-xl bg-gray-50 px-3.5 py-3 dark:bg-white/5"
|
||||
>
|
||||
<p class="whitespace-pre-wrap break-words text-xs leading-5 text-gray-700 dark:text-gray-300">
|
||||
{{ memory.content }}
|
||||
</p>
|
||||
<div class="mt-2 flex items-center gap-1.5 text-[10px] text-gray-400 dark:text-gray-500">
|
||||
<span>{{ t(`ai.global.memory.source.${memory.sourceType}`) }}</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{{ formatMemoryUpdatedAt(memory.updatedAt) }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
</Transition>
|
||||
|
||||
Reference in new issue
Block a user