fix: control plane UI fixes for recall and data view (#693)
* fix: control plane UI fixes for recall and data view - Sanitize NaN cross-encoder scores to 0.0 in reranking pipeline (Pydantic serializes NaN as JSON null, breaking UI score display) - Add null-coalesce for score in search debug view to prevent crash - Switch data view text filter from debounced onChange to Enter key (avoids slow ILIKE queries on every keystroke for large banks) - Show loading spinner in search icon during filter requests - Preserve search/tag filters when clicking "Load more" * chore: sync generated files after rebase
This commit is contained in:
parent
a94a90ea3f
commit
6bb83f4600
4 changed files with 47 additions and 29 deletions
|
|
@ -153,6 +153,8 @@ class CrossEncoderReranker:
|
|||
|
||||
# Normalize scores using sigmoid to [0, 1] range
|
||||
# Cross-encoder returns logits which can be negative
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
def sigmoid(x):
|
||||
|
|
@ -163,11 +165,20 @@ class CrossEncoderReranker:
|
|||
# Create ScoredResult objects with cross-encoder scores
|
||||
scored_results = []
|
||||
for candidate, raw_score, norm_score in zip(candidates, scores, normalized_scores):
|
||||
# Sanitize NaN scores (cross-encoder can return NaN for certain inputs).
|
||||
# NaN propagates through all downstream scoring and Pydantic serializes
|
||||
# NaN as JSON null, which breaks clients expecting numeric values.
|
||||
raw = float(raw_score)
|
||||
norm = float(norm_score)
|
||||
if math.isnan(raw):
|
||||
raw = 0.0
|
||||
if math.isnan(norm):
|
||||
norm = 0.0
|
||||
scored_result = ScoredResult(
|
||||
candidate=candidate,
|
||||
cross_encoder_score=float(raw_score),
|
||||
cross_encoder_score_normalized=float(norm_score),
|
||||
weight=float(norm_score), # Initial weight is just cross-encoder score
|
||||
cross_encoder_score=raw,
|
||||
cross_encoder_score_normalized=norm,
|
||||
weight=norm, # Initial weight is just cross-encoder score
|
||||
)
|
||||
scored_results.append(scored_result)
|
||||
|
||||
|
|
|
|||
|
|
@ -227,29 +227,15 @@ export function DataView({ factType }: DataViewProps) {
|
|||
// Reset to first page when filters change
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchQuery, tagFilters]);
|
||||
}, [tagFilters]);
|
||||
|
||||
// Debounce ref for text search
|
||||
const searchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Trigger server-side reload when text filter changes (debounced 300ms)
|
||||
useEffect(() => {
|
||||
if (searchDebounceRef.current) {
|
||||
clearTimeout(searchDebounceRef.current);
|
||||
// Trigger text search on Enter key
|
||||
const executeSearch = () => {
|
||||
if (currentBank) {
|
||||
setCurrentPage(1);
|
||||
loadData(undefined, searchQuery || undefined, tagFilters.length > 0 ? tagFilters : undefined);
|
||||
}
|
||||
searchDebounceRef.current = setTimeout(() => {
|
||||
if (currentBank) {
|
||||
loadData(
|
||||
undefined,
|
||||
searchQuery || undefined,
|
||||
tagFilters.length > 0 ? tagFilters : undefined
|
||||
);
|
||||
}
|
||||
}, 300);
|
||||
return () => {
|
||||
if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);
|
||||
};
|
||||
}, [searchQuery]);
|
||||
};
|
||||
|
||||
// Trigger server-side reload immediately when tag filters change
|
||||
useEffect(() => {
|
||||
|
|
@ -292,12 +278,22 @@ export function DataView({ factType }: DataViewProps) {
|
|||
<div className="flex items-center gap-2">
|
||||
{/* Text search */}
|
||||
<div className="relative max-w-xs flex-1">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
{loading ? (
|
||||
<RefreshCw className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none animate-spin" />
|
||||
) : (
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
)}
|
||||
<Input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Filter by text or context..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
executeSearch();
|
||||
}
|
||||
}}
|
||||
placeholder="Filter by text or context (press Enter)..."
|
||||
className="pl-8 h-9"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -356,7 +352,11 @@ export function DataView({ factType }: DataViewProps) {
|
|||
onClick={() => {
|
||||
const newLimit = Math.min(data.total_units, fetchLimit + 1000);
|
||||
setFetchLimit(newLimit);
|
||||
loadData(newLimit);
|
||||
loadData(
|
||||
newLimit,
|
||||
searchQuery || undefined,
|
||||
tagFilters.length > 0 ? tagFilters : undefined
|
||||
);
|
||||
}}
|
||||
className="ml-2 text-primary hover:underline"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -411,7 +411,7 @@ export function SearchDebugView() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-right">
|
||||
<div className="text-sm font-semibold">{score.toFixed(3)}</div>
|
||||
<div className="text-sm font-semibold">{(score ?? 0).toFixed(3)}</div>
|
||||
<div className="text-xs text-muted-foreground">score</div>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ To switch between backends:
|
|||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-codex`, `claude-code`, `anthropic`, `gemini`, `groq`, `minimax`, `ollama`, `lmstudio`, `vertexai`, `bedrock`, `litellm` | `openai` |
|
||||
| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-codex`, `claude-code`, `anthropic`, `gemini`, `groq`, `minimax`, `ollama`, `lmstudio`, `vertexai`, `bedrock`, `litellm`, `none` | `openai` |
|
||||
| `HINDSIGHT_API_LLM_API_KEY` | API key for LLM provider | - |
|
||||
| `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-5-mini` |
|
||||
| `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default |
|
||||
|
|
@ -250,6 +250,13 @@ export HINDSIGHT_API_LLM_MODEL=azure/gpt-4o
|
|||
export HINDSIGHT_API_LLM_PROVIDER=litellm
|
||||
export HINDSIGHT_API_LLM_API_KEY=your-together-api-key
|
||||
export HINDSIGHT_API_LLM_MODEL=together_ai/meta-llama/Llama-3-70b-chat-hf
|
||||
|
||||
# No LLM (chunk storage + semantic search only, no API key needed)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=none
|
||||
# Retain automatically uses chunks mode (no fact extraction)
|
||||
# Recall works normally (semantic search, BM25, graph retrieval)
|
||||
# Reflect returns HTTP 400 (requires an LLM)
|
||||
# Consolidation/observations are disabled
|
||||
```
|
||||
|
||||
:::tip OpenAI Codex, Claude Code & Vertex AI Setup
|
||||
|
|
|
|||
Loading…
Reference in a new issue