Skip to content

Commit 538769a

Browse files
authored
refactor(api): split opinion fetching for analysis views (#414)
Fixed AI label bug on analysis details page by separating analysis opinion fetching from regular opinion fetching, ensuring cluster statistics are always included in analysis views. **Analysis API refactoring:** - Split opinion fetching into two distinct code paths: * fetchOpinionsByPostSlugId: for regular opinion lists without cluster stats (new/moderated/hidden/discover feeds) * fetchAnalysisOpinionsByPostSlugId: for analysis views with cluster stats (consensus/controversial/representative opinions) - Fixed AI label bug: analysis opinions now always include cluster statistics because they use the dedicated analysis fetch path - Simplified filterTarget parameter (removed unused filter types) **Type safety improvements:** - Added AnalysisOpinionItem type that extends OpinionItem with required clustersStats array - Ensures analysis opinions always have cluster statistics attached - Prevents runtime errors from missing cluster data in UI components **Load testing fixes:** - Fixed 401 authentication errors during teardown phase - Root cause: MemoryKeyStore keys were not shared between VUs and teardown environment in k6 - Solution: Added exportKeys/importKeys methods to MemoryKeyStore - Keys now generated in setup(), exported as JSON, and imported into each VU and teardown phase - Eliminates need to regenerate keys in teardown (which was failing) **Code cleanup:** - Removed 4 unused API endpoints (fetch-consensus, fetch-majority, fetch-controversial, fetch-representative) - Removed ~3000 lines from OpenAPI spec (unused endpoint definitions) - Removed unused imports and SQL query builder functions - Simplified frontend query hooks with better cache management - Removed dead cluster stats fetching code from user service - Removed redundant SQL queries in user profile fetching Net result: 33 files changed, 1536 insertions(+), 4479 deletions(-)
1 parent d0e77d7 commit 538769a

33 files changed

Lines changed: 1536 additions & 4479 deletions

File tree

services/agora/src/api/api.ts

Lines changed: 141 additions & 405 deletions
Large diffs are not rendered by default.

services/agora/src/components/post/PostDetails.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,34 +142,39 @@ const participantCountLocal = ref(
142142
143143
const { profileData } = storeToRefs(useUserStore());
144144
145-
// Preload both analysis and comment data immediately when component mounts (only if not in compact mode)
145+
// Lazy load analysis data only when user clicks Analysis tab
146+
const isAnalysisEnabled = computed(() => !props.compactMode && currentTab.value === 'analysis');
146147
const analysisQuery = useAnalysisQuery({
147148
conversationSlugId: props.conversationData.metadata.conversationSlugId,
148149
voteCount: props.conversationData.metadata.voteCount,
149-
enabled: !props.compactMode,
150+
enabled: isAnalysisEnabled,
150151
});
151152
152153
// Preload comment queries for all filter types (only if not in compact mode)
153154
const commentsDiscoverQuery = useCommentsQuery({
154155
conversationSlugId: props.conversationData.metadata.conversationSlugId,
155156
filter: "discover",
157+
voteCount: props.conversationData.metadata.voteCount,
156158
enabled: !props.compactMode,
157159
});
158160
159161
const commentsNewQuery = useCommentsQuery({
160162
conversationSlugId: props.conversationData.metadata.conversationSlugId,
161163
filter: "new",
164+
voteCount: props.conversationData.metadata.voteCount,
162165
enabled: !props.compactMode,
163166
});
164167
165168
const commentsModeratedQuery = useCommentsQuery({
166169
conversationSlugId: props.conversationData.metadata.conversationSlugId,
167170
filter: "moderated",
171+
voteCount: props.conversationData.metadata.voteCount,
168172
enabled: !props.compactMode,
169173
});
170174
171175
const hiddenCommentsQuery = useHiddenCommentsQuery({
172176
conversationSlugId: props.conversationData.metadata.conversationSlugId,
177+
voteCount: props.conversationData.metadata.voteCount,
173178
enabled: !props.compactMode && profileData.value.isModerator,
174179
});
175180

services/agora/src/components/post/analysis/AnalysisPage.vue

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
:item-list="analysisQuery.data.value?.consensus || []"
1414
:compact-mode="currentTab === 'Summary'"
1515
:clusters="analysisQuery.data.value?.polisClusters || {}"
16+
:cluster-labels="clusterLabels"
17+
:vote-count="props.participantCount"
1618
/>
1719
</div>
1820

@@ -26,6 +28,8 @@
2628
:item-list="analysisQuery.data.value?.controversial || []"
2729
:compact-mode="currentTab === 'Summary'"
2830
:clusters="analysisQuery.data.value?.polisClusters || {}"
31+
:cluster-labels="clusterLabels"
32+
:vote-count="props.participantCount"
2933
/>
3034
</div>
3135

@@ -57,11 +61,15 @@ import {
5761
type AnalysisPageTranslations,
5862
} from "./AnalysisPage.i18n";
5963
import type { UseQueryReturnType } from "@tanstack/vue-query";
60-
import type { OpinionItem, PolisClusters } from "src/shared/types/zod";
64+
import type {
65+
AnalysisOpinionItem,
66+
PolisClusters,
67+
PolisKey,
68+
} from "src/shared/types/zod";
6169
6270
type AnalysisData = {
63-
consensus: OpinionItem[];
64-
controversial: OpinionItem[];
71+
consensus: AnalysisOpinionItem[];
72+
controversial: AnalysisOpinionItem[];
6573
polisClusters: Partial<PolisClusters>;
6674
};
6775
@@ -80,6 +88,19 @@ const currentTab = ref<ShortcutItem>("Summary");
8088
// Use the passed-in analysis query instead of creating our own
8189
const analysisQuery = props.analysisQuery;
8290
91+
// Extract only cluster labels for optimal performance (300 bytes instead of 300KB)
92+
const clusterLabels = computed(() => {
93+
const labels: Partial<Record<PolisKey, string>> = {};
94+
if (!analysisQuery.data.value?.polisClusters) return labels;
95+
96+
for (const [key, cluster] of Object.entries(analysisQuery.data.value.polisClusters)) {
97+
if (cluster?.aiLabel) {
98+
labels[key as PolisKey] = cluster.aiLabel;
99+
}
100+
}
101+
return labels;
102+
});
103+
83104
const asyncStateConfig = {
84105
loading: {
85106
text: t("loadingAnalysis"),

services/agora/src/components/post/analysis/consensusTab/ConsensusItem.vue

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
v-model="showDialog"
3131
:conversation-slug-id="props.conversationSlugId"
3232
:opinion-item="props.opinionItem"
33+
:vote-count="props.voteCount"
34+
:cluster-labels="props.clusterLabels"
3335
/>
3436
</template>
3537

@@ -38,7 +40,11 @@ import { computed, ref } from "vue";
3840
import VoteCountVisualizer from "../common/VoteCountVisualizer.vue";
3941
import OpinionAnalysisDialog from "./OpinionAnalysisDialog.vue";
4042
import OpinionGridLayout from "../common/OpinionGridLayout.vue";
41-
import type { OpinionItem } from "src/shared/types/zod";
43+
import type {
44+
AnalysisOpinionItem,
45+
PolisClusters,
46+
PolisKey,
47+
} from "src/shared/types/zod";
4248
import ZKHtmlContent from "src/components/ui-library/ZKHtmlContent.vue";
4349
import { useComponentI18n } from "src/composables/ui/useComponentI18n";
4450
import {
@@ -48,8 +54,11 @@ import {
4854
4955
const props = defineProps<{
5056
conversationSlugId: string;
51-
opinionItem: OpinionItem;
52-
opinionItemForVisualizer: OpinionItem;
57+
opinionItem: AnalysisOpinionItem;
58+
opinionItemForVisualizer: AnalysisOpinionItem;
59+
voteCount: number;
60+
polisClusters: Partial<PolisClusters>;
61+
clusterLabels: Partial<Record<PolisKey, string>>;
5362
}>();
5463
5564
const { t } = useComponentI18n<ConsensusItemTranslations>(

services/agora/src/components/post/analysis/consensusTab/ConsensusTab.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
:conversation-slug-id="props.conversationSlugId"
3131
:opinion-item="consensusItem"
3232
:opinion-item-for-visualizer="consensusItem"
33+
:vote-count="props.voteCount"
34+
:polis-clusters="props.clusters"
35+
:cluster-labels="props.clusterLabels"
3336
/>
3437
</template>
3538
</AnalysisSectionWrapper>
@@ -43,7 +46,11 @@ import AnalysisActionButton from "../common/AnalysisActionButton.vue";
4346
import EmptyStateMessage from "../common/EmptyStateMessage.vue";
4447
import ConsensusItem from "./ConsensusItem.vue";
4548
import type { ShortcutItem } from "src/utils/component/analysis/shortcutBar";
46-
import type { OpinionItem, PolisClusters } from "src/shared/types/zod";
49+
import type {
50+
AnalysisOpinionItem,
51+
PolisClusters,
52+
PolisKey,
53+
} from "src/shared/types/zod";
4754
import { useComponentI18n } from "src/composables/ui/useComponentI18n";
4855
import {
4956
consensusTabTranslations,
@@ -52,9 +59,11 @@ import {
5259
5360
const props = defineProps<{
5461
conversationSlugId: string;
55-
itemList: OpinionItem[];
62+
itemList: AnalysisOpinionItem[];
5663
compactMode: boolean;
5764
clusters: Partial<PolisClusters>;
65+
clusterLabels: Partial<Record<PolisKey, string>>;
66+
voteCount: number;
5867
}>();
5968
6069
const currentTab = defineModel<ShortcutItem>();

services/agora/src/components/post/analysis/consensusTab/OpinionAnalysisDialog.vue

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126
>
127127
<td class="group-name">
128128
{{
129-
`${formatClusterLabel(group.key, true, group.aiLabel)} (${formatAmount(group.numUsers)})`
129+
`${formatClusterLabel(group.key, true, getClusterLabel(group.key))} (${formatAmount(group.numUsers)})`
130130
}}
131131
</td>
132132
<td class="agree-cell">
@@ -177,7 +177,10 @@
177177
import ZKButton from "src/components/ui-library/ZKButton.vue";
178178
import ZKHtmlContent from "src/components/ui-library/ZKHtmlContent.vue";
179179
import OpinionIdentityCard from "src/components/post/comments/OpinionIdentityCard.vue";
180-
import type { OpinionItem } from "src/shared/types/zod";
180+
import type {
181+
AnalysisOpinionItem,
182+
PolisKey,
183+
} from "src/shared/types/zod";
181184
import { formatClusterLabel } from "src/utils/component/opinion";
182185
import { calculatePercentage } from "src/shared/util";
183186
import { formatAmount, formatPercentage } from "src/utils/common";
@@ -191,7 +194,9 @@ import {
191194
192195
const props = defineProps<{
193196
conversationSlugId: string;
194-
opinionItem: OpinionItem;
197+
opinionItem: AnalysisOpinionItem;
198+
voteCount: number;
199+
clusterLabels: Partial<Record<PolisKey, string>>;
195200
}>();
196201
197202
const { forceOpenComment } = useRouterNavigation();
@@ -202,6 +207,10 @@ const { t } = useComponentI18n<OpinionAnalysisDialogTranslations>(
202207
203208
const showDialog = defineModel<boolean>({ required: true });
204209
210+
function getClusterLabel(key: PolisKey): string | undefined {
211+
return props.clusterLabels[key];
212+
}
213+
205214
const noGroupStats = computed(() => {
206215
const totalClusteredAgrees = props.opinionItem.clustersStats.reduce(
207216
(sum, cluster) => sum + cluster.numAgrees,

services/agora/src/components/post/analysis/divisivenessTab/DivisiveTab.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
:conversation-slug-id="props.conversationSlugId"
3131
:opinion-item="consensusItem"
3232
:opinion-item-for-visualizer="consensusItem"
33+
:vote-count="props.voteCount"
34+
:polis-clusters="props.clusters"
35+
:cluster-labels="props.clusterLabels"
3336
/>
3437
</template>
3538
</AnalysisSectionWrapper>
@@ -43,7 +46,11 @@ import AnalysisActionButton from "../common/AnalysisActionButton.vue";
4346
import EmptyStateMessage from "../common/EmptyStateMessage.vue";
4447
import ConsensusItem from "../consensusTab/ConsensusItem.vue";
4548
import type { ShortcutItem } from "src/utils/component/analysis/shortcutBar";
46-
import type { OpinionItem, PolisClusters } from "src/shared/types/zod";
49+
import type {
50+
AnalysisOpinionItem,
51+
PolisClusters,
52+
PolisKey,
53+
} from "src/shared/types/zod";
4754
import { useComponentI18n } from "src/composables/ui/useComponentI18n";
4855
import {
4956
divisiveTabTranslations,
@@ -52,9 +59,11 @@ import {
5259
5360
const props = defineProps<{
5461
conversationSlugId: string;
55-
itemList: OpinionItem[];
62+
itemList: AnalysisOpinionItem[];
5663
compactMode: boolean;
5764
clusters: Partial<PolisClusters>;
65+
clusterLabels: Partial<Record<PolisKey, string>>;
66+
voteCount: number;
5867
}>();
5968
6069
const currentTab = defineModel<ShortcutItem>();

services/agora/src/components/post/analysis/opinionGroupTab/OpinionGroupComments.vue

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,21 @@
3636
:conversation-slug-id="props.conversationSlugId"
3737
:opinion-item="comment"
3838
:opinion-item-for-visualizer="getModifiedOpinionItem(comment)"
39+
:vote-count="props.voteCount"
40+
:polis-clusters="props.polisClusters"
41+
:cluster-labels="props.clusterLabels"
3942
/>
4043
</div>
4144
</div>
4245
</template>
4346

4447
<script setup lang="ts">
4548
import { computed, ref, watch } from "vue";
46-
import type { OpinionItem } from "src/shared/types/zod";
47-
import type { PolisKey } from "src/shared/types/zod";
49+
import type {
50+
AnalysisOpinionItem,
51+
PolisClusters,
52+
PolisKey,
53+
} from "src/shared/types/zod";
4854
import ConsensusItem from "../consensusTab/ConsensusItem.vue";
4955
import { useComponentI18n } from "src/composables/ui/useComponentI18n";
5056
import {
@@ -54,9 +60,12 @@ import {
5460
5561
const props = defineProps<{
5662
conversationSlugId: string;
57-
itemList: OpinionItem[];
63+
itemList: AnalysisOpinionItem[];
5864
currentClusterTab: PolisKey;
5965
hasUngroupedParticipants: boolean;
66+
voteCount: number;
67+
polisClusters: Partial<PolisClusters>;
68+
clusterLabels: Partial<Record<PolisKey, string>>;
6069
}>();
6170
6271
const { t } = useComponentI18n<OpinionGroupCommentsTranslations>(
@@ -74,7 +83,7 @@ watch(
7483
}
7584
);
7685
77-
function getActiveVotes(comment: OpinionItem) {
86+
function getActiveVotes(comment: AnalysisOpinionItem) {
7887
const currentClusterStats = comment.clustersStats.find(
7988
(cv) => cv.key === props.currentClusterTab
8089
);
@@ -136,7 +145,7 @@ function getActiveVotes(comment: OpinionItem) {
136145
}
137146
}
138147
139-
function getModifiedOpinionItem(comment: OpinionItem): OpinionItem {
148+
function getModifiedOpinionItem(comment: AnalysisOpinionItem): AnalysisOpinionItem {
140149
const activeVotes = getActiveVotes(comment);
141150
return {
142151
...comment,

services/agora/src/components/post/analysis/opinionGroupTab/OpinionGroupTab.vue

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@
5252
<OpinionGroupComments
5353
:conversation-slug-id="props.conversationSlugId"
5454
:item-list="
55-
props.clusters[currentClusterTab]?.representative ??
56-
([] as OpinionItem[])
55+
props.clusters[currentClusterTab]?.representative ?? []
5756
"
5857
:current-cluster-tab="currentClusterTab"
5958
:has-ungrouped-participants="hasUngroupedParticipants"
59+
:vote-count="props.totalParticipantCount"
60+
:polis-clusters="props.clusters"
61+
:cluster-labels="clusterLabels"
6062
@update:current-cluster-tab="currentClusterTab = $event"
6163
/>
6264
</template>
@@ -69,7 +71,6 @@
6971

7072
<script setup lang="ts">
7173
import type {
72-
OpinionItem,
7374
PolisClusters,
7475
PolisKey,
7576
} from "src/shared/types/zod";
@@ -124,4 +125,15 @@ const currentAiSummary = computed(() => {
124125
}
125126
return undefined;
126127
});
128+
129+
// Extract only cluster labels for optimal performance
130+
const clusterLabels = computed(() => {
131+
const labels: Partial<Record<PolisKey, string>> = {};
132+
for (const [key, cluster] of Object.entries(props.clusters)) {
133+
if (cluster?.aiLabel) {
134+
labels[key as PolisKey] = cluster.aiLabel;
135+
}
136+
}
137+
return labels;
138+
});
127139
</script>

services/agora/src/pages/dev/opinion-group-visualization.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ import { StandardMenuBar } from "src/components/navigation/header/variants";
7070
import DrawerLayout from "src/layouts/DrawerLayout.vue";
7171
import OpinionGroupTab from "src/components/post/analysis/opinionGroupTab/OpinionGroupTab.vue";
7272
import type {
73+
AnalysisOpinionItem,
7374
PolisClusters,
7475
PolisKey,
75-
OpinionItem,
7676
} from "src/shared/types/zod";
7777
import {
7878
opinionGroupVisualizationTranslations,
@@ -121,8 +121,8 @@ const clusterSummaries = [
121121
"This group is driven by idealistic principles and long-term vision.",
122122
];
123123
124-
function generateMockOpinions(count: number = 3): OpinionItem[] {
125-
const mockOpinions: OpinionItem[] = [];
124+
function generateMockOpinions(count: number = 3): AnalysisOpinionItem[] {
125+
const mockOpinions: AnalysisOpinionItem[] = [];
126126
127127
for (let i = 0; i < count; i++) {
128128
mockOpinions.push({

0 commit comments

Comments
 (0)