fix: address code review findings for AI chat builder
- C1: Fix race condition in handleReset — await abandonSession before
starting new session to prevent store state corruption
- I1: Extract error messages from Axios response.data.detail instead of
generic error.message — users now see helpful backend messages (quota
limits, message caps, etc.)
- I2: Add isGenerating guard in generateTree store action to prevent
concurrent generation requests on double-click
- I3: Add isResponding guard in sendMessage to prevent concurrent sends
- M5: Remove redundant type casts on flowType
- M6: Add rate limiter to DELETE /sessions/{id} for consistency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -417,7 +417,9 @@ async def import_tree(
|
||||
|
||||
|
||||
@router.delete("/sessions/{session_id}", status_code=204)
|
||||
@limiter.limit("10/minute")
|
||||
async def abandon_session(
|
||||
request: Request,
|
||||
session_id: UUID,
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
|
||||
@@ -40,7 +40,7 @@ export function AIChatBuilderPage() {
|
||||
if (resumeId && !sessionId) {
|
||||
resumeSession(resumeId)
|
||||
} else if (!sessionId && status === 'idle') {
|
||||
startSession(flowType as 'troubleshooting' | 'procedural')
|
||||
startSession(flowType)
|
||||
}
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -81,15 +81,15 @@ export function AIChatBuilderPage() {
|
||||
}
|
||||
}, [importToEditor, treeMetadata, flowType, navigate])
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
abandonSession()
|
||||
const handleReset = useCallback(async () => {
|
||||
await abandonSession()
|
||||
// Clear session from URL
|
||||
setSearchParams((prev) => {
|
||||
const next = new URLSearchParams(prev)
|
||||
next.delete('session')
|
||||
return next
|
||||
}, { replace: true })
|
||||
startSession(flowType as 'troubleshooting' | 'procedural')
|
||||
startSession(flowType)
|
||||
}, [abandonSession, startSession, flowType, setSearchParams])
|
||||
|
||||
// Show error toast
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { create } from 'zustand'
|
||||
import { AxiosError } from 'axios'
|
||||
import { aiChatApi } from '@/api/aiChat'
|
||||
import type {
|
||||
ChatMessage,
|
||||
@@ -61,6 +62,15 @@ const initialState = {
|
||||
error: null,
|
||||
}
|
||||
|
||||
function extractErrorMessage(e: unknown, fallback: string): string {
|
||||
if (e instanceof AxiosError && e.response?.data?.detail) {
|
||||
const detail = e.response.data.detail
|
||||
return typeof detail === 'string' ? detail : detail.message || fallback
|
||||
}
|
||||
if (e instanceof Error) return e.message
|
||||
return fallback
|
||||
}
|
||||
|
||||
export const useAIChatStore = create<AIChatState>((set, get) => ({
|
||||
...initialState,
|
||||
|
||||
@@ -80,14 +90,13 @@ export const useAIChatStore = create<AIChatState>((set, get) => ({
|
||||
isResponding: false,
|
||||
})
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : 'Failed to start session'
|
||||
set({ error: message, isResponding: false, status: 'idle' })
|
||||
set({ error: extractErrorMessage(e, 'Failed to start session'), isResponding: false, status: 'idle' })
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage: async (content) => {
|
||||
const { sessionId, messages } = get()
|
||||
if (!sessionId) return
|
||||
const { sessionId, messages, isResponding } = get()
|
||||
if (!sessionId || isResponding) return
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
role: 'user',
|
||||
@@ -117,14 +126,13 @@ export const useAIChatStore = create<AIChatState>((set, get) => ({
|
||||
isResponding: false,
|
||||
}))
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : 'Failed to send message'
|
||||
set({ error: message, isResponding: false })
|
||||
set({ error: extractErrorMessage(e, 'Failed to send message'), isResponding: false })
|
||||
}
|
||||
},
|
||||
|
||||
generateTree: async () => {
|
||||
const { sessionId } = get()
|
||||
if (!sessionId) return
|
||||
const { sessionId, isGenerating } = get()
|
||||
if (!sessionId || isGenerating) return
|
||||
|
||||
set({ isGenerating: true, error: null })
|
||||
|
||||
@@ -138,8 +146,7 @@ export const useAIChatStore = create<AIChatState>((set, get) => ({
|
||||
isGenerating: false,
|
||||
})
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : 'Failed to generate tree'
|
||||
set({ error: message, isGenerating: false })
|
||||
set({ error: extractErrorMessage(e, 'Failed to generate tree'), isGenerating: false })
|
||||
}
|
||||
},
|
||||
|
||||
@@ -181,8 +188,7 @@ export const useAIChatStore = create<AIChatState>((set, get) => ({
|
||||
isResponding: false,
|
||||
})
|
||||
} catch (e: unknown) {
|
||||
const message = e instanceof Error ? e.message : 'Failed to resume session'
|
||||
set({ error: message, isResponding: false })
|
||||
set({ error: extractErrorMessage(e, 'Failed to resume session'), isResponding: false })
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user