Skip to content

Commit b26ddaf

Browse files
Merge pull request #332 from Trendyol/gurubu-ai-ui
feat: gurubu-ai ui part add
2 parents 4aa1986 + a586c46 commit b26ddaf

File tree

12 files changed

+334
-34
lines changed

12 files changed

+334
-34
lines changed

gurubu-backend/sockets/groomingSocket.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ module.exports = (io) => {
6868
io.to(roomID).emit("updateNickName", result);
6969
});
7070

71-
socket.on("setIssues", (roomID, data, credentials) => {
71+
socket.on("setIssues", (roomID, data, credentials, selectedBoard) => {
7272
joinRoomMiddleware(socket, roomID, credentials);
73-
const result = setIssues(data, credentials, roomID, socket);
73+
const result = setIssues(data, credentials, roomID, socket, selectedBoard);
7474
if(result?.isSuccess === false){
7575
io.to(socket.id).emit("encounteredError", result);
7676
return;

gurubu-backend/utils/groomings.js

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

345345

346-
const setIssues = (data, credentials, roomID, socket) => {
346+
const setIssues = (data, credentials, roomID, socket, selectedBoard) => {
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-
354+
if(selectedBoard){
355+
groomings[user.roomID].selectedBoard = selectedBoard;
356+
}
355357
return groomings[user.roomID];
356358
};
357359

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

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import GroomingBoardParticipant from "./grooming-board-participant";
22
import { useGroomingRoom } from "@/contexts/GroomingRoomContext";
33
import { useEffect, useState } from "react";
44
import { AnimatePresence } from "framer-motion";
5+
import GurubuAIParticipant from "./gurubu-ai/gurubu-ai-participant";
56

67
const GroomingBoardParticipants = () => {
78
const { groomingInfo } = useGroomingRoom();
@@ -31,6 +32,7 @@ const GroomingBoardParticipants = () => {
3132
<>
3233
<ul className="grooming-board-participants">
3334
<AnimatePresence>
35+
<GurubuAIParticipant sortedParticipants={sortedParticipants} />
3436
{sortedParticipants.map((participantKey) => (
3537
<GroomingBoardParticipant
3638
key={participantKey}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { useEffect, useState, useRef } from "react";
2+
import Image from "next/image";
3+
import GurubuAITooltip from "./gurubu-ai-tooltip";
4+
import { motion } from "framer-motion";
5+
import { useGroomingRoom } from "@/contexts/GroomingRoomContext";
6+
7+
interface GurubuAIParticipantProps {
8+
sortedParticipants: string[];
9+
}
10+
11+
const GurubuAIParticipant = ({
12+
sortedParticipants,
13+
}: GurubuAIParticipantProps) => {
14+
const { groomingInfo } = useGroomingRoom();
15+
const [aiMessage, setAiMessage] = useState<string>("");
16+
const [showTooltip, setShowTooltip] = useState(false);
17+
const participantRef = useRef<HTMLLIElement>(null);
18+
19+
const isSFWCBoard = groomingInfo?.selectedBoard?.includes("SFWC");
20+
const isGroomingInfoLoaded = Boolean(Object.keys(groomingInfo).length);
21+
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+
};
33+
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 "";
44+
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+
};
51+
52+
useEffect(() => {
53+
let timeoutId: NodeJS.Timeout;
54+
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);
60+
61+
// Delay showing the tooltip to ensure proper positioning
62+
timeoutId = setTimeout(() => {
63+
setShowTooltip(true);
64+
}, 300);
65+
} else {
66+
setShowTooltip(false);
67+
}
68+
69+
return () => {
70+
if (timeoutId) {
71+
clearTimeout(timeoutId);
72+
}
73+
};
74+
75+
// add sortedParticipants dependency if you want to trigger the tooltip when the votes change
76+
}, [groomingInfo.isResultShown, isGroomingInfoLoaded]);
77+
78+
const handleCloseTooltip = () => {
79+
setShowTooltip(false);
80+
};
81+
82+
if (!groomingInfo.selectedBoard || !isSFWCBoard) {
83+
return null;
84+
}
85+
86+
return (
87+
<motion.li
88+
ref={participantRef}
89+
layout
90+
initial={{ opacity: 0, scale: 0.9 }}
91+
animate={{ opacity: 1, scale: 1 }}
92+
exit={{ opacity: 0, scale: 0.9 }}
93+
transition={{ duration: 0.3, ease: "easeInOut" }}
94+
key="gurubu-ai-participant"
95+
className="gurubu-ai-participant"
96+
>
97+
<GurubuAITooltip
98+
message={aiMessage}
99+
isVisible={showTooltip}
100+
anchorRef={participantRef}
101+
onClose={handleCloseTooltip}
102+
/>
103+
<div className="profile-container">
104+
<div className="avatar">
105+
<Image src="https://cdn.dsmcdn.com/web/develop/gurubu-ai.svg" alt="GuruBu AI" width={32} height={32} />
106+
</div>
107+
<div className="name">GuruBu AI</div>
108+
</div>
109+
<div className="score">
110+
{groomingInfo.isResultShown
111+
? Number(groomingInfo?.score)?.toFixed(0)
112+
: "Thinking..."}
113+
</div>
114+
</motion.li>
115+
);
116+
};
117+
118+
export default GurubuAIParticipant;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React, { useRef, useEffect, useState } from "react";
2+
import { createPortal } from "react-dom";
3+
import { motion, AnimatePresence } from "framer-motion";
4+
import Image from "next/image";
5+
import { useGroomingRoom } from "@/contexts/GroomingRoomContext";
6+
7+
interface GurubuAITooltipProps {
8+
message: string;
9+
isVisible: boolean;
10+
anchorRef: React.RefObject<HTMLElement>;
11+
onClose: () => void;
12+
}
13+
14+
const GurubuAITooltip = ({ message, isVisible, anchorRef, onClose }: GurubuAITooltipProps) => {
15+
const { jiraSidebarExpanded } = useGroomingRoom();
16+
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
17+
const [mounted, setMounted] = useState(false);
18+
const tooltipRef = useRef<HTMLDivElement>(null);
19+
20+
useEffect(() => {
21+
setMounted(true);
22+
return () => setMounted(false);
23+
}, []);
24+
25+
useEffect(() => {
26+
const updatePosition = () => {
27+
if (!isVisible || !anchorRef.current || !tooltipRef.current) return;
28+
29+
const anchorRect = anchorRef.current.getBoundingClientRect();
30+
const tooltipRect = tooltipRef.current.getBoundingClientRect();
31+
32+
requestAnimationFrame(() => {
33+
setTooltipPosition({
34+
top: anchorRect.top + (anchorRect.height / 2) - (tooltipRect.height / 2) + window.scrollY,
35+
left: jiraSidebarExpanded
36+
? anchorRect.right + 16 + window.scrollX // Sağ tarafta göster
37+
: anchorRect.left - tooltipRect.width - 16 + window.scrollX // Sol tarafta göster
38+
});
39+
});
40+
};
41+
42+
if (mounted) {
43+
updatePosition();
44+
window.addEventListener('resize', updatePosition);
45+
window.addEventListener('scroll', updatePosition);
46+
47+
const timeoutId = setTimeout(updatePosition, 100);
48+
49+
return () => {
50+
window.removeEventListener('resize', updatePosition);
51+
window.removeEventListener('scroll', updatePosition);
52+
clearTimeout(timeoutId);
53+
};
54+
}
55+
}, [isVisible, message, anchorRef, mounted, jiraSidebarExpanded]);
56+
57+
if (!mounted || !isVisible || !message) return null;
58+
59+
return createPortal(
60+
<AnimatePresence>
61+
{isVisible && (
62+
<motion.div
63+
ref={tooltipRef}
64+
className={`gurubu-ai-tooltip ${jiraSidebarExpanded ? 'left-arrow' : 'right-arrow'}`}
65+
initial={{ opacity: 0, x: jiraSidebarExpanded ? 10 : -10 }}
66+
animate={{ opacity: 1, x: 0 }}
67+
exit={{ opacity: 0, x: jiraSidebarExpanded ? 10 : -10 }}
68+
transition={{ duration: 0.3 }}
69+
style={{
70+
position: 'absolute',
71+
top: tooltipPosition.top,
72+
left: tooltipPosition.left,
73+
visibility: tooltipPosition.top === 0 ? 'hidden' : 'visible'
74+
}}
75+
>
76+
<button className="close-button" onClick={onClose}>
77+
<Image src="/close-icon.svg" alt="Close" width={12} height={12} />
78+
</button>
79+
<div className="tooltip-content">
80+
{message}
81+
</div>
82+
</motion.div>
83+
)}
84+
</AnimatePresence>,
85+
document.body
86+
);
87+
};
88+
89+
export default GurubuAITooltip;

gurubu-client/src/app/components/room/grooming-navbar/announcement-banner.tsx

-7
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,6 @@ const AnnouncementBanner = () => {
6161
>
6262
{currentAnnouncement.buttonText}
6363
</button>
64-
<button
65-
className="announcement-banner__close"
66-
onClick={() => setIsAnnouncementBannerVisible(false)}
67-
aria-label="Close announcement"
68-
>
69-
×
70-
</button>
7164
</div>
7265
</div>
7366
);

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

+5-1
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,15 @@ export const ImportJiraIssuesForm = ({ roomId, closeModal }: Props) => {
148148
if(response.data[currentJiraIssueIndex]){
149149
response.data[currentJiraIssueIndex].selected = true;
150150
}
151+
152+
const selectedBoardName = boards.find(board => board.id === Number(selectedBoard) as any)?.name || "";
153+
151154
socket.emit(
152155
"setIssues",
153156
roomId,
154157
response.data,
155-
userInfo.lobby.credentials
158+
userInfo.lobby.credentials,
159+
selectedBoardName
156160
);
157161
closeModal();
158162
} else {

gurubu-client/src/app/shared/interfaces.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface MetricAverages {
3737
export interface GroomingInfo {
3838
mode: string;
3939
participants: Participants;
40+
selectedBoard: string;
4041
metrics: Metric[];
4142
score: number;
4243
metricAverages: MetricAverages;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
.gurubu-ai-participant {
2+
display: flex;
3+
align-items: center;
4+
background-color: #cefedf;
5+
border-radius: 8px;
6+
margin-bottom: 8px;
7+
border-top: none !important;
8+
position: relative;
9+
10+
.profile-container {
11+
display: flex;
12+
min-width: 140px;
13+
padding: 10px 0 10px 14px;
14+
align-items: center;
15+
gap: 0 8px;
16+
.avatar {
17+
width: 32px;
18+
height: 32px;
19+
border-radius: 50%;
20+
overflow: hidden;
21+
22+
img {
23+
width: 32px;
24+
height: 32px;
25+
object-fit: cover;
26+
}
27+
}
28+
.name {
29+
font-size: 14px;
30+
flex: 1;
31+
font-weight: 400;
32+
color: #1a8236;
33+
}
34+
}
35+
36+
.score {
37+
font-size: 16px;
38+
font-weight: 600;
39+
color: #1a8236;
40+
display: flex;
41+
gap: 0 16px;
42+
width: 100%;
43+
justify-content: space-around;
44+
}
45+
}

0 commit comments

Comments
 (0)