Upgrade async GPU analysis and workbench UX
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { formatError } from '../lib/formatError'
|
||||
import { assistantApi } from '../services/api/assistant'
|
||||
import type {
|
||||
@@ -36,15 +36,61 @@ function readStoredPreference(): string {
|
||||
}
|
||||
}
|
||||
|
||||
function assistantScopeKey(
|
||||
projectId: string | null,
|
||||
areaId: string | null,
|
||||
bbox: VectorSelectionBBox | null,
|
||||
): string {
|
||||
return JSON.stringify([
|
||||
projectId,
|
||||
areaId,
|
||||
bbox?.min_x ?? null,
|
||||
bbox?.min_y ?? null,
|
||||
bbox?.max_x ?? null,
|
||||
bbox?.max_y ?? null,
|
||||
bbox?.crs ?? null,
|
||||
])
|
||||
}
|
||||
|
||||
interface AssistantConversationState {
|
||||
scopeKey: string
|
||||
messages: GeoAssistantMessage[]
|
||||
}
|
||||
|
||||
interface AssistantRequestState {
|
||||
scopeKey: string
|
||||
requestId: number
|
||||
loading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export function useGeoAssistant({ selectedProjectId, selectedAreaId, selectionBbox }: UseGeoAssistantOptions) {
|
||||
const scopeKey = assistantScopeKey(selectedProjectId, selectedAreaId, selectionBbox)
|
||||
const activeScopeRef = useRef(scopeKey)
|
||||
const latestRequestIdRef = useRef(0)
|
||||
if (activeScopeRef.current !== scopeKey) {
|
||||
activeScopeRef.current = scopeKey
|
||||
latestRequestIdRef.current += 1
|
||||
}
|
||||
|
||||
const [status, setStatus] = useState<AssistantStatus | null>(null)
|
||||
const [models, setModels] = useState<AssistantModelRead[]>([])
|
||||
const [selectedModelChoice, setSelectedModelChoice] = useState(readStoredPreference)
|
||||
const [defaultModel, setDefaultModel] = useState('')
|
||||
const [messages, setMessages] = useState<GeoAssistantMessage[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [conversation, setConversation] = useState<AssistantConversationState>({ scopeKey, messages: [] })
|
||||
const [requestState, setRequestState] = useState<AssistantRequestState>({
|
||||
scopeKey,
|
||||
requestId: 0,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
const [loadingModels, setLoadingModels] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [modelError, setModelError] = useState<string | null>(null)
|
||||
|
||||
const messages = conversation.scopeKey === scopeKey ? conversation.messages : []
|
||||
const loading = requestState.scopeKey === scopeKey && requestState.loading
|
||||
const queryError = requestState.scopeKey === scopeKey ? requestState.error : null
|
||||
const error = queryError ?? modelError
|
||||
|
||||
const selectedModel = useMemo(() => {
|
||||
const available = new Set(models.map((model) => model.name))
|
||||
@@ -62,7 +108,7 @@ export function useGeoAssistant({ selectedProjectId, selectedAreaId, selectionBb
|
||||
|
||||
const loadModels = async () => {
|
||||
setLoadingModels(true)
|
||||
setError(null)
|
||||
setModelError(null)
|
||||
try {
|
||||
const currentStatus = await assistantApi.status()
|
||||
setStatus(currentStatus)
|
||||
@@ -82,22 +128,39 @@ export function useGeoAssistant({ selectedProjectId, selectedAreaId, selectionBb
|
||||
setStatus(null)
|
||||
setModels([])
|
||||
setDefaultModel('')
|
||||
setError(formatError(requestError, 'De lokale AI-assistent kon niet worden bereikt.'))
|
||||
setModelError(formatError(requestError, 'De lokale AI-assistent kon niet worden bereikt.'))
|
||||
} finally {
|
||||
setLoadingModels(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { void loadModels() }, [])
|
||||
useEffect(() => { setMessages([]); setError(null) }, [selectedProjectId])
|
||||
useEffect(() => {
|
||||
setConversation({ scopeKey, messages: [] })
|
||||
setRequestState({
|
||||
scopeKey,
|
||||
requestId: latestRequestIdRef.current,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
}, [scopeKey])
|
||||
|
||||
const ask = async (question: string): Promise<boolean> => {
|
||||
const trimmed = question.trim()
|
||||
if (!selectedProjectId || !trimmed || !selectedModel) return false
|
||||
const requestId = latestRequestIdRef.current + 1
|
||||
latestRequestIdRef.current = requestId
|
||||
const requestScopeKey = scopeKey
|
||||
const userMessage: GeoAssistantMessage = { id: nextAssistantMessageId('user'), role: 'user', content: trimmed }
|
||||
setMessages((current) => [...current, userMessage])
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setConversation((current) => ({
|
||||
scopeKey: requestScopeKey,
|
||||
messages: [...(current.scopeKey === requestScopeKey ? current.messages : []), userMessage],
|
||||
}))
|
||||
setRequestState({ scopeKey: requestScopeKey, requestId, loading: true, error: null })
|
||||
const isLatestRequest = () => (
|
||||
latestRequestIdRef.current === requestId
|
||||
&& activeScopeRef.current === requestScopeKey
|
||||
)
|
||||
try {
|
||||
const history = messages.slice(-6).map(({ role, content }) => ({ role, content }))
|
||||
const result = await assistantApi.query(selectedProjectId, {
|
||||
@@ -107,17 +170,40 @@ export function useGeoAssistant({ selectedProjectId, selectedAreaId, selectionBb
|
||||
area_id: selectedAreaId,
|
||||
history,
|
||||
})
|
||||
setMessages((current) => [...current, { id: nextAssistantMessageId('assistant'), role: 'assistant', content: result.answer, response: result }])
|
||||
if (!isLatestRequest()) return false
|
||||
setConversation((current) => current.scopeKey === requestScopeKey ? {
|
||||
scopeKey: requestScopeKey,
|
||||
messages: [...current.messages, { id: nextAssistantMessageId('assistant'), role: 'assistant', content: result.answer, response: result }],
|
||||
} : current)
|
||||
return true
|
||||
} catch (requestError) {
|
||||
setError(formatError(requestError, 'GeoIntel kon de vraag niet beantwoorden.'))
|
||||
if (!isLatestRequest()) return false
|
||||
setRequestState({
|
||||
scopeKey: requestScopeKey,
|
||||
requestId,
|
||||
loading: false,
|
||||
error: formatError(requestError, 'GeoIntel kon de vraag niet beantwoorden.'),
|
||||
})
|
||||
return false
|
||||
} finally {
|
||||
setLoading(false)
|
||||
if (isLatestRequest()) {
|
||||
setRequestState((current) => current.scopeKey === requestScopeKey && current.requestId === requestId
|
||||
? { ...current, loading: false }
|
||||
: current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => { setMessages([]); setError(null) }
|
||||
const clear = () => {
|
||||
latestRequestIdRef.current += 1
|
||||
setConversation({ scopeKey, messages: [] })
|
||||
setRequestState({
|
||||
scopeKey,
|
||||
requestId: latestRequestIdRef.current,
|
||||
loading: false,
|
||||
error: null,
|
||||
})
|
||||
}
|
||||
|
||||
return { status, models, selectedModel, selectedModelChoice, defaultModel, messages, loading, loadingModels, error, loadModels, ask, clear, setSelectedModel }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user