goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
import React, { useState, useMemo } from 'react';
import { useDraggablePanel } from '../hooks/useDraggablePanel';
import { INDICATOR_REGISTRY, type IndicatorDef, type IndicatorCategory } from '../utils/indicatorRegistry';
import type { IndicatorConfig } from '../types';
import { getNumericParamSpec } from '../utils/indicatorParamSpec';
import NumericParamInput from './NumericParamInput';
interface IndicatorModalProps {
activeIndicators: IndicatorConfig[];
onAdd: (def: IndicatorDef, params: Record<string, number | string | boolean>) => void;
onRemove: (id: string) => void;
onClose: () => void;
}
const CATEGORIES: IndicatorCategory[] = [
'Moving Averages', 'Channels & Bands', 'Oscillators', 'Momentum',
'Trend', 'Volatility', 'Volume', 'Candlestick Patterns',
];
const IndicatorModal: React.FC<IndicatorModalProps> = ({ activeIndicators, onAdd, onRemove, onClose }) => {
const [search, setSearch] = useState('');
const [category, setCategory] = useState<IndicatorCategory | 'All'>('All');
const [selected, setSelected] = useState<IndicatorDef | null>(null);
const [params, setParams] = useState<Record<string, number | string | boolean>>({});
const filtered = useMemo(() => {
return INDICATOR_REGISTRY.filter(d => {
const matchCat = category === 'All' || d.category === category;
const matchQ = !search || d.name.toLowerCase().includes(search.toLowerCase()) || d.shortName.toLowerCase().includes(search.toLowerCase());
return matchCat && matchQ;
});
}, [search, category]);
const select = (def: IndicatorDef) => {
setSelected(def);
setParams({ ...def.defaultParams });
};
const isActive = (type: string) => activeIndicators.some(a => a.type === type);
const handleAdd = () => {
if (!selected) return;
onAdd(selected, params);
setSelected(null);
};
const handleRemoveAll = (type: string) => {
activeIndicators.filter(a => a.type === type).forEach(a => onRemove(a.id));
};
const {
panelRef,
dragging,
onHeaderPointerDown, headerTouchStyle,
panelStyle,
headerCursor,
} = useDraggablePanel({ centerOnMount: true });
return (
<div className="modal-overlay" onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}>
<div
ref={panelRef}
className="indicator-modal"
style={{
...panelStyle,
zIndex: 5001,
cursor: dragging ? 'grabbing' : undefined,
}}
onMouseDown={e => e.stopPropagation()}
>
<div
className="gc-popup-header modal-header"
onPointerDown={onHeaderPointerDown}
style={{ cursor: headerCursor, ...headerTouchStyle }}
>
<span className="gc-popup-title modal-title">📈 </span>
<span className="modal-subtitle">{INDICATOR_REGISTRY.length} </span>
<button type="button" className="gc-popup-close modal-close" onClick={onClose}></button>
</div>
<div className="modal-search">
<input
className="modal-search-input"
placeholder="지표 검색..."
value={search}
onChange={e => setSearch(e.target.value)}
autoFocus
/>
</div>
<div className="modal-cats">
{(['All', ...CATEGORIES] as Array<'All' | IndicatorCategory>).map(c => (
<button
key={c}
className={`cat-btn ${category === c ? 'active' : ''}`}
onClick={() => setCategory(c)}
>{c === 'All' ? `전체 (${INDICATOR_REGISTRY.length})` : c} {c !== 'All' && `(${INDICATOR_REGISTRY.filter(d => d.category === c).length})`}</button>
))}
</div>
<div className="modal-body">
<div className="indicator-list">
{filtered.map(def => (
<div
key={def.type}
className={`indicator-item ${selected?.type === def.type ? 'selected' : ''} ${isActive(def.type) ? 'in-use' : ''}`}
onClick={() => select(def)}
>
<div className="indicator-item-left">
<span className="indicator-name">{def.name}</span>
<span className="indicator-desc">{def.description}</span>
</div>
<div className="indicator-item-right">
{def.overlay && <span className="badge overlay"></span>}
{def.returnsMarkers && <span className="badge markers"></span>}
{isActive(def.type) ? (
<button className="rm-btn" onClick={e => { e.stopPropagation(); handleRemoveAll(def.type); }}></button>
) : (
<button className="add-btn" onClick={e => { e.stopPropagation(); select(def); onAdd(def, { ...def.defaultParams }); }}>+</button>
)}
</div>
</div>
))}
{filtered.length === 0 && (
<div className="no-results">'{search}' </div>
)}
</div>
{selected && (
<div className="indicator-settings">
<div className="settings-title">{selected.name} </div>
<div className="settings-desc">{selected.description}</div>
{Object.entries(params).map(([key, val]) => (
<div key={key} className="param-row">
<label className="param-label">{key}</label>
{typeof val === 'boolean' ? (
<input type="checkbox" checked={val as boolean} onChange={e => setParams(p => ({ ...p, [key]: e.target.checked }))} />
) : typeof val === 'number' ? (
<NumericParamInput
className="param-input"
value={val as number}
spec={getNumericParamSpec(selected.type, key, val as number)}
onChange={v => setParams(p => ({ ...p, [key]: v }))}
/>
) : (
<input
className="param-input"
type="text"
value={val as string}
onChange={e => setParams(p => ({ ...p, [key]: e.target.value }))}
/>
)}
</div>
))}
<div className="settings-actions">
<button className="settings-add-btn" onClick={handleAdd}> </button>
<button className="settings-cancel" onClick={() => setSelected(null)}></button>
</div>
{selected.plots.length > 0 && (
<div className="plot-preview">
{selected.plots.map(p => (
<div key={p.id} className="plot-legend-row">
<span className="plot-color" style={{ background: p.color }} />
<span className="plot-title">{p.title}</span>
</div>
))}
</div>
)}
</div>
)}
</div>
{activeIndicators.length > 0 && (
<div className="modal-active">
<div className="active-title"> ({activeIndicators.length})</div>
<div className="active-list">
{activeIndicators.map(a => (
<span key={a.id} className="active-tag">
{a.type}
<button onClick={() => onRemove(a.id)}></button>
</span>
))}
</div>
</div>
)}
</div>
</div>
);
};
export default IndicatorModal;