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:
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user