Skip to content

Commit f81ac05

Browse files
Merge pull request #337 from Trendyol/gurubu-ai-integration
feat: gurubu ai integration completed
2 parents c321ae5 + 7daa056 commit f81ac05

File tree

13 files changed

+207
-93
lines changed

13 files changed

+207
-93
lines changed

.commitlintrc.json

-21
This file was deleted.

.husky/commit-msg

-4
This file was deleted.

.husky/pre-commit

-2
This file was deleted.

gurubu-backend/sockets/groomingSocket.js

+14-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ const {
99
removeUserFromOngoingGrooming,
1010
setIssues,
1111
updateTimer,
12-
updateAvatar
12+
updateAvatar,
13+
setGurubuAI
1314
} = require("../utils/groomings");
1415

1516
module.exports = (io) => {
@@ -68,16 +69,26 @@ module.exports = (io) => {
6869
io.to(roomID).emit("updateNickName", result);
6970
});
7071

71-
socket.on("setIssues", (roomID, data, credentials, selectedBoard) => {
72+
socket.on("setIssues", (roomID, data, credentials) => {
7273
joinRoomMiddleware(socket, roomID, credentials);
73-
const result = setIssues(data, credentials, roomID, socket, selectedBoard);
74+
const result = setIssues(data, credentials, roomID, socket);
7475
if(result?.isSuccess === false){
7576
io.to(socket.id).emit("encounteredError", result);
7677
return;
7778
}
7879
io.to(roomID).emit("setIssues", result);
7980
});
8081

82+
socket.on("setGurubuAI", (roomID, data, credentials) => {
83+
joinRoomMiddleware(socket, roomID, credentials);
84+
const result = setGurubuAI(data, credentials, roomID, socket);
85+
if(result?.isSuccess === false){
86+
io.to(socket.id).emit("encounteredError", result);
87+
return;
88+
}
89+
io.to(roomID).emit("setGurubuAI", result);
90+
});
91+
8192
socket.on("updateTimer", (roomID, data, credentials) => {
8293
joinRoomMiddleware(socket, roomID, credentials);
8394
const result = updateTimer(data, credentials, roomID, socket);

gurubu-backend/utils/groomings.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -343,17 +343,25 @@ const getResults = (credentials, roomID, socket) => {
343343
};
344344

345345

346-
const setIssues = (data, credentials, roomID, socket, selectedBoard) => {
346+
const setIssues = (data, credentials, roomID, socket) => {
347347
const user = getCurrentUser(credentials, socket);
348348
const isRoomExpired = checkIsRoomExpired(roomID);
349349
if (!user || isRoomExpired) {
350350
return handleErrors("setIssues", roomID, socket, isRoomExpired);
351351
}
352352

353353
groomings[user.roomID].issues = data;
354-
if(selectedBoard){
355-
groomings[user.roomID].selectedBoard = selectedBoard;
354+
return groomings[user.roomID];
355+
};
356+
357+
const setGurubuAI = (data, credentials, roomID, socket) => {
358+
const user = getCurrentUser(credentials, socket);
359+
const isRoomExpired = checkIsRoomExpired(roomID);
360+
if (!user || isRoomExpired) {
361+
return handleErrors("setGurubuAI", roomID, socket, isRoomExpired);
356362
}
363+
364+
groomings[user.roomID].gurubuAI = data;
357365
return groomings[user.roomID];
358366
};
359367

@@ -507,5 +515,6 @@ module.exports = {
507515
removeUserFromOngoingGrooming,
508516
setIssues,
509517
updateTimer,
510-
updateAvatar
518+
updateAvatar,
519+
setGurubuAI
511520
};

gurubu-client/src/app/components/room/grooming-board/grooming-board-logs.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const GroomingBoardLogs = ({ roomId }: Props) => {
4444
</li>
4545
))}
4646
</ul>
47-
<GroomingBoardParticipants />
47+
<GroomingBoardParticipants roomId={roomId} />
4848
{groomingInfo.mode === GroomingMode.PlanningPoker && (
4949
<GroomingBoardResultV2 />
5050
)}

gurubu-client/src/app/components/room/grooming-board/grooming-board-participants.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import { useEffect, useState } from "react";
12
import GroomingBoardParticipant from "./grooming-board-participant";
3+
import GurubuAIParticipant from "./gurubu-ai/gurubu-ai-participant";
24
import { useGroomingRoom } from "@/contexts/GroomingRoomContext";
3-
import { useEffect, useState } from "react";
45
import { AnimatePresence } from "framer-motion";
5-
import GurubuAIParticipant from "./gurubu-ai/gurubu-ai-participant";
66

7-
const GroomingBoardParticipants = () => {
7+
interface Props {
8+
roomId: string;
9+
}
10+
11+
const GroomingBoardParticipants = ({roomId}: Props) => {
812
const { groomingInfo } = useGroomingRoom();
913
const groomingInfoParticipants = Object.keys(groomingInfo.participants || {});
1014
const [sortedParticipants, setSortedParticipants] = useState<string[]>([]);
@@ -32,7 +36,7 @@ const GroomingBoardParticipants = () => {
3236
<>
3337
<ul className="grooming-board-participants">
3438
<AnimatePresence>
35-
<GurubuAIParticipant sortedParticipants={sortedParticipants} />
39+
<GurubuAIParticipant roomId={roomId} />
3640
{sortedParticipants.map((participantKey) => (
3741
<GroomingBoardParticipant
3842
key={participantKey}

gurubu-client/src/app/components/room/grooming-board/gurubu-ai/gurubu-ai-participant.tsx

+107-50
Original file line numberDiff line numberDiff line change
@@ -3,83 +3,135 @@ import Image from "next/image";
33
import GurubuAITooltip from "./gurubu-ai-tooltip";
44
import { motion } from "framer-motion";
55
import { useGroomingRoom } from "@/contexts/GroomingRoomContext";
6+
import { storyPointService } from "@/services/storyPointService";
7+
import { useSocket } from "@/contexts/SocketContext";
68

7-
interface GurubuAIParticipantProps {
8-
sortedParticipants: string[];
9+
interface Props {
10+
roomId: string;
911
}
1012

11-
const GurubuAIParticipant = ({
12-
sortedParticipants,
13-
}: GurubuAIParticipantProps) => {
14-
const { groomingInfo } = useGroomingRoom();
15-
const [aiMessage, setAiMessage] = useState<string>("");
13+
const GurubuAIParticipant = ({ roomId }: Props) => {
14+
const socket = useSocket();
15+
const { groomingInfo, userInfo } = useGroomingRoom();
1616
const [showTooltip, setShowTooltip] = useState(false);
17+
const abortControllerRef = useRef<AbortController | null>(null);
1718
const participantRef = useRef<HTMLLIElement>(null);
18-
19-
const isSFWCBoard = groomingInfo?.selectedBoard?.includes("SFWC");
19+
const isSFWCBoard =
20+
groomingInfo?.gurubuAI?.selectedBoardName?.includes("SFWC");
2021
const isGroomingInfoLoaded = Boolean(Object.keys(groomingInfo).length);
22+
const selectedIssueIndex = groomingInfo.issues?.findIndex(
23+
(issue) => issue.selected
24+
);
25+
const isIssueIndexChanged =
26+
selectedIssueIndex !== groomingInfo?.gurubuAI?.selectedIssueIndex;
27+
const selectedBoardName = groomingInfo?.gurubuAI?.selectedBoardName;
28+
const threadId = groomingInfo?.gurubuAI?.threadId;
29+
const isAnalyzing = groomingInfo?.gurubuAI?.isAnalyzing;
30+
const isResultShown = groomingInfo?.isResultShown;
31+
const aiMessage = groomingInfo?.gurubuAI?.aiMessage;
32+
const isAdmin = userInfo?.lobby?.isAdmin;
33+
const credentials = userInfo?.lobby?.credentials;
34+
const currentIssue = groomingInfo?.issues?.[selectedIssueIndex];
2135

22-
const getAllParticipantsVotes = () => {
23-
const allParticipantsVotes: { storyPoint: string; nickname: string }[] = [];
24-
sortedParticipants?.forEach((participantKey) => {
25-
const participant = groomingInfo?.participants[participantKey];
26-
allParticipantsVotes.push({
27-
storyPoint: participant?.votes?.["storyPoint"],
28-
nickname: participant?.nickname,
29-
});
30-
});
31-
return allParticipantsVotes;
32-
};
36+
const generateAIAnalysis = async () => {
37+
try {
38+
abortControllerRef.current = new AbortController();
3339

34-
const generateAIAnalysis = (
35-
votes: { storyPoint: string; nickname: string }[]
36-
) => {
37-
const validVotes = votes.filter(
38-
(vote) =>
39-
vote.storyPoint &&
40-
vote.storyPoint !== "?" &&
41-
vote.storyPoint !== "break"
42-
);
43-
if (validVotes.length === 0) return "";
40+
const response = await storyPointService.estimateStoryPoint(
41+
{
42+
boardName: selectedBoardName || "",
43+
issueSummary: currentIssue?.summary || "",
44+
issueDescription: currentIssue?.description || "",
45+
threadId: threadId || undefined,
46+
},
47+
abortControllerRef.current.signal
48+
);
4449

45-
return `Based on the analysis of the tasks we previously scored, I believe the score for this task should be ${Number(
46-
groomingInfo?.score
47-
)?.toFixed(
48-
0
49-
)}. The acceptance criteria are as follows. Our team is experienced in this area, and my suggestion is as follows.`;
50+
return { threadId: response.threadId, message: response.response };
51+
} catch (error: any) {
52+
if (error.message === "Request was cancelled") {
53+
return "";
54+
}
55+
console.error("Error getting AI analysis:", error);
56+
return "I encountered an error while analyzing the task. Please try again.";
57+
}
5058
};
5159

5260
useEffect(() => {
53-
let timeoutId: NodeJS.Timeout;
61+
const fetchAIAnalysis = async () => {
62+
if (isAnalyzing) {
63+
abortControllerRef.current?.abort();
64+
}
65+
66+
if (isGroomingInfoLoaded && isResultShown && isSFWCBoard) {
67+
socket.emit(
68+
"setGurubuAI",
69+
roomId,
70+
{ ...groomingInfo.gurubuAI, isAnalyzing: true },
71+
credentials
72+
);
73+
const analysis = await generateAIAnalysis();
74+
if (analysis) {
75+
socket.emit(
76+
"setGurubuAI",
77+
roomId,
78+
{
79+
...groomingInfo.gurubuAI,
80+
aiMessage:
81+
typeof analysis === "string" ? analysis : analysis.message,
82+
selectedIssueIndex,
83+
threadId:
84+
typeof analysis === "string" ? undefined : analysis.threadId,
85+
isAnalyzing: false,
86+
},
87+
credentials
88+
);
89+
} else {
90+
socket.emit(
91+
"setGurubuAI",
92+
roomId,
93+
{ ...groomingInfo.gurubuAI, isAnalyzing: false },
94+
credentials
95+
);
96+
}
97+
} else {
98+
setShowTooltip(false);
99+
}
100+
};
54101

55-
if (isGroomingInfoLoaded && groomingInfo?.isResultShown && isSFWCBoard) {
56-
// example result of allParticipantsVotes: { storyPoint: "1", nickname: "John Doe" }
57-
const allParticipantsVotes = getAllParticipantsVotes();
58-
const analysis = generateAIAnalysis(allParticipantsVotes);
59-
setAiMessage(analysis);
102+
if (isAdmin && isIssueIndexChanged) {
103+
fetchAIAnalysis();
104+
}
105+
106+
return () => {
107+
if (abortControllerRef.current) {
108+
abortControllerRef.current.abort();
109+
}
110+
};
111+
}, [isResultShown, isGroomingInfoLoaded, selectedIssueIndex]);
60112

61-
// Delay showing the tooltip to ensure proper positioning
113+
useEffect(() => {
114+
let timeoutId: NodeJS.Timeout;
115+
116+
if (isResultShown && !isAnalyzing) {
62117
timeoutId = setTimeout(() => {
63118
setShowTooltip(true);
64119
}, 300);
65120
} else {
66121
setShowTooltip(false);
67122
}
68-
69123
return () => {
70124
if (timeoutId) {
71125
clearTimeout(timeoutId);
72126
}
73127
};
74-
75-
// add sortedParticipants dependency if you want to trigger the tooltip when the votes change
76-
}, [groomingInfo.isResultShown, isGroomingInfoLoaded]);
128+
}, [aiMessage, isResultShown, isAnalyzing]);
77129

78130
const handleCloseTooltip = () => {
79131
setShowTooltip(false);
80132
};
81133

82-
if (!groomingInfo.selectedBoard || !isSFWCBoard) {
134+
if (!selectedBoardName || !isSFWCBoard) {
83135
return null;
84136
}
85137

@@ -102,13 +154,18 @@ const GurubuAIParticipant = ({
102154
/>
103155
<div className="profile-container">
104156
<div className="avatar">
105-
<Image src="https://cdn.dsmcdn.com/web/develop/gurubu-ai.svg" alt="GuruBu AI" width={32} height={32} />
157+
<Image
158+
src="https://cdn.dsmcdn.com/web/develop/gurubu-ai.svg"
159+
alt="GuruBu AI"
160+
width={32}
161+
height={32}
162+
/>
106163
</div>
107164
<div className="name">GuruBu AI</div>
108165
</div>
109166
<div className="score">
110-
{groomingInfo.isResultShown
111-
? Number(groomingInfo?.score)?.toFixed(0)
167+
{isResultShown && aiMessage && !isAnalyzing
168+
? Number(aiMessage)?.toFixed(0)
112169
: "Thinking..."}
113170
</div>
114171
</motion.li>

gurubu-client/src/app/components/room/grooming-navbar/import-jira-issues.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,13 @@ export const ImportJiraIssuesForm = ({ roomId, closeModal }: Props) => {
155155
"setIssues",
156156
roomId,
157157
response.data,
158-
userInfo.lobby.credentials,
159-
selectedBoardName
158+
userInfo.lobby.credentials
159+
);
160+
socket.emit(
161+
"setGurubuAI",
162+
roomId,
163+
{ selectedBoardName },
164+
userInfo.lobby.credentials
160165
);
161166
closeModal();
162167
} else {

gurubu-client/src/app/contexts/SocketContext.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export function SocketProvider({ children }: SocketProviderProps) {
3535
setGroomingInfo(data);
3636
};
3737

38+
const setGurubuAI = (data: GroomingInfo) => {
39+
setGroomingInfo(data);
40+
};
41+
3842
const updateTimer = (data: GroomingInfo) => {
3943
setGroomingInfo(data);
4044
}
@@ -55,6 +59,7 @@ export function SocketProvider({ children }: SocketProviderProps) {
5559
socket.on("showResults", handleShowResults);
5660
socket.on("resetVotes", handleResetVotes);
5761
socket.on("setIssues", setIssues);
62+
socket.on("setGurubuAI", setGurubuAI);
5863
socket.on("updateTimer", updateTimer);
5964
socket.on("updateAvatar", updateAvatar);
6065

@@ -63,6 +68,7 @@ export function SocketProvider({ children }: SocketProviderProps) {
6368
socket.off("showResults", handleShowResults);
6469
socket.off("resetVotes", handleResetVotes);
6570
socket.off("setIssues", setIssues);
71+
socket.off("setGurubuAI", setGurubuAI);
6672
socket.off("updateTimer", updateTimer);
6773
socket.off("updateAvatar", updateAvatar);
6874
};

0 commit comments

Comments
 (0)