feat: confluence autocomplete + explanatory text

- Replace plain text inputs with SymbolAutocomplete (existing component)
  in both CorridorPanel and ConfluencePanel
- Add explanatory text to both panels describing what the visualization
  shows and how to interpret results
- Fix symbol reset bug: panels now sync with global activeSymbol store
  so symbol persists across tab switches and page reloads
- Add 'Run evaluation now' button when no corridor snapshot exists,
  wired to existing runEvaluationNow mutation
This commit is contained in:
Investor Flow Build
2026-08-11 22:49:30 -04:00
parent 9b41890d33
commit 0afc5b6f6e
2 changed files with 86 additions and 17 deletions
+44 -8
View File
@@ -1,8 +1,9 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { api } from '@/lib/trpc';
import { useActiveSymbol } from '@/stores/active-symbol-store';
import { CollapsibleSection } from '@/components/CollapsibleSection';
import { SymbolAutocomplete } from '@/components/SymbolAutocomplete';
// ---------------------------------------------------------------------------
// Types (subset of server types for display)
@@ -44,9 +45,15 @@ const BODY_BADGE: Record<string, string> = {
export function ConfluencePanel() {
const activeSymbol = useActiveSymbol((s) => s.activeSymbol);
const setActiveSymbol = useActiveSymbol((s) => s.setActive);
const [symbol, setSymbol] = useState(activeSymbol);
const [selectedRackId, setSelectedRackId] = useState<string | null>(null);
const pickSymbol = useCallback((sym: string) => {
setSymbol(sym);
setActiveSymbol(sym);
}, [setActiveSymbol]);
// Racks
const [racks, setRacks] = useState<{ system: Array<{ id: string; name: string }>; user: Array<{ id: string; name: string }> }>({ system: [], user: [] });
useEffect(() => {
@@ -80,6 +87,18 @@ export function ConfluencePanel() {
.catch(() => {});
}, []);
const [evaluating, setEvaluating] = useState(false);
const runEvaluation = async () => {
setEvaluating(true);
try {
await api.confluence.runEvaluationNow({ symbol });
window.location.reload();
} finally {
setEvaluating(false);
}
};
const latest = (evaluation?.latest as Record<string, unknown>) ?? null;
const quality = (latest?.quality as Quality) ?? null;
const assessments = (latest?.assessments as Array<{ id: string; state: string; note?: string }>) ?? [];
@@ -89,16 +108,24 @@ export function ConfluencePanel() {
return (
<div className="space-y-6">
{/* Explanatory text */}
<div className="text-xs text-fg-muted leading-relaxed">
Slot Picture combines technical, seasonal, and valuation signals into a single
directional read. Each slot fires (green dot) when its condition is met, or stays
dormant. The overall quality grade reflects how many slots fired and in which direction.
Use the rack selector to switch between different signal subsets.
</div>
{/* Header */}
<div className="flex flex-wrap items-end gap-4">
<div>
<div className="w-48">
<label className="block text-xs text-fg-muted mb-1">Symbol</label>
<input
type="text"
<SymbolAutocomplete
onPick={pickSymbol}
onSubmitRaw={(raw) => pickSymbol(raw.toUpperCase())}
value={symbol}
onChange={(e) => setSymbol(e.target.value.toUpperCase().trim())}
className="w-24 rounded bg-bg-secondary border border-fg-muted/20 px-2 py-1.5 text-sm text-fg"
maxLength={12}
placeholder="Search symbol…"
ariaLabel="Select symbol for slot picture"
/>
</div>
<div>
@@ -140,7 +167,16 @@ export function ConfluencePanel() {
)}
</div>
) : evaluation !== null ? (
<div className="text-sm text-fg-muted">No evaluations yet for {symbol}. Data will appear once the confluence evaluator runs.</div>
<div className="flex items-center gap-3 text-sm text-fg-muted">
<span>No evaluations yet for {symbol}.</span>
<button
onClick={runEvaluation}
disabled={evaluating}
className="rounded bg-bg-secondary border border-fg-muted/30 px-3 py-1.5 text-xs text-fg hover:border-fg-muted/60 disabled:opacity-50"
>
{evaluating ? 'Evaluating…' : 'Run evaluation now'}
</button>
</div>
) : null}
{/* Slot evidence grid */}
+42 -9
View File
@@ -3,6 +3,7 @@ import { useState, useEffect, useCallback } from 'react';
import { api } from '@/lib/trpc';
import { useActiveSymbol } from '@/stores/active-symbol-store';
import { CollapsibleSection } from '@/components/CollapsibleSection';
import { SymbolAutocomplete } from '@/components/SymbolAutocomplete';
// ---------------------------------------------------------------------------
// Types (subset of server types for display)
@@ -83,12 +84,14 @@ const price = (v: number | null | undefined): string =>
export function CorridorPanel() {
const activeSymbol = useActiveSymbol((s) => s.activeSymbol);
const setActiveSymbol = useActiveSymbol((s) => s.setActive);
const [symbol, setSymbol] = useState(activeSymbol);
const [data, setData] = useState<CorridorData | null>(null);
const [watchlist, setWatchlist] = useState<string[]>([]);
const [addInput, setAddInput] = useState('');
const [backtest, setBacktest] = useState<{ rows: BacktestRow[]; aggregate: BacktestAggregate } | null>(null);
const [running, setRunning] = useState(false);
const [evaluating, setEvaluating] = useState(false);
const loadCorridor = useCallback((sym: string) => {
if (!sym) return;
@@ -111,6 +114,11 @@ export function CorridorPanel() {
.catch(() => {});
}, []);
const pickSymbol = useCallback((sym: string) => {
setSymbol(sym);
setActiveSymbol(sym);
}, [setActiveSymbol]);
const addToWatchlist = async () => {
const s = addInput.trim().toUpperCase();
if (!s) return;
@@ -134,6 +142,16 @@ export function CorridorPanel() {
}
};
const runEvaluation = async () => {
setEvaluating(true);
try {
await api.confluence.runEvaluationNow({ symbol });
loadCorridor(symbol);
} finally {
setEvaluating(false);
}
};
const c = data?.corridor;
const currentPE = c?.currentPE ?? null;
const pctPosition = c?.pePercentile1y ?? null;
@@ -148,16 +166,24 @@ export function CorridorPanel() {
return (
<div className="space-y-6">
{/* Explanatory text */}
<div className="text-xs text-fg-muted leading-relaxed">
Price Corridor maps a stock's current P/E ratio against its 1-year historical range.
The green zone (below 33rd percentile) marks entries where valuation has been attractively low;
the amber zone (above 67th) marks stretched valuations.
Fair value is derived from the median P/E over the lookback window applied to trailing EPS.
</div>
{/* Header: symbol + watchlist */}
<div className="flex flex-wrap items-end gap-4">
<div>
<div className="w-48">
<label className="block text-xs text-fg-muted mb-1">Symbol</label>
<input
type="text"
<SymbolAutocomplete
onPick={pickSymbol}
onSubmitRaw={(raw) => pickSymbol(raw.toUpperCase())}
value={symbol}
onChange={(e) => setSymbol(e.target.value.toUpperCase().trim())}
className="w-24 rounded bg-bg-secondary border border-fg-muted/20 px-2 py-1.5 text-sm text-fg"
maxLength={12}
placeholder="Search symbol…"
ariaLabel="Select symbol for corridor"
/>
</div>
<div>
@@ -183,7 +209,7 @@ export function CorridorPanel() {
<div className="mt-2 flex flex-wrap gap-1.5">
{watchlist.map((s) => (
<span key={s} className="inline-flex items-center gap-1 rounded border border-fg-muted/20 bg-bg-secondary px-2 py-0.5 text-xs text-fg">
<button onClick={() => loadCorridor(s)} className="hover:text-green-400">{s}</button>
<button onClick={() => pickSymbol(s)} className="hover:text-green-400">{s}</button>
<button onClick={() => removeFromWatchlist(s)} className="text-fg-muted hover:text-red-400" aria-label={`Remove ${s}`}>×</button>
</span>
))}
@@ -260,8 +286,15 @@ export function CorridorPanel() {
)}
{data && !data.snapshot && (
<div className="text-sm text-fg-muted">
No corridor snapshot for {symbol} yet. Snapshot it via the confluence evaluator, or pick a symbol from the watchlist.
<div className="flex items-center gap-3 text-sm text-fg-muted">
<span>No corridor snapshot for {symbol} yet.</span>
<button
onClick={runEvaluation}
disabled={evaluating}
className="rounded bg-bg-secondary border border-fg-muted/30 px-3 py-1.5 text-xs text-fg hover:border-fg-muted/60 disabled:opacity-50"
>
{evaluating ? 'Evaluating…' : 'Run evaluation now'}
</button>
</div>
)}