mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(insight): add group relationship galaxy view
This commit is contained in:
1 parent
03520b4cc0
commit
2f5f97f0bf
14 files changed
+515
-23
No files matched your search
@@ -3,7 +3,7 @@ import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { SectionTabs } from '@/components/navigation'
|
||||
import ClusterView from '@/components/analysis/relationships/ClusterView.vue'
|
||||
import InteractionView from '@/components/analysis/relationships/InteractionView.vue'
|
||||
import GroupRelationshipGalaxyView from '@/components/analysis/relationships/GroupRelationshipGalaxyView.vue'
|
||||
import MentionRankingView from '@/components/analysis/relationships/MentionRankingView.vue'
|
||||
import type { TimeFilter } from '@openchatlab/shared-types'
|
||||
import { getProximityTimeFilter } from './group-relationship-filter'
|
||||
@@ -14,13 +14,13 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const activeSubTab = ref('mention-graph')
|
||||
const activeSubTab = ref('relationship-galaxy')
|
||||
const proximityTimeFilter = computed(() => getProximityTimeFilter(props.timeFilter))
|
||||
const subTabs = computed(() => [
|
||||
{
|
||||
id: 'mention-graph',
|
||||
label: t('analysis.subTabs.groupRelationships.mentionGraph'),
|
||||
icon: 'i-heroicons-arrows-right-left',
|
||||
id: 'relationship-galaxy',
|
||||
label: t('analysis.subTabs.groupRelationships.relationshipGalaxy'),
|
||||
icon: 'i-lucide-orbit',
|
||||
},
|
||||
{
|
||||
id: 'mention-ranking',
|
||||
@@ -41,8 +41,8 @@ const subTabs = computed(() => [
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<InteractionView
|
||||
v-if="activeSubTab === 'mention-graph'"
|
||||
<GroupRelationshipGalaxyView
|
||||
v-if="activeSubTab === 'relationship-galaxy'"
|
||||
:session-id="props.sessionId"
|
||||
:time-filter="props.timeFilter"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type {
|
||||
GroupRelationshipGalaxyData,
|
||||
GroupRelationshipGalaxyMemberDetail,
|
||||
RelationshipGalaxyRenderNode,
|
||||
TimeFilter,
|
||||
} from '@openchatlab/shared-types'
|
||||
import { LoadingState } from '@/components/UI'
|
||||
import LazyAvatar from '@/components/common/avatar/LazyAvatar.vue'
|
||||
import RelationshipGalaxyThreeCanvas from '@/components/charts/relationship-galaxy/RelationshipGalaxyThreeCanvas.vue'
|
||||
import { useDataService } from '@/services'
|
||||
import { reportError } from '@/services/log-report'
|
||||
import { buildGroupRelationshipGalaxyConnections } from './group-relationship-galaxy-view'
|
||||
|
||||
type GalaxyCanvasInstance = {
|
||||
focusNode: (key: string) => boolean
|
||||
fitView: () => void
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const dataService = useDataService()
|
||||
const canvasRef = ref<GalaxyCanvasInstance | null>(null)
|
||||
const galaxyData = ref<GroupRelationshipGalaxyData | null>(null)
|
||||
const selectedKey = ref<string | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const loadError = ref('')
|
||||
const webglUnavailable = ref(false)
|
||||
const isWideLayout = ref(false)
|
||||
let requestId = 0
|
||||
let detailMediaQuery: MediaQueryList | null = null
|
||||
|
||||
const selectedMember = computed(() => {
|
||||
if (!selectedKey.value) return null
|
||||
return galaxyData.value?.members.find((member) => member.key === selectedKey.value) ?? null
|
||||
})
|
||||
const connections = computed(() => buildGroupRelationshipGalaxyConnections(galaxyData.value, selectedKey.value))
|
||||
const hasGraph = computed(() => (galaxyData.value?.graph.nodes.length ?? 0) > 0)
|
||||
const safeInsetRight = computed(() => (selectedMember.value && isWideLayout.value ? 392 : 0))
|
||||
const numberFormatter = computed(
|
||||
() => new Intl.NumberFormat(locale.value, { maximumFractionDigits: 1, notation: 'compact' })
|
||||
)
|
||||
const dateFormatter = computed(
|
||||
() => new Intl.DateTimeFormat(locale.value, { year: 'numeric', month: 'short', day: 'numeric' })
|
||||
)
|
||||
|
||||
async function loadData() {
|
||||
if (!props.sessionId) return
|
||||
const currentRequestId = ++requestId
|
||||
isLoading.value = true
|
||||
loadError.value = ''
|
||||
webglUnavailable.value = false
|
||||
selectedKey.value = null
|
||||
|
||||
try {
|
||||
const result = await dataService.getGroupRelationshipGalaxy(props.sessionId, props.timeFilter)
|
||||
if (currentRequestId !== requestId) return
|
||||
galaxyData.value = result
|
||||
} catch (error) {
|
||||
if (currentRequestId !== requestId) return
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
loadError.value = message
|
||||
galaxyData.value = null
|
||||
reportError(`Group relationship galaxy load failed: ${message}`, error instanceof Error ? error.stack : undefined)
|
||||
} finally {
|
||||
if (currentRequestId === requestId) isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectNode(node: RelationshipGalaxyRenderNode) {
|
||||
selectedKey.value = node.key
|
||||
await nextTick()
|
||||
canvasRef.value?.focusNode(node.key)
|
||||
}
|
||||
|
||||
async function selectMember(member: GroupRelationshipGalaxyMemberDetail) {
|
||||
selectedKey.value = member.key
|
||||
await nextTick()
|
||||
canvasRef.value?.focusNode(member.key)
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedKey.value = null
|
||||
}
|
||||
|
||||
async function resetView() {
|
||||
selectedKey.value = null
|
||||
await nextTick()
|
||||
canvasRef.value?.fitView()
|
||||
}
|
||||
|
||||
function handleCanvasFallback() {
|
||||
webglUnavailable.value = true
|
||||
selectedKey.value = null
|
||||
}
|
||||
|
||||
function syncDetailLayout() {
|
||||
isWideLayout.value = detailMediaQuery?.matches ?? false
|
||||
}
|
||||
|
||||
function avatarText(member: GroupRelationshipGalaxyMemberDetail): string {
|
||||
return (member.displayName || member.platformId || '?').slice(0, 1)
|
||||
}
|
||||
|
||||
function formatNumber(value: number): string {
|
||||
return numberFormatter.value.format(value)
|
||||
}
|
||||
|
||||
function formatScore(value: number): string {
|
||||
return `${Math.round(value * 100)}`
|
||||
}
|
||||
|
||||
function formatTime(value: number | null): string {
|
||||
if (!value) return '—'
|
||||
return dateFormatter.value.format(new Date(value * 1000))
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.sessionId, props.timeFilter?.startTs, props.timeFilter?.endTs, props.timeFilter?.memberId],
|
||||
() => void loadData(),
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
detailMediaQuery = window.matchMedia('(min-width: 768px)')
|
||||
syncDetailLayout()
|
||||
detailMediaQuery.addEventListener('change', syncDetailLayout)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
requestId++
|
||||
detailMediaQuery?.removeEventListener('change', syncDetailLayout)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative h-full min-h-[420px] w-full overflow-hidden bg-[#050302] text-white">
|
||||
<LoadingState v-if="isLoading" variant="page" :text="t('views.groupRelationshipGalaxy.loading')" />
|
||||
|
||||
<div v-else-if="loadError" class="flex h-full items-center justify-center px-6 text-center">
|
||||
<div class="max-w-sm">
|
||||
<UIcon name="i-lucide-circle-alert" class="mx-auto h-10 w-10 text-rose-300" />
|
||||
<h3 class="mt-3 text-base font-semibold">{{ t('views.groupRelationshipGalaxy.loadFailed') }}</h3>
|
||||
<p class="mt-1 line-clamp-2 text-sm text-white/50">{{ loadError }}</p>
|
||||
<UButton class="mt-4" color="neutral" variant="soft" icon="i-lucide-refresh-cw" @click="loadData">
|
||||
{{ t('common.retry') }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="webglUnavailable" class="flex h-full items-center justify-center px-6 text-center">
|
||||
<div class="max-w-sm">
|
||||
<UIcon name="i-lucide-orbit" class="mx-auto h-11 w-11 text-amber-200/80" />
|
||||
<h3 class="mt-3 text-base font-semibold">{{ t('views.groupRelationshipGalaxy.webglUnavailable') }}</h3>
|
||||
<p class="mt-1 text-sm leading-6 text-white/50">
|
||||
{{ t('views.groupRelationshipGalaxy.webglUnavailableDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!hasGraph" class="flex h-full items-center justify-center px-6 text-center">
|
||||
<div class="max-w-sm">
|
||||
<UIcon name="i-lucide-sparkles" class="mx-auto h-11 w-11 text-sky-200/70" />
|
||||
<h3 class="mt-3 text-base font-semibold">{{ t('views.groupRelationshipGalaxy.emptyTitle') }}</h3>
|
||||
<p class="mt-1 text-sm leading-6 text-white/50">
|
||||
{{ t('views.groupRelationshipGalaxy.emptyDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="galaxyData">
|
||||
<RelationshipGalaxyThreeCanvas
|
||||
ref="canvasRef"
|
||||
:graph="galaxyData.graph"
|
||||
:selected-key="selectedKey"
|
||||
:safe-inset-right="safeInsetRight"
|
||||
:label="t('views.groupRelationshipGalaxy.canvasLabel')"
|
||||
@clear-selection="clearSelection"
|
||||
@fallback="handleCanvasFallback"
|
||||
@select-node="selectNode"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
icon="i-lucide-scan"
|
||||
color="neutral"
|
||||
variant="soft"
|
||||
size="xs"
|
||||
class="absolute left-4 top-4 z-20 border border-white/8 bg-black/25 text-white/75 backdrop-blur-md hover:bg-white/10 hover:text-white"
|
||||
:aria-label="t('views.groupRelationshipGalaxy.resetView')"
|
||||
@click="resetView"
|
||||
>
|
||||
{{ t('views.groupRelationshipGalaxy.resetView') }}
|
||||
</UButton>
|
||||
|
||||
<aside
|
||||
v-if="selectedMember"
|
||||
class="dark absolute inset-x-3 bottom-3 z-20 flex max-h-[72vh] flex-col overflow-hidden rounded-2xl border border-white/8 bg-[#0b0c12]/88 text-white shadow-overlay backdrop-blur-xl md:inset-x-auto md:bottom-4 md:right-4 md:top-4 md:max-h-none md:w-[360px]"
|
||||
>
|
||||
<UButton
|
||||
icon="i-lucide-x"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
class="absolute right-2.5 top-2.5 z-10 text-white/60 hover:bg-white/8 hover:text-white"
|
||||
:aria-label="t('views.groupRelationshipGalaxy.closeDetail')"
|
||||
@click="clearSelection"
|
||||
/>
|
||||
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4 px-4 pb-4 pt-5">
|
||||
<header class="flex shrink-0 items-center gap-3 pr-8">
|
||||
<LazyAvatar
|
||||
:src="selectedMember.avatar"
|
||||
:alt="selectedMember.displayName"
|
||||
:text="avatarText(selectedMember)"
|
||||
root-class="h-11 w-11 shrink-0 overflow-hidden rounded-xl border border-white/10"
|
||||
image-class="h-11 w-11 rounded-xl object-cover"
|
||||
fallback-class="flex h-11 w-11 items-center justify-center rounded-xl bg-sky-400/12 text-sm font-bold text-sky-200"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="truncate text-base font-semibold">{{ selectedMember.displayName }}</h3>
|
||||
<span class="rounded-full bg-white/8 px-2 py-0.5 font-mono text-[10px] text-white/55">
|
||||
#{{ selectedMember.rank }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-0.5 truncate text-xs text-white/40">{{ selectedMember.platformId }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="grid shrink-0 grid-cols-3 gap-1.5">
|
||||
<div class="rounded-xl border border-white/6 bg-white/3 px-2.5 py-2">
|
||||
<p class="text-[9px] uppercase tracking-wider text-white/35">
|
||||
{{ t('views.groupRelationshipGalaxy.messageCount') }}
|
||||
</p>
|
||||
<p class="mt-1 font-mono text-sm font-semibold">{{ formatNumber(selectedMember.messageCount) }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/6 bg-white/3 px-2.5 py-2">
|
||||
<p class="text-[9px] uppercase tracking-wider text-white/35">
|
||||
{{ t('views.groupRelationshipGalaxy.relationshipScore') }}
|
||||
</p>
|
||||
<p class="mt-1 font-mono text-sm font-semibold text-sky-200">
|
||||
{{ formatScore(selectedMember.relationshipScore) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/6 bg-white/3 px-2.5 py-2">
|
||||
<p class="text-[9px] uppercase tracking-wider text-white/35">
|
||||
{{ t('views.groupRelationshipGalaxy.lastInteraction') }}
|
||||
</p>
|
||||
<p class="mt-1 truncate text-xs font-semibold">{{ formatTime(selectedMember.lastInteractionTs) }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shrink-0">
|
||||
<h4 class="mb-2 text-[10px] font-semibold uppercase tracking-[0.16em] text-white/40">
|
||||
{{ t('views.groupRelationshipGalaxy.signalComposition') }}
|
||||
</h4>
|
||||
<div class="grid grid-cols-3 gap-1.5">
|
||||
<div class="rounded-xl bg-violet-400/8 px-2.5 py-2 text-violet-100">
|
||||
<p class="text-[10px] text-violet-100/50">{{ t('views.groupRelationshipGalaxy.replies') }}</p>
|
||||
<p class="mt-0.5 font-mono text-sm font-semibold">
|
||||
{{ formatNumber(selectedMember.replyInteractionCount) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-sky-400/8 px-2.5 py-2 text-sky-100">
|
||||
<p class="text-[10px] text-sky-100/50">{{ t('views.groupRelationshipGalaxy.mentions') }}</p>
|
||||
<p class="mt-0.5 font-mono text-sm font-semibold">
|
||||
{{ formatNumber(selectedMember.mentionInteractionCount) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-amber-300/8 px-2.5 py-2 text-amber-100">
|
||||
<p class="text-[10px] text-amber-100/50">{{ t('views.groupRelationshipGalaxy.proximity') }}</p>
|
||||
<p class="mt-0.5 font-mono text-sm font-semibold">
|
||||
{{ formatNumber(selectedMember.coOccurrenceCount) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex min-h-0 flex-1 flex-col">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<h4 class="text-[10px] font-semibold uppercase tracking-[0.16em] text-white/40">
|
||||
{{ t('views.groupRelationshipGalaxy.strongConnections') }}
|
||||
</h4>
|
||||
<span class="font-mono text-[10px] text-white/30">{{ connections.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="connections.length" class="min-h-0 flex-1 space-y-1 overflow-y-auto pr-1 scrollbar-thin">
|
||||
<button
|
||||
v-for="connection in connections"
|
||||
:key="connection.edge.id"
|
||||
type="button"
|
||||
class="group flex w-full items-center gap-2.5 rounded-xl border border-transparent px-2 py-2 text-left transition-colors hover:border-white/6 hover:bg-white/5"
|
||||
@click="selectMember(connection.member)"
|
||||
>
|
||||
<LazyAvatar
|
||||
:src="connection.member.avatar"
|
||||
:alt="connection.member.displayName"
|
||||
:text="avatarText(connection.member)"
|
||||
root-class="h-7 w-7 shrink-0 overflow-hidden rounded-lg border border-white/8"
|
||||
image-class="h-7 w-7 rounded-lg object-cover"
|
||||
fallback-class="flex h-7 w-7 items-center justify-center rounded-lg bg-white/6 text-[10px] font-semibold text-white/65"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-xs font-semibold text-white/85 group-hover:text-white">
|
||||
{{ connection.member.displayName }}
|
||||
</p>
|
||||
<p class="mt-0.5 truncate text-[10px] text-white/35">
|
||||
{{
|
||||
t('views.groupRelationshipGalaxy.connectionSignals', {
|
||||
replies: connection.edge.replyInteractionCount,
|
||||
mentions: connection.edge.mentionInteractionCount,
|
||||
proximity: connection.edge.coOccurrenceCount,
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<span class="shrink-0 font-mono text-xs font-semibold text-sky-200/80">
|
||||
{{ formatNumber(connection.edge.weight) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="py-5 text-center text-xs text-white/35">
|
||||
{{ t('views.groupRelationshipGalaxy.noConnections') }}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import type { GroupRelationshipGalaxyData } from '@openchatlab/shared-types'
|
||||
import { buildGroupRelationshipGalaxyConnections } from './group-relationship-galaxy-view'
|
||||
|
||||
const data = {
|
||||
members: [
|
||||
{ key: 'a', rank: 1 },
|
||||
{ key: 'b', rank: 2 },
|
||||
{ key: 'c', rank: 3 },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'a-c', sourceKey: 'a', targetKey: 'c', weight: 2, lastInteractionTs: 200 },
|
||||
{ id: 'a-b', sourceKey: 'b', targetKey: 'a', weight: 5, lastInteractionTs: 100 },
|
||||
{ id: 'b-c', sourceKey: 'b', targetKey: 'c', weight: 8, lastInteractionTs: 300 },
|
||||
],
|
||||
} as GroupRelationshipGalaxyData
|
||||
|
||||
test('builds the selected member connection list from either edge direction', () => {
|
||||
const connections = buildGroupRelationshipGalaxyConnections(data, 'a')
|
||||
|
||||
assert.deepEqual(
|
||||
connections.map((item) => [item.member.key, item.edge.id]),
|
||||
[
|
||||
['b', 'a-b'],
|
||||
['c', 'a-c'],
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('limits the detail list without including unrelated strong edges', () => {
|
||||
assert.deepEqual(
|
||||
buildGroupRelationshipGalaxyConnections(data, 'a', 1).map((item) => item.member.key),
|
||||
['b']
|
||||
)
|
||||
assert.deepEqual(buildGroupRelationshipGalaxyConnections(data, null), [])
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import type {
|
||||
GroupRelationshipGalaxyData,
|
||||
GroupRelationshipGalaxyEdgeDetail,
|
||||
GroupRelationshipGalaxyMemberDetail,
|
||||
} from '@openchatlab/shared-types'
|
||||
|
||||
export interface GroupRelationshipGalaxyConnection {
|
||||
member: GroupRelationshipGalaxyMemberDetail
|
||||
edge: GroupRelationshipGalaxyEdgeDetail
|
||||
}
|
||||
|
||||
export function buildGroupRelationshipGalaxyConnections(
|
||||
data: GroupRelationshipGalaxyData | null,
|
||||
selectedKey: string | null,
|
||||
limit = 10
|
||||
): GroupRelationshipGalaxyConnection[] {
|
||||
if (!data || !selectedKey) return []
|
||||
const memberByKey = new Map(data.members.map((member) => [member.key, member]))
|
||||
return data.edges
|
||||
.flatMap((edge): GroupRelationshipGalaxyConnection[] => {
|
||||
if (edge.sourceKey !== selectedKey && edge.targetKey !== selectedKey) return []
|
||||
const otherKey = edge.sourceKey === selectedKey ? edge.targetKey : edge.sourceKey
|
||||
const member = memberByKey.get(otherKey)
|
||||
return member ? [{ member, edge }] : []
|
||||
})
|
||||
.sort(
|
||||
(a, b) =>
|
||||
b.edge.weight - a.edge.weight ||
|
||||
(b.edge.lastInteractionTs ?? 0) - (a.edge.lastInteractionTs ?? 0) ||
|
||||
a.member.rank - b.member.rank ||
|
||||
a.member.key.localeCompare(b.member.key)
|
||||
)
|
||||
.slice(0, Math.max(0, limit))
|
||||
}
|
||||
@@ -71,6 +71,7 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'select-node', node: RelationshipGalaxyRenderNode): void
|
||||
(event: 'clear-selection'): void
|
||||
(event: 'fallback'): void
|
||||
}>()
|
||||
|
||||
@@ -491,7 +492,10 @@ function handlePointerLeave() {
|
||||
|
||||
function handleClick() {
|
||||
const key = hoveredKey.value
|
||||
if (!key) return
|
||||
if (!key) {
|
||||
emit('clear-selection')
|
||||
return
|
||||
}
|
||||
const object = nodeObjects.get(key)
|
||||
if (!object) return
|
||||
emit('select-node', object.sceneNode.node)
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"catchphrase": "Catchphrases"
|
||||
},
|
||||
"groupRelationships": {
|
||||
"mentionGraph": "Mention Graph",
|
||||
"relationshipGalaxy": "Relationship Galaxy",
|
||||
"mentionRanking": "Mention Ranking",
|
||||
"proximity": "Proximity"
|
||||
},
|
||||
|
||||
@@ -118,6 +118,27 @@
|
||||
"graphHint": "{nodes} members, {links} interactions",
|
||||
"noInteraction": "No mention interaction data"
|
||||
},
|
||||
"groupRelationshipGalaxy": {
|
||||
"canvasLabel": "Group interaction relationship galaxy",
|
||||
"loading": "Mapping group relationships...",
|
||||
"loadFailed": "Failed to load the relationship galaxy",
|
||||
"webglUnavailable": "The relationship galaxy is unavailable",
|
||||
"webglUnavailableDescription": "Enable hardware acceleration or try an environment that supports WebGL.",
|
||||
"emptyTitle": "Not enough interaction data",
|
||||
"emptyDescription": "At least two members need replies, mentions, or nearby messages in this time range.",
|
||||
"resetView": "Reset view",
|
||||
"closeDetail": "Close member details",
|
||||
"messageCount": "Group messages",
|
||||
"relationshipScore": "Relationship strength",
|
||||
"lastInteraction": "Last interaction",
|
||||
"signalComposition": "Interaction signals",
|
||||
"replies": "Replies",
|
||||
"mentions": "Mentions",
|
||||
"proximity": "Proximity",
|
||||
"strongConnections": "Closest connections",
|
||||
"connectionSignals": "{replies} replies · {mentions} mentions · {proximity} nearby",
|
||||
"noConnections": "No direct connections to display"
|
||||
},
|
||||
"relationship": {
|
||||
"empty": {
|
||||
"title": "No Relationship Data",
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"catchphrase": "口癖"
|
||||
},
|
||||
"groupRelationships": {
|
||||
"mentionGraph": "{'@'} 関係図",
|
||||
"relationshipGalaxy": "関係ギャラクシー",
|
||||
"mentionRanking": "{'@'} ランキング",
|
||||
"proximity": "発言の近接度"
|
||||
},
|
||||
|
||||
@@ -118,6 +118,27 @@
|
||||
"graphHint": "全 {nodes} 名のメンバー、{links} 件のインタラクション",
|
||||
"noInteraction": "メンションデータがありません"
|
||||
},
|
||||
"groupRelationshipGalaxy": {
|
||||
"canvasLabel": "グループの交流関係ギャラクシー",
|
||||
"loading": "グループ内の関係を整理しています...",
|
||||
"loadFailed": "関係ギャラクシーを読み込めませんでした",
|
||||
"webglUnavailable": "この環境ではギャラクシーを表示できません",
|
||||
"webglUnavailableDescription": "ハードウェアアクセラレーションを有効にするか、WebGL 対応環境で再試行してください。",
|
||||
"emptyTitle": "交流データが不足しています",
|
||||
"emptyDescription": "この期間には、2人以上の返信、メンション、または連続した発言が必要です。",
|
||||
"resetView": "表示をリセット",
|
||||
"closeDetail": "メンバー詳細を閉じる",
|
||||
"messageCount": "グループ発言数",
|
||||
"relationshipScore": "関係の強さ",
|
||||
"lastInteraction": "最近の交流",
|
||||
"signalComposition": "交流の内訳",
|
||||
"replies": "返信",
|
||||
"mentions": "メンション",
|
||||
"proximity": "発言の近さ",
|
||||
"strongConnections": "親しいつながり",
|
||||
"connectionSignals": "返信 {replies} · メンション {mentions} · 近接 {proximity}",
|
||||
"noConnections": "表示できる直接のつながりはありません"
|
||||
},
|
||||
"relationship": {
|
||||
"empty": {
|
||||
"title": "関係データがありません",
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"catchphrase": "口头禅"
|
||||
},
|
||||
"groupRelationships": {
|
||||
"mentionGraph": "{'@'} 关系图",
|
||||
"relationshipGalaxy": "关系星空图",
|
||||
"mentionRanking": "{'@'} 排行",
|
||||
"proximity": "发言临近度"
|
||||
},
|
||||
|
||||
@@ -118,6 +118,27 @@
|
||||
"graphHint": "共 {nodes} 位成员,{links} 条互动关系",
|
||||
"noInteraction": "暂无艾特互动数据"
|
||||
},
|
||||
"groupRelationshipGalaxy": {
|
||||
"canvasLabel": "群聊互动关系星空图",
|
||||
"loading": "正在整理群内关系...",
|
||||
"loadFailed": "关系星空图加载失败",
|
||||
"webglUnavailable": "当前环境无法显示星空图",
|
||||
"webglUnavailableDescription": "请启用浏览器硬件加速,或更换支持 WebGL 的环境后重试。",
|
||||
"emptyTitle": "暂无足够的互动关系",
|
||||
"emptyDescription": "当前时间范围内至少需要两位成员产生回复、@ 或相邻发言。",
|
||||
"resetView": "重置视角",
|
||||
"closeDetail": "关闭成员详情",
|
||||
"messageCount": "本群消息",
|
||||
"relationshipScore": "关系强度",
|
||||
"lastInteraction": "最近互动",
|
||||
"signalComposition": "互动构成",
|
||||
"replies": "回复",
|
||||
"mentions": "@ 提及",
|
||||
"proximity": "发言临近",
|
||||
"strongConnections": "关系最密切",
|
||||
"connectionSignals": "回复 {replies} · @ {mentions} · 临近 {proximity}",
|
||||
"noConnections": "暂无可展示的直接关系"
|
||||
},
|
||||
"relationship": {
|
||||
"empty": {
|
||||
"title": "暂无关系数据",
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"catchphrase": "口頭禪"
|
||||
},
|
||||
"groupRelationships": {
|
||||
"mentionGraph": "{'@'} 關係圖",
|
||||
"relationshipGalaxy": "關係星空圖",
|
||||
"mentionRanking": "{'@'} 排行",
|
||||
"proximity": "發言臨近度"
|
||||
},
|
||||
|
||||
@@ -118,6 +118,27 @@
|
||||
"graphHint": "共 {nodes} 位成員,{links} 條互動關係",
|
||||
"noInteraction": "暫無艾特互動資料"
|
||||
},
|
||||
"groupRelationshipGalaxy": {
|
||||
"canvasLabel": "群聊互動關係星空圖",
|
||||
"loading": "正在整理群內關係...",
|
||||
"loadFailed": "關係星空圖載入失敗",
|
||||
"webglUnavailable": "目前環境無法顯示星空圖",
|
||||
"webglUnavailableDescription": "請啟用瀏覽器硬體加速,或改用支援 WebGL 的環境後重試。",
|
||||
"emptyTitle": "暫無足夠的互動關係",
|
||||
"emptyDescription": "目前時間範圍內至少需要兩位成員產生回覆、@ 或相鄰發言。",
|
||||
"resetView": "重設視角",
|
||||
"closeDetail": "關閉成員詳情",
|
||||
"messageCount": "本群訊息",
|
||||
"relationshipScore": "關係強度",
|
||||
"lastInteraction": "最近互動",
|
||||
"signalComposition": "互動構成",
|
||||
"replies": "回覆",
|
||||
"mentions": "@ 提及",
|
||||
"proximity": "發言臨近",
|
||||
"strongConnections": "關係最密切",
|
||||
"connectionSignals": "回覆 {replies} · @ {mentions} · 臨近 {proximity}",
|
||||
"noConnections": "暫無可顯示的直接關係"
|
||||
},
|
||||
"relationship": {
|
||||
"empty": {
|
||||
"title": "暫無關係資料",
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 群关系 - 合并视图
|
||||
* 将 @ 关系图、@ 排行、发言临近度 三个分析整合到一个 Tab 中
|
||||
*/
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { SectionTabs } from '@/components/navigation'
|
||||
import InteractionView from '@/components/analysis/relationships/InteractionView.vue'
|
||||
import GroupRelationshipGalaxyView from '@/components/analysis/relationships/GroupRelationshipGalaxyView.vue'
|
||||
import MentionRankingView from '@/components/analysis/relationships/MentionRankingView.vue'
|
||||
import ClusterView from '@/components/analysis/relationships/ClusterView.vue'
|
||||
import type { TimeFilter } from '@openchatlab/shared-types'
|
||||
@@ -18,13 +14,13 @@ const props = defineProps<{
|
||||
timeFilter?: TimeFilter
|
||||
}>()
|
||||
|
||||
const activeSubTab = ref('mention-graph')
|
||||
const activeSubTab = ref('relationship-galaxy')
|
||||
|
||||
const subTabs = computed(() => [
|
||||
{
|
||||
id: 'mention-graph',
|
||||
label: t('analysis.subTabs.groupRelationships.mentionGraph'),
|
||||
icon: 'i-heroicons-arrows-right-left',
|
||||
id: 'relationship-galaxy',
|
||||
label: t('analysis.subTabs.groupRelationships.relationshipGalaxy'),
|
||||
icon: 'i-lucide-orbit',
|
||||
},
|
||||
{ id: 'mention-ranking', label: t('analysis.subTabs.groupRelationships.mentionRanking'), icon: 'i-heroicons-heart' },
|
||||
{ id: 'proximity', label: t('analysis.subTabs.groupRelationships.proximity'), icon: 'i-heroicons-user-group' },
|
||||
@@ -37,8 +33,8 @@ const subTabs = computed(() => [
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<InteractionView
|
||||
v-if="activeSubTab === 'mention-graph'"
|
||||
<GroupRelationshipGalaxyView
|
||||
v-if="activeSubTab === 'relationship-galaxy'"
|
||||
:session-id="props.sessionId"
|
||||
:time-filter="props.timeFilter"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user