Overhaul GeoIntel model selection and map UX

This commit is contained in:
Jens
2026-08-01 15:18:06 +02:00
parent ce3cd20356
commit 96db4c966d
13 changed files with 663 additions and 76 deletions
+39 -38
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { formatError } from '../lib/formatError'
import { assistantApi } from '../services/api/assistant'
import type {
@@ -20,6 +20,7 @@ interface UseGeoAssistantOptions {
selectionBbox: VectorSelectionBBox | null
}
const ASSISTANT_MODEL_PREFERENCE_KEY = 'geointel.assistant.model-preference.v1'
let assistantMessageSequence = 0
function nextAssistantMessageId(role: AssistantChatMessage['role']): string {
@@ -27,15 +28,38 @@ function nextAssistantMessageId(role: AssistantChatMessage['role']): string {
return `${role}-${Date.now()}-${assistantMessageSequence}`
}
function readStoredPreference(): string {
try {
return window.localStorage.getItem(ASSISTANT_MODEL_PREFERENCE_KEY) || 'automatic'
} catch {
return 'automatic'
}
}
export function useGeoAssistant({ selectedProjectId, selectedAreaId, selectionBbox }: UseGeoAssistantOptions) {
const [status, setStatus] = useState<AssistantStatus | null>(null)
const [models, setModels] = useState<AssistantModelRead[]>([])
const [selectedModel, setSelectedModel] = useState('')
const [selectedModelChoice, setSelectedModelChoice] = useState(readStoredPreference)
const [defaultModel, setDefaultModel] = useState('')
const [messages, setMessages] = useState<GeoAssistantMessage[]>([])
const [loading, setLoading] = useState(false)
const [loadingModels, setLoadingModels] = useState(false)
const [error, setError] = useState<string | null>(null)
const selectedModel = useMemo(() => {
const available = new Set(models.map((model) => model.name))
if (selectedModelChoice !== 'automatic' && available.has(selectedModelChoice)) return selectedModelChoice
if (defaultModel && available.has(defaultModel)) return defaultModel
return models[0]?.name ?? ''
}, [defaultModel, models, selectedModelChoice])
const setSelectedModel = (value: string) => {
const valid = value === 'automatic' || models.some((model) => model.name === value)
const next = valid ? value : 'automatic'
setSelectedModelChoice(next)
try { window.localStorage.setItem(ASSISTANT_MODEL_PREFERENCE_KEY, next) } catch { /* voorkeur blijft sessielokaal */ }
}
const loadModels = async () => {
setLoadingModels(true)
setError(null)
@@ -44,33 +68,28 @@ export function useGeoAssistant({ selectedProjectId, selectedAreaId, selectionBb
setStatus(currentStatus)
if (!currentStatus.enabled || !currentStatus.reachable) {
setModels([])
setSelectedModel('')
setDefaultModel('')
return
}
const result = await assistantApi.models()
setModels(result.items)
setSelectedModel((current) => {
if (current && result.items.some((model) => model.name === current)) return current
if (result.default_model && result.items.some((model) => model.name === result.default_model)) return result.default_model
return result.items[0]?.name ?? ''
})
const fallback = result.default_model && result.items.some((model) => model.name === result.default_model)
? result.default_model
: result.items[0]?.name ?? ''
setDefaultModel(fallback)
setSelectedModelChoice((current) => current === 'automatic' || result.items.some((model) => model.name === current) ? current : 'automatic')
} catch (requestError) {
setStatus(null)
setModels([])
setDefaultModel('')
setError(formatError(requestError, 'De lokale AI-assistent kon niet worden bereikt.'))
} finally {
setLoadingModels(false)
}
}
useEffect(() => {
void loadModels()
}, [])
useEffect(() => {
setMessages([])
setError(null)
}, [selectedProjectId])
useEffect(() => { void loadModels() }, [])
useEffect(() => { setMessages([]); setError(null) }, [selectedProjectId])
const ask = async (question: string): Promise<boolean> => {
const trimmed = question.trim()
@@ -88,10 +107,7 @@ export function useGeoAssistant({ selectedProjectId, selectedAreaId, selectionBb
area_id: selectedAreaId,
history,
})
setMessages((current) => [
...current,
{ id: nextAssistantMessageId('assistant'), role: 'assistant', content: result.answer, response: result },
])
setMessages((current) => [...current, { id: nextAssistantMessageId('assistant'), role: 'assistant', content: result.answer, response: result }])
return true
} catch (requestError) {
setError(formatError(requestError, 'GeoIntel kon de vraag niet beantwoorden.'))
@@ -101,22 +117,7 @@ export function useGeoAssistant({ selectedProjectId, selectedAreaId, selectionBb
}
}
const clear = () => {
setMessages([])
setError(null)
}
const clear = () => { setMessages([]); setError(null) }
return {
status,
models,
selectedModel,
messages,
loading,
loadingModels,
error,
loadModels,
ask,
clear,
setSelectedModel,
}
}
return { status, models, selectedModel, selectedModelChoice, defaultModel, messages, loading, loadingModels, error, loadModels, ask, clear, setSelectedModel }
}