mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(owner): add owner prompt modal and unify owner selection UI
Opening an unowned session first tries to auto-apply the platform owner profile; only when that fails (and the session is not dismissed) a lightweight searchable prompt pops up with Later / Don't remind / Save actions. Member management now uses an entry card opening the same modal, replacing the embedded OwnerSelector dropdown. Imports in the session store silently retry profile application per session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a75d17e310
commit
502762bcf5
14 files changed
+540
-155
No files matched your search
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* "我是谁"入口卡片(成员管理页)
|
||||
*
|
||||
* 展示当前 owner 状态,点击打开 OwnerPromptModal 统一选择入口。
|
||||
*/
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { MemberWithStats } from '@/types/analysis'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import OwnerPromptModal from './OwnerPromptModal.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const sessionStore = useSessionStore()
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
members: MemberWithStats[]
|
||||
chatType?: 'group' | 'private'
|
||||
}>()
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
function getDisplayName(member: MemberWithStats): string {
|
||||
return member.groupNickname || member.accountName || member.platformId
|
||||
}
|
||||
|
||||
const currentOwner = computed(() => {
|
||||
const ownerId = sessionStore.currentSession?.ownerId
|
||||
if (!ownerId) return null
|
||||
return props.members.find((m) => m.platformId === ownerId) || null
|
||||
})
|
||||
|
||||
const ownerDisplayName = computed(() => {
|
||||
if (currentOwner.value) return getDisplayName(currentOwner.value)
|
||||
// owner 已设置但成员列表中找不到(如成员被合并/删除)时退回显示 platformId
|
||||
return sessionStore.currentSession?.ownerId ?? ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-150 rounded-lg border border-gray-200 bg-white p-3 shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-8 w-8 items-center justify-center rounded-full bg-linear-to-br"
|
||||
:class="chatType === 'group' ? 'from-pink-400 to-pink-600' : 'from-purple-400 to-purple-600'"
|
||||
>
|
||||
<UIcon name="i-heroicons-user" class="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{
|
||||
ownerDisplayName
|
||||
? t('members.ownerPrompt.currentOwner', { name: ownerDisplayName })
|
||||
: t('members.ownerPrompt.choose')
|
||||
}}
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('members.ownerPrompt.helper') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<UButton size="sm" :variant="ownerDisplayName ? 'outline' : 'solid'" color="primary" @click="showModal = true">
|
||||
{{ ownerDisplayName ? t('members.ownerPrompt.change') : t('members.ownerPrompt.choose') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<OwnerPromptModal v-model="showModal" :session-id="sessionId" :chat-type="chatType" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,294 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* "我是谁"选择弹窗
|
||||
*
|
||||
* 两种使用方式:
|
||||
* 1. 会话页挂载(autoCheck):会话无 owner 时先尝试用平台 owner profile 自动补全,
|
||||
* 补全失败且未被"本会话不再提醒"时自动弹出
|
||||
* 2. 成员管理入口手动打开(v-model)
|
||||
*
|
||||
* 保存时会更新平台级 owner profile,并自动应用到同平台其他未设置 owner 的会话。
|
||||
*/
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { MemberWithStats } from '@/types/analysis'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { useDataService } from '@/services'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
sessionId: string
|
||||
/** 弹窗打开状态(v-model) */
|
||||
modelValue?: boolean
|
||||
chatType?: 'group' | 'private'
|
||||
/** 挂载时自动检测:无 owner 时尝试自动补全,失败且未忽略则弹出 */
|
||||
autoCheck?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: false,
|
||||
chatType: 'group',
|
||||
autoCheck: false,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'saved', ownerId: string): void
|
||||
(e: 'cleared'): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const sessionStore = useSessionStore()
|
||||
|
||||
const isOpen = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value),
|
||||
})
|
||||
|
||||
const members = ref<MemberWithStats[]>([])
|
||||
const isLoading = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const isDismissing = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedPlatformId = ref<string | null>(null)
|
||||
|
||||
const currentOwnerId = computed(() => sessionStore.currentSession?.ownerId ?? null)
|
||||
|
||||
function getDisplayName(member: MemberWithStats): string {
|
||||
return member.groupNickname || member.accountName || member.platformId
|
||||
}
|
||||
|
||||
const filteredMembers = computed(() => {
|
||||
const query = searchQuery.value.trim().toLowerCase()
|
||||
const valid = members.value.filter((m) => m.platformId && m.platformId.trim() !== '')
|
||||
if (!query) return valid
|
||||
return valid.filter((m) =>
|
||||
[m.platformId, m.accountName ?? '', m.groupNickname ?? '', ...m.aliases].some((name) =>
|
||||
name.toLowerCase().includes(query)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
async function loadMembers() {
|
||||
if (!props.sessionId) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
members.value = await useDataService().getMembers(props.sessionId)
|
||||
} catch (error) {
|
||||
console.error('加载成员列表失败:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 自动检测:尝试自动补全 owner,失败且未忽略时弹出
|
||||
async function checkAndAutoOpen() {
|
||||
if (!props.sessionId || !props.autoCheck) return
|
||||
if (currentOwnerId.value) return
|
||||
try {
|
||||
const result = await sessionStore.tryApplyOwnerProfile(props.sessionId)
|
||||
if (result.applied || result.reason === 'already_set' || result.reason === 'missing_session') return
|
||||
if (result.dismissed) return
|
||||
isOpen.value = true
|
||||
} catch (error) {
|
||||
console.error('检查会话 owner 失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!selectedPlatformId.value || !props.sessionId) return
|
||||
isSaving.value = true
|
||||
try {
|
||||
const result = await sessionStore.setOwnerAndApplyProfile(props.sessionId, selectedPlatformId.value)
|
||||
if (result.updatedSessionIds.length > 0) {
|
||||
toast.success(t('members.ownerPrompt.saved'), {
|
||||
description: t('members.ownerPrompt.appliedToOthers', { count: result.updatedSessionIds.length }),
|
||||
})
|
||||
} else {
|
||||
toast.success(t('members.ownerPrompt.saved'))
|
||||
}
|
||||
emit('saved', result.ownerId)
|
||||
isOpen.value = false
|
||||
} catch (error) {
|
||||
console.error('设置会话 owner 失败:', error)
|
||||
toast.fail(t('members.ownerPrompt.saveFailed'))
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清除当前会话 owner(不影响平台 owner profile)
|
||||
async function clearOwner() {
|
||||
if (!props.sessionId || !currentOwnerId.value) return
|
||||
isSaving.value = true
|
||||
try {
|
||||
const success = await sessionStore.updateSessionOwnerId(props.sessionId, null)
|
||||
if (success) {
|
||||
selectedPlatformId.value = null
|
||||
emit('cleared')
|
||||
isOpen.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('清除会话 owner 失败:', error)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function dismissForever() {
|
||||
if (!props.sessionId) return
|
||||
isDismissing.value = true
|
||||
try {
|
||||
await sessionStore.dismissOwnerPrompt(props.sessionId)
|
||||
isOpen.value = false
|
||||
} catch (error) {
|
||||
console.error('忽略 owner 提醒失败:', error)
|
||||
} finally {
|
||||
isDismissing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function later() {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
// 打开时加载成员并默认选中当前 owner
|
||||
watch(isOpen, (open) => {
|
||||
if (open) {
|
||||
searchQuery.value = ''
|
||||
selectedPlatformId.value = currentOwnerId.value
|
||||
loadMembers()
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.sessionId,
|
||||
() => {
|
||||
checkAndAutoOpen()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
checkAndAutoOpen()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal v-model:open="isOpen" :ui="{ content: 'max-w-md' }">
|
||||
<template #content>
|
||||
<div class="p-6">
|
||||
<!-- 头部 -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-linear-to-br"
|
||||
:class="chatType === 'group' ? 'from-pink-400 to-pink-600' : 'from-purple-400 to-purple-600'"
|
||||
>
|
||||
<UIcon name="i-heroicons-user" class="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('members.ownerPrompt.title') }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('members.ownerPrompt.subtitle') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<UButton icon="i-heroicons-x-mark" color="neutral" variant="ghost" size="sm" @click="later" />
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<UInput
|
||||
v-model="searchQuery"
|
||||
:placeholder="t('members.ownerPrompt.searchPlaceholder')"
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
class="mb-3 w-full"
|
||||
/>
|
||||
|
||||
<!-- 成员列表 -->
|
||||
<div class="h-56 overflow-y-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div v-if="isLoading" class="flex h-full items-center justify-center">
|
||||
<UIcon name="i-heroicons-arrow-path" class="h-5 w-5 animate-spin text-gray-400" />
|
||||
</div>
|
||||
<div v-else-if="filteredMembers.length === 0" class="flex h-full items-center justify-center">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ searchQuery ? t('members.ownerPrompt.noMatch') : t('members.ownerPrompt.empty') }}
|
||||
</p>
|
||||
</div>
|
||||
<ul v-else class="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
<li v-for="member in filteredMembers" :key="member.id">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-3 px-3 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
:class="selectedPlatformId === member.platformId ? 'bg-primary-50 dark:bg-primary-900/20' : ''"
|
||||
@click="selectedPlatformId = member.platformId"
|
||||
>
|
||||
<img
|
||||
v-if="member.avatar"
|
||||
:src="member.avatar"
|
||||
:alt="getDisplayName(member)"
|
||||
class="h-8 w-8 shrink-0 rounded-full object-cover"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-linear-to-br from-gray-300 to-gray-400 text-xs font-medium text-white dark:from-gray-600 dark:to-gray-700"
|
||||
>
|
||||
{{ getDisplayName(member).slice(0, 1) }}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ getDisplayName(member) }}
|
||||
</p>
|
||||
<p class="truncate text-xs text-gray-500 dark:text-gray-400">{{ member.platformId }}</p>
|
||||
</div>
|
||||
<UIcon
|
||||
v-if="selectedPlatformId === member.platformId"
|
||||
name="i-heroicons-check-circle-solid"
|
||||
class="h-5 w-5 shrink-0 text-primary-500"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- AI 价值说明 -->
|
||||
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('members.ownerPrompt.description') }}
|
||||
</p>
|
||||
|
||||
<!-- 清除当前选择(仅已设置 owner 时显示) -->
|
||||
<div v-if="currentOwnerId" class="mt-2">
|
||||
<UButton variant="link" color="neutral" size="xs" :disabled="isSaving" @click="clearOwner">
|
||||
{{ t('members.ownerPrompt.clear') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-5 flex items-center justify-between gap-2">
|
||||
<UButton
|
||||
v-if="!currentOwnerId"
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size="sm"
|
||||
:loading="isDismissing"
|
||||
@click="dismissForever"
|
||||
>
|
||||
{{ t('members.ownerPrompt.dontRemind') }}
|
||||
</UButton>
|
||||
<span v-else />
|
||||
<div class="flex gap-2">
|
||||
<UButton variant="ghost" color="neutral" @click="later">
|
||||
{{ t('members.ownerPrompt.later') }}
|
||||
</UButton>
|
||||
<UButton color="primary" :loading="isSaving" :disabled="!selectedPlatformId" @click="save">
|
||||
{{ t('members.ownerPrompt.save') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
@@ -1,110 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { MemberWithStats } from '@/types/analysis'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
const { t } = useI18n()
|
||||
const sessionStore = useSessionStore()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
members: MemberWithStats[]
|
||||
isLoading?: boolean
|
||||
chatType?: 'group' | 'private'
|
||||
}>()
|
||||
|
||||
// Owner 配置相关
|
||||
const isSavingOwner = ref(false)
|
||||
|
||||
// 获取成员显示名称
|
||||
function getDisplayName(member: MemberWithStats): string {
|
||||
return member.groupNickname || member.accountName || member.platformId
|
||||
}
|
||||
|
||||
// 当前 owner 的成员信息
|
||||
const currentOwner = computed(() => {
|
||||
const ownerId = sessionStore.currentSession?.ownerId
|
||||
if (!ownerId) return null
|
||||
return props.members.find((m) => m.platformId === ownerId) || null
|
||||
})
|
||||
|
||||
// 特殊值,用于表示"未设置"
|
||||
const UNSET_VALUE = '__UNSET__'
|
||||
|
||||
// 成员选项(用于下拉选择)
|
||||
const memberOptions = computed(() => {
|
||||
return [
|
||||
{ label: t('members.ownerSelector.unset'), value: UNSET_VALUE },
|
||||
...props.members
|
||||
// 过滤掉 platformId 为空的成员(USelect 不允许空字符串作为 value)
|
||||
.filter((m) => m.platformId && m.platformId.trim() !== '')
|
||||
.map((m) => ({
|
||||
label: `${getDisplayName(m)} (${m.platformId})`,
|
||||
value: m.platformId,
|
||||
})),
|
||||
]
|
||||
})
|
||||
|
||||
// 当前选中的 owner 值(用于 USelect)
|
||||
const selectedOwnerValue = computed(() => {
|
||||
return sessionStore.currentSession?.ownerId || UNSET_VALUE
|
||||
})
|
||||
|
||||
// 提示文字
|
||||
const hintText = computed(() => {
|
||||
if (currentOwner.value) {
|
||||
return t('members.ownerSelector.currentOwner', { name: getDisplayName(currentOwner.value) })
|
||||
}
|
||||
return t('members.ownerSelector.hint')
|
||||
})
|
||||
|
||||
// 更新 owner
|
||||
async function updateOwner(value: string) {
|
||||
const platformId = value === UNSET_VALUE ? null : value
|
||||
isSavingOwner.value = true
|
||||
try {
|
||||
await sessionStore.updateSessionOwnerId(props.sessionId, platformId)
|
||||
} catch (error) {
|
||||
console.error('更新所有者失败:', error)
|
||||
} finally {
|
||||
isSavingOwner.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-150 rounded-lg border border-gray-200 bg-white p-3 shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-8 w-8 items-center justify-center rounded-full bg-linear-to-br"
|
||||
:class="chatType === 'group' ? 'from-pink-400 to-pink-600' : 'from-purple-400 to-purple-600'"
|
||||
>
|
||||
<UIcon name="i-heroicons-user" class="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-900 dark:text-white">{{ t('members.ownerSelector.title') }}</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ hintText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<USelect
|
||||
:model-value="selectedOwnerValue"
|
||||
:items="memberOptions"
|
||||
:placeholder="t('members.ownerSelector.selectMember')"
|
||||
class="w-48"
|
||||
:disabled="isSavingOwner || isLoading"
|
||||
@update:model-value="updateOwner"
|
||||
/>
|
||||
<UIcon
|
||||
v-if="isSavingOwner"
|
||||
name="i-heroicons-arrow-path"
|
||||
class="h-4 w-4 animate-spin"
|
||||
:class="chatType === 'group' ? 'text-pink-500' : 'text-purple-500'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { MemberWithStats } from '@/types/analysis'
|
||||
import OwnerSelector from '@/components/analysis/member/OwnerSelector.vue'
|
||||
import OwnerEntryCard from '@/components/analysis/member/OwnerEntryCard.vue'
|
||||
import { useDataService } from '@/services'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -25,7 +25,7 @@ const emit = defineEmits<{
|
||||
|
||||
// 成员列表(当前页)
|
||||
const members = ref<MemberWithStats[]>([])
|
||||
const allMembers = ref<MemberWithStats[]>([]) // 用于 OwnerSelector(仅加载一次)
|
||||
const allMembers = ref<MemberWithStats[]>([]) // 用于 OwnerEntryCard(仅加载一次)
|
||||
const isLoading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
|
||||
@@ -107,7 +107,7 @@ async function loadMembers() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载所有成员(用于 OwnerSelector)
|
||||
// 加载所有成员(用于 OwnerEntryCard)
|
||||
async function loadAllMembers() {
|
||||
if (!props.sessionId) return
|
||||
try {
|
||||
@@ -166,7 +166,7 @@ async function confirmDelete() {
|
||||
if (success) {
|
||||
// 重新加载当前页数据
|
||||
await loadMembers()
|
||||
// 同时更新 allMembers(用于 OwnerSelector)
|
||||
// 同时更新 allMembers(用于 OwnerEntryCard)
|
||||
await loadAllMembers()
|
||||
// 通知父组件刷新数据
|
||||
emit('data-changed')
|
||||
@@ -275,13 +275,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Owner配置 -->
|
||||
<OwnerSelector
|
||||
class="mb-6"
|
||||
:session-id="sessionId"
|
||||
:members="allMembers"
|
||||
:is-loading="isLoading"
|
||||
chat-type="group"
|
||||
/>
|
||||
<OwnerEntryCard class="mb-6" :session-id="sessionId" :members="allMembers" chat-type="group" />
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
{
|
||||
"ownerSelector": {
|
||||
"title": "Select Your Identity",
|
||||
"unset": "Not Set",
|
||||
"selectMember": "Select Member",
|
||||
"currentOwner": "Current: {name}",
|
||||
"hint": "1. Shown on the right side of chat viewer 2. Identifies you in AI conversations"
|
||||
"ownerPrompt": {
|
||||
"title": "Who Are You?",
|
||||
"subtitle": "Select your identity in this chat",
|
||||
"description": "Your messages will appear on the right side of the chat viewer, and AI conversations can identify you for more accurate analysis. Other chats from the same platform will be filled in automatically.",
|
||||
"searchPlaceholder": "Search members",
|
||||
"noMatch": "No matching members found",
|
||||
"empty": "No member data",
|
||||
"later": "Later",
|
||||
"dontRemind": "Don't remind me for this chat",
|
||||
"save": "Save",
|
||||
"clear": "Clear current selection",
|
||||
"saved": "Your identity has been set",
|
||||
"appliedToOthers": "Automatically applied to {count} chats from the same platform",
|
||||
"saveFailed": "Failed to set identity",
|
||||
"currentOwner": "I am: {name}",
|
||||
"choose": "Choose who I am",
|
||||
"change": "Change",
|
||||
"helper": "Helps AI identify you in conversations for more accurate analysis"
|
||||
},
|
||||
"list": {
|
||||
"title": "Group Member Management",
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
{
|
||||
"ownerSelector": {
|
||||
"title": "あなたを選択してください",
|
||||
"unset": "未設定",
|
||||
"selectMember": "メンバーを選択",
|
||||
"currentOwner": "現在:{name}",
|
||||
"hint": "1. チャット履歴ビューアの右側に表示されます 2. AI との会話ではあなた本人として扱われます"
|
||||
"ownerPrompt": {
|
||||
"title": "あなたはどなたですか?",
|
||||
"subtitle": "このチャットでのあなたの身元を選択してください",
|
||||
"description": "設定するとあなたのメッセージがチャット履歴ビューアの右側に表示され、AI との会話でもあなた本人として認識され、分析がより正確になります。同じプラットフォームの他のチャットにも自動的に適用されます。",
|
||||
"searchPlaceholder": "メンバーを検索",
|
||||
"noMatch": "一致するメンバーが見つかりません",
|
||||
"empty": "メンバーデータがありません",
|
||||
"later": "後で",
|
||||
"dontRemind": "このチャットでは再通知しない",
|
||||
"save": "保存",
|
||||
"clear": "現在の選択をクリア",
|
||||
"saved": "あなたの身元を設定しました",
|
||||
"appliedToOthers": "同じプラットフォームの {count} 件のチャットに自動適用しました",
|
||||
"saveFailed": "身元の設定に失敗しました",
|
||||
"currentOwner": "私は:{name}",
|
||||
"choose": "自分を選択",
|
||||
"change": "変更",
|
||||
"helper": "設定すると AI があなたを認識し、分析がより正確になります"
|
||||
},
|
||||
"list": {
|
||||
"title": "メンバー管理",
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
{
|
||||
"ownerSelector": {
|
||||
"title": "选择你是谁",
|
||||
"unset": "未设置",
|
||||
"selectMember": "选择成员",
|
||||
"currentOwner": "当前:{name}",
|
||||
"hint": "1. 将会显示在聊天记录查看器中的右侧 2. AI对话中会标识你的身份"
|
||||
"ownerPrompt": {
|
||||
"title": "你是谁?",
|
||||
"subtitle": "选择你在这个对话中的身份",
|
||||
"description": "设置后你的消息会显示在聊天记录查看器右侧,AI 对话也能识别你的身份,分析更准确。同平台的其他对话会自动套用。",
|
||||
"searchPlaceholder": "搜索成员",
|
||||
"noMatch": "没有找到匹配的成员",
|
||||
"empty": "暂无成员数据",
|
||||
"later": "稍后",
|
||||
"dontRemind": "本会话不再提醒",
|
||||
"save": "保存",
|
||||
"clear": "清除当前选择",
|
||||
"saved": "已设置你的身份",
|
||||
"appliedToOthers": "已自动应用到 {count} 个同平台会话",
|
||||
"saveFailed": "设置身份失败",
|
||||
"currentOwner": "我是:{name}",
|
||||
"choose": "选择我是谁",
|
||||
"change": "修改",
|
||||
"helper": "设置后 AI 对话能识别你的身份,分析更准确"
|
||||
},
|
||||
"list": {
|
||||
"title": "成员管理",
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
{
|
||||
"ownerSelector": {
|
||||
"title": "選擇你是誰",
|
||||
"unset": "未設定",
|
||||
"selectMember": "選擇成員",
|
||||
"currentOwner": "目前:{name}",
|
||||
"hint": "1. 會顯示在聊天紀錄檢視器右側 2. AI 對話中也會標示你的身分"
|
||||
"ownerPrompt": {
|
||||
"title": "你是誰?",
|
||||
"subtitle": "選擇你在這個對話中的身分",
|
||||
"description": "設定後你的訊息會顯示在聊天紀錄檢視器右側,AI 對話也能識別你的身分,分析更準確。同平台的其他對話會自動套用。",
|
||||
"searchPlaceholder": "搜尋成員",
|
||||
"noMatch": "沒有找到符合的成員",
|
||||
"empty": "暫無成員資料",
|
||||
"later": "稍後",
|
||||
"dontRemind": "此對話不再提醒",
|
||||
"save": "儲存",
|
||||
"clear": "清除目前選擇",
|
||||
"saved": "已設定你的身分",
|
||||
"appliedToOthers": "已自動套用到 {count} 個同平台對話",
|
||||
"saveFailed": "設定身分失敗",
|
||||
"currentOwner": "我是:{name}",
|
||||
"choose": "選擇我是誰",
|
||||
"change": "修改",
|
||||
"helper": "設定後 AI 對話能識別你的身分,分析更準確"
|
||||
},
|
||||
"list": {
|
||||
"title": "成員管理",
|
||||
|
||||
@@ -13,6 +13,7 @@ import MemberList from '@/components/common/member/MemberList.vue'
|
||||
import NicknameHistoryEntry from './components/member/NicknameHistoryEntry.vue'
|
||||
import SessionAnalysisHeader from '@/components/layout/session/SessionAnalysisHeader.vue'
|
||||
import SessionIndexModal from '@/components/analysis/SessionIndexModal.vue'
|
||||
import OwnerPromptModal from '@/components/analysis/member/OwnerPromptModal.vue'
|
||||
import IncrementalImportModal from '@/components/analysis/IncrementalImportModal.vue'
|
||||
const MessageExportModal = defineAsyncComponent(() => import('@/components/MessageExport/MessageExportModal.vue'))
|
||||
import ActionToolsPanel from '@/components/layout/ActionToolsPanel.vue'
|
||||
@@ -35,6 +36,9 @@ const { currentSessionId } = storeToRefs(sessionStore)
|
||||
// 会话索引弹窗状态
|
||||
const showSessionIndexModal = ref(false)
|
||||
|
||||
// "我是谁"提示弹窗状态
|
||||
const showOwnerPromptModal = ref(false)
|
||||
|
||||
// 增量导入弹窗状态
|
||||
const showIncrementalImportModal = ref(false)
|
||||
|
||||
@@ -219,6 +223,15 @@ const filteredMemberCount = computed(() => {
|
||||
:message-count="session.messageCount"
|
||||
/>
|
||||
|
||||
<!-- "我是谁"提示弹窗(内部自动检测并弹出;避免与索引弹窗叠加) -->
|
||||
<OwnerPromptModal
|
||||
v-if="currentSessionId && session && !showSessionIndexModal"
|
||||
v-model="showOwnerPromptModal"
|
||||
:session-id="currentSessionId"
|
||||
chat-type="group"
|
||||
auto-check
|
||||
/>
|
||||
|
||||
<!-- 增量导入弹窗 -->
|
||||
<IncrementalImportModal
|
||||
v-if="currentSessionId && session"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { MemberWithStats } from '@/types/analysis'
|
||||
import OwnerSelector from '@/components/analysis/member/OwnerSelector.vue'
|
||||
import OwnerEntryCard from '@/components/analysis/member/OwnerEntryCard.vue'
|
||||
import { useDataService } from '@/services'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -177,13 +177,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Owner配置 -->
|
||||
<OwnerSelector
|
||||
class="mb-6"
|
||||
:session-id="sessionId"
|
||||
:members="members"
|
||||
:is-loading="isLoading"
|
||||
chat-type="private"
|
||||
/>
|
||||
<OwnerEntryCard class="mb-6" :session-id="sessionId" :members="members" chat-type="private" />
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="isLoading" class="flex h-60 items-center justify-center">
|
||||
|
||||
@@ -12,6 +12,7 @@ import ViewTab from './components/ViewTab.vue'
|
||||
import MemberManagementPanel from './components/MemberTab.vue'
|
||||
import SessionAnalysisHeader from '@/components/layout/session/SessionAnalysisHeader.vue'
|
||||
import SessionIndexModal from '@/components/analysis/SessionIndexModal.vue'
|
||||
import OwnerPromptModal from '@/components/analysis/member/OwnerPromptModal.vue'
|
||||
import IncrementalImportModal from '@/components/analysis/IncrementalImportModal.vue'
|
||||
const MessageExportModal = defineAsyncComponent(() => import('@/components/MessageExport/MessageExportModal.vue'))
|
||||
import ActionToolsPanel from '@/components/layout/ActionToolsPanel.vue'
|
||||
@@ -34,6 +35,9 @@ const { currentSessionId } = storeToRefs(sessionStore)
|
||||
// 会话索引弹窗状态
|
||||
const showSessionIndexModal = ref(false)
|
||||
|
||||
// "我是谁"提示弹窗状态
|
||||
const showOwnerPromptModal = ref(false)
|
||||
|
||||
// 增量导入弹窗状态
|
||||
const showIncrementalImportModal = ref(false)
|
||||
|
||||
@@ -229,6 +233,15 @@ const otherMemberAvatar = computed(() => {
|
||||
:message-count="session.messageCount"
|
||||
/>
|
||||
|
||||
<!-- "我是谁"提示弹窗(内部自动检测并弹出;避免与索引弹窗叠加) -->
|
||||
<OwnerPromptModal
|
||||
v-if="currentSessionId && session && !showSessionIndexModal"
|
||||
v-model="showOwnerPromptModal"
|
||||
:session-id="currentSessionId"
|
||||
chat-type="private"
|
||||
auto-check
|
||||
/>
|
||||
|
||||
<!-- 增量导入弹窗 -->
|
||||
<IncrementalImportModal
|
||||
v-if="currentSessionId && session"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
||||
import type { TimeFilter } from '@openchatlab/shared-types'
|
||||
import type { TimeFilter, ApplyOwnerProfileResult, SetOwnerAndApplyProfileResult } from '@openchatlab/shared-types'
|
||||
import type {
|
||||
MemberActivity,
|
||||
MemberWithStats,
|
||||
@@ -73,6 +73,19 @@ export class FetchDataAdapter implements DataAdapter {
|
||||
return result.success
|
||||
}
|
||||
|
||||
tryApplyOwnerProfile(sessionId: string): Promise<ApplyOwnerProfileResult> {
|
||||
return post(`/sessions/${sessionId}/owner/apply-profile`, {})
|
||||
}
|
||||
|
||||
setOwnerAndApplyProfile(sessionId: string, ownerPlatformId: string): Promise<SetOwnerAndApplyProfileResult> {
|
||||
return post(`/sessions/${sessionId}/owner/select`, { ownerPlatformId })
|
||||
}
|
||||
|
||||
async dismissOwnerPrompt(sessionId: string): Promise<boolean> {
|
||||
const result = await post<{ success: boolean }>(`/sessions/${sessionId}/owner/dismiss-prompt`, {})
|
||||
return result.success
|
||||
}
|
||||
|
||||
// ==================== 时间范围 ====================
|
||||
|
||||
getAvailableYears(sessionId: string): Promise<number[]> {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { AnalysisSession, MessageType } from '@/types/base'
|
||||
import type { TimeFilter } from '@openchatlab/shared-types'
|
||||
import type { TimeFilter, ApplyOwnerProfileResult, SetOwnerAndApplyProfileResult } from '@openchatlab/shared-types'
|
||||
import type {
|
||||
MemberActivity,
|
||||
MemberWithStats,
|
||||
@@ -86,6 +86,9 @@ export interface DataAdapter {
|
||||
deleteSession(sessionId: string): Promise<boolean>
|
||||
renameSession(sessionId: string, newName: string): Promise<boolean>
|
||||
updateSessionOwnerId(sessionId: string, ownerId: string | null): Promise<boolean>
|
||||
tryApplyOwnerProfile(sessionId: string): Promise<ApplyOwnerProfileResult>
|
||||
setOwnerAndApplyProfile(sessionId: string, ownerPlatformId: string): Promise<SetOwnerAndApplyProfileResult>
|
||||
dismissOwnerPrompt(sessionId: string): Promise<boolean>
|
||||
|
||||
// ==================== 时间范围 ====================
|
||||
|
||||
|
||||
@@ -290,6 +290,8 @@ export const useSessionStore = defineStore(
|
||||
console.error('自动生成会话索引失败:', error)
|
||||
}
|
||||
|
||||
await applyOwnerProfileAfterImport(importResult.sessionId)
|
||||
|
||||
return { success: true, diagnostics: importResult.diagnostics }
|
||||
} else {
|
||||
return {
|
||||
@@ -420,6 +422,7 @@ export const useSessionStore = defineStore(
|
||||
} catch (error) {
|
||||
console.error('自动生成会话索引失败:', error)
|
||||
}
|
||||
await applyOwnerProfileAfterImport(importResult.sessionId)
|
||||
} else {
|
||||
file.status = 'failed'
|
||||
file.error = importResult.error || 'error.import_failed'
|
||||
@@ -443,6 +446,7 @@ export const useSessionStore = defineStore(
|
||||
console.error('自动生成会话索引失败:', error)
|
||||
}
|
||||
}
|
||||
await applyOwnerProfileAfterImport(importResult.sessionId)
|
||||
} else {
|
||||
file.status = 'failed'
|
||||
file.error = importResult.error || 'error.import_failed'
|
||||
@@ -684,6 +688,53 @@ export const useSessionStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动选择"我是谁":写入当前会话 owner,更新平台 owner profile,
|
||||
* 并批量应用到同平台其他未设置 owner 的会话
|
||||
*/
|
||||
async function setOwnerAndApplyProfile(id: string, ownerPlatformId: string) {
|
||||
const result = await useDataService().setOwnerAndApplyProfile(id, ownerPlatformId)
|
||||
for (const sessionId of [id, ...result.updatedSessionIds]) {
|
||||
const session = sessions.value.find((s) => s.id === sessionId)
|
||||
if (session) {
|
||||
session.ownerId = result.ownerId
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试用已保存的平台 owner profile 自动补全会话 owner(唯一匹配才写入)
|
||||
*/
|
||||
async function tryApplyOwnerProfile(id: string) {
|
||||
const result = await useDataService().tryApplyOwnerProfile(id)
|
||||
if (result.applied && result.ownerId) {
|
||||
const session = sessions.value.find((s) => s.id === id)
|
||||
if (session) {
|
||||
session.ownerId = result.ownerId
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 本会话不再提醒"选择我是谁"(仅抑制弹窗,不影响自动补全)
|
||||
*/
|
||||
function dismissOwnerPrompt(id: string): Promise<boolean> {
|
||||
return useDataService().dismissOwnerPrompt(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入成功后静默尝试应用平台 owner profile(失败不影响导入流程)
|
||||
*/
|
||||
async function applyOwnerProfileAfterImport(id: string): Promise<void> {
|
||||
try {
|
||||
await tryApplyOwnerProfile(id)
|
||||
} catch (error) {
|
||||
console.warn('导入后应用 owner profile 失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 置顶会话 ID 列表
|
||||
const pinnedSessionIds = ref<string[]>([])
|
||||
|
||||
@@ -774,6 +825,9 @@ export const useSessionStore = defineStore(
|
||||
renameSession,
|
||||
clearSelection,
|
||||
updateSessionOwnerId,
|
||||
setOwnerAndApplyProfile,
|
||||
tryApplyOwnerProfile,
|
||||
dismissOwnerPrompt,
|
||||
togglePinSession,
|
||||
isPinned,
|
||||
// 批量导入
|
||||
|
||||
Reference in new issue
Block a user