feat: add source-grounded evolution and Ollama assistant
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-15 06:45:56 +02:00
parent 0baa9b069c
commit beacdf2560
37 changed files with 2246 additions and 58 deletions
+115
View File
@@ -0,0 +1,115 @@
import { useEffect, useState } from 'react'
import { formatError } from '../lib/formatError'
import { assistantApi } from '../services/api/assistant'
import type {
AssistantChatMessage,
AssistantModelRead,
AssistantQueryResponse,
AssistantStatus,
VectorSelectionBBox,
} from '../types'
export interface GeoAssistantMessage extends AssistantChatMessage {
id: string
response?: AssistantQueryResponse
}
interface UseGeoAssistantOptions {
selectedProjectId: string | null
selectedAreaId: string | null
selectionBbox: VectorSelectionBBox | null
}
export function useGeoAssistant({ selectedProjectId, selectedAreaId, selectionBbox }: UseGeoAssistantOptions) {
const [status, setStatus] = useState<AssistantStatus | null>(null)
const [models, setModels] = useState<AssistantModelRead[]>([])
const [selectedModel, setSelectedModel] = useState('')
const [messages, setMessages] = useState<GeoAssistantMessage[]>([])
const [loading, setLoading] = useState(false)
const [loadingModels, setLoadingModels] = useState(false)
const [error, setError] = useState<string | null>(null)
const loadModels = async () => {
setLoadingModels(true)
setError(null)
try {
const currentStatus = await assistantApi.status()
setStatus(currentStatus)
if (!currentStatus.enabled || !currentStatus.reachable) {
setModels([])
setSelectedModel('')
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 ?? ''
})
} catch (requestError) {
setStatus(null)
setModels([])
setError(formatError(requestError, 'De lokale AI-assistent kon niet worden bereikt.'))
} finally {
setLoadingModels(false)
}
}
useEffect(() => {
void loadModels()
}, [])
useEffect(() => {
setMessages([])
setError(null)
}, [selectedProjectId])
const ask = async (question: string): Promise<boolean> => {
const trimmed = question.trim()
if (!selectedProjectId || !trimmed || !selectedModel) return false
const userMessage: GeoAssistantMessage = { id: crypto.randomUUID(), role: 'user', content: trimmed }
setMessages((current) => [...current, userMessage])
setLoading(true)
setError(null)
try {
const history = messages.slice(-6).map(({ role, content }) => ({ role, content }))
const result = await assistantApi.query(selectedProjectId, {
question: trimmed,
model: selectedModel,
bbox: selectionBbox,
area_id: selectedAreaId,
history,
})
setMessages((current) => [
...current,
{ id: crypto.randomUUID(), role: 'assistant', content: result.answer, response: result },
])
return true
} catch (requestError) {
setError(formatError(requestError, 'GeoIntel kon de vraag niet beantwoorden.'))
return false
} finally {
setLoading(false)
}
}
const clear = () => {
setMessages([])
setError(null)
}
return {
status,
models,
selectedModel,
messages,
loading,
loadingModels,
error,
loadModels,
ask,
clear,
setSelectedModel,
}
}