Skip to content

Commit f4d1780

Browse files
authored
ENH: Conversation analysis page (#157)
* FEAT: Boilerplate Conversation Analysis page and routing * FEAT: Add consensus ranking and summary to analysis page * FEAT: Add group analysis and representative comments to conversation analysis * STYLE: Refactor ConversationAnalysisPage for consistent formatting and improved readability * FEAT: Enhance ConversationAnalysisPage with improved data handling and UI updates * FEAT: Add refresh option to analyze_conversation for updating analysis data * FEAT: Update ConversationAnalysis structure to use comments_by_consensus for improved data handling * STYLE: Refactor ConversationAnalysisPage for consistent formatting and improved readability * FEAT: Add MonitorConversation component for link to analysis * STYLE: Refactor MonitorConversation component for consistent formatting * TST: Update TestConversationAnalysis with new attributes
1 parent 0defbb0 commit f4d1780

5 files changed

Lines changed: 498 additions & 49 deletions

File tree

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import { useParams } from "react-router";
2+
import CoreBase from "./base";
3+
import { useQuery } from "@tanstack/react-query";
4+
import { getApi } from "../../components/api/base";
5+
import dayjs from "dayjs";
6+
import { Conversation } from "./dashboard";
7+
8+
interface CommentAnalysis {
9+
id: string;
10+
content: string;
11+
consensus: number;
12+
representativeness: number;
13+
agree_percentage: number;
14+
}
15+
16+
interface GroupAnalysis {
17+
group_id: number;
18+
users: string[];
19+
representative_comments: CommentAnalysis[];
20+
}
21+
22+
interface ConversationAnalysis {
23+
user_ids: string[];
24+
comment_ids: string[];
25+
comments_by_consensus: CommentAnalysis[];
26+
groups: GroupAnalysis[];
27+
}
28+
29+
export default function ConversationAnalysisPage() {
30+
const params = useParams();
31+
32+
const conversationId = params.conversationId;
33+
34+
const conversationResponse = useQuery({
35+
queryKey: ["conversation", conversationId],
36+
queryFn: () => getApi(`/conversations/${conversationId}`),
37+
enabled: !!conversationId,
38+
});
39+
const conversation = conversationResponse.data as Conversation;
40+
41+
const conversationAnalysisResponse = useQuery({
42+
queryKey: ["conversation-analysis", conversationId],
43+
queryFn: () => getApi(`/analysis/conversation/${conversationId}`),
44+
enabled: !!conversationId,
45+
});
46+
const conversationAnalysis =
47+
conversationAnalysisResponse.data as ConversationAnalysis;
48+
49+
if (
50+
conversationResponse.isLoading ||
51+
conversationAnalysisResponse.isLoading
52+
) {
53+
return (
54+
<CoreBase requiresLogin={true}>
55+
<div className="flex justify-center items-center h-64">
56+
<div className="text-gray-500">Loading analysis...</div>
57+
</div>
58+
</CoreBase>
59+
);
60+
}
61+
62+
return (
63+
<CoreBase requiresLogin={true}>
64+
<section className="xl:w-[90%] w-full mx-auto p-5">
65+
<h1 className="text-3xl font-bold mb-2">{conversation.name}</h1>
66+
<h2 className="text-xl font-semibold mb-8">Conversation Analysis</h2>
67+
<div className="mb-4">
68+
<h2 className="text-xl font-semibold mb-2">Summary</h2>
69+
<p>
70+
<strong>Description:</strong> {conversation.description}
71+
</p>
72+
<p>
73+
<strong>Created By:</strong> {conversation.author.username}
74+
</p>
75+
<p>
76+
<strong>Created At:</strong>{" "}
77+
{dayjs(conversation.date_created).format("MMMM D, YYYY h:mm A")}
78+
</p>
79+
<p>
80+
<strong>Total Participants:</strong>{" "}
81+
{conversationAnalysis.user_ids.length || 0}
82+
</p>
83+
<p>
84+
<strong>Total Comments:</strong>{" "}
85+
{conversationAnalysis.comment_ids.length || 0}
86+
</p>
87+
</div>
88+
<div className="mb-8">
89+
<h2 className="text-xl font-semibold mb-2">Groups</h2>
90+
<p className="mb-4">
91+
From the participants in this conversation,{" "}
92+
<strong>{conversationAnalysis.groups.length}</strong> groups emerged
93+
based on their response patterns.
94+
</p>
95+
<table className="min-w-full border border-gray-300">
96+
<thead>
97+
<tr className="bg-gray-200">
98+
<th className="border border-gray-300 px-4 py-2 text-left">
99+
Group ID
100+
</th>
101+
<th className="border border-gray-300 px-4 py-2 text-left">
102+
Number of Members
103+
</th>
104+
</tr>
105+
</thead>
106+
<tbody>
107+
{conversationAnalysis.groups.map((group) => (
108+
<tr key={group.group_id}>
109+
<td className="border border-gray-300 px-4 py-2">
110+
Group {group.group_id}
111+
</td>
112+
<td className="border border-gray-300 px-4 py-2">
113+
{group.users.length}
114+
</td>
115+
</tr>
116+
))}
117+
{conversationAnalysis.groups.length === 0 && (
118+
<tr>
119+
<td
120+
colSpan={2}
121+
className="border border-gray-300 px-4 py-2 text-center"
122+
>
123+
No groups identified.
124+
</td>
125+
</tr>
126+
)}
127+
</tbody>
128+
</table>
129+
</div>
130+
<div>
131+
<h2 className="text-xl font-semibold mb-2">Consensus</h2>
132+
<p className="mb-4">
133+
Below are the comments in the conversation ranked by their consensus
134+
scores. Higher scores indicate greater agreement across groups,
135+
while lower scores suggest the comment is more divisive.
136+
</p>
137+
<table className="min-w-full border border-gray-300">
138+
<thead>
139+
<tr className="bg-gray-200">
140+
<th className="border border-gray-300 px-4 py-2 text-left w-1/12">
141+
Rank
142+
</th>
143+
<th className="border border-gray-300 px-4 py-2 text-left w-1/2">
144+
Comment
145+
</th>
146+
<th className="border border-gray-300 px-4 py-2 text-left w-5/12">
147+
Consensus Score
148+
</th>
149+
</tr>
150+
</thead>
151+
<tbody>
152+
{conversationAnalysis.comments_by_consensus.map(
153+
(comment, index) => (
154+
<tr key={comment.id}>
155+
<td className="border border-gray-300 px-4 py-2">
156+
{index + 1}
157+
</td>
158+
<td className="border border-gray-300 px-4 py-2">
159+
{comment.content}
160+
</td>
161+
<td className="border border-gray-300 px-4 py-2">
162+
<div className="flex items-center">
163+
<div className="relative w-64 h-4 bg-gray-200 rounded">
164+
<div
165+
className={
166+
"absolute top-0 left-0 h-full rounded bg-green-500"
167+
}
168+
style={{ width: `${comment.consensus * 100}%` }}
169+
/>
170+
</div>
171+
<span className="ml-2 text-sm">
172+
{` (${comment.consensus.toFixed(2)})`}
173+
</span>
174+
</div>
175+
</td>
176+
</tr>
177+
),
178+
)}
179+
{conversationAnalysis.comments_by_consensus.length === 0 && (
180+
<tr>
181+
<td
182+
colSpan={2}
183+
className="border border-gray-300 px-4 py-2 text-center"
184+
>
185+
No comments available for analysis.
186+
</td>
187+
</tr>
188+
)}
189+
</tbody>
190+
</table>
191+
</div>
192+
<div className="mt-8">
193+
<h2 className="text-xl font-semibold mb-2">Representation</h2>
194+
<p className="mb-4">
195+
In each of the identified groups, the following comments best
196+
represent the views of that group.
197+
</p>
198+
{conversationAnalysis.groups.map((group) => (
199+
<div key={group.group_id} className="mb-6">
200+
<h3 className="text-lg font-semibold mb-2">
201+
Group {group.group_id} (Members: {group.users.length})
202+
</h3>
203+
{group.representative_comments.length === 0 && (
204+
<p>No representative comments for this group.</p>
205+
)}
206+
{group.representative_comments.length > 0 && (
207+
<table className="min-w-full border border-gray-300">
208+
<thead>
209+
<tr className="bg-gray-200">
210+
<th className="border border-gray-300 px-4 py-2 text-left w-1/2">
211+
Comment
212+
</th>
213+
<th className="border border-gray-300 px-4 py-2 text-left w-1/4">
214+
Percentage of Group Agreeing
215+
</th>
216+
<th className="border border-gray-300 px-4 py-2 text-left w-1/4">
217+
Representation Score
218+
</th>
219+
</tr>
220+
</thead>
221+
<tbody>
222+
{group.representative_comments.map((comment) => (
223+
<tr key={comment.id}>
224+
<td className="border border-gray-300 px-4 py-2">
225+
{comment.content}
226+
</td>
227+
<td className="border border-gray-300 px-4 py-2">
228+
{`${(comment.agree_percentage * 100).toFixed(0)}%`}
229+
</td>
230+
<td className="border border-gray-300 px-4 py-2">
231+
<div className="flex items-center">
232+
<div className="relative w-32 h-4 bg-gray-200 rounded">
233+
<div
234+
className={
235+
"absolute top-0 left-0 h-full rounded bg-blue-500"
236+
}
237+
style={{
238+
width: `${
239+
(comment.representativeness * 100) / 5
240+
}%`,
241+
}}
242+
/>
243+
</div>
244+
<span className="ml-2 text-sm">
245+
{` (${comment.representativeness.toFixed(2)})`}
246+
</span>
247+
</div>
248+
</td>
249+
</tr>
250+
))}
251+
</tbody>
252+
</table>
253+
)}
254+
</div>
255+
))}
256+
</div>
257+
</section>
258+
</CoreBase>
259+
);
260+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { Conversation } from "../../app/core/dashboard";
2+
import { useOutletContext } from "react-router";
3+
4+
export default function MonitorConversation() {
5+
const { conversation } = useOutletContext<{ conversation: Conversation }>();
6+
7+
return (
8+
<div className="[95%] max-w-4xl mx-auto flex flex-col items-start space-y-4 p-4">
9+
<h2 className="text-2xl font-bold">Conversation Analysis</h2>
10+
<a
11+
href={`/conversation/${conversation.id}/analysis`}
12+
className="underline"
13+
>
14+
<button className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">
15+
View Analysis
16+
</button>
17+
</a>
18+
</div>
19+
);
20+
}

client/src/main.tsx

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,66 @@
1-
import { StrictMode } from "react";
2-
import { createRoot } from "react-dom/client";
3-
import "./index.css";
4-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5-
import { createBrowserRouter, RouterProvider } from "react-router";
6-
import LoginPage from "./app/auth/login";
7-
import RegisterPage from "./app/auth/register";
8-
import ConversationConfigPage from "./app/core/ConversationConfigPage";
9-
import ConversationPage from "./app/core/conversation";
10-
import DashboardPage from "./app/core/dashboard";
11-
import { AuthProvider } from "./components/context/AuthProvider";
12-
import ManageComments from "./components/conversation/ManageComments";
13-
import ManageDistribution from "./components/conversation/ManageDistribution";
1+
import { StrictMode } from 'react';
2+
import { createRoot } from 'react-dom/client';
3+
import './index.css';
4+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5+
import { createBrowserRouter, RouterProvider } from 'react-router';
6+
import LoginPage from './app/auth/login';
7+
import RegisterPage from './app/auth/register';
8+
import ConversationConfigPage from './app/core/ConversationConfigPage';
9+
import ConversationPage from './app/core/conversation';
10+
import DashboardPage from './app/core/dashboard';
11+
import { AuthProvider } from './components/context/AuthProvider';
12+
import ManageComments from './components/conversation/ManageComments';
13+
import ManageDistribution from './components/conversation/ManageDistribution';
14+
import ConversationAnalysisPage from './app/core/ConversationAnalysisPage';
15+
import MonitorConversation from './components/conversation/MonitorConversation';
1416

1517
const queryClient = new QueryClient();
1618

1719
const router = createBrowserRouter([
1820
{
19-
path: "/",
21+
path: '/',
2022
children: [
2123
{ index: true, Component: DashboardPage },
22-
{ path: "login", Component: LoginPage },
23-
{ path: "register", Component: RegisterPage },
24-
{ path: "home", Component: DashboardPage },
25-
{ path: "dashboard", Component: DashboardPage },
24+
{ path: 'login', Component: LoginPage },
25+
{ path: 'register', Component: RegisterPage },
26+
{ path: 'home', Component: DashboardPage },
27+
{ path: 'dashboard', Component: DashboardPage },
2628
{
27-
path: "conversation",
29+
path: 'conversation',
2830
children: [
2931
{ index: true, Component: DashboardPage },
3032
{
31-
path: "new",
33+
path: 'new',
3234
Component: ConversationConfigPage,
3335
},
3436
{
35-
path: ":conversationId",
37+
path: ':conversationId',
3638
children: [
3739
{ index: true, Component: ConversationPage },
3840
{
39-
path: "edit",
41+
path: 'edit',
4042
Component: ConversationConfigPage,
4143
children: [
4244
{ index: true, Component: ManageComments },
4345
{
44-
path: "monitor",
45-
Component: () => <></>,
46+
path: 'monitor',
47+
Component: MonitorConversation,
4648
},
4749
{
48-
path: "distribute",
50+
path: 'distribute',
4951
Component: ManageDistribution,
5052
},
5153
{
52-
path: "moderate",
54+
path: 'moderate',
5355
Component: ManageComments,
5456
children: [{ index: true, Component: ManageComments }],
5557
},
5658
],
5759
},
60+
{
61+
path: 'analysis',
62+
Component: ConversationAnalysisPage,
63+
},
5864
],
5965
},
6066
],
@@ -63,12 +69,12 @@ const router = createBrowserRouter([
6369
},
6470
]);
6571

66-
createRoot(document.getElementById("root")!).render(
72+
createRoot(document.getElementById('root')!).render(
6773
<StrictMode>
6874
<QueryClientProvider client={queryClient}>
6975
<AuthProvider>
7076
<RouterProvider router={router} />
7177
</AuthProvider>
7278
</QueryClientProvider>
73-
</StrictMode>,
79+
</StrictMode>
7480
);

0 commit comments

Comments
 (0)