Skip to content

Commit bcad577

Browse files
feat: clean up live streaming implementation
1 parent 84f7981 commit bcad577

File tree

14 files changed

+253
-315
lines changed

14 files changed

+253
-315
lines changed

src/api/lichess/streaming.ts

Lines changed: 28 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { Chess } from 'chess.ts'
3-
import { AnalyzedGame, Player, StockfishEvaluation } from 'src/types'
43
import { GameTree } from 'src/types/base/tree'
54
import { AvailableMoves } from 'src/types/training'
5+
import {
6+
LiveGame,
7+
Player,
8+
StockfishEvaluation,
9+
StreamedGame,
10+
StreamedMove,
11+
} from 'src/types'
612

7-
// Re-use the readStream utility from analysis.ts
813
const readStream = (processLine: (data: any) => void) => (response: any) => {
914
const stream = response.body.getReader()
1015
const matcher = /\r?\n/
@@ -32,15 +37,14 @@ const readStream = (processLine: (data: any) => void) => (response: any) => {
3237
return loop()
3338
}
3439

35-
// Get current Lichess TV game information
3640
export const getLichessTVGame = async () => {
3741
const res = await fetch('https://lichess.org/api/tv/channels')
3842
if (!res.ok) {
3943
throw new Error('Failed to fetch Lichess TV data')
4044
}
4145
const data = await res.json()
4246

43-
// Return the best game (highest rated players)
47+
// Return the best rapid game (highest rated players)
4448
const bestChannel = data.rapid
4549
if (!bestChannel?.gameId) {
4650
throw new Error('No TV game available')
@@ -53,7 +57,6 @@ export const getLichessTVGame = async () => {
5357
}
5458
}
5559

56-
// Get basic game information from Lichess API
5760
export const getLichessGameInfo = async (gameId: string) => {
5861
const res = await fetch(`https://lichess.org/api/game/${gameId}`)
5962
if (!res.ok) {
@@ -62,11 +65,10 @@ export const getLichessGameInfo = async (gameId: string) => {
6265
return res.json()
6366
}
6467

65-
// Stream live moves from a Lichess game
6668
export const streamLichessGame = async (
6769
gameId: string,
68-
onGameStart: (data: any) => void,
69-
onMove: (data: any) => void,
70+
onGameStart: (data: StreamedGame) => void,
71+
onMove: (data: StreamedMove) => void,
7072
onComplete: () => void,
7173
abortSignal?: AbortSignal,
7274
) => {
@@ -79,55 +81,18 @@ export const streamLichessGame = async (
7981
},
8082
})
8183

82-
let gameStarted = false
83-
8484
const onMessage = (message: any) => {
85-
console.log('Raw message received:', message)
86-
8785
if (message.id) {
88-
// This is the initial game state with full game info
8986
console.log('Game start message:', message)
90-
onGameStart(message)
91-
gameStarted = true
87+
onGameStart(message as StreamedGame)
9288
} else if (message.uci || message.lm) {
93-
// This is a move - handle both formats: {"fen":"...", "uci":"e2e4"} or {"fen":"...", "lm":"e2e4"}
94-
// If we haven't received the initial game state yet, trigger game start with a minimal state
95-
if (!gameStarted) {
96-
console.log(
97-
'First move received without initial game state, creating minimal game',
98-
)
99-
onGameStart({
100-
id: gameId, // Use the gameId we're streaming
101-
players: {
102-
white: { user: { name: 'White' } },
103-
black: { user: { name: 'Black' } },
104-
},
105-
fen: message.fen,
106-
})
107-
gameStarted = true
108-
}
109-
89+
console.log('Move message:', message)
11090
onMove({
11191
fen: message.fen,
11292
uci: message.uci || message.lm,
11393
wc: message.wc,
11494
bc: message.bc,
11595
})
116-
} else if (message.fen && !message.uci && !message.lm) {
117-
// This is the initial position - could be the first message for a starting game
118-
console.log('Initial position received:', message)
119-
if (!gameStarted) {
120-
console.log('Initial position message, creating game')
121-
onGameStart({
122-
id: gameId,
123-
players: {
124-
white: { user: { name: 'White' } },
125-
black: { user: { name: 'Black' } },
126-
},
127-
fen: message.fen,
128-
})
129-
gameStarted = true
130-
}
13196
} else {
13297
console.log('Unknown message format:', message)
13398
}
@@ -155,27 +120,25 @@ export const streamLichessGame = async (
155120
}
156121
}
157122

158-
// Convert Lichess game data to our AnalyzedGame format for live streaming
159123
export const createAnalyzedGameFromLichessStream = (
160124
gameData: any,
161-
): AnalyzedGame => {
125+
): LiveGame => {
162126
const { players, id } = gameData
163127

164128
const whitePlayer: Player = {
165-
name: players?.white?.user?.name || 'White',
129+
name: players?.white?.user?.id || 'White',
166130
rating: players?.white?.rating,
167131
}
168132

169133
const blackPlayer: Player = {
170-
name: players?.black?.user?.name || 'Black',
134+
name: players?.black?.user?.id || 'Black',
171135
rating: players?.black?.rating,
172136
}
173137

174-
// Use the starting position as our tree root - we'll build moves incrementally
175-
const startingFen = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
138+
const startingFen =
139+
gameData.initialFen ||
140+
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
176141

177-
// Build moves array with just the initial position for now
178-
// Moves will be added as they come in via the stream
179142
const gameStates = [
180143
{
181144
board: startingFen,
@@ -186,35 +149,28 @@ export const createAnalyzedGameFromLichessStream = (
186149
},
187150
]
188151

189-
// Create game tree starting from the beginning
190152
const tree = new GameTree(startingFen)
191153

192154
return {
193155
id: `stream-${id}`,
194156
blackPlayer,
195157
whitePlayer,
158+
gameType: 'stream',
159+
type: 'stream' as const,
196160
moves: gameStates,
197161
availableMoves: new Array(gameStates.length).fill({}) as AvailableMoves[],
198-
gameType: 'blitz', // Default to blitz, could be detected from game data
199-
termination: {
200-
result: '*', // Live game in progress
201-
winner: undefined,
202-
condition: 'Live',
203-
},
204-
maiaEvaluations: new Array(gameStates.length).fill({}),
205-
stockfishEvaluations: new Array(gameStates.length).fill(undefined) as (
206-
| StockfishEvaluation
207-
| undefined
208-
)[],
162+
termination: undefined,
163+
maiaEvaluations: [],
164+
stockfishEvaluations: [],
165+
loadedFen: gameData.fen,
166+
loaded: false,
209167
tree,
210-
type: 'stream' as const, // Use stream type for live streams
211-
} as AnalyzedGame
168+
} as LiveGame
212169
}
213170

214-
// Parse a move from the Lichess stream format and update game state
215171
export const parseLichessStreamMove = (
216-
moveData: any,
217-
currentGame: AnalyzedGame,
172+
moveData: StreamedMove,
173+
currentGame: LiveGame,
218174
) => {
219175
const { uci, fen } = moveData
220176

src/components/Analysis/StreamAnalysis.tsx

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,31 @@ import React, {
55
useCallback,
66
useContext,
77
} from 'react'
8-
import { motion, AnimatePresence } from 'framer-motion'
9-
import { useRouter } from 'next/router'
8+
import { motion } from 'framer-motion'
109
import type { Key } from 'chessground/types'
1110
import { Chess, PieceSymbol } from 'chess.ts'
1211
import type { DrawShape } from 'chessground/draw'
13-
import toast from 'react-hot-toast'
1412

15-
import {
16-
AnalyzedGame,
17-
MaiaEvaluation,
18-
StockfishEvaluation,
19-
GameNode,
20-
} from 'src/types'
21-
import { StreamState, ClockState } from 'src/hooks/useLichessStreamController'
2213
import { WindowSizeContext } from 'src/contexts'
23-
import { PlayerInfo } from 'src/components/Common/PlayerInfo'
14+
import { MAIA_MODELS } from 'src/constants/common'
15+
import { GameInfo } from 'src/components/Common/GameInfo'
2416
import { GameBoard } from 'src/components/Board/GameBoard'
17+
import { PlayerInfo } from 'src/components/Common/PlayerInfo'
2518
import { MovesContainer } from 'src/components/Board/MovesContainer'
19+
import { AnalyzedGame, GameNode, StreamState, ClockState } from 'src/types'
2620
import { BoardController } from 'src/components/Board/BoardController'
2721
import { PromotionOverlay } from 'src/components/Board/PromotionOverlay'
28-
import { GameInfo } from 'src/components/Common/GameInfo'
2922
import { AnalysisSidebar } from 'src/components/Analysis'
3023
import { ConfigurableScreens } from 'src/components/Analysis/ConfigurableScreens'
31-
import { MAIA_MODELS } from 'src/constants/common'
24+
import { useAnalysisController } from 'src/hooks/useAnalysisController'
3225

3326
interface Props {
3427
game: AnalyzedGame
3528
streamState: StreamState
3629
clockState: ClockState
3730
onReconnect: () => void
3831
onStopStream: () => void
39-
analysisController: any // This should be typed properly based on your analysis controller
40-
userNavigatedAway?: boolean
41-
onSyncWithLive?: () => void
32+
analysisController: ReturnType<typeof useAnalysisController>
4233
}
4334

4435
export const StreamAnalysis: React.FC<Props> = ({
@@ -48,10 +39,7 @@ export const StreamAnalysis: React.FC<Props> = ({
4839
onReconnect,
4940
onStopStream,
5041
analysisController,
51-
userNavigatedAway = false,
52-
onSyncWithLive,
5342
}) => {
54-
const router = useRouter()
5543
const { width } = useContext(WindowSizeContext)
5644
const isMobile = useMemo(() => width > 0 && width <= 670, [width])
5745

@@ -61,7 +49,6 @@ export const StreamAnalysis: React.FC<Props> = ({
6149
[string, string] | null
6250
>(null)
6351

64-
// Reset hover arrow when current node changes
6552
useEffect(() => {
6653
setHoverArrow(null)
6754
}, [analysisController.currentNode])
@@ -185,9 +172,9 @@ export const StreamAnalysis: React.FC<Props> = ({
185172
{player.rating ? <>({player.rating})</> : null}
186173
</span>
187174
</div>
188-
{game.termination.winner === (index == 0 ? 'white' : 'black') ? (
175+
{game.termination?.winner === (index == 0 ? 'white' : 'black') ? (
189176
<p className="text-xs text-engine-3">1</p>
190-
) : game.termination.winner !== 'none' ? (
177+
) : game.termination?.winner !== 'none' ? (
191178
<p className="text-xs text-human-3">0</p>
192179
) : game.termination === undefined ? (
193180
<></>
@@ -208,16 +195,16 @@ export const StreamAnalysis: React.FC<Props> = ({
208195
<div className="flex items-center gap-1">
209196
{streamState.isLive ? (
210197
<span className="font-medium text-red-400">LIVE</span>
211-
) : game.termination.winner === 'none' ? (
198+
) : game.termination?.winner === 'none' ? (
212199
<span className="font-medium text-primary/80">½-½</span>
213200
) : (
214201
<span className="font-medium">
215202
<span className="text-primary/70">
216-
{game.termination.winner === 'white' ? '1' : '0'}
203+
{game.termination?.winner === 'white' ? '1' : '0'}
217204
</span>
218205
<span className="text-primary/70">-</span>
219206
<span className="text-primary/70">
220-
{game.termination.winner === 'black' ? '1' : '0'}
207+
{game.termination?.winner === 'black' ? '1' : '0'}
221208
</span>
222209
</span>
223210
)}
@@ -343,7 +330,7 @@ export const StreamAnalysis: React.FC<Props> = ({
343330
color={
344331
analysisController.orientation === 'white' ? 'black' : 'white'
345332
}
346-
termination={game.termination.winner}
333+
termination={game.termination?.winner}
347334
clock={
348335
clockState
349336
? analysisController.orientation === 'white'
@@ -400,7 +387,7 @@ export const StreamAnalysis: React.FC<Props> = ({
400387
color={
401388
analysisController.orientation === 'white' ? 'white' : 'black'
402389
}
403-
termination={game.termination.winner}
390+
termination={game.termination?.winner}
404391
showArrowLegend={true}
405392
clock={
406393
clockState

src/components/Common/DelayedLoading.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { Loading } from './Loading'
44

55
interface DelayedLoadingProps {
66
isLoading: boolean
7+
transparent?: boolean
78
delay?: number
89
children: React.ReactNode
910
}
1011

1112
export const DelayedLoading: React.FC<DelayedLoadingProps> = ({
1213
isLoading,
14+
transparent = false,
1315
delay = 1000,
1416
children,
1517
}) => {
@@ -44,7 +46,7 @@ export const DelayedLoading: React.FC<DelayedLoadingProps> = ({
4446
transition={{ duration: 0.3 }}
4547
className="my-auto"
4648
>
47-
<Loading />
49+
<Loading transparent={transparent} />
4850
</motion.div>
4951
) : !isLoading ? (
5052
<motion.div

src/components/Common/GameInfo.tsx

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

2322
export const GameInfo: React.FC<Props> = ({
@@ -31,7 +30,6 @@ export const GameInfo: React.FC<Props> = ({
3130
showGameListButton,
3231
onGameListClick,
3332
streamState,
34-
additionalActions,
3533
}: Props) => {
3634
const { startTour } = useTour()
3735

@@ -66,7 +64,6 @@ export const GameInfo: React.FC<Props> = ({
6664
? 'Disconnected'
6765
: 'Connecting...'}
6866
</span>
69-
{additionalActions}
7067
</div>
7168
)}
7269
{currentMaiaModel && setCurrentMaiaModel && (

src/components/Common/Loading.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ const states = [
1212
'r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/3P1N2/PPP2PPP/RNBQK2R b KQkq - 0 4',
1313
]
1414

15-
export const Loading: React.FC = () => {
15+
interface LoadingProps {
16+
transparent?: boolean
17+
}
18+
19+
export const Loading: React.FC<LoadingProps> = ({ transparent = false }) => {
1620
const [currentIndex, setCurrentIndex] = useState(0)
1721
const [renderKey, setRenderKey] = useState(0)
1822

@@ -33,9 +37,19 @@ export const Loading: React.FC = () => {
3337
}, [currentIndex])
3438

3539
return (
36-
<div className="my-40 flex w-screen items-center justify-center bg-backdrop md:my-auto">
40+
<div
41+
className={`my-40 flex w-screen items-center justify-center ${
42+
transparent
43+
? 'absolute left-0 top-0 h-screen bg-backdrop/90'
44+
: 'bg-backdrop'
45+
} md:my-auto`}
46+
>
3747
<div className="flex flex-col items-center gap-4">
38-
<div className="h-[50vw] w-[50vw] opacity-50 md:h-[30vh] md:w-[30vh]">
48+
<div
49+
className={`h-[50vw] w-[50vw] md:h-[30vh] md:w-[30vh] ${
50+
!transparent ? 'opacity-50' : 'opacity-100'
51+
}`}
52+
>
3953
<div className="h-full w-full">
4054
<Chessground
4155
key={renderKey}

0 commit comments

Comments
 (0)