Skip to content

Commit 84f7981

Browse files
fix: allow users to change position during live-stream
1 parent 7b6c2b1 commit 84f7981

File tree

4 files changed

+83
-34
lines changed

4 files changed

+83
-34
lines changed

src/components/Analysis/StreamAnalysis.tsx

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ interface Props {
3737
onReconnect: () => void
3838
onStopStream: () => void
3939
analysisController: any // This should be typed properly based on your analysis controller
40+
userNavigatedAway?: boolean
41+
onSyncWithLive?: () => void
4042
}
4143

4244
export const StreamAnalysis: React.FC<Props> = ({
@@ -46,6 +48,8 @@ export const StreamAnalysis: React.FC<Props> = ({
4648
onReconnect,
4749
onStopStream,
4850
analysisController,
51+
userNavigatedAway = false,
52+
onSyncWithLive,
4953
}) => {
5054
const router = useRouter()
5155
const { width } = useContext(WindowSizeContext)
@@ -98,21 +102,9 @@ export const StreamAnalysis: React.FC<Props> = ({
98102
if (analysisController.currentNode.mainChild?.move === moveString) {
99103
// Existing main line move - navigate to it
100104
analysisController.goToNode(analysisController.currentNode.mainChild)
101-
} else if (
102-
!analysisController.currentNode.mainChild &&
103-
analysisController.currentNode.isMainline
104-
) {
105-
// No main child exists AND we're on main line - create main line move
106-
const newMainMove = game.tree.addMainMove(
107-
analysisController.currentNode,
108-
newFen,
109-
moveString,
110-
san,
111-
analysisController.currentMaiaModel,
112-
)
113-
analysisController.goToNode(newMainMove)
114105
} else {
115-
// Either main child exists but different move, OR we're in a variation - create variation
106+
// For stream analysis, ALWAYS create variations for player moves
107+
// This preserves the live game mainline and allows exploration
116108
const newVariation = game.tree.addVariation(
117109
analysisController.currentNode,
118110
newFen,
@@ -179,9 +171,6 @@ export const StreamAnalysis: React.FC<Props> = ({
179171
return chess.turn() === 'w' ? 'white' : 'black'
180172
}, [analysisController.currentNode])
181173

182-
// Check if we're using dummy game data (waiting for real game data)
183-
const isWaitingForGameData = game.id === ''
184-
185174
const NestedGameInfo = () => (
186175
<div className="flex w-full flex-col">
187176
<div className="hidden md:block">

src/components/Common/GameInfo.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface Props {
1717
isConnected: boolean
1818
error: string | null
1919
}
20+
additionalActions?: React.ReactNode
2021
}
2122

2223
export const GameInfo: React.FC<Props> = ({
@@ -30,6 +31,7 @@ export const GameInfo: React.FC<Props> = ({
3031
showGameListButton,
3132
onGameListClick,
3233
streamState,
34+
additionalActions,
3335
}: Props) => {
3436
const { startTour } = useTour()
3537

@@ -64,6 +66,7 @@ export const GameInfo: React.FC<Props> = ({
6466
? 'Disconnected'
6567
: 'Connecting...'}
6668
</span>
69+
{additionalActions}
6770
</div>
6871
)}
6972
{currentMaiaModel && setCurrentMaiaModel && (

src/components/Home/LiveChessBoard.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ import { getLichessTVGame, streamLichessGame } from 'src/api/lichess/streaming'
88
interface LiveGameData {
99
gameId: string
1010
white?: {
11-
name: string
11+
user: {
12+
id: string
13+
name: string
14+
}
1215
rating?: number
1316
}
1417
black?: {
15-
name: string
18+
user: {
19+
id: string
20+
name: string
21+
}
1622
rating?: number
1723
}
1824
currentFen?: string
@@ -36,8 +42,8 @@ export const LiveChessBoard: React.FC = () => {
3642
}
3743
setLiveGame({
3844
gameId: gameData.id || gameData.gameId,
39-
white: gameData.players?.white?.user || gameData.white,
40-
black: gameData.players?.black?.user || gameData.black,
45+
white: gameData.players?.white || gameData.white,
46+
black: gameData.players?.black || gameData.black,
4147
currentFen: gameData.fen,
4248
isLive: true,
4349
})
@@ -158,9 +164,9 @@ export const LiveChessBoard: React.FC = () => {
158164
<div className="relative">
159165
{/* Live indicator */}
160166
{liveGame?.isLive && (
161-
<div className="absolute -right-4 -top-4 z-10 flex items-center gap-1 rounded-full bg-red-500 px-2 py-1">
167+
<div className="absolute -right-5 -top-3 z-10 flex items-center gap-1 rounded-full bg-red-500 px-1.5 py-0.5">
162168
<div className="h-1.5 w-1.5 animate-pulse rounded-full bg-white" />
163-
<span className="text-xs font-semibold text-white">LIVE</span>
169+
<span className="text-xxs font-semibold text-white">LIVE</span>
164170
</div>
165171
)}
166172

@@ -191,7 +197,7 @@ export const LiveChessBoard: React.FC = () => {
191197
{/* Game info on hover */}
192198
{isHovered && liveGame && (
193199
<motion.div
194-
className="absolute left-[calc(100%+0.5rem)] top-3 flex w-48 flex-col items-center justify-center rounded border border-white/10 bg-background-1/60"
200+
className="absolute left-[calc(100%+0.5rem)] top-0 flex w-48 flex-col items-center justify-center rounded border border-white/10 bg-background-1/60"
195201
initial={{ opacity: 0, x: -10 }}
196202
animate={{ opacity: 1, x: 0 }}
197203
exit={{ opacity: 0, x: -10 }}
@@ -202,26 +208,26 @@ export const LiveChessBoard: React.FC = () => {
202208
Lichess TV Analysis
203209
</span>
204210
</div>
205-
<div className="flex flex-row items-center gap-1 px-2 pt-2 text-xxs">
211+
<div className="flex flex-col items-center gap-1 px-2 pt-2 text-xxs">
206212
<div className="flex items-center justify-between">
207213
<div className="flex items-center gap-1">
208214
<div className="h-2 w-2 rounded-full border bg-white" />
209215
<span className="font-medium">
210-
{liveGame.white?.name || 'White'}
216+
{liveGame.white?.user?.id.slice(0, 10) || 'White'}
211217
</span>
212218
{liveGame.white?.rating && (
213219
<span className="text-secondary">
214220
({liveGame.white.rating})
215221
</span>
216-
)}
222+
)}{' '}
223+
vs.
217224
</div>
218225
</div>
219-
<span className="text-secondary">vs.</span>
220226
<div className="flex items-center justify-between">
221227
<div className="flex items-center gap-1">
222228
<div className="h-2 w-2 rounded-full bg-black" />
223229
<span className="font-medium">
224-
{liveGame.black?.name || 'Black'}
230+
{liveGame.black?.user?.id.slice(0, 10) || 'Black'}
225231
</span>
226232
{liveGame.black?.rating && (
227233
<span className="text-secondary">

src/pages/analysis/stream/[gameId].tsx

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState, useMemo, useRef } from 'react'
1+
import React, { useEffect, useState, useMemo, useRef, useCallback } from 'react'
22
import { NextPage } from 'next'
33
import { useRouter } from 'next/router'
44
import Head from 'next/head'
@@ -73,13 +73,61 @@ const StreamAnalysisPage: NextPage = () => {
7373
// Use the current streaming game or dummy game for analysis controller
7474
// Disable backend analysis saving for stream page since this is live data
7575
const currentGame = streamController.game || dummyGame
76-
const analysisController = useAnalysisController(currentGame, undefined, false)
76+
const analysisController = useAnalysisController(
77+
currentGame,
78+
undefined,
79+
false,
80+
)
81+
82+
// Set current node to follow live moves only if user hasn't manually navigated
83+
const [userNavigatedAway, setUserNavigatedAway] = useState(false)
84+
const lastGameMoveCount = useRef(0)
7785

78-
// Set current node to follow live moves
7986
useEffect(() => {
8087
if (streamController.game?.tree && analysisController) {
8188
try {
82-
// For live streaming, we want to follow the latest move
89+
const mainLine = streamController.game.tree.getMainLine()
90+
const currentMoveCount = mainLine.length
91+
92+
// If new moves have been added to the game
93+
if (currentMoveCount > lastGameMoveCount.current) {
94+
lastGameMoveCount.current = currentMoveCount
95+
96+
// Only auto-follow if user hasn't manually navigated away
97+
if (!userNavigatedAway) {
98+
// Find the last node in the main line
99+
let currentNode = streamController.game.tree.getRoot()
100+
while (currentNode.mainChild) {
101+
currentNode = currentNode.mainChild
102+
}
103+
104+
if (currentNode) {
105+
analysisController.setCurrentNode(currentNode)
106+
}
107+
}
108+
}
109+
} catch (error) {
110+
console.error('Error setting current node:', error)
111+
}
112+
}
113+
}, [streamController.game, analysisController, userNavigatedAway])
114+
115+
// Track when user manually navigates
116+
const originalGoToNode = analysisController?.goToNode
117+
useEffect(() => {
118+
if (analysisController && originalGoToNode) {
119+
// Wrap the goToNode function to track manual navigation
120+
analysisController.goToNode = (node: any) => {
121+
setUserNavigatedAway(true)
122+
originalGoToNode(node)
123+
}
124+
}
125+
}, [analysisController, originalGoToNode])
126+
127+
// Function to re-sync with live game
128+
const syncWithLiveGame = useCallback(() => {
129+
if (streamController.game?.tree && analysisController) {
130+
try {
83131
// Find the last node in the main line
84132
let currentNode = streamController.game.tree.getRoot()
85133
while (currentNode.mainChild) {
@@ -88,9 +136,10 @@ const StreamAnalysisPage: NextPage = () => {
88136

89137
if (currentNode) {
90138
analysisController.setCurrentNode(currentNode)
139+
setUserNavigatedAway(false) // Reset the navigation flag
91140
}
92141
} catch (error) {
93-
console.error('Error setting current node:', error)
142+
console.error('Error syncing with live game:', error)
94143
}
95144
}
96145
}, [streamController.game, analysisController])
@@ -180,6 +229,8 @@ const StreamAnalysisPage: NextPage = () => {
180229
onReconnect={streamController.reconnect}
181230
onStopStream={streamController.stopStream}
182231
analysisController={analysisController}
232+
userNavigatedAway={userNavigatedAway}
233+
onSyncWithLive={syncWithLiveGame}
183234
/>
184235
)}
185236
</TreeControllerContext.Provider>

0 commit comments

Comments
 (0)