Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: gurubu ai integration completed #337

Merged
merged 2 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions .commitlintrc.json

This file was deleted.

4 changes: 0 additions & 4 deletions .husky/commit-msg

This file was deleted.

2 changes: 0 additions & 2 deletions .husky/pre-commit

This file was deleted.

17 changes: 14 additions & 3 deletions gurubu-backend/sockets/groomingSocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const {
removeUserFromOngoingGrooming,
setIssues,
updateTimer,
updateAvatar
updateAvatar,
setGurubuAI
} = require("../utils/groomings");

module.exports = (io) => {
Expand Down Expand Up @@ -68,16 +69,26 @@ module.exports = (io) => {
io.to(roomID).emit("updateNickName", result);
});

socket.on("setIssues", (roomID, data, credentials, selectedBoard) => {
socket.on("setIssues", (roomID, data, credentials) => {
joinRoomMiddleware(socket, roomID, credentials);
const result = setIssues(data, credentials, roomID, socket, selectedBoard);
const result = setIssues(data, credentials, roomID, socket);
if(result?.isSuccess === false){
io.to(socket.id).emit("encounteredError", result);
return;
}
io.to(roomID).emit("setIssues", result);
});

socket.on("setGurubuAI", (roomID, data, credentials) => {
joinRoomMiddleware(socket, roomID, credentials);
const result = setGurubuAI(data, credentials, roomID, socket);
if(result?.isSuccess === false){
io.to(socket.id).emit("encounteredError", result);
return;
}
io.to(roomID).emit("setGurubuAI", result);
});

socket.on("updateTimer", (roomID, data, credentials) => {
joinRoomMiddleware(socket, roomID, credentials);
const result = updateTimer(data, credentials, roomID, socket);
Expand Down
17 changes: 13 additions & 4 deletions gurubu-backend/utils/groomings.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,17 +343,25 @@ const getResults = (credentials, roomID, socket) => {
};


const setIssues = (data, credentials, roomID, socket, selectedBoard) => {
const setIssues = (data, credentials, roomID, socket) => {
const user = getCurrentUser(credentials, socket);
const isRoomExpired = checkIsRoomExpired(roomID);
if (!user || isRoomExpired) {
return handleErrors("setIssues", roomID, socket, isRoomExpired);
}

groomings[user.roomID].issues = data;
if(selectedBoard){
groomings[user.roomID].selectedBoard = selectedBoard;
return groomings[user.roomID];
};

const setGurubuAI = (data, credentials, roomID, socket) => {
const user = getCurrentUser(credentials, socket);
const isRoomExpired = checkIsRoomExpired(roomID);
if (!user || isRoomExpired) {
return handleErrors("setGurubuAI", roomID, socket, isRoomExpired);
}

groomings[user.roomID].gurubuAI = data;
return groomings[user.roomID];
};

Expand Down Expand Up @@ -507,5 +515,6 @@ module.exports = {
removeUserFromOngoingGrooming,
setIssues,
updateTimer,
updateAvatar
updateAvatar,
setGurubuAI
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const GroomingBoardLogs = ({ roomId }: Props) => {
</li>
))}
</ul>
<GroomingBoardParticipants />
<GroomingBoardParticipants roomId={roomId} />
{groomingInfo.mode === GroomingMode.PlanningPoker && (
<GroomingBoardResultV2 />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useEffect, useState } from "react";
import GroomingBoardParticipant from "./grooming-board-participant";
import GurubuAIParticipant from "./gurubu-ai/gurubu-ai-participant";
import { useGroomingRoom } from "@/contexts/GroomingRoomContext";
import { useEffect, useState } from "react";
import { AnimatePresence } from "framer-motion";
import GurubuAIParticipant from "./gurubu-ai/gurubu-ai-participant";

const GroomingBoardParticipants = () => {
interface Props {
roomId: string;
}

const GroomingBoardParticipants = ({roomId}: Props) => {
const { groomingInfo } = useGroomingRoom();
const groomingInfoParticipants = Object.keys(groomingInfo.participants || {});
const [sortedParticipants, setSortedParticipants] = useState<string[]>([]);
Expand Down Expand Up @@ -32,7 +36,7 @@ const GroomingBoardParticipants = () => {
<>
<ul className="grooming-board-participants">
<AnimatePresence>
<GurubuAIParticipant sortedParticipants={sortedParticipants} />
<GurubuAIParticipant roomId={roomId} />
{sortedParticipants.map((participantKey) => (
<GroomingBoardParticipant
key={participantKey}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +3,135 @@ import Image from "next/image";
import GurubuAITooltip from "./gurubu-ai-tooltip";
import { motion } from "framer-motion";
import { useGroomingRoom } from "@/contexts/GroomingRoomContext";
import { storyPointService } from "@/services/storyPointService";
import { useSocket } from "@/contexts/SocketContext";

interface GurubuAIParticipantProps {
sortedParticipants: string[];
interface Props {
roomId: string;
}

const GurubuAIParticipant = ({
sortedParticipants,
}: GurubuAIParticipantProps) => {
const { groomingInfo } = useGroomingRoom();
const [aiMessage, setAiMessage] = useState<string>("");
const GurubuAIParticipant = ({ roomId }: Props) => {
const socket = useSocket();
const { groomingInfo, userInfo } = useGroomingRoom();
const [showTooltip, setShowTooltip] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const participantRef = useRef<HTMLLIElement>(null);

const isSFWCBoard = groomingInfo?.selectedBoard?.includes("SFWC");
const isSFWCBoard =
groomingInfo?.gurubuAI?.selectedBoardName?.includes("SFWC");
const isGroomingInfoLoaded = Boolean(Object.keys(groomingInfo).length);
const selectedIssueIndex = groomingInfo.issues?.findIndex(
(issue) => issue.selected
);
const isIssueIndexChanged =
selectedIssueIndex !== groomingInfo?.gurubuAI?.selectedIssueIndex;
const selectedBoardName = groomingInfo?.gurubuAI?.selectedBoardName;
const threadId = groomingInfo?.gurubuAI?.threadId;
const isAnalyzing = groomingInfo?.gurubuAI?.isAnalyzing;
const isResultShown = groomingInfo?.isResultShown;
const aiMessage = groomingInfo?.gurubuAI?.aiMessage;
const isAdmin = userInfo?.lobby?.isAdmin;
const credentials = userInfo?.lobby?.credentials;
const currentIssue = groomingInfo?.issues?.[selectedIssueIndex];

const getAllParticipantsVotes = () => {
const allParticipantsVotes: { storyPoint: string; nickname: string }[] = [];
sortedParticipants?.forEach((participantKey) => {
const participant = groomingInfo?.participants[participantKey];
allParticipantsVotes.push({
storyPoint: participant?.votes?.["storyPoint"],
nickname: participant?.nickname,
});
});
return allParticipantsVotes;
};
const generateAIAnalysis = async () => {
try {
abortControllerRef.current = new AbortController();

const generateAIAnalysis = (
votes: { storyPoint: string; nickname: string }[]
) => {
const validVotes = votes.filter(
(vote) =>
vote.storyPoint &&
vote.storyPoint !== "?" &&
vote.storyPoint !== "break"
);
if (validVotes.length === 0) return "";
const response = await storyPointService.estimateStoryPoint(
{
boardName: selectedBoardName || "",
issueSummary: currentIssue?.summary || "",
issueDescription: currentIssue?.description || "",
threadId: threadId || undefined,
},
abortControllerRef.current.signal
);

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

useEffect(() => {
let timeoutId: NodeJS.Timeout;
const fetchAIAnalysis = async () => {
if (isAnalyzing) {
abortControllerRef.current?.abort();
}

if (isGroomingInfoLoaded && isResultShown && isSFWCBoard) {
socket.emit(
"setGurubuAI",
roomId,
{ ...groomingInfo.gurubuAI, isAnalyzing: true },
credentials
);
const analysis = await generateAIAnalysis();
if (analysis) {
socket.emit(
"setGurubuAI",
roomId,
{
...groomingInfo.gurubuAI,
aiMessage:
typeof analysis === "string" ? analysis : analysis.message,
selectedIssueIndex,
threadId:
typeof analysis === "string" ? undefined : analysis.threadId,
isAnalyzing: false,
},
credentials
);
} else {
socket.emit(
"setGurubuAI",
roomId,
{ ...groomingInfo.gurubuAI, isAnalyzing: false },
credentials
);
}
} else {
setShowTooltip(false);
}
};

if (isGroomingInfoLoaded && groomingInfo?.isResultShown && isSFWCBoard) {
// example result of allParticipantsVotes: { storyPoint: "1", nickname: "John Doe" }
const allParticipantsVotes = getAllParticipantsVotes();
const analysis = generateAIAnalysis(allParticipantsVotes);
setAiMessage(analysis);
if (isAdmin && isIssueIndexChanged) {
fetchAIAnalysis();
}

return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [isResultShown, isGroomingInfoLoaded, selectedIssueIndex]);

// Delay showing the tooltip to ensure proper positioning
useEffect(() => {
let timeoutId: NodeJS.Timeout;

if (isResultShown && !isAnalyzing) {
timeoutId = setTimeout(() => {
setShowTooltip(true);
}, 300);
} else {
setShowTooltip(false);
}

return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};

// add sortedParticipants dependency if you want to trigger the tooltip when the votes change
}, [groomingInfo.isResultShown, isGroomingInfoLoaded]);
}, [aiMessage, isResultShown, isAnalyzing]);

const handleCloseTooltip = () => {
setShowTooltip(false);
};

if (!groomingInfo.selectedBoard || !isSFWCBoard) {
if (!selectedBoardName || !isSFWCBoard) {
return null;
}

Expand All @@ -102,13 +154,18 @@ const GurubuAIParticipant = ({
/>
<div className="profile-container">
<div className="avatar">
<Image src="https://cdn.dsmcdn.com/web/develop/gurubu-ai.svg" alt="GuruBu AI" width={32} height={32} />
<Image
src="https://cdn.dsmcdn.com/web/develop/gurubu-ai.svg"
alt="GuruBu AI"
width={32}
height={32}
/>
</div>
<div className="name">GuruBu AI</div>
</div>
<div className="score">
{groomingInfo.isResultShown
? Number(groomingInfo?.score)?.toFixed(0)
{isResultShown && aiMessage && !isAnalyzing
? Number(aiMessage)?.toFixed(0)
: "Thinking..."}
</div>
</motion.li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,13 @@ export const ImportJiraIssuesForm = ({ roomId, closeModal }: Props) => {
"setIssues",
roomId,
response.data,
userInfo.lobby.credentials,
selectedBoardName
userInfo.lobby.credentials
);
socket.emit(
"setGurubuAI",
roomId,
{ selectedBoardName },
userInfo.lobby.credentials
);
closeModal();
} else {
Expand Down
6 changes: 6 additions & 0 deletions gurubu-client/src/app/contexts/SocketContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export function SocketProvider({ children }: SocketProviderProps) {
setGroomingInfo(data);
};

const setGurubuAI = (data: GroomingInfo) => {
setGroomingInfo(data);
};

const updateTimer = (data: GroomingInfo) => {
setGroomingInfo(data);
}
Expand All @@ -55,6 +59,7 @@ export function SocketProvider({ children }: SocketProviderProps) {
socket.on("showResults", handleShowResults);
socket.on("resetVotes", handleResetVotes);
socket.on("setIssues", setIssues);
socket.on("setGurubuAI", setGurubuAI);
socket.on("updateTimer", updateTimer);
socket.on("updateAvatar", updateAvatar);

Expand All @@ -63,6 +68,7 @@ export function SocketProvider({ children }: SocketProviderProps) {
socket.off("showResults", handleShowResults);
socket.off("resetVotes", handleResetVotes);
socket.off("setIssues", setIssues);
socket.off("setGurubuAI", setGurubuAI);
socket.off("updateTimer", updateTimer);
socket.off("updateAvatar", updateAvatar);
};
Expand Down
Loading
Loading