Add DeepSeek/xAI/OpenAI-compatible sentiment providers; custom dark dropdown
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 32s
Deploy / deploy (push) Successful in 22s

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:
2026-06-13 12:42:04 +02:00
co-authored by Claude Fable 5
parent d53ed972d1
commit 126c3b3c17
16 changed files with 521 additions and 98 deletions
+23 -18
View File
@@ -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}>