@@ -3,83 +3,135 @@ import Image from "next/image";
3
3
import GurubuAITooltip from "./gurubu-ai-tooltip" ;
4
4
import { motion } from "framer-motion" ;
5
5
import { useGroomingRoom } from "@/contexts/GroomingRoomContext" ;
6
+ import { storyPointService } from "@/services/storyPointService" ;
7
+ import { useSocket } from "@/contexts/SocketContext" ;
6
8
7
- interface GurubuAIParticipantProps {
8
- sortedParticipants : string [ ] ;
9
+ interface Props {
10
+ roomId : string ;
9
11
}
10
12
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 ( ) ;
16
16
const [ showTooltip , setShowTooltip ] = useState ( false ) ;
17
+ const abortControllerRef = useRef < AbortController | null > ( null ) ;
17
18
const participantRef = useRef < HTMLLIElement > ( null ) ;
18
-
19
- const isSFWCBoard = groomingInfo ?. selectedBoard ?. includes ( "SFWC" ) ;
19
+ const isSFWCBoard =
20
+ groomingInfo ?. gurubuAI ?. selectedBoardName ?. includes ( "SFWC" ) ;
20
21
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 ] ;
21
35
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 ( ) ;
33
39
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
+ ) ;
44
49
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
+ }
50
58
} ;
51
59
52
60
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
+ } ;
54
101
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 ] ) ;
60
112
61
- // Delay showing the tooltip to ensure proper positioning
113
+ useEffect ( ( ) => {
114
+ let timeoutId : NodeJS . Timeout ;
115
+
116
+ if ( isResultShown && ! isAnalyzing ) {
62
117
timeoutId = setTimeout ( ( ) => {
63
118
setShowTooltip ( true ) ;
64
119
} , 300 ) ;
65
120
} else {
66
121
setShowTooltip ( false ) ;
67
122
}
68
-
69
123
return ( ) => {
70
124
if ( timeoutId ) {
71
125
clearTimeout ( timeoutId ) ;
72
126
}
73
127
} ;
74
-
75
- // add sortedParticipants dependency if you want to trigger the tooltip when the votes change
76
- } , [ groomingInfo . isResultShown , isGroomingInfoLoaded ] ) ;
128
+ } , [ aiMessage , isResultShown , isAnalyzing ] ) ;
77
129
78
130
const handleCloseTooltip = ( ) => {
79
131
setShowTooltip ( false ) ;
80
132
} ;
81
133
82
- if ( ! groomingInfo . selectedBoard || ! isSFWCBoard ) {
134
+ if ( ! selectedBoardName || ! isSFWCBoard ) {
83
135
return null ;
84
136
}
85
137
@@ -102,13 +154,18 @@ const GurubuAIParticipant = ({
102
154
/>
103
155
< div className = "profile-container" >
104
156
< 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
+ />
106
163
</ div >
107
164
< div className = "name" > GuruBu AI</ div >
108
165
</ div >
109
166
< div className = "score" >
110
- { groomingInfo . isResultShown
111
- ? Number ( groomingInfo ?. score ) ?. toFixed ( 0 )
167
+ { isResultShown && aiMessage && ! isAnalyzing
168
+ ? Number ( aiMessage ) ?. toFixed ( 0 )
112
169
: "Thinking..." }
113
170
</ div >
114
171
</ motion . li >
0 commit comments