Add DeepSeek/xAI/OpenAI-compatible sentiment providers; custom dark dropdown
Providers (admin-switchable, no redeploy): - DeepSeek and any OpenAI-compatible endpoint (OpenRouter, Together, Groq, local Ollama) via a generic Chat Completions adapter + base_url - xAI Grok with Live Search (search_parameters web+X, citations) — grounded tier alongside OpenAI and Gemini - DeepSeek / generic compatible endpoints are ungrounded (no web search); UI shows an amber warning and labels each provider's grounding - Optional env fallbacks DEEPSEEK_API_KEY / XAI_API_KEY UI: replace native <select> (unstyleable white popup on Windows) with a custom dark Dropdown component everywhere — sentiment provider, scanner filters, market sort, indicators, admin universe, user role. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
||||
useUpdateSentimentSettings,
|
||||
useTestSentimentProvider,
|
||||
} from '../../hooks/useAdmin';
|
||||
import { Select } from '../ui/Field';
|
||||
import { Dropdown, type DropdownOption } from '../ui/Dropdown';
|
||||
import { SkeletonTable } from '../ui/Skeleton';
|
||||
import type { SentimentTestResult } from '../../lib/types';
|
||||
|
||||
@@ -14,6 +14,18 @@ const SOURCE_LABEL: Record<string, string> = {
|
||||
none: 'not configured',
|
||||
};
|
||||
|
||||
const PROVIDER_LABELS: Record<string, string> = {
|
||||
openai: 'OpenAI — web search',
|
||||
gemini: 'Google Gemini — web search',
|
||||
deepseek: 'DeepSeek — cheap, no web search',
|
||||
xai: 'xAI Grok — Live Search',
|
||||
openai_compatible: 'OpenAI-compatible — custom URL',
|
||||
};
|
||||
|
||||
function providerLabel(p: string): string {
|
||||
return PROVIDER_LABELS[p] ?? p;
|
||||
}
|
||||
|
||||
export function SentimentProviderSettings() {
|
||||
const { data, isLoading, isError, error } = useSentimentSettings();
|
||||
const update = useUpdateSentimentSettings();
|
||||
@@ -22,12 +34,14 @@ export function SentimentProviderSettings() {
|
||||
const [provider, setProvider] = useState('openai');
|
||||
const [model, setModel] = useState('');
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [baseUrl, setBaseUrl] = useState('');
|
||||
const [testResult, setTestResult] = useState<SentimentTestResult | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setProvider(data.provider);
|
||||
setModel(data.model);
|
||||
setBaseUrl(data.base_url ?? '');
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
@@ -35,10 +49,17 @@ export function SentimentProviderSettings() {
|
||||
if (isError) return <p className="text-sm text-red-400">{(error as Error)?.message || 'Failed to load sentiment settings'}</p>;
|
||||
if (!data) return null;
|
||||
|
||||
const grounded = data.web_search_providers ?? ['openai', 'gemini'];
|
||||
const needsBaseUrl = (data.custom_base_url_providers ?? ['openai_compatible']).includes(provider);
|
||||
const isGrounded = grounded.includes(provider);
|
||||
|
||||
const providerOptions: DropdownOption[] = data.valid_providers.map((p) => ({
|
||||
value: p,
|
||||
label: providerLabel(p),
|
||||
}));
|
||||
|
||||
const onProviderChange = (next: string) => {
|
||||
setProvider(next);
|
||||
// Auto-fill the model with the new provider's default unless the user has a
|
||||
// custom value that isn't the previous provider's default.
|
||||
const defaults = data.default_models;
|
||||
if (!model || Object.values(defaults).includes(model)) {
|
||||
setModel(defaults[next] ?? '');
|
||||
@@ -50,6 +71,7 @@ export function SentimentProviderSettings() {
|
||||
update.mutate({
|
||||
provider,
|
||||
model,
|
||||
...(needsBaseUrl ? { base_url: baseUrl } : {}),
|
||||
...(apiKey ? { api_key: apiKey } : {}),
|
||||
});
|
||||
setApiKey('');
|
||||
@@ -74,11 +96,7 @@ export function SentimentProviderSettings() {
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-gray-400">Provider</span>
|
||||
<Select value={provider} onChange={(e) => onProviderChange(e.target.value)} className="w-full !py-2">
|
||||
{data.valid_providers.map((p) => (
|
||||
<option key={p} value={p}>{p}</option>
|
||||
))}
|
||||
</Select>
|
||||
<Dropdown value={provider} onChange={onProviderChange} options={providerOptions} ariaLabel="Sentiment provider" />
|
||||
</label>
|
||||
|
||||
<label className="block space-y-1">
|
||||
@@ -87,12 +105,33 @@ export function SentimentProviderSettings() {
|
||||
type="text"
|
||||
value={model}
|
||||
onChange={(e) => setModel(e.target.value)}
|
||||
placeholder={data.default_models[provider] ?? ''}
|
||||
placeholder={data.default_models[provider] || 'model id'}
|
||||
className="w-full input-glass px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{needsBaseUrl && (
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-gray-400">Base URL</span>
|
||||
<input
|
||||
type="text"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder="https://openrouter.ai/api/v1"
|
||||
className="w-full input-glass px-3 py-2 text-sm"
|
||||
/>
|
||||
<span className="text-[11px] text-gray-600">OpenAI-compatible Chat Completions endpoint (OpenRouter, Together, Groq, local Ollama…).</span>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{!isGrounded && (
|
||||
<div className="rounded-lg border border-amber-500/20 bg-amber-500/10 px-4 py-2.5 text-xs text-amber-300">
|
||||
⚠ No live web search. This provider scores sentiment from the model's training knowledge,
|
||||
not current news — cheaper, but not real-time. OpenAI and Gemini are grounded in live search.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-gray-400">API Key</span>
|
||||
<input
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useUpdateTickerUniverseSetting,
|
||||
} from '../../hooks/useAdmin';
|
||||
import type { TickerUniverse } from '../../lib/types';
|
||||
import { Dropdown } from '../ui/Dropdown';
|
||||
|
||||
const UNIVERSE_OPTIONS: Array<{ value: TickerUniverse; label: string }> = [
|
||||
{ value: 'sp500', label: 'S&P 500' },
|
||||
@@ -50,18 +51,11 @@ export function TickerUniverseBootstrap() {
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<label className="block space-y-1 md:col-span-2">
|
||||
<span className="text-xs text-gray-400">Default Universe</span>
|
||||
<select
|
||||
<Dropdown
|
||||
value={universe}
|
||||
onChange={(e) => setUniverse(e.target.value as TickerUniverse)}
|
||||
className="w-full input-glass px-3 py-2 text-sm"
|
||||
disabled={isLoading || updateDefault.isPending || bootstrap.isPending}
|
||||
>
|
||||
{UNIVERSE_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
onChange={(v) => setUniverse(v as TickerUniverse)}
|
||||
options={UNIVERSE_OPTIONS.map((o) => ({ value: o.value, label: o.label }))}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex items-end gap-2 pb-2">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useUsers, useCreateUser, useUpdateAccess, useResetPassword } from '../../hooks/useAdmin';
|
||||
import { SkeletonTable } from '../ui/Skeleton';
|
||||
import { Dropdown } from '../ui/Dropdown';
|
||||
import type { AdminUser } from '../../lib/types';
|
||||
|
||||
export function UserTable() {
|
||||
@@ -53,10 +54,15 @@ export function UserTable() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs text-gray-400">Role</label>
|
||||
<select value={newRole} onChange={(e) => setNewRole(e.target.value)} className="input-glass px-3 py-2 text-sm">
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<Dropdown
|
||||
value={newRole}
|
||||
onChange={setNewRole}
|
||||
className="w-32"
|
||||
options={[
|
||||
{ value: 'user', label: 'User' },
|
||||
{ value: 'admin', label: 'Admin' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-300 pb-1">
|
||||
<input type="checkbox" checked={newAccess} onChange={(e) => setNewAccess(e.target.checked)}
|
||||
|
||||
@@ -9,7 +9,8 @@ import { useToast } from '../ui/Toast';
|
||||
import { Button } from '../ui/Button';
|
||||
import { Callout } from '../ui/Callout';
|
||||
import { Disclosure } from '../ui/Disclosure';
|
||||
import { Field, Input, Select } from '../ui/Field';
|
||||
import { Field, Input } from '../ui/Field';
|
||||
import { Dropdown } from '../ui/Dropdown';
|
||||
import { triggerJob } from '../../api/admin';
|
||||
import type { TradeSetup } from '../../lib/types';
|
||||
import { RECOMMENDATION_ACTION_GLOSSARY, RECOMMENDATION_ACTION_LABELS } from '../../lib/recommendation';
|
||||
@@ -178,15 +179,17 @@ export function SetupsPanel() {
|
||||
</div>
|
||||
</Field>
|
||||
<Field label="Direction" htmlFor="direction">
|
||||
<Select
|
||||
<Dropdown
|
||||
id="direction"
|
||||
value={directionFilter}
|
||||
onChange={(e) => setDirectionFilter(e.target.value as DirectionFilter)}
|
||||
>
|
||||
<option value="both">Both</option>
|
||||
<option value="long">Long</option>
|
||||
<option value="short">Short</option>
|
||||
</Select>
|
||||
onChange={(v) => setDirectionFilter(v as DirectionFilter)}
|
||||
className="w-32"
|
||||
options={[
|
||||
{ value: 'both', label: 'Both' },
|
||||
{ value: 'long', label: 'Long' },
|
||||
{ value: 'short', label: 'Short' },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Min Confidence" htmlFor="min-confidence">
|
||||
<Input
|
||||
@@ -201,18 +204,20 @@ export function SetupsPanel() {
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Recommended Action" htmlFor="action">
|
||||
<Select
|
||||
<Dropdown
|
||||
id="action"
|
||||
value={actionFilter}
|
||||
onChange={(e) => setActionFilter(e.target.value as ActionFilter)}
|
||||
>
|
||||
<option value="all">All</option>
|
||||
<option value="LONG_HIGH">{RECOMMENDATION_ACTION_LABELS.LONG_HIGH}</option>
|
||||
<option value="LONG_MODERATE">{RECOMMENDATION_ACTION_LABELS.LONG_MODERATE}</option>
|
||||
<option value="SHORT_HIGH">{RECOMMENDATION_ACTION_LABELS.SHORT_HIGH}</option>
|
||||
<option value="SHORT_MODERATE">{RECOMMENDATION_ACTION_LABELS.SHORT_MODERATE}</option>
|
||||
<option value="NEUTRAL">{RECOMMENDATION_ACTION_LABELS.NEUTRAL}</option>
|
||||
</Select>
|
||||
onChange={(v) => setActionFilter(v as ActionFilter)}
|
||||
className="w-56"
|
||||
options={[
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'LONG_HIGH', label: RECOMMENDATION_ACTION_LABELS.LONG_HIGH },
|
||||
{ value: 'LONG_MODERATE', label: RECOMMENDATION_ACTION_LABELS.LONG_MODERATE },
|
||||
{ value: 'SHORT_HIGH', label: RECOMMENDATION_ACTION_LABELS.SHORT_HIGH },
|
||||
{ value: 'SHORT_MODERATE', label: RECOMMENDATION_ACTION_LABELS.SHORT_MODERATE },
|
||||
{ value: 'NEUTRAL', label: RECOMMENDATION_ACTION_LABELS.NEUTRAL },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<div className="ml-auto">
|
||||
<Button onClick={() => scanMutation.mutate()} loading={scanMutation.isPending}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getIndicator, getEMACross } from '../../api/indicators';
|
||||
import { Select } from '../ui/Field';
|
||||
import { Dropdown } from '../ui/Dropdown';
|
||||
import type { IndicatorResult, EMACrossResult } from '../../lib/types';
|
||||
|
||||
const INDICATOR_TYPES = ['ADX', 'EMA', 'RSI', 'ATR', 'volume_profile', 'pivot_points'] as const;
|
||||
@@ -86,16 +86,12 @@ export function IndicatorSelector({ symbol }: IndicatorSelectorProps) {
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-widest text-gray-500">Indicators</h3>
|
||||
|
||||
<div className="mb-4">
|
||||
<Select
|
||||
<Dropdown
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
className="w-full !py-2.5"
|
||||
>
|
||||
<option value="">Select indicator…</option>
|
||||
{INDICATOR_TYPES.map((type) => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</Select>
|
||||
onChange={setSelectedType}
|
||||
placeholder="Select indicator…"
|
||||
options={INDICATOR_TYPES.map((type) => ({ value: type, label: type }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedType && indicatorQuery.isLoading && (
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export interface DropdownOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: DropdownOption[];
|
||||
id?: string;
|
||||
className?: string;
|
||||
ariaLabel?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully dark, custom dropdown — replaces native <select>, whose popup list
|
||||
* can't be reliably styled (white background on Windows). Button + absolutely
|
||||
* positioned menu, click-outside and Escape to close.
|
||||
*/
|
||||
export function Dropdown({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
id,
|
||||
className = '',
|
||||
ariaLabel,
|
||||
placeholder = 'Select…',
|
||||
}: DropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const selected = options.find((o) => o.value === value);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDocClick = (e: MouseEvent) => {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', onDocClick);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onDocClick);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className={`relative ${className}`}>
|
||||
<button
|
||||
type="button"
|
||||
id={id}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
aria-label={ariaLabel}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="input-glass flex w-full items-center justify-between gap-2 px-3 py-1.5 text-left text-sm"
|
||||
>
|
||||
<span className={selected ? 'text-gray-200' : 'text-gray-500'}>
|
||||
{selected ? selected.label : placeholder}
|
||||
</span>
|
||||
<svg
|
||||
className={`h-4 w-4 shrink-0 text-gray-500 transition-transform duration-150 ${open ? 'rotate-180' : ''}`}
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M6 8l4 4 4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<ul
|
||||
role="listbox"
|
||||
className="absolute z-50 mt-1 max-h-64 w-full overflow-auto rounded-lg border border-white/[0.1] bg-[#151911] p-1 shadow-2xl shadow-black/50"
|
||||
>
|
||||
{options.map((opt) => {
|
||||
const isSelected = opt.value === value;
|
||||
return (
|
||||
<li key={opt.value} role="option" aria-selected={isSelected}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(opt.value);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={`flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm transition-colors duration-100 ${
|
||||
isSelected
|
||||
? 'bg-blue-400/15 text-blue-200'
|
||||
: 'text-gray-300 hover:bg-white/[0.06] hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
<span>{opt.label}</span>
|
||||
{isSelected && (
|
||||
<svg className="h-3.5 w-3.5 text-blue-300" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.7 5.3a1 1 0 010 1.4l-7.5 7.5a1 1 0 01-1.4 0L3.3 9.7a1 1 0 011.4-1.4l3.3 3.3 6.8-6.8a1 1 0 011.4 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InputHTMLAttributes, ReactNode, SelectHTMLAttributes } from 'react';
|
||||
import type { InputHTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
interface FieldProps {
|
||||
label: string;
|
||||
@@ -21,11 +21,3 @@ export function Field({ label, htmlFor, children }: FieldProps) {
|
||||
export function Input({ className = '', ...rest }: InputHTMLAttributes<HTMLInputElement>) {
|
||||
return <input className={`input-glass px-3 py-1.5 text-sm ${className}`} {...rest} />;
|
||||
}
|
||||
|
||||
export function Select({ className = '', children, ...rest }: SelectHTMLAttributes<HTMLSelectElement>) {
|
||||
return (
|
||||
<select className={`input-glass px-3 py-1.5 text-sm [&>option]:bg-[#151911] ${className}`} {...rest}>
|
||||
{children}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -165,10 +165,14 @@ export interface ActivationConfig {
|
||||
export interface SentimentProviderConfig {
|
||||
provider: string;
|
||||
model: string;
|
||||
base_url: string;
|
||||
api_key_configured: boolean;
|
||||
api_key_source: 'database' | 'environment' | 'none';
|
||||
web_search: boolean;
|
||||
valid_providers: string[];
|
||||
default_models: Record<string, string>;
|
||||
web_search_providers: string[];
|
||||
custom_base_url_providers: string[];
|
||||
}
|
||||
|
||||
export interface SentimentTestResult {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { RankingsTable } from '../components/rankings/RankingsTable';
|
||||
import { WeightsForm } from '../components/rankings/WeightsForm';
|
||||
import { Callout } from '../components/ui/Callout';
|
||||
import { Disclosure } from '../components/ui/Disclosure';
|
||||
import { Select } from '../components/ui/Field';
|
||||
import { Dropdown } from '../components/ui/Dropdown';
|
||||
import { PageHeader } from '../components/ui/PageHeader';
|
||||
import { SkeletonTable } from '../components/ui/Skeleton';
|
||||
import { Tabs } from '../components/ui/Tabs';
|
||||
@@ -72,16 +72,17 @@ function WatchlistPanel() {
|
||||
<AddTickerForm />
|
||||
<label className="flex items-center gap-2 text-xs text-gray-400">
|
||||
<span>Sort by</span>
|
||||
<Select
|
||||
<Dropdown
|
||||
value={sortMode}
|
||||
onChange={(event) => setSortMode(event.target.value as SortMode)}
|
||||
className="!py-1 !text-xs"
|
||||
>
|
||||
<option value="score_desc">Score (high → low)</option>
|
||||
<option value="score_asc">Score (low → high)</option>
|
||||
<option value="name_asc">Name (A → Z)</option>
|
||||
<option value="name_desc">Name (Z → A)</option>
|
||||
</Select>
|
||||
onChange={(v) => setSortMode(v as SortMode)}
|
||||
className="w-44"
|
||||
options={[
|
||||
{ value: 'score_desc', label: 'Score (high → low)' },
|
||||
{ value: 'score_asc', label: 'Score (low → high)' },
|
||||
{ value: 'name_asc', label: 'Name (A → Z)' },
|
||||
{ value: 'name_desc', label: 'Name (Z → A)' },
|
||||
]}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/activation.ts","./src/api/admin.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/fundamentals.ts","./src/api/health.ts","./src/api/indicators.ts","./src/api/ingestion.ts","./src/api/ohlcv.ts","./src/api/performance.ts","./src/api/scores.ts","./src/api/sentiment.ts","./src/api/sr-levels.ts","./src/api/tickers.ts","./src/api/trades.ts","./src/api/watchlist.ts","./src/components/admin/activationsettings.tsx","./src/components/admin/datacleanup.tsx","./src/components/admin/jobcontrols.tsx","./src/components/admin/pipelinereadinesspanel.tsx","./src/components/admin/recommendationsettings.tsx","./src/components/admin/sentimentprovidersettings.tsx","./src/components/admin/settingsform.tsx","./src/components/admin/tickermanagement.tsx","./src/components/admin/tickeruniversebootstrap.tsx","./src/components/admin/usertable.tsx","./src/components/auth/protectedroute.tsx","./src/components/charts/candlestickchart.tsx","./src/components/layout/appshell.tsx","./src/components/layout/mobilenav.tsx","./src/components/layout/sidebar.tsx","./src/components/rankings/rankingstable.tsx","./src/components/rankings/weightsform.tsx","./src/components/scanner/tradetable.tsx","./src/components/signals/setupspanel.tsx","./src/components/signals/trackrecordpanel.tsx","./src/components/ticker/dimensionbreakdownpanel.tsx","./src/components/ticker/fundamentalspanel.tsx","./src/components/ticker/indicatorselector.tsx","./src/components/ticker/recommendationpanel.tsx","./src/components/ticker/sroverlay.tsx","./src/components/ticker/sentimentpanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/callout.tsx","./src/components/ui/confirmdialog.tsx","./src/components/ui/disclosure.tsx","./src/components/ui/field.tsx","./src/components/ui/pageheader.tsx","./src/components/ui/scorecard.tsx","./src/components/ui/section.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/watchlist/addtickerform.tsx","./src/components/watchlist/watchlisttable.tsx","./src/hooks/useactivation.ts","./src/hooks/useadmin.ts","./src/hooks/useauth.ts","./src/hooks/usefetchsymboldata.ts","./src/hooks/useperformance.ts","./src/hooks/usescores.ts","./src/hooks/usetickerdetail.ts","./src/hooks/usetickers.ts","./src/hooks/usetrades.ts","./src/hooks/usewatchlist.ts","./src/lib/format.ts","./src/lib/ingestionstatus.ts","./src/lib/qualification.ts","./src/lib/recommendation.ts","./src/lib/types.ts","./src/pages/adminpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/loginpage.tsx","./src/pages/marketpage.tsx","./src/pages/registerpage.tsx","./src/pages/signalspage.tsx","./src/pages/tickerdetailpage.tsx","./src/stores/authstore.ts"],"version":"5.6.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/activation.ts","./src/api/admin.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/fundamentals.ts","./src/api/health.ts","./src/api/indicators.ts","./src/api/ingestion.ts","./src/api/ohlcv.ts","./src/api/performance.ts","./src/api/scores.ts","./src/api/sentiment.ts","./src/api/sr-levels.ts","./src/api/tickers.ts","./src/api/trades.ts","./src/api/watchlist.ts","./src/components/admin/activationsettings.tsx","./src/components/admin/datacleanup.tsx","./src/components/admin/jobcontrols.tsx","./src/components/admin/pipelinereadinesspanel.tsx","./src/components/admin/recommendationsettings.tsx","./src/components/admin/sentimentprovidersettings.tsx","./src/components/admin/settingsform.tsx","./src/components/admin/tickermanagement.tsx","./src/components/admin/tickeruniversebootstrap.tsx","./src/components/admin/usertable.tsx","./src/components/auth/protectedroute.tsx","./src/components/charts/candlestickchart.tsx","./src/components/layout/appshell.tsx","./src/components/layout/mobilenav.tsx","./src/components/layout/sidebar.tsx","./src/components/rankings/rankingstable.tsx","./src/components/rankings/weightsform.tsx","./src/components/scanner/tradetable.tsx","./src/components/signals/setupspanel.tsx","./src/components/signals/trackrecordpanel.tsx","./src/components/ticker/dimensionbreakdownpanel.tsx","./src/components/ticker/fundamentalspanel.tsx","./src/components/ticker/indicatorselector.tsx","./src/components/ticker/recommendationpanel.tsx","./src/components/ticker/sroverlay.tsx","./src/components/ticker/sentimentpanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/callout.tsx","./src/components/ui/confirmdialog.tsx","./src/components/ui/disclosure.tsx","./src/components/ui/dropdown.tsx","./src/components/ui/field.tsx","./src/components/ui/pageheader.tsx","./src/components/ui/scorecard.tsx","./src/components/ui/section.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/watchlist/addtickerform.tsx","./src/components/watchlist/watchlisttable.tsx","./src/hooks/useactivation.ts","./src/hooks/useadmin.ts","./src/hooks/useauth.ts","./src/hooks/usefetchsymboldata.ts","./src/hooks/useperformance.ts","./src/hooks/usescores.ts","./src/hooks/usetickerdetail.ts","./src/hooks/usetickers.ts","./src/hooks/usetrades.ts","./src/hooks/usewatchlist.ts","./src/lib/format.ts","./src/lib/ingestionstatus.ts","./src/lib/qualification.ts","./src/lib/recommendation.ts","./src/lib/types.ts","./src/pages/adminpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/loginpage.tsx","./src/pages/marketpage.tsx","./src/pages/registerpage.tsx","./src/pages/signalspage.tsx","./src/pages/tickerdetailpage.tsx","./src/stores/authstore.ts"],"version":"5.6.3"}
|
||||
Reference in New Issue
Block a user