diff --git a/apps/web-wasm/src/components/session/insights/GroupRelationshipInsights.vue b/apps/web-wasm/src/components/session/insights/GroupRelationshipInsights.vue index fd771950..67d25136 100644 --- a/apps/web-wasm/src/components/session/insights/GroupRelationshipInsights.vue +++ b/apps/web-wasm/src/components/session/insights/GroupRelationshipInsights.vue @@ -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(() => [
- diff --git a/src/components/analysis/relationships/GroupRelationshipGalaxyView.vue b/src/components/analysis/relationships/GroupRelationshipGalaxyView.vue new file mode 100644 index 00000000..2ea242f7 --- /dev/null +++ b/src/components/analysis/relationships/GroupRelationshipGalaxyView.vue @@ -0,0 +1,337 @@ + + + diff --git a/src/components/analysis/relationships/group-relationship-galaxy-view.test.ts b/src/components/analysis/relationships/group-relationship-galaxy-view.test.ts new file mode 100644 index 00000000..0e8c1933 --- /dev/null +++ b/src/components/analysis/relationships/group-relationship-galaxy-view.test.ts @@ -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), []) +}) diff --git a/src/components/analysis/relationships/group-relationship-galaxy-view.ts b/src/components/analysis/relationships/group-relationship-galaxy-view.ts new file mode 100644 index 00000000..2352c581 --- /dev/null +++ b/src/components/analysis/relationships/group-relationship-galaxy-view.ts @@ -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)) +} diff --git a/src/components/charts/relationship-galaxy/RelationshipGalaxyThreeCanvas.vue b/src/components/charts/relationship-galaxy/RelationshipGalaxyThreeCanvas.vue index 18741d45..5d8d3fea 100644 --- a/src/components/charts/relationship-galaxy/RelationshipGalaxyThreeCanvas.vue +++ b/src/components/charts/relationship-galaxy/RelationshipGalaxyThreeCanvas.vue @@ -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) diff --git a/src/i18n/locales/en-US/analysis.json b/src/i18n/locales/en-US/analysis.json index 74eb49ee..564734d2 100644 --- a/src/i18n/locales/en-US/analysis.json +++ b/src/i18n/locales/en-US/analysis.json @@ -81,7 +81,7 @@ "catchphrase": "Catchphrases" }, "groupRelationships": { - "mentionGraph": "Mention Graph", + "relationshipGalaxy": "Relationship Galaxy", "mentionRanking": "Mention Ranking", "proximity": "Proximity" }, diff --git a/src/i18n/locales/en-US/views.json b/src/i18n/locales/en-US/views.json index 6425b630..ff5d1af4 100644 --- a/src/i18n/locales/en-US/views.json +++ b/src/i18n/locales/en-US/views.json @@ -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", diff --git a/src/i18n/locales/ja-JP/analysis.json b/src/i18n/locales/ja-JP/analysis.json index e5af05bc..16ed14fb 100644 --- a/src/i18n/locales/ja-JP/analysis.json +++ b/src/i18n/locales/ja-JP/analysis.json @@ -81,7 +81,7 @@ "catchphrase": "口癖" }, "groupRelationships": { - "mentionGraph": "{'@'} 関係図", + "relationshipGalaxy": "関係ギャラクシー", "mentionRanking": "{'@'} ランキング", "proximity": "発言の近接度" }, diff --git a/src/i18n/locales/ja-JP/views.json b/src/i18n/locales/ja-JP/views.json index bfdcf074..3f54b87e 100644 --- a/src/i18n/locales/ja-JP/views.json +++ b/src/i18n/locales/ja-JP/views.json @@ -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": "関係データがありません", diff --git a/src/i18n/locales/zh-CN/analysis.json b/src/i18n/locales/zh-CN/analysis.json index f7b218e3..fd061066 100644 --- a/src/i18n/locales/zh-CN/analysis.json +++ b/src/i18n/locales/zh-CN/analysis.json @@ -82,7 +82,7 @@ "catchphrase": "口头禅" }, "groupRelationships": { - "mentionGraph": "{'@'} 关系图", + "relationshipGalaxy": "关系星空图", "mentionRanking": "{'@'} 排行", "proximity": "发言临近度" }, diff --git a/src/i18n/locales/zh-CN/views.json b/src/i18n/locales/zh-CN/views.json index 0ca222e6..79e55eea 100644 --- a/src/i18n/locales/zh-CN/views.json +++ b/src/i18n/locales/zh-CN/views.json @@ -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": "暂无关系数据", diff --git a/src/i18n/locales/zh-TW/analysis.json b/src/i18n/locales/zh-TW/analysis.json index 4298d1bc..dbbbb21f 100644 --- a/src/i18n/locales/zh-TW/analysis.json +++ b/src/i18n/locales/zh-TW/analysis.json @@ -81,7 +81,7 @@ "catchphrase": "口頭禪" }, "groupRelationships": { - "mentionGraph": "{'@'} 關係圖", + "relationshipGalaxy": "關係星空圖", "mentionRanking": "{'@'} 排行", "proximity": "發言臨近度" }, diff --git a/src/i18n/locales/zh-TW/views.json b/src/i18n/locales/zh-TW/views.json index 6e735ce4..fff2422a 100644 --- a/src/i18n/locales/zh-TW/views.json +++ b/src/i18n/locales/zh-TW/views.json @@ -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": "暫無關係資料", diff --git a/src/pages/group-chat/components/insights/GroupRelationships.vue b/src/pages/group-chat/components/insights/GroupRelationships.vue index 275e2da7..cd4edc05 100644 --- a/src/pages/group-chat/components/insights/GroupRelationships.vue +++ b/src/pages/group-chat/components/insights/GroupRelationships.vue @@ -1,12 +1,8 @@