ranranrunforit commited on
Commit
c341e23
·
verified ·
1 Parent(s): 16520a2

Upload 25 files

Browse files
_ds_bundle.js ADDED
@@ -0,0 +1,1535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* @ds-bundle: {"format":3,"namespace":"ChanCompassSpectrum2DesignSystem_b52b37","components":[{"name":"Button","sourcePath":"components/actions/Button.jsx"},{"name":"Card","sourcePath":"components/containers/Card.jsx"},{"name":"InlineAlert","sourcePath":"components/containers/InlineAlert.jsx"},{"name":"Checkbox","sourcePath":"components/forms/Checkbox.jsx"},{"name":"Field","sourcePath":"components/forms/Field.jsx"},{"name":"Switch","sourcePath":"components/forms/Switch.jsx"},{"name":"Tabs","sourcePath":"components/navigation/Tabs.jsx"},{"name":"Badge","sourcePath":"components/status/Badge.jsx"},{"name":"SignalBadge","sourcePath":"components/status/SignalBadge.jsx"},{"name":"StatusLight","sourcePath":"components/status/StatusLight.jsx"}],"sourceHashes":{"components/actions/Button.jsx":"c9f7501c202c","components/containers/Card.jsx":"9316d0c88d32","components/containers/InlineAlert.jsx":"5ceb8bdaac5f","components/forms/Checkbox.jsx":"59c34e791a76","components/forms/Field.jsx":"39157a609b42","components/forms/Switch.jsx":"38f4684e0ef2","components/navigation/Tabs.jsx":"5488bca491fa","components/status/Badge.jsx":"3e62358e3a0b","components/status/SignalBadge.jsx":"df7989e74422","components/status/StatusLight.jsx":"f7d206a1642b","ui_kits/chan-compass/app.jsx":"4b80024bd468","ui_kits/chan-compass/data.js":"5821344b625c","ui_kits/chan-compass/views.jsx":"c9ec2aee076f"},"inlinedExternals":[],"unexposedExports":[]} */
2
+
3
+ (() => {
4
+
5
+ const __ds_ns = (window.ChanCompassSpectrum2DesignSystem_b52b37 = window.ChanCompassSpectrum2DesignSystem_b52b37 || {});
6
+
7
+ const __ds_scope = {};
8
+
9
+ (__ds_ns.__errors = __ds_ns.__errors || []);
10
+
11
+ // components/actions/Button.jsx
12
+ try { (() => {
13
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
14
+ /* Inject component CSS once. Spectrum 2 buttons: fully-rounded "pill" by
15
+ default, medium height 32px, 600 weight, soft state transitions. */
16
+ const CSS = `
17
+ .s2-btn{
18
+ --_bg: var(--accent); --_bgh: var(--accent-hover); --_bgd: var(--accent-down);
19
+ --_fg: var(--text-on-accent); --_bd: transparent;
20
+ display:inline-flex; align-items:center; justify-content:center; gap:var(--space-100);
21
+ font:var(--font-weight-semibold) var(--font-size-100)/1 var(--font-sans);
22
+ height:var(--control-height-md); padding:0 var(--space-250);
23
+ border-radius:var(--radius-full); border:var(--border-width-200) solid var(--_bd);
24
+ background:var(--_bg); color:var(--_fg); cursor:pointer; white-space:nowrap;
25
+ transition:var(--transition-colors), transform var(--duration-fast) var(--ease-out);
26
+ user-select:none;
27
+ }
28
+ .s2-btn:hover{ background:var(--_bgh); }
29
+ .s2-btn:active{ background:var(--_bgd); transform:scale(.98); }
30
+ .s2-btn:focus-visible{ outline:var(--focus-ring-width) solid var(--focus-ring); outline-offset:var(--focus-ring-offset); }
31
+ .s2-btn[disabled]{ background:var(--surface-subtle); color:var(--text-disabled); border-color:transparent; cursor:default; transform:none; }
32
+ .s2-btn--square{ border-radius:var(--radius-default); }
33
+ .s2-btn--sm{ height:var(--control-height-sm); padding:0 var(--space-200); font-size:var(--font-size-75); }
34
+ .s2-btn--lg{ height:var(--control-height-lg); padding:0 var(--space-300); font-size:var(--font-size-200); }
35
+ /* secondary — outline */
36
+ .s2-btn--secondary{ --_bg:var(--surface-card); --_fg:var(--text-body); --_bd:var(--border-strong); }
37
+ .s2-btn--secondary:hover{ --_bg:var(--surface-subtle); --_bd:var(--text-secondary); }
38
+ .s2-btn--secondary:active{ --_bg:var(--s2-gray-100); }
39
+ /* negative */
40
+ .s2-btn--negative{ --_bg:var(--negative); --_bgh:var(--negative-hover); --_bgd:var(--s2-red-1100); }
41
+ /* quiet — no chrome until hover */
42
+ .s2-btn--quiet{ --_bg:transparent; --_fg:var(--text-body); --_bd:transparent; }
43
+ .s2-btn--quiet:hover{ --_bg:var(--surface-subtle); }
44
+ .s2-btn--quiet:active{ --_bg:var(--s2-gray-100); }
45
+ `;
46
+ let injected = false;
47
+ function useCSS() {
48
+ if (typeof document !== 'undefined' && !injected) {
49
+ const el = document.createElement('style');
50
+ el.textContent = CSS;
51
+ el.dataset.s2 = 'button';
52
+ document.head.appendChild(el);
53
+ injected = true;
54
+ }
55
+ }
56
+ function Button({
57
+ variant = 'accent',
58
+ size = 'md',
59
+ shape = 'pill',
60
+ icon,
61
+ children,
62
+ className = '',
63
+ ...rest
64
+ }) {
65
+ useCSS();
66
+ const cls = ['s2-btn', variant !== 'accent' && `s2-btn--${variant}`, size !== 'md' && `s2-btn--${size}`, shape === 'square' && 's2-btn--square', className].filter(Boolean).join(' ');
67
+ return /*#__PURE__*/React.createElement("button", _extends({
68
+ className: cls
69
+ }, rest), icon, children);
70
+ }
71
+ Object.assign(__ds_scope, { Button });
72
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/actions/Button.jsx", error: String((e && e.message) || e) }); }
73
+
74
+ // components/containers/Card.jsx
75
+ try { (() => {
76
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
77
+ const CSS = `
78
+ .s2-card{
79
+ background:var(--surface-card); border:var(--border-width-100) solid var(--border-hairline);
80
+ border-radius:var(--card-radius); box-shadow:var(--shadow-raised);
81
+ overflow:clip;
82
+ }
83
+ .s2-card--elevated{ box-shadow:var(--shadow-elevated); }
84
+ .s2-card--flat{ box-shadow:none; }
85
+ .s2-card--quiet{ background:transparent; border:none; box-shadow:none; }
86
+ .s2-card__head{ display:flex; align-items:flex-start; justify-content:space-between; gap:var(--space-200); padding:var(--space-250) var(--space-300) 0; }
87
+ .s2-card__titles{ display:flex; flex-direction:column; gap:2px; }
88
+ .s2-card__title{ font:var(--type-h3); color:var(--text-heading); }
89
+ .s2-card__subtitle{ font:var(--type-detail); color:var(--text-muted); }
90
+ .s2-card__body{ padding:var(--space-250) var(--space-300) var(--space-300); }
91
+ .s2-card__head + .s2-card__body{ padding-top:var(--space-200); }
92
+ `;
93
+ let injected = false;
94
+ function useCSS() {
95
+ if (typeof document !== 'undefined' && !injected) {
96
+ const e = document.createElement('style');
97
+ e.textContent = CSS;
98
+ e.dataset.s2 = 'card';
99
+ document.head.appendChild(e);
100
+ injected = true;
101
+ }
102
+ }
103
+ function Card({
104
+ title,
105
+ subtitle,
106
+ action,
107
+ elevation = 'raised',
108
+ children,
109
+ className = '',
110
+ ...rest
111
+ }) {
112
+ useCSS();
113
+ const cls = ['s2-card', elevation !== 'raised' && `s2-card--${elevation}`, className].filter(Boolean).join(' ');
114
+ const hasHead = title || subtitle || action;
115
+ return /*#__PURE__*/React.createElement("section", _extends({
116
+ className: cls
117
+ }, rest), hasHead && /*#__PURE__*/React.createElement("header", {
118
+ className: "s2-card__head"
119
+ }, /*#__PURE__*/React.createElement("div", {
120
+ className: "s2-card__titles"
121
+ }, title && /*#__PURE__*/React.createElement("span", {
122
+ className: "s2-card__title"
123
+ }, title), subtitle && /*#__PURE__*/React.createElement("span", {
124
+ className: "s2-card__subtitle"
125
+ }, subtitle)), action), /*#__PURE__*/React.createElement("div", {
126
+ className: "s2-card__body"
127
+ }, children));
128
+ }
129
+ Object.assign(__ds_scope, { Card });
130
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/containers/Card.jsx", error: String((e && e.message) || e) }); }
131
+
132
+ // components/containers/InlineAlert.jsx
133
+ try { (() => {
134
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
135
+ /* Spectrum 2 in-line message. A framed notice tied to a region of the page.
136
+ `ai` variant is the signature accent-framed panel used for local-LLM output. */
137
+ const CSS = `
138
+ .s2-alert{
139
+ display:flex; gap:var(--space-150); padding:var(--space-200) var(--space-250);
140
+ border-radius:var(--radius-lg); border:var(--border-width-100) solid var(--border-default);
141
+ background:var(--surface-card); font:var(--type-body); color:var(--text-body);
142
+ }
143
+ .s2-alert__icon{ flex:none; width:18px; height:18px; display:grid; place-items:center; margin-top:1px; }
144
+ .s2-alert__body{ display:flex; flex-direction:column; gap:2px; min-width:0; }
145
+ .s2-alert__title{ font:var(--type-ui-bold); color:var(--text-heading); }
146
+ .s2-alert--informative{ border-color:var(--accent); background:var(--informative-subtle); }
147
+ .s2-alert--informative .s2-alert__icon{ color:var(--accent); }
148
+ .s2-alert--positive{ border-color:var(--positive); background:var(--positive-subtle); }
149
+ .s2-alert--positive .s2-alert__icon{ color:var(--positive); }
150
+ .s2-alert--negative{ border-color:var(--negative); background:var(--negative-subtle); }
151
+ .s2-alert--negative .s2-alert__icon{ color:var(--negative); }
152
+ .s2-alert--notice{ border-color:var(--notice); background:var(--notice-subtle); }
153
+ .s2-alert--notice .s2-alert__icon{ color:var(--notice); }
154
+ .s2-alert--ai{
155
+ border:var(--border-width-200) solid var(--accent); border-inline-start-width:6px;
156
+ border-radius:var(--radius-lg); background:var(--surface-card); box-shadow:var(--shadow-accent);
157
+ }
158
+ .s2-alert--ai .s2-alert__title{ color:var(--accent-text); }
159
+ `;
160
+ let injected = false;
161
+ function useCSS() {
162
+ if (typeof document !== 'undefined' && !injected) {
163
+ const e = document.createElement('style');
164
+ e.textContent = CSS;
165
+ e.dataset.s2 = 'alert';
166
+ document.head.appendChild(e);
167
+ injected = true;
168
+ }
169
+ }
170
+ const ICONS = {
171
+ informative: 'M9 1a8 8 0 100 16A8 8 0 009 1zm0 3.2a1.1 1.1 0 110 2.2 1.1 1.1 0 010-2.2zM10.2 14H7.8v-.9l.9-.2V9.1l-.9-.2V8h2.4v4.9l.9.2v.9z',
172
+ positive: 'M9 1a8 8 0 100 16A8 8 0 009 1zm4.2 5.4l-5 5.2a.7.7 0 01-1 0L4.8 9a.7.7 0 011-1l1.9 1.9 4.5-4.6a.7.7 0 011 1z',
173
+ negative: 'M9 1a8 8 0 100 16A8 8 0 009 1zm1.1 11.8H7.9v-2.2h2.2v2.2zm0-3.6H7.9V4.2h2.2v5z',
174
+ notice: 'M9.9 1.6l7 12.1a1 1 0 01-.9 1.5H2a1 1 0 01-.9-1.5l7-12.1a1 1 0 011.8 0zM7.9 6.2v3.8h2.2V6.2H7.9zm0 5.2v2h2.2v-2H7.9z',
175
+ ai: 'M9 1l1.8 4.3L15 7l-4.2 1.7L9 13 7.2 8.7 3 7l4.2-1.7L9 1z'
176
+ };
177
+ function InlineAlert({
178
+ variant = 'informative',
179
+ title,
180
+ icon,
181
+ children,
182
+ className = '',
183
+ ...rest
184
+ }) {
185
+ useCSS();
186
+ const path = ICONS[variant] || ICONS.informative;
187
+ return /*#__PURE__*/React.createElement("div", _extends({
188
+ className: ['s2-alert', `s2-alert--${variant}`, className].filter(Boolean).join(' ')
189
+ }, rest), /*#__PURE__*/React.createElement("span", {
190
+ className: "s2-alert__icon"
191
+ }, icon || /*#__PURE__*/React.createElement("svg", {
192
+ width: "18",
193
+ height: "18",
194
+ viewBox: "0 0 18 18",
195
+ fill: "currentColor"
196
+ }, /*#__PURE__*/React.createElement("path", {
197
+ d: path
198
+ }))), /*#__PURE__*/React.createElement("div", {
199
+ className: "s2-alert__body"
200
+ }, title && /*#__PURE__*/React.createElement("span", {
201
+ className: "s2-alert__title"
202
+ }, title), children && /*#__PURE__*/React.createElement("div", null, children)));
203
+ }
204
+ Object.assign(__ds_scope, { InlineAlert });
205
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/containers/InlineAlert.jsx", error: String((e && e.message) || e) }); }
206
+
207
+ // components/forms/Checkbox.jsx
208
+ try { (() => {
209
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
210
+ const CSS = `
211
+ .s2-check{ display:inline-flex; align-items:center; gap:var(--space-100); cursor:pointer; font:var(--type-ui); color:var(--text-body); user-select:none; }
212
+ .s2-check input{ position:absolute; opacity:0; width:0; height:0; }
213
+ .s2-check__box{
214
+ width:16px; height:16px; flex:none; border-radius:var(--radius-sm);
215
+ border:var(--border-width-200) solid var(--border-field); background:var(--surface-card);
216
+ display:grid; place-items:center; transition:var(--transition-colors);
217
+ }
218
+ .s2-check:hover .s2-check__box{ border-color:var(--text-secondary); }
219
+ .s2-check__box svg{ width:11px; height:11px; opacity:0; color:#fff; }
220
+ .s2-check input:checked + .s2-check__box{ background:var(--accent); border-color:var(--accent); }
221
+ .s2-check input:checked + .s2-check__box svg{ opacity:1; }
222
+ .s2-check input:focus-visible + .s2-check__box{ outline:var(--focus-ring-width) solid var(--focus-ring); outline-offset:var(--focus-ring-offset); }
223
+ .s2-check input:disabled + .s2-check__box{ background:var(--surface-subtle); border-color:var(--border-default); }
224
+ `;
225
+ let injected = false;
226
+ function useCSS() {
227
+ if (typeof document !== 'undefined' && !injected) {
228
+ const e = document.createElement('style');
229
+ e.textContent = CSS;
230
+ e.dataset.s2 = 'checkbox';
231
+ document.head.appendChild(e);
232
+ injected = true;
233
+ }
234
+ }
235
+ function Checkbox({
236
+ checked,
237
+ onChange,
238
+ children,
239
+ disabled = false,
240
+ ...rest
241
+ }) {
242
+ useCSS();
243
+ return /*#__PURE__*/React.createElement("label", {
244
+ className: "s2-check"
245
+ }, /*#__PURE__*/React.createElement("input", _extends({
246
+ type: "checkbox",
247
+ checked: checked,
248
+ onChange: onChange,
249
+ disabled: disabled
250
+ }, rest)), /*#__PURE__*/React.createElement("span", {
251
+ className: "s2-check__box"
252
+ }, /*#__PURE__*/React.createElement("svg", {
253
+ viewBox: "0 0 12 12",
254
+ fill: "none"
255
+ }, /*#__PURE__*/React.createElement("path", {
256
+ d: "M1 6l3.2 3.5L11 2.5",
257
+ stroke: "currentColor",
258
+ strokeWidth: "2",
259
+ strokeLinecap: "round",
260
+ strokeLinejoin: "round"
261
+ }))), children && /*#__PURE__*/React.createElement("span", null, children));
262
+ }
263
+ Object.assign(__ds_scope, { Checkbox });
264
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/forms/Checkbox.jsx", error: String((e && e.message) || e) }); }
265
+
266
+ // components/forms/Field.jsx
267
+ try { (() => {
268
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
269
+ const CSS = `
270
+ .s2-field{ display:flex; flex-direction:column; gap:var(--space-75); }
271
+ .s2-field__label{ font:var(--type-ui-bold); color:var(--text-heading); }
272
+ .s2-field__req{ color:var(--negative); margin-inline-start:2px; }
273
+ .s2-field__input{
274
+ height:var(--control-height-md); padding:0 var(--space-150);
275
+ font:var(--type-ui); color:var(--text-body);
276
+ background:var(--surface-card);
277
+ border:var(--border-width-200) solid var(--border-field);
278
+ border-radius:var(--field-radius); outline:none;
279
+ transition:var(--transition-colors), box-shadow var(--duration-fast) var(--ease-out);
280
+ }
281
+ .s2-field__input::placeholder{ color:var(--text-muted); }
282
+ .s2-field__input:hover{ border-color:var(--text-secondary); }
283
+ .s2-field__input:focus{ border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
284
+ .s2-field--textarea .s2-field__input{ height:auto; padding:var(--space-150); resize:vertical; line-height:var(--line-height-body); font-family:var(--font-sans); }
285
+ .s2-field--mono .s2-field__input{ font-family:var(--font-code); font-size:var(--font-size-75); }
286
+ .s2-field--invalid .s2-field__input{ border-color:var(--negative); }
287
+ .s2-field--invalid .s2-field__input:focus{ box-shadow:0 0 0 1px var(--negative); }
288
+ .s2-field__help{ font:var(--type-detail); color:var(--text-muted); }
289
+ .s2-field--invalid .s2-field__help{ color:var(--negative); }
290
+ .s2-field__input:disabled{ background:var(--surface-subtle); color:var(--text-disabled); border-color:var(--border-default); cursor:default; }
291
+ `;
292
+ let injected = false;
293
+ function useCSS() {
294
+ if (typeof document !== 'undefined' && !injected) {
295
+ const el = document.createElement('style');
296
+ el.textContent = CSS;
297
+ el.dataset.s2 = 'field';
298
+ document.head.appendChild(el);
299
+ injected = true;
300
+ }
301
+ }
302
+ function Field({
303
+ label,
304
+ value,
305
+ onChange,
306
+ placeholder,
307
+ type = 'text',
308
+ multiline = false,
309
+ rows = 4,
310
+ mono = false,
311
+ required = false,
312
+ invalid = false,
313
+ help,
314
+ disabled = false,
315
+ className = '',
316
+ ...rest
317
+ }) {
318
+ useCSS();
319
+ const cls = ['s2-field', multiline && 's2-field--textarea', mono && 's2-field--mono', invalid && 's2-field--invalid', className].filter(Boolean).join(' ');
320
+ const inputProps = {
321
+ className: 's2-field__input',
322
+ value,
323
+ onChange,
324
+ placeholder,
325
+ disabled,
326
+ ...rest
327
+ };
328
+ return /*#__PURE__*/React.createElement("label", {
329
+ className: cls
330
+ }, label && /*#__PURE__*/React.createElement("span", {
331
+ className: "s2-field__label"
332
+ }, label, required && /*#__PURE__*/React.createElement("span", {
333
+ className: "s2-field__req"
334
+ }, "*")), multiline ? /*#__PURE__*/React.createElement("textarea", _extends({
335
+ rows: rows
336
+ }, inputProps)) : /*#__PURE__*/React.createElement("input", _extends({
337
+ type: type
338
+ }, inputProps)), help && /*#__PURE__*/React.createElement("span", {
339
+ className: "s2-field__help"
340
+ }, help));
341
+ }
342
+ Object.assign(__ds_scope, { Field });
343
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/forms/Field.jsx", error: String((e && e.message) || e) }); }
344
+
345
+ // components/forms/Switch.jsx
346
+ try { (() => {
347
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
348
+ const CSS = `
349
+ .s2-switch{ display:inline-flex; align-items:center; gap:var(--space-150); cursor:pointer; font:var(--type-ui); color:var(--text-body); user-select:none; }
350
+ .s2-switch input{ position:absolute; opacity:0; width:0; height:0; }
351
+ .s2-switch__track{
352
+ width:32px; height:18px; flex:none; border-radius:var(--radius-full);
353
+ background:var(--s2-gray-300); position:relative; transition:background var(--duration-fast) var(--ease-out);
354
+ }
355
+ .s2-switch__track::after{
356
+ content:''; position:absolute; top:2px; left:2px; width:14px; height:14px;
357
+ border-radius:var(--radius-full); background:#fff; box-shadow:var(--shadow-raised);
358
+ transition:transform var(--duration-default) var(--ease-out);
359
+ }
360
+ .s2-switch:hover .s2-switch__track{ background:var(--s2-gray-400); }
361
+ .s2-switch input:checked + .s2-switch__track{ background:var(--accent); }
362
+ .s2-switch input:checked + .s2-switch__track::after{ transform:translateX(14px); }
363
+ .s2-switch input:focus-visible + .s2-switch__track{ outline:var(--focus-ring-width) solid var(--focus-ring); outline-offset:var(--focus-ring-offset); }
364
+ .s2-switch input:disabled + .s2-switch__track{ background:var(--border-default); }
365
+ `;
366
+ let injected = false;
367
+ function useCSS() {
368
+ if (typeof document !== 'undefined' && !injected) {
369
+ const e = document.createElement('style');
370
+ e.textContent = CSS;
371
+ e.dataset.s2 = 'switch';
372
+ document.head.appendChild(e);
373
+ injected = true;
374
+ }
375
+ }
376
+ function Switch({
377
+ checked,
378
+ onChange,
379
+ children,
380
+ disabled = false,
381
+ ...rest
382
+ }) {
383
+ useCSS();
384
+ return /*#__PURE__*/React.createElement("label", {
385
+ className: "s2-switch"
386
+ }, /*#__PURE__*/React.createElement("input", _extends({
387
+ type: "checkbox",
388
+ role: "switch",
389
+ checked: checked,
390
+ onChange: onChange,
391
+ disabled: disabled
392
+ }, rest)), /*#__PURE__*/React.createElement("span", {
393
+ className: "s2-switch__track"
394
+ }), children && /*#__PURE__*/React.createElement("span", null, children));
395
+ }
396
+ Object.assign(__ds_scope, { Switch });
397
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/forms/Switch.jsx", error: String((e && e.message) || e) }); }
398
+
399
+ // components/navigation/Tabs.jsx
400
+ try { (() => {
401
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
402
+ /* Spectrum 2 quiet tabs with an accent underline — the primary navigation in
403
+ Chan Compass (Signals / Rotation / News / Research / Automation / Model). */
404
+ const CSS = `
405
+ .s2-tabs{ display:flex; gap:var(--space-300); border-bottom:var(--border-width-200) solid var(--border-default); }
406
+ .s2-tab{
407
+ appearance:none; background:none; border:none; cursor:pointer;
408
+ padding:var(--space-150) 2px; margin-bottom:-2px;
409
+ font:var(--font-weight-semibold) var(--font-size-200)/1.2 var(--font-sans);
410
+ color:var(--text-muted); border-bottom:var(--border-width-200) solid transparent;
411
+ display:inline-flex; align-items:center; gap:var(--space-100);
412
+ transition:var(--transition-colors);
413
+ }
414
+ .s2-tab:hover{ color:var(--text-body); }
415
+ .s2-tab--selected{ color:var(--accent); border-bottom-color:var(--accent); }
416
+ .s2-tab:focus-visible{ outline:var(--focus-ring-width) solid var(--focus-ring); outline-offset:2px; border-radius:var(--radius-sm); }
417
+ .s2-tab__count{
418
+ font:var(--font-weight-semibold) var(--font-size-50)/1 var(--font-sans);
419
+ background:var(--surface-subtle); color:var(--text-secondary);
420
+ padding:2px 6px; border-radius:var(--radius-full);
421
+ }
422
+ .s2-tab--selected .s2-tab__count{ background:var(--accent-subtle); color:var(--accent-text); }
423
+ `;
424
+ let injected = false;
425
+ function useCSS() {
426
+ if (typeof document !== 'undefined' && !injected) {
427
+ const e = document.createElement('style');
428
+ e.textContent = CSS;
429
+ e.dataset.s2 = 'tabs';
430
+ document.head.appendChild(e);
431
+ injected = true;
432
+ }
433
+ }
434
+ function Tabs({
435
+ items = [],
436
+ value,
437
+ onChange,
438
+ className = '',
439
+ ...rest
440
+ }) {
441
+ useCSS();
442
+ return /*#__PURE__*/React.createElement("div", _extends({
443
+ className: ['s2-tabs', className].filter(Boolean).join(' '),
444
+ role: "tablist"
445
+ }, rest), items.map(it => {
446
+ const id = typeof it === 'string' ? it : it.id;
447
+ const label = typeof it === 'string' ? it : it.label;
448
+ const selected = id === value;
449
+ return /*#__PURE__*/React.createElement("button", {
450
+ key: id,
451
+ role: "tab",
452
+ "aria-selected": selected,
453
+ className: ['s2-tab', selected && 's2-tab--selected'].filter(Boolean).join(' '),
454
+ onClick: () => onChange && onChange(id)
455
+ }, typeof it === 'object' && it.icon, label, typeof it === 'object' && it.count != null && /*#__PURE__*/React.createElement("span", {
456
+ className: "s2-tab__count"
457
+ }, it.count));
458
+ }));
459
+ }
460
+ Object.assign(__ds_scope, { Tabs });
461
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/navigation/Tabs.jsx", error: String((e && e.message) || e) }); }
462
+
463
+ // components/status/Badge.jsx
464
+ try { (() => {
465
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
466
+ const CSS = `
467
+ .s2-badge{
468
+ display:inline-flex; align-items:center; gap:var(--space-75);
469
+ height:20px; padding:0 var(--space-150); border-radius:var(--radius-full);
470
+ font:var(--font-weight-semibold) var(--font-size-75)/1 var(--font-sans);
471
+ white-space:nowrap;
472
+ }
473
+ .s2-badge--neutral{ background:var(--surface-subtle); color:var(--text-secondary); }
474
+ .s2-badge--accent{ background:var(--accent-subtle); color:var(--accent-text); }
475
+ .s2-badge--positive{ background:var(--positive-subtle); color:var(--positive-hover); }
476
+ .s2-badge--negative{ background:var(--negative-subtle); color:var(--negative-hover); }
477
+ .s2-badge--notice{ background:var(--notice-subtle); color:var(--notice); }
478
+ .s2-badge--bold{ color:#fff; }
479
+ .s2-badge--bold.s2-badge--accent{ background:var(--accent); }
480
+ .s2-badge--bold.s2-badge--positive{ background:var(--positive); }
481
+ .s2-badge--bold.s2-badge--negative{ background:var(--negative); }
482
+ .s2-badge--bold.s2-badge--neutral{ background:var(--text-secondary); }
483
+ `;
484
+ let injected = false;
485
+ function useCSS() {
486
+ if (typeof document !== 'undefined' && !injected) {
487
+ const e = document.createElement('style');
488
+ e.textContent = CSS;
489
+ e.dataset.s2 = 'badge';
490
+ document.head.appendChild(e);
491
+ injected = true;
492
+ }
493
+ }
494
+ function Badge({
495
+ variant = 'neutral',
496
+ bold = false,
497
+ children,
498
+ className = '',
499
+ ...rest
500
+ }) {
501
+ useCSS();
502
+ const cls = ['s2-badge', `s2-badge--${variant}`, bold && 's2-badge--bold', className].filter(Boolean).join(' ');
503
+ return /*#__PURE__*/React.createElement("span", _extends({
504
+ className: cls
505
+ }, rest), children);
506
+ }
507
+ Object.assign(__ds_scope, { Badge });
508
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/status/Badge.jsx", error: String((e && e.message) || e) }); }
509
+
510
+ // components/status/SignalBadge.jsx
511
+ try { (() => {
512
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
513
+ /* Domain component: the BUY / SELL / HOLD / WAIT signal pill that drives the
514
+ Signals table. Maps a Chan-engine action to a Spectrum 2 semantic color. */
515
+ const CSS = `
516
+ .s2-signal{
517
+ display:inline-flex; align-items:center; gap:6px;
518
+ height:24px; padding:0 var(--space-150); border-radius:var(--radius-full);
519
+ font:var(--font-weight-bold) var(--font-size-75)/1 var(--font-sans);
520
+ letter-spacing:.04em; text-transform:uppercase; white-space:nowrap; color:#fff;
521
+ }
522
+ .s2-signal__glyph{ width:7px; height:7px; border-radius:var(--radius-full); background:currentColor; opacity:.9; }
523
+ .s2-signal--buy{ background:var(--positive); }
524
+ .s2-signal--sell{ background:var(--negative); }
525
+ .s2-signal--hold{ background:var(--accent); }
526
+ .s2-signal--wait{ background:var(--surface-subtle); color:var(--text-secondary); }
527
+ .s2-signal--watch{ background:var(--notice); }
528
+ `;
529
+ let injected = false;
530
+ function useCSS() {
531
+ if (typeof document !== 'undefined' && !injected) {
532
+ const e = document.createElement('style');
533
+ e.textContent = CSS;
534
+ e.dataset.s2 = 'signal';
535
+ document.head.appendChild(e);
536
+ injected = true;
537
+ }
538
+ }
539
+ const MAP = {
540
+ BUY: 'buy',
541
+ SELL: 'sell',
542
+ HOLD: 'hold',
543
+ WAIT: 'wait',
544
+ WATCH: 'watch'
545
+ };
546
+ function SignalBadge({
547
+ action = 'WAIT',
548
+ className = '',
549
+ ...rest
550
+ }) {
551
+ useCSS();
552
+ const key = String(action).toUpperCase();
553
+ const kind = MAP[key] || 'wait';
554
+ return /*#__PURE__*/React.createElement("span", _extends({
555
+ className: ['s2-signal', `s2-signal--${kind}`, className].filter(Boolean).join(' ')
556
+ }, rest), /*#__PURE__*/React.createElement("span", {
557
+ className: "s2-signal__glyph"
558
+ }), key);
559
+ }
560
+ Object.assign(__ds_scope, { SignalBadge });
561
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/status/SignalBadge.jsx", error: String((e && e.message) || e) }); }
562
+
563
+ // components/status/StatusLight.jsx
564
+ try { (() => {
565
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
566
+ const CSS = `
567
+ .s2-status{ display:inline-flex; align-items:center; gap:var(--space-100); font:var(--type-ui); color:var(--text-body); }
568
+ .s2-status__dot{ width:9px; height:9px; flex:none; border-radius:var(--radius-full); background:var(--flat); }
569
+ .s2-status--positive .s2-status__dot{ background:var(--positive); }
570
+ .s2-status--negative .s2-status__dot{ background:var(--negative); }
571
+ .s2-status--notice .s2-status__dot{ background:var(--notice); }
572
+ .s2-status--accent .s2-status__dot{ background:var(--accent); }
573
+ .s2-status--neutral .s2-status__dot{ background:var(--text-muted); }
574
+ .s2-status--live .s2-status__dot{ box-shadow:0 0 0 0 currentColor; animation:s2-pulse 1.8s var(--ease-out) infinite; color:var(--positive); }
575
+ @keyframes s2-pulse{ 0%{ box-shadow:0 0 0 0 rgba(0,122,57,.35);} 70%{ box-shadow:0 0 0 6px rgba(0,122,57,0);} 100%{ box-shadow:0 0 0 0 rgba(0,122,57,0);} }
576
+ `;
577
+ let injected = false;
578
+ function useCSS() {
579
+ if (typeof document !== 'undefined' && !injected) {
580
+ const e = document.createElement('style');
581
+ e.textContent = CSS;
582
+ e.dataset.s2 = 'status';
583
+ document.head.appendChild(e);
584
+ injected = true;
585
+ }
586
+ }
587
+ function StatusLight({
588
+ variant = 'neutral',
589
+ live = false,
590
+ children,
591
+ className = '',
592
+ ...rest
593
+ }) {
594
+ useCSS();
595
+ const cls = ['s2-status', `s2-status--${variant}`, live && 's2-status--live', className].filter(Boolean).join(' ');
596
+ return /*#__PURE__*/React.createElement("span", _extends({
597
+ className: cls
598
+ }, rest), /*#__PURE__*/React.createElement("span", {
599
+ className: "s2-status__dot"
600
+ }), children);
601
+ }
602
+ Object.assign(__ds_scope, { StatusLight });
603
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/status/StatusLight.jsx", error: String((e && e.message) || e) }); }
604
+
605
+ // ui_kits/chan-compass/app.jsx
606
+ try { (() => {
607
+ /* Chan Compass UI kit — app shell. Header + Tabs + routed content + footer. */
608
+ const _DS = window.ChanCompassSpectrum2DesignSystem_b52b37;
609
+ const {
610
+ Tabs: CCTabs,
611
+ StatusLight: CCStatus,
612
+ Badge: CCBadge
613
+ } = _DS;
614
+ const {
615
+ useState: useS,
616
+ useEffect: useE
617
+ } = React;
618
+ function Glyph({
619
+ name,
620
+ size = 18,
621
+ color,
622
+ style
623
+ }) {
624
+ useE(() => {
625
+ if (window.lucide) window.lucide.createIcons();
626
+ });
627
+ return /*#__PURE__*/React.createElement("i", {
628
+ "data-lucide": name,
629
+ style: {
630
+ width: size,
631
+ height: size,
632
+ color,
633
+ display: 'inline-flex',
634
+ ...style
635
+ }
636
+ });
637
+ }
638
+ function App() {
639
+ const [tab, setTab] = useS('signals');
640
+ const items = [{
641
+ id: 'signals',
642
+ label: 'Signals',
643
+ icon: /*#__PURE__*/React.createElement(Glyph, {
644
+ name: "trending-up",
645
+ size: 16
646
+ })
647
+ }, {
648
+ id: 'rotation',
649
+ label: 'Sector Rotation',
650
+ icon: /*#__PURE__*/React.createElement(Glyph, {
651
+ name: "refresh-cw",
652
+ size: 16
653
+ })
654
+ }, {
655
+ id: 'news',
656
+ label: 'Watchlist News',
657
+ icon: /*#__PURE__*/React.createElement(Glyph, {
658
+ name: "newspaper",
659
+ size: 16
660
+ }),
661
+ count: 2
662
+ }, {
663
+ id: 'research',
664
+ label: 'Auto Research',
665
+ icon: /*#__PURE__*/React.createElement(Glyph, {
666
+ name: "flask-conical",
667
+ size: 16
668
+ })
669
+ }, {
670
+ id: 'model',
671
+ label: 'Model',
672
+ icon: /*#__PURE__*/React.createElement(Glyph, {
673
+ name: "cpu",
674
+ size: 16
675
+ })
676
+ }];
677
+ const View = {
678
+ signals: window.SignalsView,
679
+ rotation: window.RotationView,
680
+ news: window.NewsView,
681
+ research: window.ResearchView,
682
+ model: window.ModelView
683
+ }[tab];
684
+ return /*#__PURE__*/React.createElement("div", {
685
+ className: "cc-app"
686
+ }, /*#__PURE__*/React.createElement("header", {
687
+ className: "cc-header"
688
+ }, /*#__PURE__*/React.createElement("div", {
689
+ className: "cc-brand"
690
+ }, /*#__PURE__*/React.createElement("div", {
691
+ className: "cc-mark"
692
+ }, /*#__PURE__*/React.createElement(Glyph, {
693
+ name: "compass",
694
+ size: 22,
695
+ color: "#fff"
696
+ })), /*#__PURE__*/React.createElement("div", {
697
+ className: "cc-brand-text"
698
+ }, /*#__PURE__*/React.createElement("div", {
699
+ className: "cc-wordmark"
700
+ }, "Chan Compass ", /*#__PURE__*/React.createElement("span", null, "\xB7 US Markets")), /*#__PURE__*/React.createElement("div", {
701
+ className: "cc-tagline"
702
+ }, "Multi-timeframe \u7F20\u8BBA signal engine \xB7 fully local"))), /*#__PURE__*/React.createElement("div", {
703
+ className: "cc-header-right"
704
+ }, /*#__PURE__*/React.createElement(CCBadge, {
705
+ variant: "accent"
706
+ }, /*#__PURE__*/React.createElement(Glyph, {
707
+ name: "cpu",
708
+ size: 12,
709
+ style: {
710
+ marginRight: 4
711
+ }
712
+ }), " Local \xB7 llama.cpp"), /*#__PURE__*/React.createElement(CCStatus, {
713
+ variant: "positive",
714
+ live: true
715
+ }, "Market open"))), /*#__PURE__*/React.createElement("div", {
716
+ className: "cc-navbar"
717
+ }, /*#__PURE__*/React.createElement(CCTabs, {
718
+ value: tab,
719
+ onChange: setTab,
720
+ items: items
721
+ })), /*#__PURE__*/React.createElement("main", {
722
+ className: "cc-main"
723
+ }, View ? /*#__PURE__*/React.createElement(View, null) : null), /*#__PURE__*/React.createElement("footer", {
724
+ className: "cc-footer"
725
+ }, /*#__PURE__*/React.createElement("span", null, "Chan Compass \xB7 educational tool, not investment advice"), /*#__PURE__*/React.createElement("span", {
726
+ className: "cc-foot-sep"
727
+ }, "\xB7"), /*#__PURE__*/React.createElement("span", null, "data: Yahoo Finance"), /*#__PURE__*/React.createElement("span", {
728
+ className: "cc-foot-sep"
729
+ }, "\xB7"), /*#__PURE__*/React.createElement("span", null, "design language: Adobe Spectrum 2")));
730
+ }
731
+ ReactDOM.createRoot(document.getElementById('root')).render(/*#__PURE__*/React.createElement(App, null));
732
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/chan-compass/app.jsx", error: String((e && e.message) || e) }); }
733
+
734
+ // ui_kits/chan-compass/data.js
735
+ try { (() => {
736
+ // Fake data for the Chan Compass UI kit — shapes mirror the real signal_runner /
737
+ // rotation engine output, values are illustrative.
738
+ window.CC_DATA = {
739
+ asof: 'Jun 12, 2026 · 18:10 ET',
740
+ signals: [{
741
+ t: 'NVDA',
742
+ action: 'BUY',
743
+ price: 178.42,
744
+ chg: +2.43,
745
+ conf: 0.82,
746
+ weight: 8,
747
+ stop: 172.40,
748
+ zone: '176.9 – 179.2',
749
+ note: 'Weekly pivot uplift confirmed; daily B1 armed off divergence low.'
750
+ }, {
751
+ t: 'MSFT',
752
+ action: 'BUY',
753
+ price: 472.10,
754
+ chg: +0.91,
755
+ conf: 0.74,
756
+ weight: 6,
757
+ stop: 458.00,
758
+ zone: '468 – 474',
759
+ note: '30m/15m nested-interval bottom confirms weekly hold-and-add.'
760
+ }, {
761
+ t: 'AMD',
762
+ action: 'HOLD',
763
+ price: 142.88,
764
+ chg: +1.12,
765
+ conf: 0.61,
766
+ weight: 5,
767
+ stop: 133.50,
768
+ zone: '—',
769
+ note: 'Unconfirmed daily sell inside weekly uptrend → hold (armed).'
770
+ }, {
771
+ t: 'AAPL',
772
+ action: 'HOLD',
773
+ price: 214.55,
774
+ chg: -0.34,
775
+ conf: 0.58,
776
+ weight: 5,
777
+ stop: 205.10,
778
+ zone: '—',
779
+ note: 'Riding pivot; no sublevel sell confirmation yet.'
780
+ }, {
781
+ t: 'TSLA',
782
+ action: 'SELL',
783
+ price: 243.17,
784
+ chg: -3.08,
785
+ conf: 0.69,
786
+ weight: 0,
787
+ stop: 251.00,
788
+ zone: '242 – 246',
789
+ note: 'Daily S3 pivot breakdown; nested-interval top confirmed.'
790
+ }, {
791
+ t: 'META',
792
+ action: 'WATCH',
793
+ price: 611.40,
794
+ chg: +0.22,
795
+ conf: 0.44,
796
+ weight: 0,
797
+ stop: '—',
798
+ zone: '—',
799
+ note: 'Approaching weekly pivot edge; no signal armed.'
800
+ }, {
801
+ t: 'AMZN',
802
+ action: 'WAIT',
803
+ price: 201.66,
804
+ chg: -0.51,
805
+ conf: 0.39,
806
+ weight: 0,
807
+ stop: '—',
808
+ zone: '—',
809
+ note: 'Inside consolidation; wait for level resolution.'
810
+ }, {
811
+ t: 'GOOGL',
812
+ action: 'WAIT',
813
+ price: 182.03,
814
+ chg: +0.08,
815
+ conf: 0.35,
816
+ weight: 0,
817
+ stop: '—',
818
+ zone: '—',
819
+ note: 'No multi-timeframe alignment.'
820
+ }],
821
+ rotation: [{
822
+ s: 'Technology',
823
+ sym: 'XLK',
824
+ d1: +1.84,
825
+ d5: +3.2,
826
+ d20: +6.1,
827
+ rs: +2.4
828
+ }, {
829
+ s: 'Comm. Services',
830
+ sym: 'XLC',
831
+ d1: +1.12,
832
+ d5: +2.1,
833
+ d20: +4.4,
834
+ rs: +1.6
835
+ }, {
836
+ s: 'Financials',
837
+ sym: 'XLF',
838
+ d1: +0.64,
839
+ d5: +1.0,
840
+ d20: +2.2,
841
+ rs: +0.5
842
+ }, {
843
+ s: 'Energy',
844
+ sym: 'XLE',
845
+ d1: +0.41,
846
+ d5: -0.8,
847
+ d20: -1.9,
848
+ rs: -1.1
849
+ }, {
850
+ s: 'Industrials',
851
+ sym: 'XLI',
852
+ d1: +0.18,
853
+ d5: +0.6,
854
+ d20: +1.1,
855
+ rs: +0.2
856
+ }, {
857
+ s: 'Health Care',
858
+ sym: 'XLV',
859
+ d1: -0.22,
860
+ d5: -0.4,
861
+ d20: +0.3,
862
+ rs: -0.4
863
+ }, {
864
+ s: 'Cons. Staples',
865
+ sym: 'XLP',
866
+ d1: -0.38,
867
+ d5: -0.9,
868
+ d20: -1.2,
869
+ rs: -0.9
870
+ }, {
871
+ s: 'Utilities',
872
+ sym: 'XLU',
873
+ d1: -0.51,
874
+ d5: -1.3,
875
+ d20: -2.0,
876
+ rs: -1.3
877
+ }, {
878
+ s: 'Real Estate',
879
+ sym: 'XLRE',
880
+ d1: -0.74,
881
+ d5: -1.6,
882
+ d20: -2.8,
883
+ rs: -1.7
884
+ }, {
885
+ s: 'Materials',
886
+ sym: 'XLB',
887
+ d1: -0.88,
888
+ d5: -1.1,
889
+ d20: -1.5,
890
+ rs: -0.8
891
+ }, {
892
+ s: 'Cons. Disc.',
893
+ sym: 'XLY',
894
+ d1: -1.06,
895
+ d5: -2.0,
896
+ d20: -3.4,
897
+ rs: -2.1
898
+ }],
899
+ reports: ['NVDA · 2026-06-12', 'TSLA · 2026-06-11', 'MSFT · 2026-06-10', 'AMD · 2026-06-09']
900
+ };
901
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/chan-compass/data.js", error: String((e && e.message) || e) }); }
902
+
903
+ // ui_kits/chan-compass/views.jsx
904
+ try { (() => {
905
+ /* Chan Compass UI kit — view components. Each consumes the design-system
906
+ primitives from the bundle namespace + Lucide line icons. Exported to window
907
+ for app.jsx to compose. */
908
+ const DS = window.ChanCompassSpectrum2DesignSystem_b52b37;
909
+ const {
910
+ Button,
911
+ Field,
912
+ Checkbox,
913
+ Switch,
914
+ Card,
915
+ InlineAlert,
916
+ Badge,
917
+ StatusLight,
918
+ SignalBadge,
919
+ Tabs
920
+ } = DS;
921
+ const {
922
+ useState,
923
+ useEffect
924
+ } = React;
925
+ function Icon({
926
+ name,
927
+ size = 18,
928
+ color,
929
+ style
930
+ }) {
931
+ // Lucide renders by replacing <i data-lucide>. Re-run after mount.
932
+ useEffect(() => {
933
+ if (window.lucide) window.lucide.createIcons();
934
+ });
935
+ return /*#__PURE__*/React.createElement("i", {
936
+ "data-lucide": name,
937
+ style: {
938
+ width: size,
939
+ height: size,
940
+ color,
941
+ display: 'inline-flex',
942
+ ...style
943
+ }
944
+ });
945
+ }
946
+ const fmtPct = v => typeof v === 'number' ? (v > 0 ? '+' : '') + v.toFixed(2) + '%' : v;
947
+ const dirColor = v => v > 0 ? 'var(--up)' : v < 0 ? 'var(--down)' : 'var(--flat)';
948
+
949
+ /* ─────────────────────────── KPI strip ─────────────────────────── */
950
+ function Kpi({
951
+ label,
952
+ value,
953
+ tone
954
+ }) {
955
+ return /*#__PURE__*/React.createElement("div", {
956
+ style: {
957
+ flex: 1,
958
+ padding: '14px 18px',
959
+ background: 'var(--surface-card)',
960
+ border: '1px solid var(--border-hairline)',
961
+ borderRadius: 'var(--radius-lg)',
962
+ boxShadow: 'var(--shadow-raised)'
963
+ }
964
+ }, /*#__PURE__*/React.createElement("div", {
965
+ style: {
966
+ font: 'var(--font-weight-bold) 28px/1 var(--font-sans)',
967
+ color: tone || 'var(--text-heading)'
968
+ }
969
+ }, value), /*#__PURE__*/React.createElement("div", {
970
+ style: {
971
+ font: 'var(--type-detail)',
972
+ color: 'var(--text-muted)',
973
+ marginTop: 4
974
+ }
975
+ }, label));
976
+ }
977
+
978
+ /* ─────────────────────────── Signals ─────────────────────────── */
979
+ function SignalsView() {
980
+ const D = window.CC_DATA;
981
+ const [pool, setPool] = useState('AAPL, MSFT, NVDA, TSLA, AMZN, GOOGL, META, AMD');
982
+ const [force, setForce] = useState(false);
983
+ const [running, setRunning] = useState(false);
984
+ const [ran, setRan] = useState(true);
985
+ const [sel, setSel] = useState('NVDA');
986
+ const [explained, setExplained] = useState(false);
987
+ const rows = D.signals;
988
+ const counts = rows.reduce((a, r) => {
989
+ a[r.action] = (a[r.action] || 0) + 1;
990
+ return a;
991
+ }, {});
992
+ const cur = rows.find(r => r.t === sel) || rows[0];
993
+ const run = () => {
994
+ setRunning(true);
995
+ setRan(false);
996
+ setExplained(false);
997
+ setTimeout(() => {
998
+ setRunning(false);
999
+ setRan(true);
1000
+ }, 1100);
1001
+ };
1002
+ return /*#__PURE__*/React.createElement("div", {
1003
+ style: {
1004
+ display: 'flex',
1005
+ flexDirection: 'column',
1006
+ gap: 'var(--space-300)'
1007
+ }
1008
+ }, /*#__PURE__*/React.createElement("div", {
1009
+ style: {
1010
+ display: 'flex',
1011
+ gap: 'var(--space-200)'
1012
+ }
1013
+ }, /*#__PURE__*/React.createElement(Kpi, {
1014
+ label: "BUY signals",
1015
+ value: counts.BUY || 0,
1016
+ tone: "var(--up)"
1017
+ }), /*#__PURE__*/React.createElement(Kpi, {
1018
+ label: "SELL signals",
1019
+ value: counts.SELL || 0,
1020
+ tone: "var(--down)"
1021
+ }), /*#__PURE__*/React.createElement(Kpi, {
1022
+ label: "Holding",
1023
+ value: counts.HOLD || 0,
1024
+ tone: "var(--accent)"
1025
+ }), /*#__PURE__*/React.createElement(Kpi, {
1026
+ label: "Watch / wait",
1027
+ value: (counts.WATCH || 0) + (counts.WAIT || 0)
1028
+ })), /*#__PURE__*/React.createElement(Card, {
1029
+ title: "Tomorrow's plan",
1030
+ subtitle: "Long-hold mode \xB7 sorted BUY \u2192 SELL \u2192 HOLD \u2192 WAIT",
1031
+ action: /*#__PURE__*/React.createElement(Badge, {
1032
+ variant: "neutral"
1033
+ }, D.asof)
1034
+ }, /*#__PURE__*/React.createElement("div", {
1035
+ style: {
1036
+ display: 'flex',
1037
+ gap: 'var(--space-200)',
1038
+ alignItems: 'flex-end',
1039
+ marginBottom: 'var(--space-250)'
1040
+ }
1041
+ }, /*#__PURE__*/React.createElement(Field, {
1042
+ label: "Ticker pool",
1043
+ mono: true,
1044
+ value: pool,
1045
+ onChange: e => setPool(e.target.value),
1046
+ className: "grow",
1047
+ help: "Comma separated \xB7 the unchanged Chan engine runs over each"
1048
+ }), /*#__PURE__*/React.createElement(Checkbox, {
1049
+ checked: force,
1050
+ onChange: e => setForce(e.target.checked)
1051
+ }, "Force fresh download"), /*#__PURE__*/React.createElement(Button, {
1052
+ variant: "accent",
1053
+ onClick: run,
1054
+ disabled: running
1055
+ }, running ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Icon, {
1056
+ name: "loader",
1057
+ size: 15
1058
+ }), " Running\u2026") : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Icon, {
1059
+ name: "play",
1060
+ size: 15
1061
+ }), " Run analysis"))), /*#__PURE__*/React.createElement("div", {
1062
+ className: "cc-table"
1063
+ }, /*#__PURE__*/React.createElement("table", null, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "Ticker"), /*#__PURE__*/React.createElement("th", null, "Signal"), /*#__PURE__*/React.createElement("th", {
1064
+ className: "num"
1065
+ }, "Price"), /*#__PURE__*/React.createElement("th", {
1066
+ className: "num"
1067
+ }, "\u0394 Day"), /*#__PURE__*/React.createElement("th", null, "Buy/sell zone"), /*#__PURE__*/React.createElement("th", {
1068
+ className: "num"
1069
+ }, "Conf."), /*#__PURE__*/React.createElement("th", {
1070
+ className: "num"
1071
+ }, "Stop"), /*#__PURE__*/React.createElement("th", {
1072
+ className: "num"
1073
+ }, "Wt %"))), /*#__PURE__*/React.createElement("tbody", null, rows.map(r => /*#__PURE__*/React.createElement("tr", {
1074
+ key: r.t,
1075
+ className: r.t === sel ? 'sel' : '',
1076
+ onClick: () => {
1077
+ setSel(r.t);
1078
+ setExplained(false);
1079
+ }
1080
+ }, /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("span", {
1081
+ className: "mono bold"
1082
+ }, r.t)), /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement(SignalBadge, {
1083
+ action: r.action
1084
+ })), /*#__PURE__*/React.createElement("td", {
1085
+ className: "num mono"
1086
+ }, r.price.toFixed(2)), /*#__PURE__*/React.createElement("td", {
1087
+ className: "num mono",
1088
+ style: {
1089
+ color: dirColor(r.chg)
1090
+ }
1091
+ }, fmtPct(r.chg)), /*#__PURE__*/React.createElement("td", {
1092
+ className: "mono dim"
1093
+ }, r.zone), /*#__PURE__*/React.createElement("td", {
1094
+ className: "num"
1095
+ }, /*#__PURE__*/React.createElement(ConfBar, {
1096
+ v: r.conf
1097
+ })), /*#__PURE__*/React.createElement("td", {
1098
+ className: "num mono dim"
1099
+ }, r.stop === '—' ? '—' : typeof r.stop === 'number' ? r.stop.toFixed(2) : r.stop), /*#__PURE__*/React.createElement("td", {
1100
+ className: "num mono"
1101
+ }, r.weight ? r.weight : '—'))))))), /*#__PURE__*/React.createElement("div", {
1102
+ style: {
1103
+ display: 'grid',
1104
+ gridTemplateColumns: '1fr 1fr',
1105
+ gap: 'var(--space-300)',
1106
+ alignItems: 'start'
1107
+ }
1108
+ }, /*#__PURE__*/React.createElement(Card, {
1109
+ title: `Decision log · ${cur.t}`,
1110
+ subtitle: "The engine's full multi-timeframe ruling chain"
1111
+ }, /*#__PURE__*/React.createElement("div", {
1112
+ className: "cc-log"
1113
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
1114
+ className: "mono dim"
1115
+ }, "\u6708\u7EBF monthly"), " \u4E0A\u6DA8\u8D8B\u52BF \xB7 \u4E2D\u67A2\u4E0A\u79FB \u2713"), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
1116
+ className: "mono dim"
1117
+ }, "\u5468\u7EBF weekly"), " \u7B2C\u4E09\u7C7B\u4E70\u70B9 B3 \xB7 \u64CD\u4F5C\u7EA7\u522B \u2713"), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
1118
+ className: "mono dim"
1119
+ }, "\u65E5\u7EBF daily"), " ", cur.action === 'SELL' ? 'S3 中枢破位 · 确认顶背驰' : 'B1 背驰 · 已武装 (armed)', " "), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
1120
+ className: "mono dim"
1121
+ }, "60m / 30m"), " \u533A\u95F4\u5957\u786E\u8BA4 nested-interval \u2713"), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
1122
+ className: "mono dim"
1123
+ }, "15m / 5m"), " \u6B21\u7EA7\u522B", cur.action === 'SELL' ? '卖点确认' : '买点未确认 → 持有'), /*#__PURE__*/React.createElement("div", {
1124
+ className: "cc-log-note"
1125
+ }, cur.note)), /*#__PURE__*/React.createElement("div", {
1126
+ style: {
1127
+ marginTop: 'var(--space-200)'
1128
+ }
1129
+ }, /*#__PURE__*/React.createElement(Button, {
1130
+ variant: "secondary",
1131
+ size: "sm",
1132
+ onClick: () => setExplained(true)
1133
+ }, /*#__PURE__*/React.createElement(Icon, {
1134
+ name: "languages",
1135
+ size: 14
1136
+ }), " Explain in English (local LLM)"))), explained ? /*#__PURE__*/React.createElement(InlineAlert, {
1137
+ variant: "ai",
1138
+ title: "Translator sub-agent \xB7 Qwen3-1.7B \xB7 llama.cpp"
1139
+ }, /*#__PURE__*/React.createElement("div", {
1140
+ style: {
1141
+ display: 'flex',
1142
+ flexDirection: 'column',
1143
+ gap: 6,
1144
+ marginTop: 4
1145
+ }
1146
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("b", null, "Action:"), " ", cur.action, cur.action === 'BUY' ? ' — add on weekly pivot uplift' : cur.action === 'SELL' ? ' — exit, top confirmed' : ' — hold the position', "."), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("b", null, "Why:"), " Monthly & weekly gates passed; daily ", cur.action === 'SELL' ? 'broke the pivot (S3)' : 'B1 armed off the divergence low', "; intraday ", cur.action === 'SELL' ? 'confirmed the top' : 'has not confirmed a sell', "."), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("b", null, "Invalidation:"), " ", cur.stop === '—' ? 'no active stop' : `flips below ${typeof cur.stop === 'number' ? cur.stop.toFixed(2) : cur.stop}`, "."))) : /*#__PURE__*/React.createElement("div", {
1147
+ className: "cc-empty"
1148
+ }, /*#__PURE__*/React.createElement(Icon, {
1149
+ name: "sparkles",
1150
+ size: 22,
1151
+ color: "var(--text-disabled)"
1152
+ }), /*#__PURE__*/React.createElement("div", null, "AI explanation appears here.", /*#__PURE__*/React.createElement("br", null), "Select a ticker, then \u201CExplain in English\u201D."))));
1153
+ }
1154
+ function ConfBar({
1155
+ v
1156
+ }) {
1157
+ return /*#__PURE__*/React.createElement("span", {
1158
+ style: {
1159
+ display: 'inline-flex',
1160
+ alignItems: 'center',
1161
+ gap: 6,
1162
+ justifyContent: 'flex-end'
1163
+ }
1164
+ }, /*#__PURE__*/React.createElement("span", {
1165
+ style: {
1166
+ width: 46,
1167
+ height: 6,
1168
+ borderRadius: 'var(--radius-full)',
1169
+ background: 'var(--surface-subtle)',
1170
+ overflow: 'hidden'
1171
+ }
1172
+ }, /*#__PURE__*/React.createElement("span", {
1173
+ style: {
1174
+ display: 'block',
1175
+ height: '100%',
1176
+ width: `${v * 100}%`,
1177
+ background: v > 0.7 ? 'var(--positive)' : v > 0.5 ? 'var(--accent)' : 'var(--s2-gray-400)'
1178
+ }
1179
+ })), /*#__PURE__*/React.createElement("span", {
1180
+ className: "mono dim",
1181
+ style: {
1182
+ fontSize: 11
1183
+ }
1184
+ }, Math.round(v * 100)));
1185
+ }
1186
+
1187
+ /* ─────────────────────────── Rotation ─────────────────────────── */
1188
+ function RotationView() {
1189
+ const D = window.CC_DATA;
1190
+ const [ai, setAi] = useState(false);
1191
+ const max = Math.max(...D.rotation.map(r => Math.abs(r.d1)));
1192
+ return /*#__PURE__*/React.createElement("div", {
1193
+ style: {
1194
+ display: 'flex',
1195
+ flexDirection: 'column',
1196
+ gap: 'var(--space-300)'
1197
+ }
1198
+ }, /*#__PURE__*/React.createElement(InlineAlert, {
1199
+ variant: "informative",
1200
+ title: "Where capital is flowing"
1201
+ }, "11 SPDR sector ETFs (full S&P 500). Flow proxy = change % \xD7 dollar volume; RS = return minus SPY."), /*#__PURE__*/React.createElement("div", {
1202
+ style: {
1203
+ display: 'grid',
1204
+ gridTemplateColumns: '1.4fr 1fr',
1205
+ gap: 'var(--space-300)',
1206
+ alignItems: 'start'
1207
+ }
1208
+ }, /*#__PURE__*/React.createElement(Card, {
1209
+ title: "Sector rotation",
1210
+ subtitle: "1-day flow \xB7 today",
1211
+ action: /*#__PURE__*/React.createElement(Button, {
1212
+ variant: "secondary",
1213
+ size: "sm"
1214
+ }, /*#__PURE__*/React.createElement(Icon, {
1215
+ name: "refresh-cw",
1216
+ size: 14
1217
+ }), " Refresh")
1218
+ }, /*#__PURE__*/React.createElement("div", {
1219
+ className: "cc-flow"
1220
+ }, D.rotation.map(r => /*#__PURE__*/React.createElement("div", {
1221
+ key: r.sym,
1222
+ className: "cc-flow-row"
1223
+ }, /*#__PURE__*/React.createElement("span", {
1224
+ className: "cc-flow-name"
1225
+ }, /*#__PURE__*/React.createElement("span", {
1226
+ className: "mono bold"
1227
+ }, r.sym), " ", /*#__PURE__*/React.createElement("span", {
1228
+ className: "dim"
1229
+ }, r.s)), /*#__PURE__*/React.createElement("span", {
1230
+ className: "cc-flow-track"
1231
+ }, /*#__PURE__*/React.createElement("span", {
1232
+ className: "cc-flow-bar",
1233
+ style: {
1234
+ width: `${Math.abs(r.d1) / max * 50}%`,
1235
+ marginLeft: r.d1 < 0 ? `${50 - Math.abs(r.d1) / max * 50}%` : '50%',
1236
+ background: dirColor(r.d1)
1237
+ }
1238
+ }), /*#__PURE__*/React.createElement("span", {
1239
+ className: "cc-flow-mid"
1240
+ })), /*#__PURE__*/React.createElement("span", {
1241
+ className: "num mono",
1242
+ style: {
1243
+ color: dirColor(r.d1),
1244
+ width: 54
1245
+ }
1246
+ }, fmtPct(r.d1)))))), /*#__PURE__*/React.createElement("div", {
1247
+ style: {
1248
+ display: 'flex',
1249
+ flexDirection: 'column',
1250
+ gap: 'var(--space-200)'
1251
+ }
1252
+ }, /*#__PURE__*/React.createElement(Card, {
1253
+ title: "Multi-window trend",
1254
+ elevation: "raised"
1255
+ }, /*#__PURE__*/React.createElement("div", {
1256
+ className: "cc-table compact"
1257
+ }, /*#__PURE__*/React.createElement("table", null, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "ETF"), /*#__PURE__*/React.createElement("th", {
1258
+ className: "num"
1259
+ }, "5-day"), /*#__PURE__*/React.createElement("th", {
1260
+ className: "num"
1261
+ }, "20-day"), /*#__PURE__*/React.createElement("th", {
1262
+ className: "num"
1263
+ }, "RS vs SPY"))), /*#__PURE__*/React.createElement("tbody", null, D.rotation.slice(0, 6).map(r => /*#__PURE__*/React.createElement("tr", {
1264
+ key: r.sym
1265
+ }, /*#__PURE__*/React.createElement("td", {
1266
+ className: "mono bold"
1267
+ }, r.sym), /*#__PURE__*/React.createElement("td", {
1268
+ className: "num mono",
1269
+ style: {
1270
+ color: dirColor(r.d5)
1271
+ }
1272
+ }, fmtPct(r.d5)), /*#__PURE__*/React.createElement("td", {
1273
+ className: "num mono",
1274
+ style: {
1275
+ color: dirColor(r.d20)
1276
+ }
1277
+ }, fmtPct(r.d20)), /*#__PURE__*/React.createElement("td", {
1278
+ className: "num mono",
1279
+ style: {
1280
+ color: dirColor(r.rs)
1281
+ }
1282
+ }, fmtPct(r.rs)))))))), /*#__PURE__*/React.createElement(Button, {
1283
+ variant: "accent",
1284
+ onClick: () => setAi(true)
1285
+ }, /*#__PURE__*/React.createElement(Icon, {
1286
+ name: "bot",
1287
+ size: 15
1288
+ }), " AI narrative (local LLM)"), ai && /*#__PURE__*/React.createElement(InlineAlert, {
1289
+ variant: "ai",
1290
+ title: "Narrator sub-agent \xB7 Qwen3-1.7B"
1291
+ }, "Capital is rotating ", /*#__PURE__*/React.createElement("b", null, "into Technology & Comm. Services"), " (XLK +1.84%, leading on both 5- and 20-day) and", /*#__PURE__*/React.createElement("b", null, " out of Consumer Discretionary & Real Estate"), ". Today's moves agree with the monthly trend \u2014 risk-on persists. ", /*#__PURE__*/React.createElement("b", null, "Watch:"), " Energy's 1-day pop diverges from its negative 20-day; likely a bounce, not a turn."))));
1292
+ }
1293
+
1294
+ /* ─────────────────────────── Research ─────────────────────────── */
1295
+ function ResearchView() {
1296
+ const D = window.CC_DATA;
1297
+ const [tic, setTic] = useState('');
1298
+ const [step, setStep] = useState(0); // 0 idle, 1..5 running, 6 done
1299
+ const steps = ['PLAN', 'Fundamentals', 'Quarterly financials', 'Price action + Chan engine', 'News', 'Report'];
1300
+ const run = () => {
1301
+ setStep(1);
1302
+ let i = 1;
1303
+ const tick = () => {
1304
+ i++;
1305
+ setStep(i);
1306
+ if (i < 6) setTimeout(tick, 650);
1307
+ };
1308
+ setTimeout(tick, 650);
1309
+ };
1310
+ return /*#__PURE__*/React.createElement("div", {
1311
+ style: {
1312
+ display: 'grid',
1313
+ gridTemplateColumns: '1fr 300px',
1314
+ gap: 'var(--space-300)',
1315
+ alignItems: 'start'
1316
+ }
1317
+ }, /*#__PURE__*/React.createElement(Card, {
1318
+ title: "Auto research agent",
1319
+ subtitle: "Fully local \xB7 PLAN \u2192 5 evidence tools \u2192 sectioned report"
1320
+ }, /*#__PURE__*/React.createElement("div", {
1321
+ style: {
1322
+ display: 'flex',
1323
+ gap: 'var(--space-150)',
1324
+ alignItems: 'flex-end',
1325
+ marginBottom: 'var(--space-250)'
1326
+ }
1327
+ }, /*#__PURE__*/React.createElement(Field, {
1328
+ label: "Ticker",
1329
+ placeholder: "e.g. NVDA",
1330
+ value: tic,
1331
+ onChange: e => setTic(e.target.value),
1332
+ className: "grow"
1333
+ }), /*#__PURE__*/React.createElement(Button, {
1334
+ variant: "accent",
1335
+ onClick: run,
1336
+ disabled: step > 0 && step < 6
1337
+ }, /*#__PURE__*/React.createElement(Icon, {
1338
+ name: "bot",
1339
+ size: 15
1340
+ }), " Run research agent")), step === 0 ? /*#__PURE__*/React.createElement("div", {
1341
+ className: "cc-empty tall"
1342
+ }, /*#__PURE__*/React.createElement(Icon, {
1343
+ name: "file-search",
1344
+ size: 24,
1345
+ color: "var(--text-disabled)"
1346
+ }), /*#__PURE__*/React.createElement("div", null, "Agent trace + sectioned report appear here.")) : /*#__PURE__*/React.createElement("div", {
1347
+ style: {
1348
+ display: 'flex',
1349
+ flexDirection: 'column',
1350
+ gap: 10
1351
+ }
1352
+ }, steps.map((s, idx) => /*#__PURE__*/React.createElement("div", {
1353
+ key: s,
1354
+ className: "cc-step"
1355
+ }, /*#__PURE__*/React.createElement("span", {
1356
+ className: `cc-step-dot ${step > idx ? 'done' : step === idx + 1 ? 'active' : ''}`
1357
+ }, step > idx + 1 ? /*#__PURE__*/React.createElement(Icon, {
1358
+ name: "check",
1359
+ size: 12,
1360
+ color: "#fff"
1361
+ }) : idx + 1), /*#__PURE__*/React.createElement("span", {
1362
+ style: {
1363
+ color: step > idx ? 'var(--text-body)' : 'var(--text-muted)'
1364
+ }
1365
+ }, s))), step >= 6 && /*#__PURE__*/React.createElement(InlineAlert, {
1366
+ variant: "ai",
1367
+ title: `Analyst · ${tic || 'NVDA'} · Qwen3-4B`
1368
+ }, /*#__PURE__*/React.createElement("b", null, "Valuation:"), " rich vs 5-yr median but justified by datacenter demand.", /*#__PURE__*/React.createElement("b", null, " Moat:"), " CUDA lock-in + supply-chain priority. ", /*#__PURE__*/React.createElement("b", null, "Chan timing:"), " weekly B3 active, daily B1 armed \u2014 accumulation favorable. ", /*#__PURE__*/React.createElement("b", null, "Risks:"), " export controls, hyperscaler capex digestion."))), /*#__PURE__*/React.createElement(Card, {
1369
+ title: "Report library",
1370
+ subtitle: "Auto + manual \xB7 /data",
1371
+ elevation: "raised"
1372
+ }, /*#__PURE__*/React.createElement("div", {
1373
+ className: "cc-list"
1374
+ }, D.reports.map((r, i) => /*#__PURE__*/React.createElement("button", {
1375
+ key: r,
1376
+ className: `cc-list-item ${i === 0 ? 'sel' : ''}`
1377
+ }, /*#__PURE__*/React.createElement(Icon, {
1378
+ name: "file-text",
1379
+ size: 15,
1380
+ color: "var(--text-muted)"
1381
+ }), /*#__PURE__*/React.createElement("span", {
1382
+ className: "mono"
1383
+ }, r))))));
1384
+ }
1385
+
1386
+ /* ─────────────────────────── News ─────────────────────────── */
1387
+ function NewsView() {
1388
+ const [hold, setHold] = useState('NVDA, TSLA, AAPL');
1389
+ const [checked, setChecked] = useState(false);
1390
+ return /*#__PURE__*/React.createElement("div", {
1391
+ style: {
1392
+ display: 'flex',
1393
+ flexDirection: 'column',
1394
+ gap: 'var(--space-300)',
1395
+ maxWidth: 760
1396
+ }
1397
+ }, /*#__PURE__*/React.createElement(Card, {
1398
+ title: "Watchlist news",
1399
+ subtitle: "For each holding, only today's news is checked"
1400
+ }, /*#__PURE__*/React.createElement("div", {
1401
+ style: {
1402
+ display: 'flex',
1403
+ gap: 'var(--space-150)',
1404
+ alignItems: 'flex-end',
1405
+ marginBottom: 'var(--space-200)'
1406
+ }
1407
+ }, /*#__PURE__*/React.createElement(Field, {
1408
+ label: "My holdings",
1409
+ mono: true,
1410
+ value: hold,
1411
+ onChange: e => setHold(e.target.value),
1412
+ className: "grow"
1413
+ }), /*#__PURE__*/React.createElement(Button, {
1414
+ variant: "secondary"
1415
+ }, /*#__PURE__*/React.createElement(Icon, {
1416
+ name: "save",
1417
+ size: 14
1418
+ }), " Save"), /*#__PURE__*/React.createElement(Button, {
1419
+ variant: "accent",
1420
+ onClick: () => setChecked(true)
1421
+ }, /*#__PURE__*/React.createElement(Icon, {
1422
+ name: "search",
1423
+ size: 15
1424
+ }), " Check today's news")), !checked ? /*#__PURE__*/React.createElement("div", {
1425
+ className: "cc-empty"
1426
+ }, /*#__PURE__*/React.createElement(Icon, {
1427
+ name: "newspaper",
1428
+ size: 22,
1429
+ color: "var(--text-disabled)"
1430
+ }), /*#__PURE__*/React.createElement("div", null, "News briefs appear here.")) : /*#__PURE__*/React.createElement("div", {
1431
+ style: {
1432
+ display: 'flex',
1433
+ flexDirection: 'column',
1434
+ gap: 'var(--space-150)'
1435
+ }
1436
+ }, /*#__PURE__*/React.createElement(InlineAlert, {
1437
+ variant: "ai",
1438
+ title: "NVDA \xB7 AI brief"
1439
+ }, "New datacenter GPU order from a hyperscaler reported pre-market; reinforces the active weekly B3 buy structure."), /*#__PURE__*/React.createElement(InlineAlert, {
1440
+ variant: "ai",
1441
+ title: "TSLA \xB7 AI brief"
1442
+ }, "Delivery-miss headline; aligns with today's daily S3 breakdown \u2014 confirms the SELL."), /*#__PURE__*/React.createElement("div", {
1443
+ className: "cc-quiet"
1444
+ }, /*#__PURE__*/React.createElement(StatusLight, {
1445
+ variant: "neutral"
1446
+ }, "Quiet today \xB7 AAPL")))));
1447
+ }
1448
+
1449
+ /* ─────────────────────────── Model ─────────────────────────── */
1450
+ function ModelView() {
1451
+ const models = ['Qwen3-1.7B', 'Qwen3-4B', 'Qwen3-8B', 'Qwen3-14B'];
1452
+ const [pick, setPick] = useState('Qwen3-4B');
1453
+ const [loaded, setLoaded] = useState('Qwen3-4B');
1454
+ return /*#__PURE__*/React.createElement("div", {
1455
+ style: {
1456
+ display: 'flex',
1457
+ flexDirection: 'column',
1458
+ gap: 'var(--space-300)',
1459
+ maxWidth: 680
1460
+ }
1461
+ }, /*#__PURE__*/React.createElement(InlineAlert, {
1462
+ variant: "informative",
1463
+ title: "Everything runs locally through llama.cpp"
1464
+ }, "Qwen3 GGUF weights, all below the 32B cap. Nothing leaves the machine. Signals, rotation & news never depend on it."), /*#__PURE__*/React.createElement(Card, {
1465
+ title: "Analyst (deep) model",
1466
+ subtitle: "Sub-agent pool \xB7 fast 1.7B fixed + this deep model"
1467
+ }, /*#__PURE__*/React.createElement("div", {
1468
+ className: "cc-radios"
1469
+ }, models.map(m => /*#__PURE__*/React.createElement("label", {
1470
+ key: m,
1471
+ className: `cc-radio ${pick === m ? 'sel' : ''}`,
1472
+ onClick: () => setPick(m)
1473
+ }, /*#__PURE__*/React.createElement("span", {
1474
+ className: "cc-radio-dot"
1475
+ }), /*#__PURE__*/React.createElement("span", {
1476
+ className: "mono"
1477
+ }, m), loaded === m && /*#__PURE__*/React.createElement(Badge, {
1478
+ variant: "positive"
1479
+ }, "loaded")))), /*#__PURE__*/React.createElement("div", {
1480
+ style: {
1481
+ display: 'flex',
1482
+ gap: 'var(--space-150)',
1483
+ marginTop: 'var(--space-250)',
1484
+ alignItems: 'center'
1485
+ }
1486
+ }, /*#__PURE__*/React.createElement(Button, {
1487
+ variant: "accent",
1488
+ onClick: () => setLoaded(pick)
1489
+ }, /*#__PURE__*/React.createElement(Icon, {
1490
+ name: "download",
1491
+ size: 15
1492
+ }), " Load model"), /*#__PURE__*/React.createElement(Button, {
1493
+ variant: "quiet"
1494
+ }, /*#__PURE__*/React.createElement(Icon, {
1495
+ name: "refresh-cw",
1496
+ size: 14
1497
+ }), " Refresh status"), /*#__PURE__*/React.createElement("span", {
1498
+ style: {
1499
+ marginLeft: 'auto'
1500
+ }
1501
+ }, /*#__PURE__*/React.createElement(StatusLight, {
1502
+ variant: "positive",
1503
+ live: true
1504
+ }, loaded, " ready")))));
1505
+ }
1506
+ Object.assign(window, {
1507
+ SignalsView,
1508
+ RotationView,
1509
+ ResearchView,
1510
+ NewsView,
1511
+ ModelView
1512
+ });
1513
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/chan-compass/views.jsx", error: String((e && e.message) || e) }); }
1514
+
1515
+ __ds_ns.Button = __ds_scope.Button;
1516
+
1517
+ __ds_ns.Card = __ds_scope.Card;
1518
+
1519
+ __ds_ns.InlineAlert = __ds_scope.InlineAlert;
1520
+
1521
+ __ds_ns.Checkbox = __ds_scope.Checkbox;
1522
+
1523
+ __ds_ns.Field = __ds_scope.Field;
1524
+
1525
+ __ds_ns.Switch = __ds_scope.Switch;
1526
+
1527
+ __ds_ns.Tabs = __ds_scope.Tabs;
1528
+
1529
+ __ds_ns.Badge = __ds_scope.Badge;
1530
+
1531
+ __ds_ns.SignalBadge = __ds_scope.SignalBadge;
1532
+
1533
+ __ds_ns.StatusLight = __ds_scope.StatusLight;
1534
+
1535
+ })();
app.py CHANGED
@@ -1,514 +1,5 @@
1
- """
2
- Chan Compass — US edition · Spectrum 2 frontend (redesigned layout)
3
- ====================================================================
4
- Same backend, cleaner face. EVERY feature of v3.9 is preserved — all six
5
- tabs, every callback, every timer, the email rows, the fine-tune export and
6
- the trace publisher — only the layout is reorganised for a simpler, more
7
- beautiful Adobe Spectrum 2 experience:
8
-
9
- • each tab opens with a framed "control bar" grouping its primary inputs
10
- • the per-tab "Email this result" row is tucked into a collapsible accordion
11
- • short eyebrow labels + help text replace dense paragraphs
12
- • the signature accent-framed AI output panel (.llm-out)
13
-
14
- The UI is mounted on a server with gr.mount_gradio_app (see server.py) — or
15
- just `demo.launch()` here. Backend modules are imported unchanged.
16
- """
17
- from __future__ import annotations
18
-
19
- import os
20
- import threading
21
-
22
- import gradio as gr
23
- import pandas as pd
24
-
25
- import automation
26
- import emailer
27
- import finetune_data
28
- import llm_local
29
- import news_watch
30
- import paths
31
- import research_agent
32
- import rotation
33
- import signal_runner
34
-
35
- # Spectrum 2 theme — single source of truth (tokens mirror the design system).
36
- from theme import THEME as theme, CSS as S2_CSS
37
-
38
-
39
- # ════════════════════════════════════════════════════ UI callbacks ══
40
- # (identical to v3.9 — backend logic untouched)
41
- def ui_run_signals(tickers_text, force):
42
- """Signals ONLY — pure rule engine, zero LLM, parallel downloads. Fast."""
43
- import time
44
- t0 = time.time()
45
- tickers = [t for t in tickers_text.replace("\n", ",").split(",") if t.strip()]
46
- df, details, summary, errors = signal_runner.run_signals(tickers or None,
47
- force=bool(force))
48
- if errors:
49
- summary += " · some tickers skipped (data not ready yet — run again)"
50
- automation.STATE["signals_df"] = df
51
- automation.STATE["signals_details"] = details
52
- automation.STATE["signals_summary"] = summary
53
- choices = sorted(details.keys())
54
- return (
55
- df if df is not None else pd.DataFrame(),
56
- f"{summary} · {time.time()-t0:.1f}s (rule engine only — no LLM in this path)",
57
- gr.update(choices=choices, value=(choices[0] if choices else None)),
58
- )
59
-
60
-
61
- def ui_show_detail(ticker):
62
- if not ticker:
63
- return "Select a ticker after running the analysis."
64
- raw = signal_runner.stock_raw_read(ticker)
65
- if not raw:
66
- return "No data for this ticker yet — run the analysis first."
67
- return f"**Raw read:**\n\n{raw}"
68
-
69
-
70
- def ui_explain_detail(ticker):
71
- raw = signal_runner.stock_raw_read(ticker or "")
72
- if not raw:
73
- yield "Run the analysis and select a ticker first."
74
- return
75
- # The full Chan ruling chain (Chinese) is kept BACKSTAGE in STATE and fed to
76
- # the model alongside the English raw read, so the summary reflects the real
77
- # multi-timeframe reasoning — but the chain is never shown and output is
78
- # English only. (This restores the merged raw-read + ruling-chain logic.)
79
- chain = automation.STATE.get("signals_details", {}).get(ticker or "", "")
80
- chain_core = chain.split("日线买卖点逐项诊断")[0].strip()[:2000] if chain else ""
81
- prompt = ("You are an equity analyst. Write a SHORT plain-English summary "
82
- "(≤100 words) for a long-term holder of a US stock: the situation "
83
- "today, whether to act or wait, and the key price levels.\n"
84
- "Use the FACT LINE for the numbers, and the RULING CHAIN (a "
85
- "Chinese multi-timeframe Chan-theory decision log) for the reasoning "
86
- "— translate and synthesize it; output ENGLISH ONLY, no Chinese "
87
- "characters, do not quote the log, no disclaimers.\n\n"
88
- f"FACT LINE:\n{raw}")
89
- if chain_core:
90
- prompt += f"\n\nRULING CHAIN (translate & synthesize, don't quote):\n{chain_core}"
91
- yield "🤖 _Summary Sub-Agent (Chan-Tuned Qwen3-1.7B · llama.cpp) is explaining…_"
92
- final = ""
93
- for acc in llm_local.chat_stream(prompt, max_tokens=260, temperature=0.2,
94
- worker="translator"):
95
- final = acc
96
- yield "🤖 **AI Summary (Summary Sub-Agent · Chan-Tuned Qwen3-1.7B):**\n\n" + acc
97
- # capture (raw read → narrative) as a fine-tuning pair (🎯 Well-Tuned)
98
- try:
99
- import finetune_data
100
- finetune_data.record(raw, final)
101
- except Exception:
102
- pass
103
-
104
-
105
- def ui_refresh_rotation():
106
- """Tables only — instant. AI narrative is a separate on-demand button."""
107
- d1, d5, d20, asof = rotation.build_rotation(force=True)
108
- automation.STATE["rotation"] = (d1, d5, d20, asof)
109
- raw = rotation.rotation_brief(d1, d5, d20)
110
- automation.STATE["rotation_narrative"] = raw
111
- return (rotation.fmt_table(d1), rotation.fmt_table(d5), rotation.fmt_table(d20),
112
- f"Sector flows as of **{asof}**", f"**Raw read:**\n{raw}")
113
-
114
-
115
- def ui_rotation_ai():
116
- d1, d5, d20, asof = automation.STATE["rotation"]
117
- if d1 is None:
118
- yield "Refresh the rotation tables first."
119
- return
120
- brief = rotation.rotation_brief(d1, d5, d20)
121
- prompt = ("You are a US equity market strategist. Based only on the sector flow "
122
- "data below (SPDR ETF proxy: change% × dollar volume, plus RS vs SPY), "
123
- "write a crisp brief (<150 words): 1) where capital is rotating INTO/OUT "
124
- "OF; 2) do 1-day moves agree with the 5/20-day trend; 3) one watch item. "
125
- "No disclaimers.\n\nDATA:\n" + brief[:2200])
126
- yield ("🤖 _Narrator sub-agent (Qwen3-1.7B · llama.cpp) is reading the flow "
127
- "tables — first words in ~5-15s…_")
128
- for acc in llm_local.chat_stream(prompt, max_tokens=340, worker="narrator"):
129
- yield "🤖 **Narrator sub-agent (Qwen3-1.7B · llama.cpp):**\n\n" + acc
130
-
131
-
132
- def ui_save_holdings(text):
133
- saved = news_watch.save_holdings(text.replace("\n", ",").split(","))
134
- return f"Saved {len(saved)} holding(s): {', '.join(saved) if saved else '—'}"
135
-
136
-
137
- def ui_check_news():
138
- last = ""
139
- for md in news_watch.check_holdings_news_stream():
140
- last = md
141
- yield md
142
- automation.STATE["news_md"] = last
143
-
144
-
145
- def ui_research(ticker):
146
- progress, report = "", ""
147
- for progress, report in research_agent.run_research_stream(ticker):
148
- yield progress, report, gr.update()
149
- reports = research_agent.list_reports()
150
- newest = reports[0] if reports else None
151
- yield progress, report, gr.update(choices=reports, value=newest)
152
-
153
-
154
- def ui_open_report(fname):
155
- return research_agent.read_report(fname)
156
-
157
-
158
- def ui_load_model(name):
159
- return llm_local.load_model(name, worker="deep")
160
-
161
-
162
- _SELFTEST = {"done": False, "result": ""}
163
-
164
-
165
- def _publish_traces(repo_id: str):
166
- """One-click: upload /data/traces to the Hub as a dataset, using the
167
- HF_TOKEN secret. No command line needed."""
168
- repo_id = (repo_id or "").strip()
169
- if "/" not in repo_id:
170
- return "⚠️ Enter a repo id like `username/dataset-name`."
171
- token = (os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
172
- or "").strip()
173
- if not token:
174
- return ("⚠️ No `HF_TOKEN` secret found. Add it in Space → Settings → "
175
- "Variables and secrets (a **write** token), then try again.")
176
- try:
177
- import paths
178
- traces = [f for f in os.listdir(paths.TRACES_DIR) if f.endswith(".json")]
179
- except OSError:
180
- traces = []
181
- if not traces:
182
- return "⚠️ No traces yet — run a few Auto Research reports first."
183
- try:
184
- from huggingface_hub import HfApi
185
- api = HfApi(token=token)
186
- api.create_repo(repo_id, repo_type="dataset", exist_ok=True)
187
- # a small README so the dataset page explains itself
188
- card = (
189
- "---\nlicense: mit\ntags: [agent-trace, finance, chan-theory, llama-cpp]\n---\n\n"
190
- "# Chan Compass — agent traces\n\n"
191
- "JSON traces from the Chan Compass multi-agent research desk. Each file "
192
- "is one ticker's run: the plan, every evidence-tool call and its result, "
193
- "and each local sub-agent's request and response. Shared for the "
194
- "Build Small hackathon (*Sharing is Caring*).\n\n"
195
- "*Educational data — not investment advice.*\n")
196
- import tempfile
197
- rp = os.path.join(tempfile.gettempdir(), "README.md")
198
- with open(rp, "w", encoding="utf-8") as f:
199
- f.write(card)
200
- api.upload_file(path_or_fileobj=rp, path_in_repo="README.md",
201
- repo_id=repo_id, repo_type="dataset")
202
- api.upload_folder(folder_path=paths.TRACES_DIR, path_in_repo="traces",
203
- repo_id=repo_id, repo_type="dataset")
204
- url = f"https://huggingface.co/datasets/{repo_id}"
205
- return (f"✅ Published **{len(traces)}** trace(s) to [{repo_id}]({url}). "
206
- f"Put that link in your submission for the *Sharing is Caring* badge.")
207
- except Exception as e:
208
- return f"❌ Upload failed: {e}"
209
-
210
-
211
- def _export_dataset():
212
- n = finetune_data.count()
213
- if n == 0:
214
- return ("⚠️ **0 training pairs captured yet.** Pairs are saved only when "
215
- "the **Signals → AI summary** finishes with a model loaded. Steps: "
216
- "1) Model tab — wait for the Summary sub-agent to show ✅; "
217
- "2) Signals — Run analysis, pick a ticker, click **AI summary**, "
218
- "let it finish; repeat a few times; 3) come back and Export.",
219
- gr.update(visible=False))
220
- path = finetune_data.export()
221
- if not path:
222
- return ("⚠️ Export failed to write the file (storage error). Try again.",
223
- gr.update(visible=False))
224
- # Copy to a folder under the app's working dir, which Gradio serves
225
- # reliably (the /data bucket and /tmp are not in Gradio's allowed paths).
226
- import shutil
227
- served_dir = os.path.join(os.getcwd(), "exports")
228
- os.makedirs(served_dir, exist_ok=True)
229
- served = os.path.join(served_dir, os.path.basename(path))
230
- try:
231
- shutil.copy(path, served)
232
- except OSError:
233
- served = path
234
- msg = (f"✅ Exported **{n}** captured pair(s). Download below, then follow "
235
- f"`finetune/FINETUNE_GUIDE.md` to LoRA-tune Qwen3-1.7B and publish it.")
236
- return msg, gr.update(value=served, visible=True)
237
-
238
-
239
- def _run_selftest():
240
- """Run the self-test now (used by both the auto-timer and the manual button)
241
- and cache the verdict so the two never fight over the output box."""
242
- out = llm_local.quick_test()
243
- ok = "not loaded" not in out and "error" not in out.lower()
244
- _SELFTEST["done"] = True
245
- _SELFTEST["result"] = (("✅ **Every agent is OK now** — self-test passed:\n\n" + out)
246
- if ok else ("⚠️ Self-test finished with issues:\n\n" + out))
247
- return _SELFTEST["result"]
248
-
249
-
250
- def _auto_selftest():
251
- """Auto-runs ONCE the moment every sub-agent is loaded. After that it returns
252
- the cached verdict unchanged, so it never overwrites a manual test result."""
253
- if _SELFTEST["done"]:
254
- return _SELFTEST["result"]
255
- workers = llm_local.WORKERS
256
- if not all(w["llm"] is not None for w in workers.values()):
257
- ready = sum(1 for w in workers.values() if w["llm"] is not None)
258
- return f"⏳ Loading sub-agents… {ready}/{len(workers)} ready (self-test will run automatically)."
259
- return _run_selftest()
260
-
261
-
262
- def _manual_selftest():
263
- """Manual button: always re-runs and shows the fresh verdict."""
264
- return _run_selftest()
265
-
266
-
267
- # ════════════════════════════════════════════════════ layout ══
268
- _GR_MAJOR = int(gr.__version__.split(".")[0])
269
- _style_kw = {} if _GR_MAJOR >= 6 else {"theme": theme, "css": S2_CSS}
270
-
271
- with gr.Blocks(title="Chan Compass · US", **_style_kw) as demo:
272
- with gr.Column(elem_id="s2-app"):
273
- gr.HTML("""
274
- <div id="s2-hero">
275
- <div class="mark">🧭</div>
276
- <div>
277
- <h1>Chan Compass <span>· US Markets</span></h1>
278
- <p>Multi-timeframe 缠论 (Chan theory) engine — monthly → 1m nested-interval
279
- confirmation · sector rotation · local sub-agent pool (llama.cpp).</p>
280
- </div>
281
- <div class="chips">
282
- <span>🧠 Local · no cloud</span><span>🦙 llama.cpp</span>
283
- <span>🤖 4 sub-agents</span><span>📊 Yahoo Finance</span>
284
- <span>⏰ 18:10 ET</span><span>🎨 Spectrum 2</span>
285
- </div>
286
- </div>""")
287
-
288
- # ─────────────────────────────────────── Signals ──
289
- with gr.Tab("📈 Signals"):
290
- gr.Markdown("STEP 1 · SCAN YOUR POOL", elem_classes=["s2-eyebrow"])
291
- with gr.Group(elem_classes=["s2-bar"]):
292
- with gr.Row():
293
- tickers_in = gr.Textbox(value=", ".join(signal_runner.DEFAULT_POOL),
294
- label="Ticker pool (comma separated)", scale=4)
295
- force_cb = gr.Checkbox(value=False, label="Force fresh download", scale=1)
296
- run_btn = gr.Button("▶ Run analysis", variant="primary", scale=1)
297
- sig_summary = gr.Markdown(automation.STATE["signals_summary"])
298
- sig_table = gr.Dataframe(label="Tomorrow's plan — long-hold mode (sorted: BUY → SELL → HOLD → WAIT)",
299
- interactive=False, wrap=True)
300
-
301
- gr.Markdown("STEP 2 · EXPLAIN ONE TICKER", elem_classes=["s2-eyebrow"])
302
- gr.Markdown("Pick a ticker for a plain-English raw read, then let the Summary "
303
- "sub-agent write an AI summary.", elem_classes=["s2-help"])
304
- with gr.Group(elem_classes=["s2-bar"]):
305
- with gr.Row():
306
- detail_pick = gr.Dropdown(choices=[], label="Ticker", scale=2)
307
- explain_btn = gr.Button("🤖 AI summary (local LLM)", variant="primary", scale=1)
308
- detail_box = gr.Markdown(label="Raw read")
309
- explain_box = gr.Markdown(elem_classes=["llm-out"])
310
- with gr.Accordion("✉ Email this summary", open=False):
311
- with gr.Row():
312
- sig_email = gr.Textbox(label="Send to", scale=4,
313
- placeholder="name@example.com (comma-separate for several)")
314
- sig_email_btn = gr.Button("✉ Send Email", scale=1)
315
- sig_email_status = gr.Markdown()
316
-
317
- # ─────────────────────────────────────── Sector Rotation ──
318
- with gr.Tab("🔄 Sector Rotation"):
319
- gr.Markdown("Capital rotation across the 11 SPDR sector ETFs (full S&P 500). "
320
- "**Flow proxy = change % × dollar volume**; RS = return minus SPY. "
321
- "True per-sector fund-flow feeds are paid data — this is the standard free proxy.",
322
- elem_classes=["s2-help"])
323
- with gr.Group(elem_classes=["s2-bar"]):
324
- with gr.Row():
325
- rot_btn = gr.Button("↻ Refresh rotation (instant)", variant="primary", scale=1)
326
- rot_ai_btn = gr.Button("🤖 AI narrative (local LLM)", scale=1)
327
- rot_asof = gr.Markdown("Press refresh (or Run analysis on the Signals tab).")
328
- with gr.Row():
329
- rot_1d = gr.Dataframe(label="1-Day (today's rotation)", interactive=False)
330
- with gr.Row():
331
- rot_5d = gr.Dataframe(label="5-Day (week trend)", interactive=False)
332
- rot_20d = gr.Dataframe(label="20-Day (month trend)", interactive=False)
333
- rot_ai = gr.Markdown(label="AI rotation narrative", elem_classes=["llm-out"])
334
- with gr.Accordion("✉ Email this narrative", open=False):
335
- with gr.Row():
336
- rot_email = gr.Textbox(label="Send to", scale=4,
337
- placeholder="name@example.com (comma-separate for several)")
338
- rot_email_btn = gr.Button("✉ Send Email", scale=1)
339
- rot_email_status = gr.Markdown()
340
-
341
- # ─────────────────────────────────────── Watchlist News ──
342
- with gr.Tab("📰 Watchlist News"):
343
- gr.Markdown("Daily rule: for each **holding**, only **today's** news is checked. "
344
- "News found → AI brief below. No news → ticker is listed under *Quiet today*.",
345
- elem_classes=["s2-help"])
346
- with gr.Group(elem_classes=["s2-bar"]):
347
- with gr.Row():
348
- hold_in = gr.Textbox(value=", ".join(news_watch.load_holdings()),
349
- label="My holdings (comma separated)", scale=3)
350
- save_btn = gr.Button("💾 Save holdings", scale=1)
351
- news_btn = gr.Button("🔍 Check today's news", variant="primary", scale=1)
352
- hold_status = gr.Markdown()
353
- news_out = gr.Markdown(elem_classes=["llm-out"])
354
- with gr.Accordion("✉ Email this news brief", open=False):
355
- with gr.Row():
356
- news_email = gr.Textbox(label="Send to", scale=4,
357
- placeholder="name@example.com (comma-separate for several)")
358
- news_email_btn = gr.Button("✉ Send Email", scale=1)
359
- news_email_status = gr.Markdown()
360
-
361
- # ─────────────────────────────────────── Auto Research ──
362
- with gr.Tab("🧪 Auto Research"):
363
- gr.Markdown("**Multi-step research agent** (fully local): PLAN → 5 evidence tools "
364
- "(fundamentals · quarterly financials · price action · **the Chan engine "
365
- "itself** · news) → section-by-section analysis → report. Every step is "
366
- "logged to a JSON **agent trace**. New pool tickers get a report "
367
- "**auto-generated** by the daily pipeline.", elem_classes=["s2-help"])
368
- with gr.Group(elem_classes=["s2-bar"]):
369
- with gr.Row():
370
- res_in = gr.Textbox(label="Ticker", placeholder="e.g. NVDA", scale=3)
371
- res_btn = gr.Button("🤖 Run research agent", variant="primary", scale=1)
372
- res_progress = gr.Markdown()
373
- res_out = gr.Markdown(elem_classes=["llm-out"])
374
- with gr.Accordion("✉ Email this report", open=False):
375
- with gr.Row():
376
- res_email = gr.Textbox(label="Send to", scale=4,
377
- placeholder="name@example.com (comma-separate for several)")
378
- res_email_btn = gr.Button("✉ Send Email", scale=1)
379
- res_email_status = gr.Markdown()
380
-
381
- gr.Markdown("REPORT LIBRARY", elem_classes=["s2-eyebrow"])
382
- gr.Markdown("Auto + manual reports, stored on `/data`.", elem_classes=["s2-help"])
383
- with gr.Group(elem_classes=["s2-bar"]):
384
- with gr.Row():
385
- rep_pick = gr.Dropdown(choices=research_agent.list_reports(),
386
- label="Saved reports", scale=3)
387
- rep_open = gr.Button("📂 Open report", scale=1)
388
- rep_view = gr.Markdown()
389
-
390
- # ─────────────────────────────────────── Automation ──
391
- with gr.Tab("⏰ Automation"):
392
- gr.Markdown(paths.storage_status())
393
- auto_md = gr.Markdown(automation.schedule_info())
394
- with gr.Group(elem_classes=["s2-bar"]):
395
- with gr.Row():
396
- auto_now = gr.Button("⚡ Run now", variant="primary")
397
- auto_msg = gr.Markdown()
398
- auto_log = gr.Textbox(lines=14, label="Pipeline log (live — updates every 2s)",
399
- elem_id="detail-log")
400
- traces_md = gr.Markdown(research_agent.list_traces())
401
- gr.Markdown("SHARE AGENT TRACES", elem_classes=["s2-eyebrow"])
402
- gr.Markdown("Publish every JSON agent trace in `/data/traces` as a Hugging Face "
403
- "**dataset** (one click, uses your `HF_TOKEN` secret — no command line). "
404
- "Earns *Sharing is Caring*.", elem_classes=["s2-help"])
405
- with gr.Group(elem_classes=["s2-bar"]):
406
- with gr.Row():
407
- trace_repo = gr.Textbox(
408
- value="ranranrunforit/chan-compass-agent-traces",
409
- label="Dataset repo id", scale=3)
410
- trace_pub = gr.Button("📡 Publish traces as dataset", variant="primary", scale=1)
411
- trace_pub_status = gr.Markdown()
412
- trace_pub.click(_publish_traces, trace_repo, trace_pub_status)
413
- auto_log_timer = gr.Timer(2.0)
414
-
415
- def _auto_tick():
416
- log = "\n".join(automation.STATE["log"][-40:]) or "(no log yet)"
417
- return log, research_agent.list_traces()
418
-
419
- auto_log_timer.tick(_auto_tick, None, [auto_log, traces_md])
420
-
421
- # ─────────────────────────────────────── Model ──
422
- with gr.Tab("🧠 Model"):
423
- gr.Markdown("All AI runs **locally** through **llama.cpp** with Qwen3 GGUF weights — "
424
- "every option is far below the 32B cap, and nothing leaves the machine. "
425
- "**First load installs the runtime + downloads the GGUF (one-time, usually "
426
- "1–3 min; worst case ~15 min if it compiles).** Signals/rotation/news never "
427
- "depend on it.", elem_classes=["s2-help"])
428
- gr.Markdown("**Sub-agent pool:** Summary (Chan-Tuned Qwen3-1.7B, fixed) handles "
429
- "Explain / rotation narrative / news briefs; `deep` Analyst writes research "
430
- "reports. Each has its own lock — they run in parallel.", elem_classes=["s2-help"])
431
- with gr.Group(elem_classes=["s2-bar"]):
432
- model_pick = gr.Radio(choices=list(llm_local.MODEL_ZOO.keys()),
433
- value=llm_local.DEFAULT_MODEL, label="Analyst (deep) model")
434
- with gr.Row():
435
- load_btn = gr.Button("⬇ Load model", variant="primary", scale=1)
436
- test_btn = gr.Button("⚡ Test sub-agents now", scale=1)
437
- model_status = gr.Markdown(llm_local.status())
438
- model_test_out = gr.Markdown()
439
- model_timer = gr.Timer(2.0)
440
- model_timer.tick(lambda: llm_local.status(), None, model_status)
441
- autotest_timer = gr.Timer(3.0)
442
- autotest_timer.tick(_auto_selftest, None, model_test_out)
443
-
444
- gr.Markdown("🎯 FINE-TUNING DATASET", elem_classes=["s2-eyebrow"])
445
- gr.Markdown("Every Signals **AI summary** you run is saved as a (raw read → narrative) "
446
- "training pair on `/data`. Capture a few hundred, export the JSONL, then "
447
- "follow `FINETUNE_GUIDE.md` to LoRA-tune Qwen3-1.7B and publish it.",
448
- elem_classes=["s2-help"])
449
- ft_status = gr.Markdown(finetune_data.status_line())
450
- with gr.Group(elem_classes=["s2-bar"]):
451
- ft_export = gr.Button("⬇ Export dataset (JSONL)", variant="primary")
452
- ft_out = gr.Markdown()
453
- ft_file = gr.File(label="Download training data", visible=False)
454
- ft_timer = gr.Timer(3.0)
455
- ft_timer.tick(lambda: finetune_data.status_line(), None, ft_status)
456
- ft_export.click(_export_dataset, None, [ft_out, ft_file])
457
-
458
- gr.Markdown("Chan Compass · educational tool, not investment advice · "
459
- "data: Yahoo Finance · design language: Adobe Spectrum 2",
460
- elem_classes=["s2-footnote"])
461
-
462
- # ─────────────────────────────────────── wiring (unchanged) ──
463
- run_btn.click(ui_run_signals, [tickers_in, force_cb],
464
- [sig_table, sig_summary, detail_pick])
465
- detail_pick.change(ui_show_detail, detail_pick, detail_box)
466
- explain_btn.click(ui_explain_detail, detail_pick, explain_box)
467
- rot_btn.click(ui_refresh_rotation, None, [rot_1d, rot_5d, rot_20d, rot_asof, rot_ai])
468
- rot_ai_btn.click(ui_rotation_ai, None, rot_ai)
469
- save_btn.click(ui_save_holdings, hold_in, hold_status)
470
- news_btn.click(ui_check_news, None, news_out)
471
- res_btn.click(ui_research, res_in, [res_progress, res_out, rep_pick])
472
- sig_email_btn.click(lambda body, to: emailer.send_result(body, to, "Signals summary"),
473
- [explain_box, sig_email], sig_email_status)
474
- rot_email_btn.click(lambda body, to: emailer.send_result(body, to, "Sector rotation"),
475
- [rot_ai, rot_email], rot_email_status)
476
- news_email_btn.click(lambda body, to: emailer.send_result(body, to, "Watchlist news"),
477
- [news_out, news_email], news_email_status)
478
- res_email_btn.click(lambda body, to: emailer.send_result(body, to, "Research report"),
479
- [res_out, res_email], res_email_status)
480
- rep_open.click(ui_open_report, rep_pick, rep_view)
481
- auto_now.click(lambda: automation.run_pipeline(force=True), None, auto_msg)
482
- load_btn.click(ui_load_model, model_pick, model_status)
483
- test_btn.click(_manual_selftest, None, model_test_out)
484
-
485
-
486
- # ════════════════════════════════════════════════════ boot ══
487
- automation.start_scheduler()
488
-
489
-
490
- def _auto_load_model():
491
- try:
492
- automation._log("Auto-loading sub-agents (llama.cpp)…")
493
- llm_local.auto_load_all()
494
- except Exception as e:
495
- automation._log(f"Sub-agent auto-load failed: {e}")
496
-
497
-
498
- try:
499
- import paths as _paths
500
- automation._log(_paths.storage_status())
501
- except Exception:
502
- pass
503
- if os.environ.get("AUTO_LOAD_MODEL", "1") == "1":
504
- threading.Thread(target=_auto_load_model, daemon=True).start()
505
 
506
 
507
  if __name__ == "__main__":
508
- import paths as _p
509
- _allowed = [os.path.join(os.getcwd(), "exports"), _p.DATASET_DIR]
510
- os.makedirs(_allowed[0], exist_ok=True)
511
- if _GR_MAJOR >= 6:
512
- demo.launch(theme=theme, css=S2_CSS, allowed_paths=_allowed)
513
- else:
514
- demo.launch(allowed_paths=_allowed)
 
1
+ from server import app, launch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
 
4
  if __name__ == "__main__":
5
+ launch()
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,7 +1,9 @@
1
- gradio>=5.49
 
2
  pandas>=2.0
3
  numpy>=1.24
4
  pyarrow>=14
5
  yfinance>=0.2.40
6
  apscheduler>=3.10
7
  huggingface_hub>=0.23
 
 
1
+ gradio>=6.18.0
2
+ fastapi>=0.115.2,<1.0
3
  pandas>=2.0
4
  numpy>=1.24
5
  pyarrow>=14
6
  yfinance>=0.2.40
7
  apscheduler>=3.10
8
  huggingface_hub>=0.23
9
+ python-multipart>=0.0.9
server.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ server.py - custom Spectrum 2 frontend for Gradio Server mode.
3
+
4
+ This is the Off-Brand entry point: instead of the default Gradio component
5
+ render, a gradio.Server instance serves the hand-built React UI
6
+ (ui_kits/chan-compass/) and exposes the unchanged Python backend as JSON/SSE
7
+ endpoints. The HF Space uses sdk: gradio and runs app.py, which calls
8
+ app.launch().
9
+
10
+ Architecture:
11
+ gradio.Server app
12
+ /api/... -> JSON + Server-Sent-Events endpoints calling the backend
13
+ / -> the React frontend (static), Spectrum 2 design system
14
+
15
+ Backend modules (signal_runner, rotation, news_watch, research_agent,
16
+ automation, emailer, finetune_data, llm_local) are imported and called with no
17
+ business-logic changes.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+
24
+ from fastapi import Request
25
+ from fastapi.responses import StreamingResponse, JSONResponse
26
+ from fastapi.staticfiles import StaticFiles
27
+ from gradio import Server
28
+
29
+ import paths # sets up /data, sys.path
30
+ import automation
31
+ import signal_runner
32
+ import rotation
33
+ import news_watch
34
+ import research_agent
35
+ import emailer
36
+ import finetune_data
37
+ import llm_local
38
+
39
+ HERE = os.path.dirname(os.path.abspath(__file__))
40
+ UI_DIR = os.path.join(HERE, "ui_kits", "chan-compass")
41
+
42
+ app = Server(title="Chan Compass · US")
43
+
44
+
45
+ # ───────────────────────── helpers ─────────────────────────
46
+ def _sse(gen):
47
+ """Wrap a text generator as Server-Sent Events (one 'data:' per chunk)."""
48
+ def stream():
49
+ for chunk in gen:
50
+ yield f"data: {json.dumps({'text': chunk})}\n\n"
51
+ yield "event: done\ndata: {}\n\n"
52
+ return StreamingResponse(stream(), media_type="text/event-stream")
53
+
54
+
55
+ def _df_records(df):
56
+ if df is None or not hasattr(df, "to_dict"):
57
+ return []
58
+ return df.to_dict(orient="records")
59
+
60
+
61
+ # ───────────────────────── Signals ─────────────────────────
62
+ @app.post("/api/signals/run")
63
+ async def signals_run(req: Request):
64
+ body = await req.json()
65
+ tickers = [t.strip().upper() for t in
66
+ (body.get("pool") or "").replace("\n", ",").split(",") if t.strip()]
67
+ force = bool(body.get("force"))
68
+ df, details, summary, errors = signal_runner.run_signals(tickers or None, force=force)
69
+ automation.STATE["signals_df"] = df
70
+ automation.STATE["signals_details"] = details
71
+ automation.STATE["signals_summary"] = summary
72
+ return JSONResponse({"rows": _df_records(df), "summary": summary,
73
+ "tickers": sorted(details.keys())})
74
+
75
+
76
+ @app.get("/api/signals/raw")
77
+ async def signals_raw(ticker: str):
78
+ return JSONResponse({"raw": signal_runner.stock_raw_read(ticker)})
79
+
80
+
81
+ @app.get("/api/signals/summary")
82
+ async def signals_summary(ticker: str):
83
+ raw = signal_runner.stock_raw_read(ticker or "")
84
+ if not raw:
85
+ return _sse(iter(["Run the analysis and select a ticker first."]))
86
+ chain = automation.STATE.get("signals_details", {}).get(ticker or "", "")
87
+ chain_core = chain.split("日线买卖点逐项诊断")[0].strip()[:2000] if chain else ""
88
+ prompt = ("You are an equity analyst. Write a SHORT plain-English summary "
89
+ "(≤100 words) for a long-term holder of a US stock: the situation "
90
+ "today, whether to act or wait, and the key price levels.\n"
91
+ "Use the FACT LINE for the numbers, and the RULING CHAIN (a Chinese "
92
+ "multi-timeframe Chan-theory decision log) for the reasoning — "
93
+ "translate and synthesize it; output ENGLISH ONLY, no Chinese "
94
+ "characters, do not quote the log, no disclaimers.\n\n"
95
+ f"FACT LINE:\n{raw}")
96
+ if chain_core:
97
+ prompt += f"\n\nRULING CHAIN (translate & synthesize, don't quote):\n{chain_core}"
98
+
99
+ def gen():
100
+ final = ""
101
+ for acc in llm_local.chat_stream(prompt, max_tokens=240, temperature=0.2,
102
+ worker="translator"):
103
+ final = acc
104
+ yield acc
105
+ try:
106
+ finetune_data.record(raw, final)
107
+ except Exception:
108
+ pass
109
+ return _sse(gen())
110
+
111
+
112
+ # ───────────────────────── Sector Rotation ─────────────────────────
113
+ @app.get("/api/rotation")
114
+ async def rotation_tables():
115
+ d1, d5, d20, asof = rotation.build_rotation(force=True)
116
+ automation.STATE["rotation"] = (d1, d5, d20, asof)
117
+ return JSONResponse({
118
+ "asof": asof,
119
+ "d1": _df_records(rotation.fmt_table(d1)),
120
+ "d5": _df_records(rotation.fmt_table(d5)),
121
+ "d20": _df_records(rotation.fmt_table(d20)),
122
+ })
123
+
124
+
125
+ @app.get("/api/rotation/narrative")
126
+ async def rotation_narrative():
127
+ rot = automation.STATE.get("rotation")
128
+ if not rot or rot[0] is None:
129
+ return _sse(iter(["Refresh the rotation tables first."]))
130
+ d1, d5, d20, _ = rot
131
+ brief = rotation.rotation_brief(d1, d5, d20)
132
+ prompt = ("You are a US equity market strategist. Based only on the sector flow "
133
+ "data below (SPDR ETF proxy: change% × dollar volume, plus RS vs SPY), "
134
+ "write a crisp brief (<150 words): 1) where capital is rotating "
135
+ "INTO/OUT OF; 2) do 1-day moves agree with the 5/20-day trend; 3) one "
136
+ "watch item. No disclaimers.\n\nDATA:\n" + brief[:2200])
137
+ return _sse(llm_local.chat_stream(prompt, max_tokens=340, worker="narrator"))
138
+
139
+
140
+ # ───────────────────────── Watchlist News ─────────────────────────
141
+ @app.get("/api/news/holdings")
142
+ async def news_holdings():
143
+ return JSONResponse({"holdings": news_watch.load_holdings()})
144
+
145
+
146
+ @app.post("/api/news/save")
147
+ async def news_save(req: Request):
148
+ body = await req.json()
149
+ tickers = [t.strip().upper() for t in
150
+ (body.get("holdings") or "").replace("\n", ",").split(",") if t.strip()]
151
+ news_watch.save_holdings(tickers)
152
+ return JSONResponse({"saved": tickers})
153
+
154
+
155
+ @app.get("/api/news/check")
156
+ async def news_check():
157
+ return _sse(news_watch.check_holdings_news_stream())
158
+
159
+
160
+ # ───────────────────────── Auto Research ─────────────────────────
161
+ @app.get("/api/research/run")
162
+ async def research_run(ticker: str):
163
+ def gen():
164
+ last_report = ""
165
+ for progress, report in research_agent.run_research_stream(ticker):
166
+ last_report = report
167
+ yield json.dumps({"progress": progress, "report": report})
168
+ yield json.dumps({"progress": "__done__", "report": last_report,
169
+ "reports": research_agent.list_reports()})
170
+
171
+ def stream():
172
+ for chunk in gen():
173
+ yield f"data: {chunk}\n\n"
174
+ yield "event: done\ndata: {}\n\n"
175
+ return StreamingResponse(stream(), media_type="text/event-stream")
176
+
177
+
178
+ @app.get("/api/research/reports")
179
+ async def research_reports():
180
+ return JSONResponse({"reports": research_agent.list_reports()})
181
+
182
+
183
+ @app.get("/api/research/report")
184
+ async def research_report(name: str):
185
+ return JSONResponse({"markdown": research_agent.read_report(name)})
186
+
187
+
188
+ # ───────────────────────── Automation ─────────────────────────
189
+ @app.post("/api/automation/run")
190
+ async def automation_run():
191
+ msg = automation.run_pipeline(force=True)
192
+ return JSONResponse({"message": msg})
193
+
194
+
195
+ @app.get("/api/automation/state")
196
+ async def automation_state():
197
+ return JSONResponse({
198
+ "log": automation.STATE.get("log", [])[-40:],
199
+ "schedule": automation.schedule_info(),
200
+ "traces": research_agent.list_traces(),
201
+ })
202
+
203
+
204
+ @app.post("/api/automation/publish-traces")
205
+ async def automation_publish(req: Request):
206
+ body = await req.json()
207
+ import trace_publish
208
+ return JSONResponse({"status": trace_publish.publish_traces(body.get("repo", ""))})
209
+
210
+
211
+ # ───────────────────────── Model ─────────────────────────
212
+ @app.get("/api/model/status")
213
+ async def model_status():
214
+ return JSONResponse({
215
+ "status": llm_local.status(),
216
+ "workers": {k: {"model": w["model"], "ready": w["llm"] is not None,
217
+ "stage": w["stage"]}
218
+ for k, w in llm_local.WORKERS.items()},
219
+ })
220
+
221
+
222
+ @app.post("/api/model/test")
223
+ async def model_test():
224
+ return JSONResponse({"result": llm_local.quick_test()})
225
+
226
+
227
+ @app.get("/api/model/finetune-status")
228
+ async def finetune_status():
229
+ return JSONResponse({"status": finetune_data.status_line(),
230
+ "count": finetune_data.count()})
231
+
232
+
233
+ @app.post("/api/model/export-dataset")
234
+ async def export_dataset():
235
+ path = finetune_data.export()
236
+ return JSONResponse({"path": path or "", "count": finetune_data.count()})
237
+
238
+
239
+ # ───────────────────────── Email (all tabs) ─────────────────────────
240
+ @app.post("/api/email")
241
+ async def send_email(req: Request):
242
+ body = await req.json()
243
+ status = emailer.send_result(body.get("content", ""), body.get("to", ""),
244
+ body.get("tag", "Chan Compass"))
245
+ return JSONResponse({"status": status})
246
+
247
+
248
+ # ───────────────────────── static frontend ─────────────────────────
249
+ # Mounted last so /api/* wins. html=True serves index.html at /.
250
+ app.mount("/", StaticFiles(directory=UI_DIR, html=True), name="ui")
251
+
252
+
253
+ # ───────────────────────── startup ─────────────────────────
254
+ @app.on_event("startup")
255
+ def _startup():
256
+ automation.start_scheduler()
257
+ if os.environ.get("AUTO_LOAD_MODEL", "1") == "1":
258
+ import threading
259
+ threading.Thread(target=llm_local.auto_load_all, daemon=True).start()
260
+
261
+
262
+ def launch():
263
+ app.launch(
264
+ server_name=os.environ.get("GRADIO_SERVER_NAME", "0.0.0.0"),
265
+ server_port=int(os.environ.get("GRADIO_SERVER_PORT", os.environ.get("PORT", "7860"))),
266
+ )
267
+
268
+
269
+ if __name__ == "__main__":
270
+ launch()
trace_publish.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """trace_publish.py - upload /data/traces to the Hub as a dataset, using the
2
+ HF_TOKEN secret. No command line; the custom gradio.Server app calls it directly.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import tempfile
8
+
9
+ import paths
10
+
11
+
12
+ def publish_traces(repo_id: str) -> str:
13
+ repo_id = (repo_id or "").strip()
14
+ if "/" not in repo_id:
15
+ return "⚠️ Enter a repo id like `username/dataset-name`."
16
+ token = (os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
17
+ or "").strip()
18
+ if not token:
19
+ return ("⚠️ No `HF_TOKEN` secret found. Add it in Space → Settings → "
20
+ "Variables and secrets (a **write** token), then try again.")
21
+ try:
22
+ traces = [f for f in os.listdir(paths.TRACES_DIR) if f.endswith(".json")]
23
+ except OSError:
24
+ traces = []
25
+ if not traces:
26
+ return "⚠️ No traces yet — run a few Auto Research reports first."
27
+ try:
28
+ from huggingface_hub import HfApi
29
+ api = HfApi(token=token)
30
+ api.create_repo(repo_id, repo_type="dataset", exist_ok=True)
31
+ card = (
32
+ "---\nlicense: mit\ntags: [agent-trace, finance, chan-theory, llama-cpp]\n---\n\n"
33
+ "# Chan Compass — agent traces\n\n"
34
+ "JSON traces from the Chan Compass multi-agent research desk. Each file "
35
+ "is one ticker's run: the plan, every evidence-tool call and its result, "
36
+ "and each local sub-agent's request and response. Shared for the "
37
+ "Build Small hackathon (*Sharing is Caring*).\n\n"
38
+ "*Educational data — not investment advice.*\n")
39
+ rp = os.path.join(tempfile.gettempdir(), "README.md")
40
+ with open(rp, "w", encoding="utf-8") as f:
41
+ f.write(card)
42
+ api.upload_file(path_or_fileobj=rp, path_in_repo="README.md",
43
+ repo_id=repo_id, repo_type="dataset")
44
+ api.upload_folder(folder_path=paths.TRACES_DIR, path_in_repo="traces",
45
+ repo_id=repo_id, repo_type="dataset")
46
+ url = f"https://huggingface.co/datasets/{repo_id}"
47
+ return (f"✅ Published {len(traces)} trace(s) to {repo_id} ({url}). "
48
+ f"Put that link in your submission for the Sharing is Caring badge.")
49
+ except Exception as e:
50
+ return f"❌ Upload failed: {e}"
ui_kits/chan-compass/_ds_bundle.js ADDED
@@ -0,0 +1,1535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* @ds-bundle: {"format":3,"namespace":"ChanCompassSpectrum2DesignSystem_b52b37","components":[{"name":"Button","sourcePath":"components/actions/Button.jsx"},{"name":"Card","sourcePath":"components/containers/Card.jsx"},{"name":"InlineAlert","sourcePath":"components/containers/InlineAlert.jsx"},{"name":"Checkbox","sourcePath":"components/forms/Checkbox.jsx"},{"name":"Field","sourcePath":"components/forms/Field.jsx"},{"name":"Switch","sourcePath":"components/forms/Switch.jsx"},{"name":"Tabs","sourcePath":"components/navigation/Tabs.jsx"},{"name":"Badge","sourcePath":"components/status/Badge.jsx"},{"name":"SignalBadge","sourcePath":"components/status/SignalBadge.jsx"},{"name":"StatusLight","sourcePath":"components/status/StatusLight.jsx"}],"sourceHashes":{"components/actions/Button.jsx":"c9f7501c202c","components/containers/Card.jsx":"9316d0c88d32","components/containers/InlineAlert.jsx":"5ceb8bdaac5f","components/forms/Checkbox.jsx":"59c34e791a76","components/forms/Field.jsx":"39157a609b42","components/forms/Switch.jsx":"38f4684e0ef2","components/navigation/Tabs.jsx":"5488bca491fa","components/status/Badge.jsx":"3e62358e3a0b","components/status/SignalBadge.jsx":"df7989e74422","components/status/StatusLight.jsx":"f7d206a1642b","ui_kits/chan-compass/app.jsx":"4b80024bd468","ui_kits/chan-compass/data.js":"5821344b625c","ui_kits/chan-compass/views.jsx":"c9ec2aee076f"},"inlinedExternals":[],"unexposedExports":[]} */
2
+
3
+ (() => {
4
+
5
+ const __ds_ns = (window.ChanCompassSpectrum2DesignSystem_b52b37 = window.ChanCompassSpectrum2DesignSystem_b52b37 || {});
6
+
7
+ const __ds_scope = {};
8
+
9
+ (__ds_ns.__errors = __ds_ns.__errors || []);
10
+
11
+ // components/actions/Button.jsx
12
+ try { (() => {
13
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
14
+ /* Inject component CSS once. Spectrum 2 buttons: fully-rounded "pill" by
15
+ default, medium height 32px, 600 weight, soft state transitions. */
16
+ const CSS = `
17
+ .s2-btn{
18
+ --_bg: var(--accent); --_bgh: var(--accent-hover); --_bgd: var(--accent-down);
19
+ --_fg: var(--text-on-accent); --_bd: transparent;
20
+ display:inline-flex; align-items:center; justify-content:center; gap:var(--space-100);
21
+ font:var(--font-weight-semibold) var(--font-size-100)/1 var(--font-sans);
22
+ height:var(--control-height-md); padding:0 var(--space-250);
23
+ border-radius:var(--radius-full); border:var(--border-width-200) solid var(--_bd);
24
+ background:var(--_bg); color:var(--_fg); cursor:pointer; white-space:nowrap;
25
+ transition:var(--transition-colors), transform var(--duration-fast) var(--ease-out);
26
+ user-select:none;
27
+ }
28
+ .s2-btn:hover{ background:var(--_bgh); }
29
+ .s2-btn:active{ background:var(--_bgd); transform:scale(.98); }
30
+ .s2-btn:focus-visible{ outline:var(--focus-ring-width) solid var(--focus-ring); outline-offset:var(--focus-ring-offset); }
31
+ .s2-btn[disabled]{ background:var(--surface-subtle); color:var(--text-disabled); border-color:transparent; cursor:default; transform:none; }
32
+ .s2-btn--square{ border-radius:var(--radius-default); }
33
+ .s2-btn--sm{ height:var(--control-height-sm); padding:0 var(--space-200); font-size:var(--font-size-75); }
34
+ .s2-btn--lg{ height:var(--control-height-lg); padding:0 var(--space-300); font-size:var(--font-size-200); }
35
+ /* secondary — outline */
36
+ .s2-btn--secondary{ --_bg:var(--surface-card); --_fg:var(--text-body); --_bd:var(--border-strong); }
37
+ .s2-btn--secondary:hover{ --_bg:var(--surface-subtle); --_bd:var(--text-secondary); }
38
+ .s2-btn--secondary:active{ --_bg:var(--s2-gray-100); }
39
+ /* negative */
40
+ .s2-btn--negative{ --_bg:var(--negative); --_bgh:var(--negative-hover); --_bgd:var(--s2-red-1100); }
41
+ /* quiet — no chrome until hover */
42
+ .s2-btn--quiet{ --_bg:transparent; --_fg:var(--text-body); --_bd:transparent; }
43
+ .s2-btn--quiet:hover{ --_bg:var(--surface-subtle); }
44
+ .s2-btn--quiet:active{ --_bg:var(--s2-gray-100); }
45
+ `;
46
+ let injected = false;
47
+ function useCSS() {
48
+ if (typeof document !== 'undefined' && !injected) {
49
+ const el = document.createElement('style');
50
+ el.textContent = CSS;
51
+ el.dataset.s2 = 'button';
52
+ document.head.appendChild(el);
53
+ injected = true;
54
+ }
55
+ }
56
+ function Button({
57
+ variant = 'accent',
58
+ size = 'md',
59
+ shape = 'pill',
60
+ icon,
61
+ children,
62
+ className = '',
63
+ ...rest
64
+ }) {
65
+ useCSS();
66
+ const cls = ['s2-btn', variant !== 'accent' && `s2-btn--${variant}`, size !== 'md' && `s2-btn--${size}`, shape === 'square' && 's2-btn--square', className].filter(Boolean).join(' ');
67
+ return /*#__PURE__*/React.createElement("button", _extends({
68
+ className: cls
69
+ }, rest), icon, children);
70
+ }
71
+ Object.assign(__ds_scope, { Button });
72
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/actions/Button.jsx", error: String((e && e.message) || e) }); }
73
+
74
+ // components/containers/Card.jsx
75
+ try { (() => {
76
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
77
+ const CSS = `
78
+ .s2-card{
79
+ background:var(--surface-card); border:var(--border-width-100) solid var(--border-hairline);
80
+ border-radius:var(--card-radius); box-shadow:var(--shadow-raised);
81
+ overflow:clip;
82
+ }
83
+ .s2-card--elevated{ box-shadow:var(--shadow-elevated); }
84
+ .s2-card--flat{ box-shadow:none; }
85
+ .s2-card--quiet{ background:transparent; border:none; box-shadow:none; }
86
+ .s2-card__head{ display:flex; align-items:flex-start; justify-content:space-between; gap:var(--space-200); padding:var(--space-250) var(--space-300) 0; }
87
+ .s2-card__titles{ display:flex; flex-direction:column; gap:2px; }
88
+ .s2-card__title{ font:var(--type-h3); color:var(--text-heading); }
89
+ .s2-card__subtitle{ font:var(--type-detail); color:var(--text-muted); }
90
+ .s2-card__body{ padding:var(--space-250) var(--space-300) var(--space-300); }
91
+ .s2-card__head + .s2-card__body{ padding-top:var(--space-200); }
92
+ `;
93
+ let injected = false;
94
+ function useCSS() {
95
+ if (typeof document !== 'undefined' && !injected) {
96
+ const e = document.createElement('style');
97
+ e.textContent = CSS;
98
+ e.dataset.s2 = 'card';
99
+ document.head.appendChild(e);
100
+ injected = true;
101
+ }
102
+ }
103
+ function Card({
104
+ title,
105
+ subtitle,
106
+ action,
107
+ elevation = 'raised',
108
+ children,
109
+ className = '',
110
+ ...rest
111
+ }) {
112
+ useCSS();
113
+ const cls = ['s2-card', elevation !== 'raised' && `s2-card--${elevation}`, className].filter(Boolean).join(' ');
114
+ const hasHead = title || subtitle || action;
115
+ return /*#__PURE__*/React.createElement("section", _extends({
116
+ className: cls
117
+ }, rest), hasHead && /*#__PURE__*/React.createElement("header", {
118
+ className: "s2-card__head"
119
+ }, /*#__PURE__*/React.createElement("div", {
120
+ className: "s2-card__titles"
121
+ }, title && /*#__PURE__*/React.createElement("span", {
122
+ className: "s2-card__title"
123
+ }, title), subtitle && /*#__PURE__*/React.createElement("span", {
124
+ className: "s2-card__subtitle"
125
+ }, subtitle)), action), /*#__PURE__*/React.createElement("div", {
126
+ className: "s2-card__body"
127
+ }, children));
128
+ }
129
+ Object.assign(__ds_scope, { Card });
130
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/containers/Card.jsx", error: String((e && e.message) || e) }); }
131
+
132
+ // components/containers/InlineAlert.jsx
133
+ try { (() => {
134
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
135
+ /* Spectrum 2 in-line message. A framed notice tied to a region of the page.
136
+ `ai` variant is the signature accent-framed panel used for local-LLM output. */
137
+ const CSS = `
138
+ .s2-alert{
139
+ display:flex; gap:var(--space-150); padding:var(--space-200) var(--space-250);
140
+ border-radius:var(--radius-lg); border:var(--border-width-100) solid var(--border-default);
141
+ background:var(--surface-card); font:var(--type-body); color:var(--text-body);
142
+ }
143
+ .s2-alert__icon{ flex:none; width:18px; height:18px; display:grid; place-items:center; margin-top:1px; }
144
+ .s2-alert__body{ display:flex; flex-direction:column; gap:2px; min-width:0; }
145
+ .s2-alert__title{ font:var(--type-ui-bold); color:var(--text-heading); }
146
+ .s2-alert--informative{ border-color:var(--accent); background:var(--informative-subtle); }
147
+ .s2-alert--informative .s2-alert__icon{ color:var(--accent); }
148
+ .s2-alert--positive{ border-color:var(--positive); background:var(--positive-subtle); }
149
+ .s2-alert--positive .s2-alert__icon{ color:var(--positive); }
150
+ .s2-alert--negative{ border-color:var(--negative); background:var(--negative-subtle); }
151
+ .s2-alert--negative .s2-alert__icon{ color:var(--negative); }
152
+ .s2-alert--notice{ border-color:var(--notice); background:var(--notice-subtle); }
153
+ .s2-alert--notice .s2-alert__icon{ color:var(--notice); }
154
+ .s2-alert--ai{
155
+ border:var(--border-width-200) solid var(--accent); border-inline-start-width:6px;
156
+ border-radius:var(--radius-lg); background:var(--surface-card); box-shadow:var(--shadow-accent);
157
+ }
158
+ .s2-alert--ai .s2-alert__title{ color:var(--accent-text); }
159
+ `;
160
+ let injected = false;
161
+ function useCSS() {
162
+ if (typeof document !== 'undefined' && !injected) {
163
+ const e = document.createElement('style');
164
+ e.textContent = CSS;
165
+ e.dataset.s2 = 'alert';
166
+ document.head.appendChild(e);
167
+ injected = true;
168
+ }
169
+ }
170
+ const ICONS = {
171
+ informative: 'M9 1a8 8 0 100 16A8 8 0 009 1zm0 3.2a1.1 1.1 0 110 2.2 1.1 1.1 0 010-2.2zM10.2 14H7.8v-.9l.9-.2V9.1l-.9-.2V8h2.4v4.9l.9.2v.9z',
172
+ positive: 'M9 1a8 8 0 100 16A8 8 0 009 1zm4.2 5.4l-5 5.2a.7.7 0 01-1 0L4.8 9a.7.7 0 011-1l1.9 1.9 4.5-4.6a.7.7 0 011 1z',
173
+ negative: 'M9 1a8 8 0 100 16A8 8 0 009 1zm1.1 11.8H7.9v-2.2h2.2v2.2zm0-3.6H7.9V4.2h2.2v5z',
174
+ notice: 'M9.9 1.6l7 12.1a1 1 0 01-.9 1.5H2a1 1 0 01-.9-1.5l7-12.1a1 1 0 011.8 0zM7.9 6.2v3.8h2.2V6.2H7.9zm0 5.2v2h2.2v-2H7.9z',
175
+ ai: 'M9 1l1.8 4.3L15 7l-4.2 1.7L9 13 7.2 8.7 3 7l4.2-1.7L9 1z'
176
+ };
177
+ function InlineAlert({
178
+ variant = 'informative',
179
+ title,
180
+ icon,
181
+ children,
182
+ className = '',
183
+ ...rest
184
+ }) {
185
+ useCSS();
186
+ const path = ICONS[variant] || ICONS.informative;
187
+ return /*#__PURE__*/React.createElement("div", _extends({
188
+ className: ['s2-alert', `s2-alert--${variant}`, className].filter(Boolean).join(' ')
189
+ }, rest), /*#__PURE__*/React.createElement("span", {
190
+ className: "s2-alert__icon"
191
+ }, icon || /*#__PURE__*/React.createElement("svg", {
192
+ width: "18",
193
+ height: "18",
194
+ viewBox: "0 0 18 18",
195
+ fill: "currentColor"
196
+ }, /*#__PURE__*/React.createElement("path", {
197
+ d: path
198
+ }))), /*#__PURE__*/React.createElement("div", {
199
+ className: "s2-alert__body"
200
+ }, title && /*#__PURE__*/React.createElement("span", {
201
+ className: "s2-alert__title"
202
+ }, title), children && /*#__PURE__*/React.createElement("div", null, children)));
203
+ }
204
+ Object.assign(__ds_scope, { InlineAlert });
205
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/containers/InlineAlert.jsx", error: String((e && e.message) || e) }); }
206
+
207
+ // components/forms/Checkbox.jsx
208
+ try { (() => {
209
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
210
+ const CSS = `
211
+ .s2-check{ display:inline-flex; align-items:center; gap:var(--space-100); cursor:pointer; font:var(--type-ui); color:var(--text-body); user-select:none; }
212
+ .s2-check input{ position:absolute; opacity:0; width:0; height:0; }
213
+ .s2-check__box{
214
+ width:16px; height:16px; flex:none; border-radius:var(--radius-sm);
215
+ border:var(--border-width-200) solid var(--border-field); background:var(--surface-card);
216
+ display:grid; place-items:center; transition:var(--transition-colors);
217
+ }
218
+ .s2-check:hover .s2-check__box{ border-color:var(--text-secondary); }
219
+ .s2-check__box svg{ width:11px; height:11px; opacity:0; color:#fff; }
220
+ .s2-check input:checked + .s2-check__box{ background:var(--accent); border-color:var(--accent); }
221
+ .s2-check input:checked + .s2-check__box svg{ opacity:1; }
222
+ .s2-check input:focus-visible + .s2-check__box{ outline:var(--focus-ring-width) solid var(--focus-ring); outline-offset:var(--focus-ring-offset); }
223
+ .s2-check input:disabled + .s2-check__box{ background:var(--surface-subtle); border-color:var(--border-default); }
224
+ `;
225
+ let injected = false;
226
+ function useCSS() {
227
+ if (typeof document !== 'undefined' && !injected) {
228
+ const e = document.createElement('style');
229
+ e.textContent = CSS;
230
+ e.dataset.s2 = 'checkbox';
231
+ document.head.appendChild(e);
232
+ injected = true;
233
+ }
234
+ }
235
+ function Checkbox({
236
+ checked,
237
+ onChange,
238
+ children,
239
+ disabled = false,
240
+ ...rest
241
+ }) {
242
+ useCSS();
243
+ return /*#__PURE__*/React.createElement("label", {
244
+ className: "s2-check"
245
+ }, /*#__PURE__*/React.createElement("input", _extends({
246
+ type: "checkbox",
247
+ checked: checked,
248
+ onChange: onChange,
249
+ disabled: disabled
250
+ }, rest)), /*#__PURE__*/React.createElement("span", {
251
+ className: "s2-check__box"
252
+ }, /*#__PURE__*/React.createElement("svg", {
253
+ viewBox: "0 0 12 12",
254
+ fill: "none"
255
+ }, /*#__PURE__*/React.createElement("path", {
256
+ d: "M1 6l3.2 3.5L11 2.5",
257
+ stroke: "currentColor",
258
+ strokeWidth: "2",
259
+ strokeLinecap: "round",
260
+ strokeLinejoin: "round"
261
+ }))), children && /*#__PURE__*/React.createElement("span", null, children));
262
+ }
263
+ Object.assign(__ds_scope, { Checkbox });
264
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/forms/Checkbox.jsx", error: String((e && e.message) || e) }); }
265
+
266
+ // components/forms/Field.jsx
267
+ try { (() => {
268
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
269
+ const CSS = `
270
+ .s2-field{ display:flex; flex-direction:column; gap:var(--space-75); }
271
+ .s2-field__label{ font:var(--type-ui-bold); color:var(--text-heading); }
272
+ .s2-field__req{ color:var(--negative); margin-inline-start:2px; }
273
+ .s2-field__input{
274
+ height:var(--control-height-md); padding:0 var(--space-150);
275
+ font:var(--type-ui); color:var(--text-body);
276
+ background:var(--surface-card);
277
+ border:var(--border-width-200) solid var(--border-field);
278
+ border-radius:var(--field-radius); outline:none;
279
+ transition:var(--transition-colors), box-shadow var(--duration-fast) var(--ease-out);
280
+ }
281
+ .s2-field__input::placeholder{ color:var(--text-muted); }
282
+ .s2-field__input:hover{ border-color:var(--text-secondary); }
283
+ .s2-field__input:focus{ border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
284
+ .s2-field--textarea .s2-field__input{ height:auto; padding:var(--space-150); resize:vertical; line-height:var(--line-height-body); font-family:var(--font-sans); }
285
+ .s2-field--mono .s2-field__input{ font-family:var(--font-code); font-size:var(--font-size-75); }
286
+ .s2-field--invalid .s2-field__input{ border-color:var(--negative); }
287
+ .s2-field--invalid .s2-field__input:focus{ box-shadow:0 0 0 1px var(--negative); }
288
+ .s2-field__help{ font:var(--type-detail); color:var(--text-muted); }
289
+ .s2-field--invalid .s2-field__help{ color:var(--negative); }
290
+ .s2-field__input:disabled{ background:var(--surface-subtle); color:var(--text-disabled); border-color:var(--border-default); cursor:default; }
291
+ `;
292
+ let injected = false;
293
+ function useCSS() {
294
+ if (typeof document !== 'undefined' && !injected) {
295
+ const el = document.createElement('style');
296
+ el.textContent = CSS;
297
+ el.dataset.s2 = 'field';
298
+ document.head.appendChild(el);
299
+ injected = true;
300
+ }
301
+ }
302
+ function Field({
303
+ label,
304
+ value,
305
+ onChange,
306
+ placeholder,
307
+ type = 'text',
308
+ multiline = false,
309
+ rows = 4,
310
+ mono = false,
311
+ required = false,
312
+ invalid = false,
313
+ help,
314
+ disabled = false,
315
+ className = '',
316
+ ...rest
317
+ }) {
318
+ useCSS();
319
+ const cls = ['s2-field', multiline && 's2-field--textarea', mono && 's2-field--mono', invalid && 's2-field--invalid', className].filter(Boolean).join(' ');
320
+ const inputProps = {
321
+ className: 's2-field__input',
322
+ value,
323
+ onChange,
324
+ placeholder,
325
+ disabled,
326
+ ...rest
327
+ };
328
+ return /*#__PURE__*/React.createElement("label", {
329
+ className: cls
330
+ }, label && /*#__PURE__*/React.createElement("span", {
331
+ className: "s2-field__label"
332
+ }, label, required && /*#__PURE__*/React.createElement("span", {
333
+ className: "s2-field__req"
334
+ }, "*")), multiline ? /*#__PURE__*/React.createElement("textarea", _extends({
335
+ rows: rows
336
+ }, inputProps)) : /*#__PURE__*/React.createElement("input", _extends({
337
+ type: type
338
+ }, inputProps)), help && /*#__PURE__*/React.createElement("span", {
339
+ className: "s2-field__help"
340
+ }, help));
341
+ }
342
+ Object.assign(__ds_scope, { Field });
343
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/forms/Field.jsx", error: String((e && e.message) || e) }); }
344
+
345
+ // components/forms/Switch.jsx
346
+ try { (() => {
347
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
348
+ const CSS = `
349
+ .s2-switch{ display:inline-flex; align-items:center; gap:var(--space-150); cursor:pointer; font:var(--type-ui); color:var(--text-body); user-select:none; }
350
+ .s2-switch input{ position:absolute; opacity:0; width:0; height:0; }
351
+ .s2-switch__track{
352
+ width:32px; height:18px; flex:none; border-radius:var(--radius-full);
353
+ background:var(--s2-gray-300); position:relative; transition:background var(--duration-fast) var(--ease-out);
354
+ }
355
+ .s2-switch__track::after{
356
+ content:''; position:absolute; top:2px; left:2px; width:14px; height:14px;
357
+ border-radius:var(--radius-full); background:#fff; box-shadow:var(--shadow-raised);
358
+ transition:transform var(--duration-default) var(--ease-out);
359
+ }
360
+ .s2-switch:hover .s2-switch__track{ background:var(--s2-gray-400); }
361
+ .s2-switch input:checked + .s2-switch__track{ background:var(--accent); }
362
+ .s2-switch input:checked + .s2-switch__track::after{ transform:translateX(14px); }
363
+ .s2-switch input:focus-visible + .s2-switch__track{ outline:var(--focus-ring-width) solid var(--focus-ring); outline-offset:var(--focus-ring-offset); }
364
+ .s2-switch input:disabled + .s2-switch__track{ background:var(--border-default); }
365
+ `;
366
+ let injected = false;
367
+ function useCSS() {
368
+ if (typeof document !== 'undefined' && !injected) {
369
+ const e = document.createElement('style');
370
+ e.textContent = CSS;
371
+ e.dataset.s2 = 'switch';
372
+ document.head.appendChild(e);
373
+ injected = true;
374
+ }
375
+ }
376
+ function Switch({
377
+ checked,
378
+ onChange,
379
+ children,
380
+ disabled = false,
381
+ ...rest
382
+ }) {
383
+ useCSS();
384
+ return /*#__PURE__*/React.createElement("label", {
385
+ className: "s2-switch"
386
+ }, /*#__PURE__*/React.createElement("input", _extends({
387
+ type: "checkbox",
388
+ role: "switch",
389
+ checked: checked,
390
+ onChange: onChange,
391
+ disabled: disabled
392
+ }, rest)), /*#__PURE__*/React.createElement("span", {
393
+ className: "s2-switch__track"
394
+ }), children && /*#__PURE__*/React.createElement("span", null, children));
395
+ }
396
+ Object.assign(__ds_scope, { Switch });
397
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/forms/Switch.jsx", error: String((e && e.message) || e) }); }
398
+
399
+ // components/navigation/Tabs.jsx
400
+ try { (() => {
401
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
402
+ /* Spectrum 2 quiet tabs with an accent underline — the primary navigation in
403
+ Chan Compass (Signals / Rotation / News / Research / Automation / Model). */
404
+ const CSS = `
405
+ .s2-tabs{ display:flex; gap:var(--space-300); border-bottom:var(--border-width-200) solid var(--border-default); }
406
+ .s2-tab{
407
+ appearance:none; background:none; border:none; cursor:pointer;
408
+ padding:var(--space-150) 2px; margin-bottom:-2px;
409
+ font:var(--font-weight-semibold) var(--font-size-200)/1.2 var(--font-sans);
410
+ color:var(--text-muted); border-bottom:var(--border-width-200) solid transparent;
411
+ display:inline-flex; align-items:center; gap:var(--space-100);
412
+ transition:var(--transition-colors);
413
+ }
414
+ .s2-tab:hover{ color:var(--text-body); }
415
+ .s2-tab--selected{ color:var(--accent); border-bottom-color:var(--accent); }
416
+ .s2-tab:focus-visible{ outline:var(--focus-ring-width) solid var(--focus-ring); outline-offset:2px; border-radius:var(--radius-sm); }
417
+ .s2-tab__count{
418
+ font:var(--font-weight-semibold) var(--font-size-50)/1 var(--font-sans);
419
+ background:var(--surface-subtle); color:var(--text-secondary);
420
+ padding:2px 6px; border-radius:var(--radius-full);
421
+ }
422
+ .s2-tab--selected .s2-tab__count{ background:var(--accent-subtle); color:var(--accent-text); }
423
+ `;
424
+ let injected = false;
425
+ function useCSS() {
426
+ if (typeof document !== 'undefined' && !injected) {
427
+ const e = document.createElement('style');
428
+ e.textContent = CSS;
429
+ e.dataset.s2 = 'tabs';
430
+ document.head.appendChild(e);
431
+ injected = true;
432
+ }
433
+ }
434
+ function Tabs({
435
+ items = [],
436
+ value,
437
+ onChange,
438
+ className = '',
439
+ ...rest
440
+ }) {
441
+ useCSS();
442
+ return /*#__PURE__*/React.createElement("div", _extends({
443
+ className: ['s2-tabs', className].filter(Boolean).join(' '),
444
+ role: "tablist"
445
+ }, rest), items.map(it => {
446
+ const id = typeof it === 'string' ? it : it.id;
447
+ const label = typeof it === 'string' ? it : it.label;
448
+ const selected = id === value;
449
+ return /*#__PURE__*/React.createElement("button", {
450
+ key: id,
451
+ role: "tab",
452
+ "aria-selected": selected,
453
+ className: ['s2-tab', selected && 's2-tab--selected'].filter(Boolean).join(' '),
454
+ onClick: () => onChange && onChange(id)
455
+ }, typeof it === 'object' && it.icon, label, typeof it === 'object' && it.count != null && /*#__PURE__*/React.createElement("span", {
456
+ className: "s2-tab__count"
457
+ }, it.count));
458
+ }));
459
+ }
460
+ Object.assign(__ds_scope, { Tabs });
461
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/navigation/Tabs.jsx", error: String((e && e.message) || e) }); }
462
+
463
+ // components/status/Badge.jsx
464
+ try { (() => {
465
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
466
+ const CSS = `
467
+ .s2-badge{
468
+ display:inline-flex; align-items:center; gap:var(--space-75);
469
+ height:20px; padding:0 var(--space-150); border-radius:var(--radius-full);
470
+ font:var(--font-weight-semibold) var(--font-size-75)/1 var(--font-sans);
471
+ white-space:nowrap;
472
+ }
473
+ .s2-badge--neutral{ background:var(--surface-subtle); color:var(--text-secondary); }
474
+ .s2-badge--accent{ background:var(--accent-subtle); color:var(--accent-text); }
475
+ .s2-badge--positive{ background:var(--positive-subtle); color:var(--positive-hover); }
476
+ .s2-badge--negative{ background:var(--negative-subtle); color:var(--negative-hover); }
477
+ .s2-badge--notice{ background:var(--notice-subtle); color:var(--notice); }
478
+ .s2-badge--bold{ color:#fff; }
479
+ .s2-badge--bold.s2-badge--accent{ background:var(--accent); }
480
+ .s2-badge--bold.s2-badge--positive{ background:var(--positive); }
481
+ .s2-badge--bold.s2-badge--negative{ background:var(--negative); }
482
+ .s2-badge--bold.s2-badge--neutral{ background:var(--text-secondary); }
483
+ `;
484
+ let injected = false;
485
+ function useCSS() {
486
+ if (typeof document !== 'undefined' && !injected) {
487
+ const e = document.createElement('style');
488
+ e.textContent = CSS;
489
+ e.dataset.s2 = 'badge';
490
+ document.head.appendChild(e);
491
+ injected = true;
492
+ }
493
+ }
494
+ function Badge({
495
+ variant = 'neutral',
496
+ bold = false,
497
+ children,
498
+ className = '',
499
+ ...rest
500
+ }) {
501
+ useCSS();
502
+ const cls = ['s2-badge', `s2-badge--${variant}`, bold && 's2-badge--bold', className].filter(Boolean).join(' ');
503
+ return /*#__PURE__*/React.createElement("span", _extends({
504
+ className: cls
505
+ }, rest), children);
506
+ }
507
+ Object.assign(__ds_scope, { Badge });
508
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/status/Badge.jsx", error: String((e && e.message) || e) }); }
509
+
510
+ // components/status/SignalBadge.jsx
511
+ try { (() => {
512
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
513
+ /* Domain component: the BUY / SELL / HOLD / WAIT signal pill that drives the
514
+ Signals table. Maps a Chan-engine action to a Spectrum 2 semantic color. */
515
+ const CSS = `
516
+ .s2-signal{
517
+ display:inline-flex; align-items:center; gap:6px;
518
+ height:24px; padding:0 var(--space-150); border-radius:var(--radius-full);
519
+ font:var(--font-weight-bold) var(--font-size-75)/1 var(--font-sans);
520
+ letter-spacing:.04em; text-transform:uppercase; white-space:nowrap; color:#fff;
521
+ }
522
+ .s2-signal__glyph{ width:7px; height:7px; border-radius:var(--radius-full); background:currentColor; opacity:.9; }
523
+ .s2-signal--buy{ background:var(--positive); }
524
+ .s2-signal--sell{ background:var(--negative); }
525
+ .s2-signal--hold{ background:var(--accent); }
526
+ .s2-signal--wait{ background:var(--surface-subtle); color:var(--text-secondary); }
527
+ .s2-signal--watch{ background:var(--notice); }
528
+ `;
529
+ let injected = false;
530
+ function useCSS() {
531
+ if (typeof document !== 'undefined' && !injected) {
532
+ const e = document.createElement('style');
533
+ e.textContent = CSS;
534
+ e.dataset.s2 = 'signal';
535
+ document.head.appendChild(e);
536
+ injected = true;
537
+ }
538
+ }
539
+ const MAP = {
540
+ BUY: 'buy',
541
+ SELL: 'sell',
542
+ HOLD: 'hold',
543
+ WAIT: 'wait',
544
+ WATCH: 'watch'
545
+ };
546
+ function SignalBadge({
547
+ action = 'WAIT',
548
+ className = '',
549
+ ...rest
550
+ }) {
551
+ useCSS();
552
+ const key = String(action).toUpperCase();
553
+ const kind = MAP[key] || 'wait';
554
+ return /*#__PURE__*/React.createElement("span", _extends({
555
+ className: ['s2-signal', `s2-signal--${kind}`, className].filter(Boolean).join(' ')
556
+ }, rest), /*#__PURE__*/React.createElement("span", {
557
+ className: "s2-signal__glyph"
558
+ }), key);
559
+ }
560
+ Object.assign(__ds_scope, { SignalBadge });
561
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/status/SignalBadge.jsx", error: String((e && e.message) || e) }); }
562
+
563
+ // components/status/StatusLight.jsx
564
+ try { (() => {
565
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
566
+ const CSS = `
567
+ .s2-status{ display:inline-flex; align-items:center; gap:var(--space-100); font:var(--type-ui); color:var(--text-body); }
568
+ .s2-status__dot{ width:9px; height:9px; flex:none; border-radius:var(--radius-full); background:var(--flat); }
569
+ .s2-status--positive .s2-status__dot{ background:var(--positive); }
570
+ .s2-status--negative .s2-status__dot{ background:var(--negative); }
571
+ .s2-status--notice .s2-status__dot{ background:var(--notice); }
572
+ .s2-status--accent .s2-status__dot{ background:var(--accent); }
573
+ .s2-status--neutral .s2-status__dot{ background:var(--text-muted); }
574
+ .s2-status--live .s2-status__dot{ box-shadow:0 0 0 0 currentColor; animation:s2-pulse 1.8s var(--ease-out) infinite; color:var(--positive); }
575
+ @keyframes s2-pulse{ 0%{ box-shadow:0 0 0 0 rgba(0,122,57,.35);} 70%{ box-shadow:0 0 0 6px rgba(0,122,57,0);} 100%{ box-shadow:0 0 0 0 rgba(0,122,57,0);} }
576
+ `;
577
+ let injected = false;
578
+ function useCSS() {
579
+ if (typeof document !== 'undefined' && !injected) {
580
+ const e = document.createElement('style');
581
+ e.textContent = CSS;
582
+ e.dataset.s2 = 'status';
583
+ document.head.appendChild(e);
584
+ injected = true;
585
+ }
586
+ }
587
+ function StatusLight({
588
+ variant = 'neutral',
589
+ live = false,
590
+ children,
591
+ className = '',
592
+ ...rest
593
+ }) {
594
+ useCSS();
595
+ const cls = ['s2-status', `s2-status--${variant}`, live && 's2-status--live', className].filter(Boolean).join(' ');
596
+ return /*#__PURE__*/React.createElement("span", _extends({
597
+ className: cls
598
+ }, rest), /*#__PURE__*/React.createElement("span", {
599
+ className: "s2-status__dot"
600
+ }), children);
601
+ }
602
+ Object.assign(__ds_scope, { StatusLight });
603
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "components/status/StatusLight.jsx", error: String((e && e.message) || e) }); }
604
+
605
+ // ui_kits/chan-compass/app.jsx
606
+ try { (() => {
607
+ /* Chan Compass UI kit — app shell. Header + Tabs + routed content + footer. */
608
+ const _DS = window.ChanCompassSpectrum2DesignSystem_b52b37;
609
+ const {
610
+ Tabs: CCTabs,
611
+ StatusLight: CCStatus,
612
+ Badge: CCBadge
613
+ } = _DS;
614
+ const {
615
+ useState: useS,
616
+ useEffect: useE
617
+ } = React;
618
+ function Glyph({
619
+ name,
620
+ size = 18,
621
+ color,
622
+ style
623
+ }) {
624
+ useE(() => {
625
+ if (window.lucide) window.lucide.createIcons();
626
+ });
627
+ return /*#__PURE__*/React.createElement("i", {
628
+ "data-lucide": name,
629
+ style: {
630
+ width: size,
631
+ height: size,
632
+ color,
633
+ display: 'inline-flex',
634
+ ...style
635
+ }
636
+ });
637
+ }
638
+ function App() {
639
+ const [tab, setTab] = useS('signals');
640
+ const items = [{
641
+ id: 'signals',
642
+ label: 'Signals',
643
+ icon: /*#__PURE__*/React.createElement(Glyph, {
644
+ name: "trending-up",
645
+ size: 16
646
+ })
647
+ }, {
648
+ id: 'rotation',
649
+ label: 'Sector Rotation',
650
+ icon: /*#__PURE__*/React.createElement(Glyph, {
651
+ name: "refresh-cw",
652
+ size: 16
653
+ })
654
+ }, {
655
+ id: 'news',
656
+ label: 'Watchlist News',
657
+ icon: /*#__PURE__*/React.createElement(Glyph, {
658
+ name: "newspaper",
659
+ size: 16
660
+ }),
661
+ count: 2
662
+ }, {
663
+ id: 'research',
664
+ label: 'Auto Research',
665
+ icon: /*#__PURE__*/React.createElement(Glyph, {
666
+ name: "flask-conical",
667
+ size: 16
668
+ })
669
+ }, {
670
+ id: 'model',
671
+ label: 'Model',
672
+ icon: /*#__PURE__*/React.createElement(Glyph, {
673
+ name: "cpu",
674
+ size: 16
675
+ })
676
+ }];
677
+ const View = {
678
+ signals: window.SignalsView,
679
+ rotation: window.RotationView,
680
+ news: window.NewsView,
681
+ research: window.ResearchView,
682
+ model: window.ModelView
683
+ }[tab];
684
+ return /*#__PURE__*/React.createElement("div", {
685
+ className: "cc-app"
686
+ }, /*#__PURE__*/React.createElement("header", {
687
+ className: "cc-header"
688
+ }, /*#__PURE__*/React.createElement("div", {
689
+ className: "cc-brand"
690
+ }, /*#__PURE__*/React.createElement("div", {
691
+ className: "cc-mark"
692
+ }, /*#__PURE__*/React.createElement(Glyph, {
693
+ name: "compass",
694
+ size: 22,
695
+ color: "#fff"
696
+ })), /*#__PURE__*/React.createElement("div", {
697
+ className: "cc-brand-text"
698
+ }, /*#__PURE__*/React.createElement("div", {
699
+ className: "cc-wordmark"
700
+ }, "Chan Compass ", /*#__PURE__*/React.createElement("span", null, "\xB7 US Markets")), /*#__PURE__*/React.createElement("div", {
701
+ className: "cc-tagline"
702
+ }, "Multi-timeframe \u7F20\u8BBA signal engine \xB7 fully local"))), /*#__PURE__*/React.createElement("div", {
703
+ className: "cc-header-right"
704
+ }, /*#__PURE__*/React.createElement(CCBadge, {
705
+ variant: "accent"
706
+ }, /*#__PURE__*/React.createElement(Glyph, {
707
+ name: "cpu",
708
+ size: 12,
709
+ style: {
710
+ marginRight: 4
711
+ }
712
+ }), " Local \xB7 llama.cpp"), /*#__PURE__*/React.createElement(CCStatus, {
713
+ variant: "positive",
714
+ live: true
715
+ }, "Market open"))), /*#__PURE__*/React.createElement("div", {
716
+ className: "cc-navbar"
717
+ }, /*#__PURE__*/React.createElement(CCTabs, {
718
+ value: tab,
719
+ onChange: setTab,
720
+ items: items
721
+ })), /*#__PURE__*/React.createElement("main", {
722
+ className: "cc-main"
723
+ }, View ? /*#__PURE__*/React.createElement(View, null) : null), /*#__PURE__*/React.createElement("footer", {
724
+ className: "cc-footer"
725
+ }, /*#__PURE__*/React.createElement("span", null, "Chan Compass \xB7 educational tool, not investment advice"), /*#__PURE__*/React.createElement("span", {
726
+ className: "cc-foot-sep"
727
+ }, "\xB7"), /*#__PURE__*/React.createElement("span", null, "data: Yahoo Finance"), /*#__PURE__*/React.createElement("span", {
728
+ className: "cc-foot-sep"
729
+ }, "\xB7"), /*#__PURE__*/React.createElement("span", null, "design language: Adobe Spectrum 2")));
730
+ }
731
+ ReactDOM.createRoot(document.getElementById('root')).render(/*#__PURE__*/React.createElement(App, null));
732
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/chan-compass/app.jsx", error: String((e && e.message) || e) }); }
733
+
734
+ // ui_kits/chan-compass/data.js
735
+ try { (() => {
736
+ // Fake data for the Chan Compass UI kit — shapes mirror the real signal_runner /
737
+ // rotation engine output, values are illustrative.
738
+ window.CC_DATA = {
739
+ asof: 'Jun 12, 2026 · 18:10 ET',
740
+ signals: [{
741
+ t: 'NVDA',
742
+ action: 'BUY',
743
+ price: 178.42,
744
+ chg: +2.43,
745
+ conf: 0.82,
746
+ weight: 8,
747
+ stop: 172.40,
748
+ zone: '176.9 – 179.2',
749
+ note: 'Weekly pivot uplift confirmed; daily B1 armed off divergence low.'
750
+ }, {
751
+ t: 'MSFT',
752
+ action: 'BUY',
753
+ price: 472.10,
754
+ chg: +0.91,
755
+ conf: 0.74,
756
+ weight: 6,
757
+ stop: 458.00,
758
+ zone: '468 – 474',
759
+ note: '30m/15m nested-interval bottom confirms weekly hold-and-add.'
760
+ }, {
761
+ t: 'AMD',
762
+ action: 'HOLD',
763
+ price: 142.88,
764
+ chg: +1.12,
765
+ conf: 0.61,
766
+ weight: 5,
767
+ stop: 133.50,
768
+ zone: '—',
769
+ note: 'Unconfirmed daily sell inside weekly uptrend → hold (armed).'
770
+ }, {
771
+ t: 'AAPL',
772
+ action: 'HOLD',
773
+ price: 214.55,
774
+ chg: -0.34,
775
+ conf: 0.58,
776
+ weight: 5,
777
+ stop: 205.10,
778
+ zone: '—',
779
+ note: 'Riding pivot; no sublevel sell confirmation yet.'
780
+ }, {
781
+ t: 'TSLA',
782
+ action: 'SELL',
783
+ price: 243.17,
784
+ chg: -3.08,
785
+ conf: 0.69,
786
+ weight: 0,
787
+ stop: 251.00,
788
+ zone: '242 – 246',
789
+ note: 'Daily S3 pivot breakdown; nested-interval top confirmed.'
790
+ }, {
791
+ t: 'META',
792
+ action: 'WATCH',
793
+ price: 611.40,
794
+ chg: +0.22,
795
+ conf: 0.44,
796
+ weight: 0,
797
+ stop: '—',
798
+ zone: '—',
799
+ note: 'Approaching weekly pivot edge; no signal armed.'
800
+ }, {
801
+ t: 'AMZN',
802
+ action: 'WAIT',
803
+ price: 201.66,
804
+ chg: -0.51,
805
+ conf: 0.39,
806
+ weight: 0,
807
+ stop: '—',
808
+ zone: '—',
809
+ note: 'Inside consolidation; wait for level resolution.'
810
+ }, {
811
+ t: 'GOOGL',
812
+ action: 'WAIT',
813
+ price: 182.03,
814
+ chg: +0.08,
815
+ conf: 0.35,
816
+ weight: 0,
817
+ stop: '—',
818
+ zone: '—',
819
+ note: 'No multi-timeframe alignment.'
820
+ }],
821
+ rotation: [{
822
+ s: 'Technology',
823
+ sym: 'XLK',
824
+ d1: +1.84,
825
+ d5: +3.2,
826
+ d20: +6.1,
827
+ rs: +2.4
828
+ }, {
829
+ s: 'Comm. Services',
830
+ sym: 'XLC',
831
+ d1: +1.12,
832
+ d5: +2.1,
833
+ d20: +4.4,
834
+ rs: +1.6
835
+ }, {
836
+ s: 'Financials',
837
+ sym: 'XLF',
838
+ d1: +0.64,
839
+ d5: +1.0,
840
+ d20: +2.2,
841
+ rs: +0.5
842
+ }, {
843
+ s: 'Energy',
844
+ sym: 'XLE',
845
+ d1: +0.41,
846
+ d5: -0.8,
847
+ d20: -1.9,
848
+ rs: -1.1
849
+ }, {
850
+ s: 'Industrials',
851
+ sym: 'XLI',
852
+ d1: +0.18,
853
+ d5: +0.6,
854
+ d20: +1.1,
855
+ rs: +0.2
856
+ }, {
857
+ s: 'Health Care',
858
+ sym: 'XLV',
859
+ d1: -0.22,
860
+ d5: -0.4,
861
+ d20: +0.3,
862
+ rs: -0.4
863
+ }, {
864
+ s: 'Cons. Staples',
865
+ sym: 'XLP',
866
+ d1: -0.38,
867
+ d5: -0.9,
868
+ d20: -1.2,
869
+ rs: -0.9
870
+ }, {
871
+ s: 'Utilities',
872
+ sym: 'XLU',
873
+ d1: -0.51,
874
+ d5: -1.3,
875
+ d20: -2.0,
876
+ rs: -1.3
877
+ }, {
878
+ s: 'Real Estate',
879
+ sym: 'XLRE',
880
+ d1: -0.74,
881
+ d5: -1.6,
882
+ d20: -2.8,
883
+ rs: -1.7
884
+ }, {
885
+ s: 'Materials',
886
+ sym: 'XLB',
887
+ d1: -0.88,
888
+ d5: -1.1,
889
+ d20: -1.5,
890
+ rs: -0.8
891
+ }, {
892
+ s: 'Cons. Disc.',
893
+ sym: 'XLY',
894
+ d1: -1.06,
895
+ d5: -2.0,
896
+ d20: -3.4,
897
+ rs: -2.1
898
+ }],
899
+ reports: ['NVDA · 2026-06-12', 'TSLA · 2026-06-11', 'MSFT · 2026-06-10', 'AMD · 2026-06-09']
900
+ };
901
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/chan-compass/data.js", error: String((e && e.message) || e) }); }
902
+
903
+ // ui_kits/chan-compass/views.jsx
904
+ try { (() => {
905
+ /* Chan Compass UI kit — view components. Each consumes the design-system
906
+ primitives from the bundle namespace + Lucide line icons. Exported to window
907
+ for app.jsx to compose. */
908
+ const DS = window.ChanCompassSpectrum2DesignSystem_b52b37;
909
+ const {
910
+ Button,
911
+ Field,
912
+ Checkbox,
913
+ Switch,
914
+ Card,
915
+ InlineAlert,
916
+ Badge,
917
+ StatusLight,
918
+ SignalBadge,
919
+ Tabs
920
+ } = DS;
921
+ const {
922
+ useState,
923
+ useEffect
924
+ } = React;
925
+ function Icon({
926
+ name,
927
+ size = 18,
928
+ color,
929
+ style
930
+ }) {
931
+ // Lucide renders by replacing <i data-lucide>. Re-run after mount.
932
+ useEffect(() => {
933
+ if (window.lucide) window.lucide.createIcons();
934
+ });
935
+ return /*#__PURE__*/React.createElement("i", {
936
+ "data-lucide": name,
937
+ style: {
938
+ width: size,
939
+ height: size,
940
+ color,
941
+ display: 'inline-flex',
942
+ ...style
943
+ }
944
+ });
945
+ }
946
+ const fmtPct = v => typeof v === 'number' ? (v > 0 ? '+' : '') + v.toFixed(2) + '%' : v;
947
+ const dirColor = v => v > 0 ? 'var(--up)' : v < 0 ? 'var(--down)' : 'var(--flat)';
948
+
949
+ /* ─────────────────────────── KPI strip ─────────────────────────── */
950
+ function Kpi({
951
+ label,
952
+ value,
953
+ tone
954
+ }) {
955
+ return /*#__PURE__*/React.createElement("div", {
956
+ style: {
957
+ flex: 1,
958
+ padding: '14px 18px',
959
+ background: 'var(--surface-card)',
960
+ border: '1px solid var(--border-hairline)',
961
+ borderRadius: 'var(--radius-lg)',
962
+ boxShadow: 'var(--shadow-raised)'
963
+ }
964
+ }, /*#__PURE__*/React.createElement("div", {
965
+ style: {
966
+ font: 'var(--font-weight-bold) 28px/1 var(--font-sans)',
967
+ color: tone || 'var(--text-heading)'
968
+ }
969
+ }, value), /*#__PURE__*/React.createElement("div", {
970
+ style: {
971
+ font: 'var(--type-detail)',
972
+ color: 'var(--text-muted)',
973
+ marginTop: 4
974
+ }
975
+ }, label));
976
+ }
977
+
978
+ /* ─────────────────────────── Signals ─────────────────────────── */
979
+ function SignalsView() {
980
+ const D = window.CC_DATA;
981
+ const [pool, setPool] = useState('AAPL, MSFT, NVDA, TSLA, AMZN, GOOGL, META, AMD');
982
+ const [force, setForce] = useState(false);
983
+ const [running, setRunning] = useState(false);
984
+ const [ran, setRan] = useState(true);
985
+ const [sel, setSel] = useState('NVDA');
986
+ const [explained, setExplained] = useState(false);
987
+ const rows = D.signals;
988
+ const counts = rows.reduce((a, r) => {
989
+ a[r.action] = (a[r.action] || 0) + 1;
990
+ return a;
991
+ }, {});
992
+ const cur = rows.find(r => r.t === sel) || rows[0];
993
+ const run = () => {
994
+ setRunning(true);
995
+ setRan(false);
996
+ setExplained(false);
997
+ setTimeout(() => {
998
+ setRunning(false);
999
+ setRan(true);
1000
+ }, 1100);
1001
+ };
1002
+ return /*#__PURE__*/React.createElement("div", {
1003
+ style: {
1004
+ display: 'flex',
1005
+ flexDirection: 'column',
1006
+ gap: 'var(--space-300)'
1007
+ }
1008
+ }, /*#__PURE__*/React.createElement("div", {
1009
+ style: {
1010
+ display: 'flex',
1011
+ gap: 'var(--space-200)'
1012
+ }
1013
+ }, /*#__PURE__*/React.createElement(Kpi, {
1014
+ label: "BUY signals",
1015
+ value: counts.BUY || 0,
1016
+ tone: "var(--up)"
1017
+ }), /*#__PURE__*/React.createElement(Kpi, {
1018
+ label: "SELL signals",
1019
+ value: counts.SELL || 0,
1020
+ tone: "var(--down)"
1021
+ }), /*#__PURE__*/React.createElement(Kpi, {
1022
+ label: "Holding",
1023
+ value: counts.HOLD || 0,
1024
+ tone: "var(--accent)"
1025
+ }), /*#__PURE__*/React.createElement(Kpi, {
1026
+ label: "Watch / wait",
1027
+ value: (counts.WATCH || 0) + (counts.WAIT || 0)
1028
+ })), /*#__PURE__*/React.createElement(Card, {
1029
+ title: "Tomorrow's plan",
1030
+ subtitle: "Long-hold mode \xB7 sorted BUY \u2192 SELL \u2192 HOLD \u2192 WAIT",
1031
+ action: /*#__PURE__*/React.createElement(Badge, {
1032
+ variant: "neutral"
1033
+ }, D.asof)
1034
+ }, /*#__PURE__*/React.createElement("div", {
1035
+ style: {
1036
+ display: 'flex',
1037
+ gap: 'var(--space-200)',
1038
+ alignItems: 'flex-end',
1039
+ marginBottom: 'var(--space-250)'
1040
+ }
1041
+ }, /*#__PURE__*/React.createElement(Field, {
1042
+ label: "Ticker pool",
1043
+ mono: true,
1044
+ value: pool,
1045
+ onChange: e => setPool(e.target.value),
1046
+ className: "grow",
1047
+ help: "Comma separated \xB7 the unchanged Chan engine runs over each"
1048
+ }), /*#__PURE__*/React.createElement(Checkbox, {
1049
+ checked: force,
1050
+ onChange: e => setForce(e.target.checked)
1051
+ }, "Force fresh download"), /*#__PURE__*/React.createElement(Button, {
1052
+ variant: "accent",
1053
+ onClick: run,
1054
+ disabled: running
1055
+ }, running ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Icon, {
1056
+ name: "loader",
1057
+ size: 15
1058
+ }), " Running\u2026") : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Icon, {
1059
+ name: "play",
1060
+ size: 15
1061
+ }), " Run analysis"))), /*#__PURE__*/React.createElement("div", {
1062
+ className: "cc-table"
1063
+ }, /*#__PURE__*/React.createElement("table", null, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "Ticker"), /*#__PURE__*/React.createElement("th", null, "Signal"), /*#__PURE__*/React.createElement("th", {
1064
+ className: "num"
1065
+ }, "Price"), /*#__PURE__*/React.createElement("th", {
1066
+ className: "num"
1067
+ }, "\u0394 Day"), /*#__PURE__*/React.createElement("th", null, "Buy/sell zone"), /*#__PURE__*/React.createElement("th", {
1068
+ className: "num"
1069
+ }, "Conf."), /*#__PURE__*/React.createElement("th", {
1070
+ className: "num"
1071
+ }, "Stop"), /*#__PURE__*/React.createElement("th", {
1072
+ className: "num"
1073
+ }, "Wt %"))), /*#__PURE__*/React.createElement("tbody", null, rows.map(r => /*#__PURE__*/React.createElement("tr", {
1074
+ key: r.t,
1075
+ className: r.t === sel ? 'sel' : '',
1076
+ onClick: () => {
1077
+ setSel(r.t);
1078
+ setExplained(false);
1079
+ }
1080
+ }, /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement("span", {
1081
+ className: "mono bold"
1082
+ }, r.t)), /*#__PURE__*/React.createElement("td", null, /*#__PURE__*/React.createElement(SignalBadge, {
1083
+ action: r.action
1084
+ })), /*#__PURE__*/React.createElement("td", {
1085
+ className: "num mono"
1086
+ }, r.price.toFixed(2)), /*#__PURE__*/React.createElement("td", {
1087
+ className: "num mono",
1088
+ style: {
1089
+ color: dirColor(r.chg)
1090
+ }
1091
+ }, fmtPct(r.chg)), /*#__PURE__*/React.createElement("td", {
1092
+ className: "mono dim"
1093
+ }, r.zone), /*#__PURE__*/React.createElement("td", {
1094
+ className: "num"
1095
+ }, /*#__PURE__*/React.createElement(ConfBar, {
1096
+ v: r.conf
1097
+ })), /*#__PURE__*/React.createElement("td", {
1098
+ className: "num mono dim"
1099
+ }, r.stop === '—' ? '—' : typeof r.stop === 'number' ? r.stop.toFixed(2) : r.stop), /*#__PURE__*/React.createElement("td", {
1100
+ className: "num mono"
1101
+ }, r.weight ? r.weight : '—'))))))), /*#__PURE__*/React.createElement("div", {
1102
+ style: {
1103
+ display: 'grid',
1104
+ gridTemplateColumns: '1fr 1fr',
1105
+ gap: 'var(--space-300)',
1106
+ alignItems: 'start'
1107
+ }
1108
+ }, /*#__PURE__*/React.createElement(Card, {
1109
+ title: `Decision log · ${cur.t}`,
1110
+ subtitle: "The engine's full multi-timeframe ruling chain"
1111
+ }, /*#__PURE__*/React.createElement("div", {
1112
+ className: "cc-log"
1113
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
1114
+ className: "mono dim"
1115
+ }, "\u6708\u7EBF monthly"), " \u4E0A\u6DA8\u8D8B\u52BF \xB7 \u4E2D\u67A2\u4E0A\u79FB \u2713"), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
1116
+ className: "mono dim"
1117
+ }, "\u5468\u7EBF weekly"), " \u7B2C\u4E09\u7C7B\u4E70\u70B9 B3 \xB7 \u64CD\u4F5C\u7EA7\u522B \u2713"), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
1118
+ className: "mono dim"
1119
+ }, "\u65E5\u7EBF daily"), " ", cur.action === 'SELL' ? 'S3 中枢破位 · 确认顶背驰' : 'B1 背驰 · 已武装 (armed)', " "), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
1120
+ className: "mono dim"
1121
+ }, "60m / 30m"), " \u533A\u95F4\u5957\u786E\u8BA4 nested-interval \u2713"), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
1122
+ className: "mono dim"
1123
+ }, "15m / 5m"), " \u6B21\u7EA7\u522B", cur.action === 'SELL' ? '卖点确认' : '买点未确认 → 持有'), /*#__PURE__*/React.createElement("div", {
1124
+ className: "cc-log-note"
1125
+ }, cur.note)), /*#__PURE__*/React.createElement("div", {
1126
+ style: {
1127
+ marginTop: 'var(--space-200)'
1128
+ }
1129
+ }, /*#__PURE__*/React.createElement(Button, {
1130
+ variant: "secondary",
1131
+ size: "sm",
1132
+ onClick: () => setExplained(true)
1133
+ }, /*#__PURE__*/React.createElement(Icon, {
1134
+ name: "languages",
1135
+ size: 14
1136
+ }), " Explain in English (local LLM)"))), explained ? /*#__PURE__*/React.createElement(InlineAlert, {
1137
+ variant: "ai",
1138
+ title: "Translator sub-agent \xB7 Qwen3-1.7B \xB7 llama.cpp"
1139
+ }, /*#__PURE__*/React.createElement("div", {
1140
+ style: {
1141
+ display: 'flex',
1142
+ flexDirection: 'column',
1143
+ gap: 6,
1144
+ marginTop: 4
1145
+ }
1146
+ }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("b", null, "Action:"), " ", cur.action, cur.action === 'BUY' ? ' — add on weekly pivot uplift' : cur.action === 'SELL' ? ' — exit, top confirmed' : ' — hold the position', "."), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("b", null, "Why:"), " Monthly & weekly gates passed; daily ", cur.action === 'SELL' ? 'broke the pivot (S3)' : 'B1 armed off the divergence low', "; intraday ", cur.action === 'SELL' ? 'confirmed the top' : 'has not confirmed a sell', "."), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("b", null, "Invalidation:"), " ", cur.stop === '—' ? 'no active stop' : `flips below ${typeof cur.stop === 'number' ? cur.stop.toFixed(2) : cur.stop}`, "."))) : /*#__PURE__*/React.createElement("div", {
1147
+ className: "cc-empty"
1148
+ }, /*#__PURE__*/React.createElement(Icon, {
1149
+ name: "sparkles",
1150
+ size: 22,
1151
+ color: "var(--text-disabled)"
1152
+ }), /*#__PURE__*/React.createElement("div", null, "AI explanation appears here.", /*#__PURE__*/React.createElement("br", null), "Select a ticker, then \u201CExplain in English\u201D."))));
1153
+ }
1154
+ function ConfBar({
1155
+ v
1156
+ }) {
1157
+ return /*#__PURE__*/React.createElement("span", {
1158
+ style: {
1159
+ display: 'inline-flex',
1160
+ alignItems: 'center',
1161
+ gap: 6,
1162
+ justifyContent: 'flex-end'
1163
+ }
1164
+ }, /*#__PURE__*/React.createElement("span", {
1165
+ style: {
1166
+ width: 46,
1167
+ height: 6,
1168
+ borderRadius: 'var(--radius-full)',
1169
+ background: 'var(--surface-subtle)',
1170
+ overflow: 'hidden'
1171
+ }
1172
+ }, /*#__PURE__*/React.createElement("span", {
1173
+ style: {
1174
+ display: 'block',
1175
+ height: '100%',
1176
+ width: `${v * 100}%`,
1177
+ background: v > 0.7 ? 'var(--positive)' : v > 0.5 ? 'var(--accent)' : 'var(--s2-gray-400)'
1178
+ }
1179
+ })), /*#__PURE__*/React.createElement("span", {
1180
+ className: "mono dim",
1181
+ style: {
1182
+ fontSize: 11
1183
+ }
1184
+ }, Math.round(v * 100)));
1185
+ }
1186
+
1187
+ /* ─────────────────────────── Rotation ─────────────────────────── */
1188
+ function RotationView() {
1189
+ const D = window.CC_DATA;
1190
+ const [ai, setAi] = useState(false);
1191
+ const max = Math.max(...D.rotation.map(r => Math.abs(r.d1)));
1192
+ return /*#__PURE__*/React.createElement("div", {
1193
+ style: {
1194
+ display: 'flex',
1195
+ flexDirection: 'column',
1196
+ gap: 'var(--space-300)'
1197
+ }
1198
+ }, /*#__PURE__*/React.createElement(InlineAlert, {
1199
+ variant: "informative",
1200
+ title: "Where capital is flowing"
1201
+ }, "11 SPDR sector ETFs (full S&P 500). Flow proxy = change % \xD7 dollar volume; RS = return minus SPY."), /*#__PURE__*/React.createElement("div", {
1202
+ style: {
1203
+ display: 'grid',
1204
+ gridTemplateColumns: '1.4fr 1fr',
1205
+ gap: 'var(--space-300)',
1206
+ alignItems: 'start'
1207
+ }
1208
+ }, /*#__PURE__*/React.createElement(Card, {
1209
+ title: "Sector rotation",
1210
+ subtitle: "1-day flow \xB7 today",
1211
+ action: /*#__PURE__*/React.createElement(Button, {
1212
+ variant: "secondary",
1213
+ size: "sm"
1214
+ }, /*#__PURE__*/React.createElement(Icon, {
1215
+ name: "refresh-cw",
1216
+ size: 14
1217
+ }), " Refresh")
1218
+ }, /*#__PURE__*/React.createElement("div", {
1219
+ className: "cc-flow"
1220
+ }, D.rotation.map(r => /*#__PURE__*/React.createElement("div", {
1221
+ key: r.sym,
1222
+ className: "cc-flow-row"
1223
+ }, /*#__PURE__*/React.createElement("span", {
1224
+ className: "cc-flow-name"
1225
+ }, /*#__PURE__*/React.createElement("span", {
1226
+ className: "mono bold"
1227
+ }, r.sym), " ", /*#__PURE__*/React.createElement("span", {
1228
+ className: "dim"
1229
+ }, r.s)), /*#__PURE__*/React.createElement("span", {
1230
+ className: "cc-flow-track"
1231
+ }, /*#__PURE__*/React.createElement("span", {
1232
+ className: "cc-flow-bar",
1233
+ style: {
1234
+ width: `${Math.abs(r.d1) / max * 50}%`,
1235
+ marginLeft: r.d1 < 0 ? `${50 - Math.abs(r.d1) / max * 50}%` : '50%',
1236
+ background: dirColor(r.d1)
1237
+ }
1238
+ }), /*#__PURE__*/React.createElement("span", {
1239
+ className: "cc-flow-mid"
1240
+ })), /*#__PURE__*/React.createElement("span", {
1241
+ className: "num mono",
1242
+ style: {
1243
+ color: dirColor(r.d1),
1244
+ width: 54
1245
+ }
1246
+ }, fmtPct(r.d1)))))), /*#__PURE__*/React.createElement("div", {
1247
+ style: {
1248
+ display: 'flex',
1249
+ flexDirection: 'column',
1250
+ gap: 'var(--space-200)'
1251
+ }
1252
+ }, /*#__PURE__*/React.createElement(Card, {
1253
+ title: "Multi-window trend",
1254
+ elevation: "raised"
1255
+ }, /*#__PURE__*/React.createElement("div", {
1256
+ className: "cc-table compact"
1257
+ }, /*#__PURE__*/React.createElement("table", null, /*#__PURE__*/React.createElement("thead", null, /*#__PURE__*/React.createElement("tr", null, /*#__PURE__*/React.createElement("th", null, "ETF"), /*#__PURE__*/React.createElement("th", {
1258
+ className: "num"
1259
+ }, "5-day"), /*#__PURE__*/React.createElement("th", {
1260
+ className: "num"
1261
+ }, "20-day"), /*#__PURE__*/React.createElement("th", {
1262
+ className: "num"
1263
+ }, "RS vs SPY"))), /*#__PURE__*/React.createElement("tbody", null, D.rotation.slice(0, 6).map(r => /*#__PURE__*/React.createElement("tr", {
1264
+ key: r.sym
1265
+ }, /*#__PURE__*/React.createElement("td", {
1266
+ className: "mono bold"
1267
+ }, r.sym), /*#__PURE__*/React.createElement("td", {
1268
+ className: "num mono",
1269
+ style: {
1270
+ color: dirColor(r.d5)
1271
+ }
1272
+ }, fmtPct(r.d5)), /*#__PURE__*/React.createElement("td", {
1273
+ className: "num mono",
1274
+ style: {
1275
+ color: dirColor(r.d20)
1276
+ }
1277
+ }, fmtPct(r.d20)), /*#__PURE__*/React.createElement("td", {
1278
+ className: "num mono",
1279
+ style: {
1280
+ color: dirColor(r.rs)
1281
+ }
1282
+ }, fmtPct(r.rs)))))))), /*#__PURE__*/React.createElement(Button, {
1283
+ variant: "accent",
1284
+ onClick: () => setAi(true)
1285
+ }, /*#__PURE__*/React.createElement(Icon, {
1286
+ name: "bot",
1287
+ size: 15
1288
+ }), " AI narrative (local LLM)"), ai && /*#__PURE__*/React.createElement(InlineAlert, {
1289
+ variant: "ai",
1290
+ title: "Narrator sub-agent \xB7 Qwen3-1.7B"
1291
+ }, "Capital is rotating ", /*#__PURE__*/React.createElement("b", null, "into Technology & Comm. Services"), " (XLK +1.84%, leading on both 5- and 20-day) and", /*#__PURE__*/React.createElement("b", null, " out of Consumer Discretionary & Real Estate"), ". Today's moves agree with the monthly trend \u2014 risk-on persists. ", /*#__PURE__*/React.createElement("b", null, "Watch:"), " Energy's 1-day pop diverges from its negative 20-day; likely a bounce, not a turn."))));
1292
+ }
1293
+
1294
+ /* ─────────────────────────── Research ─────────────────────────── */
1295
+ function ResearchView() {
1296
+ const D = window.CC_DATA;
1297
+ const [tic, setTic] = useState('');
1298
+ const [step, setStep] = useState(0); // 0 idle, 1..5 running, 6 done
1299
+ const steps = ['PLAN', 'Fundamentals', 'Quarterly financials', 'Price action + Chan engine', 'News', 'Report'];
1300
+ const run = () => {
1301
+ setStep(1);
1302
+ let i = 1;
1303
+ const tick = () => {
1304
+ i++;
1305
+ setStep(i);
1306
+ if (i < 6) setTimeout(tick, 650);
1307
+ };
1308
+ setTimeout(tick, 650);
1309
+ };
1310
+ return /*#__PURE__*/React.createElement("div", {
1311
+ style: {
1312
+ display: 'grid',
1313
+ gridTemplateColumns: '1fr 300px',
1314
+ gap: 'var(--space-300)',
1315
+ alignItems: 'start'
1316
+ }
1317
+ }, /*#__PURE__*/React.createElement(Card, {
1318
+ title: "Auto research agent",
1319
+ subtitle: "Fully local \xB7 PLAN \u2192 5 evidence tools \u2192 sectioned report"
1320
+ }, /*#__PURE__*/React.createElement("div", {
1321
+ style: {
1322
+ display: 'flex',
1323
+ gap: 'var(--space-150)',
1324
+ alignItems: 'flex-end',
1325
+ marginBottom: 'var(--space-250)'
1326
+ }
1327
+ }, /*#__PURE__*/React.createElement(Field, {
1328
+ label: "Ticker",
1329
+ placeholder: "e.g. NVDA",
1330
+ value: tic,
1331
+ onChange: e => setTic(e.target.value),
1332
+ className: "grow"
1333
+ }), /*#__PURE__*/React.createElement(Button, {
1334
+ variant: "accent",
1335
+ onClick: run,
1336
+ disabled: step > 0 && step < 6
1337
+ }, /*#__PURE__*/React.createElement(Icon, {
1338
+ name: "bot",
1339
+ size: 15
1340
+ }), " Run research agent")), step === 0 ? /*#__PURE__*/React.createElement("div", {
1341
+ className: "cc-empty tall"
1342
+ }, /*#__PURE__*/React.createElement(Icon, {
1343
+ name: "file-search",
1344
+ size: 24,
1345
+ color: "var(--text-disabled)"
1346
+ }), /*#__PURE__*/React.createElement("div", null, "Agent trace + sectioned report appear here.")) : /*#__PURE__*/React.createElement("div", {
1347
+ style: {
1348
+ display: 'flex',
1349
+ flexDirection: 'column',
1350
+ gap: 10
1351
+ }
1352
+ }, steps.map((s, idx) => /*#__PURE__*/React.createElement("div", {
1353
+ key: s,
1354
+ className: "cc-step"
1355
+ }, /*#__PURE__*/React.createElement("span", {
1356
+ className: `cc-step-dot ${step > idx ? 'done' : step === idx + 1 ? 'active' : ''}`
1357
+ }, step > idx + 1 ? /*#__PURE__*/React.createElement(Icon, {
1358
+ name: "check",
1359
+ size: 12,
1360
+ color: "#fff"
1361
+ }) : idx + 1), /*#__PURE__*/React.createElement("span", {
1362
+ style: {
1363
+ color: step > idx ? 'var(--text-body)' : 'var(--text-muted)'
1364
+ }
1365
+ }, s))), step >= 6 && /*#__PURE__*/React.createElement(InlineAlert, {
1366
+ variant: "ai",
1367
+ title: `Analyst · ${tic || 'NVDA'} · Qwen3-4B`
1368
+ }, /*#__PURE__*/React.createElement("b", null, "Valuation:"), " rich vs 5-yr median but justified by datacenter demand.", /*#__PURE__*/React.createElement("b", null, " Moat:"), " CUDA lock-in + supply-chain priority. ", /*#__PURE__*/React.createElement("b", null, "Chan timing:"), " weekly B3 active, daily B1 armed \u2014 accumulation favorable. ", /*#__PURE__*/React.createElement("b", null, "Risks:"), " export controls, hyperscaler capex digestion."))), /*#__PURE__*/React.createElement(Card, {
1369
+ title: "Report library",
1370
+ subtitle: "Auto + manual \xB7 /data",
1371
+ elevation: "raised"
1372
+ }, /*#__PURE__*/React.createElement("div", {
1373
+ className: "cc-list"
1374
+ }, D.reports.map((r, i) => /*#__PURE__*/React.createElement("button", {
1375
+ key: r,
1376
+ className: `cc-list-item ${i === 0 ? 'sel' : ''}`
1377
+ }, /*#__PURE__*/React.createElement(Icon, {
1378
+ name: "file-text",
1379
+ size: 15,
1380
+ color: "var(--text-muted)"
1381
+ }), /*#__PURE__*/React.createElement("span", {
1382
+ className: "mono"
1383
+ }, r))))));
1384
+ }
1385
+
1386
+ /* ─────────────────────────── News ─────────────────────────── */
1387
+ function NewsView() {
1388
+ const [hold, setHold] = useState('NVDA, TSLA, AAPL');
1389
+ const [checked, setChecked] = useState(false);
1390
+ return /*#__PURE__*/React.createElement("div", {
1391
+ style: {
1392
+ display: 'flex',
1393
+ flexDirection: 'column',
1394
+ gap: 'var(--space-300)',
1395
+ maxWidth: 760
1396
+ }
1397
+ }, /*#__PURE__*/React.createElement(Card, {
1398
+ title: "Watchlist news",
1399
+ subtitle: "For each holding, only today's news is checked"
1400
+ }, /*#__PURE__*/React.createElement("div", {
1401
+ style: {
1402
+ display: 'flex',
1403
+ gap: 'var(--space-150)',
1404
+ alignItems: 'flex-end',
1405
+ marginBottom: 'var(--space-200)'
1406
+ }
1407
+ }, /*#__PURE__*/React.createElement(Field, {
1408
+ label: "My holdings",
1409
+ mono: true,
1410
+ value: hold,
1411
+ onChange: e => setHold(e.target.value),
1412
+ className: "grow"
1413
+ }), /*#__PURE__*/React.createElement(Button, {
1414
+ variant: "secondary"
1415
+ }, /*#__PURE__*/React.createElement(Icon, {
1416
+ name: "save",
1417
+ size: 14
1418
+ }), " Save"), /*#__PURE__*/React.createElement(Button, {
1419
+ variant: "accent",
1420
+ onClick: () => setChecked(true)
1421
+ }, /*#__PURE__*/React.createElement(Icon, {
1422
+ name: "search",
1423
+ size: 15
1424
+ }), " Check today's news")), !checked ? /*#__PURE__*/React.createElement("div", {
1425
+ className: "cc-empty"
1426
+ }, /*#__PURE__*/React.createElement(Icon, {
1427
+ name: "newspaper",
1428
+ size: 22,
1429
+ color: "var(--text-disabled)"
1430
+ }), /*#__PURE__*/React.createElement("div", null, "News briefs appear here.")) : /*#__PURE__*/React.createElement("div", {
1431
+ style: {
1432
+ display: 'flex',
1433
+ flexDirection: 'column',
1434
+ gap: 'var(--space-150)'
1435
+ }
1436
+ }, /*#__PURE__*/React.createElement(InlineAlert, {
1437
+ variant: "ai",
1438
+ title: "NVDA \xB7 AI brief"
1439
+ }, "New datacenter GPU order from a hyperscaler reported pre-market; reinforces the active weekly B3 buy structure."), /*#__PURE__*/React.createElement(InlineAlert, {
1440
+ variant: "ai",
1441
+ title: "TSLA \xB7 AI brief"
1442
+ }, "Delivery-miss headline; aligns with today's daily S3 breakdown \u2014 confirms the SELL."), /*#__PURE__*/React.createElement("div", {
1443
+ className: "cc-quiet"
1444
+ }, /*#__PURE__*/React.createElement(StatusLight, {
1445
+ variant: "neutral"
1446
+ }, "Quiet today \xB7 AAPL")))));
1447
+ }
1448
+
1449
+ /* ─────────────────────────── Model ─────────────────────────── */
1450
+ function ModelView() {
1451
+ const models = ['Qwen3-1.7B', 'Qwen3-4B', 'Qwen3-8B', 'Qwen3-14B'];
1452
+ const [pick, setPick] = useState('Qwen3-4B');
1453
+ const [loaded, setLoaded] = useState('Qwen3-4B');
1454
+ return /*#__PURE__*/React.createElement("div", {
1455
+ style: {
1456
+ display: 'flex',
1457
+ flexDirection: 'column',
1458
+ gap: 'var(--space-300)',
1459
+ maxWidth: 680
1460
+ }
1461
+ }, /*#__PURE__*/React.createElement(InlineAlert, {
1462
+ variant: "informative",
1463
+ title: "Everything runs locally through llama.cpp"
1464
+ }, "Qwen3 GGUF weights, all below the 32B cap. Nothing leaves the machine. Signals, rotation & news never depend on it."), /*#__PURE__*/React.createElement(Card, {
1465
+ title: "Analyst (deep) model",
1466
+ subtitle: "Sub-agent pool \xB7 fast 1.7B fixed + this deep model"
1467
+ }, /*#__PURE__*/React.createElement("div", {
1468
+ className: "cc-radios"
1469
+ }, models.map(m => /*#__PURE__*/React.createElement("label", {
1470
+ key: m,
1471
+ className: `cc-radio ${pick === m ? 'sel' : ''}`,
1472
+ onClick: () => setPick(m)
1473
+ }, /*#__PURE__*/React.createElement("span", {
1474
+ className: "cc-radio-dot"
1475
+ }), /*#__PURE__*/React.createElement("span", {
1476
+ className: "mono"
1477
+ }, m), loaded === m && /*#__PURE__*/React.createElement(Badge, {
1478
+ variant: "positive"
1479
+ }, "loaded")))), /*#__PURE__*/React.createElement("div", {
1480
+ style: {
1481
+ display: 'flex',
1482
+ gap: 'var(--space-150)',
1483
+ marginTop: 'var(--space-250)',
1484
+ alignItems: 'center'
1485
+ }
1486
+ }, /*#__PURE__*/React.createElement(Button, {
1487
+ variant: "accent",
1488
+ onClick: () => setLoaded(pick)
1489
+ }, /*#__PURE__*/React.createElement(Icon, {
1490
+ name: "download",
1491
+ size: 15
1492
+ }), " Load model"), /*#__PURE__*/React.createElement(Button, {
1493
+ variant: "quiet"
1494
+ }, /*#__PURE__*/React.createElement(Icon, {
1495
+ name: "refresh-cw",
1496
+ size: 14
1497
+ }), " Refresh status"), /*#__PURE__*/React.createElement("span", {
1498
+ style: {
1499
+ marginLeft: 'auto'
1500
+ }
1501
+ }, /*#__PURE__*/React.createElement(StatusLight, {
1502
+ variant: "positive",
1503
+ live: true
1504
+ }, loaded, " ready")))));
1505
+ }
1506
+ Object.assign(window, {
1507
+ SignalsView,
1508
+ RotationView,
1509
+ ResearchView,
1510
+ NewsView,
1511
+ ModelView
1512
+ });
1513
+ })(); } catch (e) { __ds_ns.__errors.push({ path: "ui_kits/chan-compass/views.jsx", error: String((e && e.message) || e) }); }
1514
+
1515
+ __ds_ns.Button = __ds_scope.Button;
1516
+
1517
+ __ds_ns.Card = __ds_scope.Card;
1518
+
1519
+ __ds_ns.InlineAlert = __ds_scope.InlineAlert;
1520
+
1521
+ __ds_ns.Checkbox = __ds_scope.Checkbox;
1522
+
1523
+ __ds_ns.Field = __ds_scope.Field;
1524
+
1525
+ __ds_ns.Switch = __ds_scope.Switch;
1526
+
1527
+ __ds_ns.Tabs = __ds_scope.Tabs;
1528
+
1529
+ __ds_ns.Badge = __ds_scope.Badge;
1530
+
1531
+ __ds_ns.SignalBadge = __ds_scope.SignalBadge;
1532
+
1533
+ __ds_ns.StatusLight = __ds_scope.StatusLight;
1534
+
1535
+ })();
ui_kits/chan-compass/api.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* api.js - Chan Compass frontend -> Gradio Server backend.
2
+ JSON helpers + a Server-Sent-Events reader for streaming LLM output. */
3
+ window.CCApi = (function () {
4
+ async function jget(url) {
5
+ const r = await fetch(url);
6
+ if (!r.ok) throw new Error(await r.text());
7
+ return r.json();
8
+ }
9
+ async function jpost(url, body) {
10
+ const r = await fetch(url, {
11
+ method: "POST",
12
+ headers: { "Content-Type": "application/json" },
13
+ body: JSON.stringify(body || {}),
14
+ });
15
+ if (!r.ok) throw new Error(await r.text());
16
+ return r.json();
17
+ }
18
+
19
+ /* Stream an SSE endpoint. onChunk(obj) per message; resolves on `done`. */
20
+ function stream(url, onChunk) {
21
+ return new Promise((resolve, reject) => {
22
+ const es = new EventSource(url);
23
+ es.onmessage = (e) => {
24
+ try { onChunk(JSON.parse(e.data)); } catch (_) { onChunk({ text: e.data }); }
25
+ };
26
+ es.addEventListener("done", () => { es.close(); resolve(); });
27
+ es.onerror = () => { es.close(); resolve(); };
28
+ });
29
+ }
30
+
31
+ return {
32
+ runSignals: (pool, force) => jpost("/api/signals/run", { pool, force }),
33
+ signalSummary: (ticker, onChunk) =>
34
+ stream("/api/signals/summary?ticker=" + encodeURIComponent(ticker), onChunk),
35
+ rotation: () => jget("/api/rotation"),
36
+ rotationNarrative: (onChunk) => stream("/api/rotation/narrative", onChunk),
37
+ holdings: () => jget("/api/news/holdings"),
38
+ saveHoldings: (holdings) => jpost("/api/news/save", { holdings }),
39
+ checkNews: (onChunk) => stream("/api/news/check", onChunk),
40
+ runResearch: (ticker, onChunk) =>
41
+ stream("/api/research/run?ticker=" + encodeURIComponent(ticker), onChunk),
42
+ reports: () => jget("/api/research/reports"),
43
+ report: (name) => jget("/api/research/report?name=" + encodeURIComponent(name)),
44
+ autoRun: () => jpost("/api/automation/run", {}),
45
+ autoState: () => jget("/api/automation/state"),
46
+ publishTraces: (repo) => jpost("/api/automation/publish-traces", { repo }),
47
+ modelStatus: () => jget("/api/model/status"),
48
+ modelTest: () => jpost("/api/model/test", {}),
49
+ finetuneStatus: () => jget("/api/model/finetune-status"),
50
+ exportDataset: () => jpost("/api/model/export-dataset", {}),
51
+ email: (content, to, tag) => jpost("/api/email", { content, to, tag }),
52
+ };
53
+ })();
ui_kits/chan-compass/app.css ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ Chan Compass UI kit — app-level styling.
3
+ Tokens come from styles.css; this is layout + the few patterns
4
+ (data table, flow bars, AI empty states) the kit needs.
5
+ ============================================================ */
6
+
7
+ .cc-app {
8
+ max-width: var(--container-max);
9
+ margin: 0 auto;
10
+ min-height: 100vh;
11
+ display: flex;
12
+ flex-direction: column;
13
+ background: var(--canvas);
14
+ }
15
+
16
+ /* ---- Header ---- */
17
+ .cc-header {
18
+ display: flex; align-items: center; justify-content: space-between;
19
+ gap: var(--space-300);
20
+ padding: var(--space-300) var(--space-400) var(--space-250);
21
+ }
22
+ .cc-brand { display: flex; align-items: center; gap: var(--space-200); }
23
+ .cc-mark {
24
+ width: 44px; height: 44px; flex: none; border-radius: var(--radius-lg);
25
+ display: grid; place-items: center;
26
+ background: linear-gradient(135deg, var(--s2-blue-900) 0%, var(--s2-indigo-700) 60%, var(--s2-purple-900) 100%);
27
+ box-shadow: var(--shadow-accent);
28
+ }
29
+ .cc-wordmark { font: var(--font-weight-bold) 22px/1.1 var(--font-sans); color: var(--text-heading); letter-spacing: -.01em; }
30
+ .cc-wordmark span { font-weight: var(--font-weight-regular); color: var(--text-muted); }
31
+ .cc-tagline { font: var(--type-detail); color: var(--text-muted); margin-top: 3px; }
32
+ .cc-header-right { display: flex; align-items: center; gap: var(--space-200); }
33
+
34
+ /* ---- Nav ---- */
35
+ .cc-navbar { padding: 0 var(--space-400); }
36
+
37
+ /* ---- Main ---- */
38
+ .cc-main { flex: 1; padding: var(--space-400); }
39
+
40
+ /* ---- Footer ---- */
41
+ .cc-footer {
42
+ display: flex; align-items: center; gap: var(--space-100); flex-wrap: wrap;
43
+ padding: var(--space-300) var(--space-400) var(--space-400);
44
+ font: var(--type-detail); color: var(--text-muted);
45
+ border-top: 1px solid var(--border-hairline); margin: var(--space-200) var(--space-400) 0;
46
+ }
47
+ .cc-foot-sep { color: var(--border-strong); }
48
+
49
+ /* ---- grow helper for Field ---- */
50
+ .grow { flex: 1; min-width: 0; }
51
+
52
+ /* ---- Data table ---- */
53
+ .cc-table { border: 1px solid var(--border-hairline); border-radius: var(--radius-lg); overflow: hidden; }
54
+ .cc-table table { width: 100%; border-collapse: collapse; font: var(--type-ui); }
55
+ .cc-table thead th {
56
+ text-align: left; font: var(--font-weight-semibold) var(--font-size-75)/1 var(--font-sans);
57
+ color: var(--text-secondary); text-transform: uppercase; letter-spacing: .04em;
58
+ padding: 11px 14px; background: var(--surface-subtle); border-bottom: 1px solid var(--border-hairline);
59
+ }
60
+ .cc-table th.num, .cc-table td.num { text-align: right; }
61
+ .cc-table tbody td { padding: 11px 14px; border-bottom: 1px solid var(--border-hairline); color: var(--text-body); }
62
+ .cc-table tbody tr:last-child td { border-bottom: none; }
63
+ .cc-table tbody tr { cursor: pointer; transition: background var(--duration-fast) var(--ease-default); }
64
+ .cc-table tbody tr:hover { background: var(--surface-subtle); }
65
+ .cc-table tbody tr.sel { background: var(--accent-subtle); }
66
+ .cc-table tbody tr.sel td { box-shadow: inset 2px 0 0 var(--accent); }
67
+ .cc-table.compact tbody td, .cc-table.compact thead th { padding: 8px 12px; }
68
+
69
+ .mono { font-family: var(--font-code); }
70
+ .bold { font-weight: var(--font-weight-bold); color: var(--text-heading); }
71
+ .dim { color: var(--text-muted); }
72
+
73
+ /* ---- Decision log ---- */
74
+ .cc-log {
75
+ display: flex; flex-direction: column; gap: 8px;
76
+ padding: var(--space-200); background: var(--surface-subtle);
77
+ border-radius: var(--radius-default); font: var(--type-body); color: var(--text-body);
78
+ }
79
+ .cc-log .dim { display: inline-block; width: 116px; }
80
+ .cc-log-note { margin-top: 6px; padding-top: 10px; border-top: 1px solid var(--border-hairline); color: var(--text-secondary); }
81
+
82
+ /* ---- AI empty state ---- */
83
+ .cc-empty {
84
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
85
+ min-height: 160px; text-align: center; color: var(--text-muted); font: var(--type-body);
86
+ border: 1.5px dashed var(--border-default); border-radius: var(--radius-lg); padding: var(--space-300);
87
+ }
88
+ .cc-empty.tall { min-height: 220px; }
89
+
90
+ /* ---- Flow bars (rotation) ---- */
91
+ .cc-flow { display: flex; flex-direction: column; gap: 9px; }
92
+ .cc-flow-row { display: grid; grid-template-columns: 1fr 130px auto; align-items: center; gap: var(--space-200); }
93
+ .cc-flow-name { font: var(--type-ui); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
94
+ .cc-flow-track { position: relative; height: 16px; background: var(--surface-subtle); border-radius: var(--radius-sm); }
95
+ .cc-flow-bar { position: absolute; top: 0; height: 100%; border-radius: var(--radius-sm); }
96
+ .cc-flow-mid { position: absolute; left: 50%; top: -2px; bottom: -2px; width: 1px; background: var(--border-strong); }
97
+
98
+ /* ---- Step tracker (research) ---- */
99
+ .cc-step { display: flex; align-items: center; gap: var(--space-150); font: var(--type-body); }
100
+ .cc-step-dot {
101
+ width: 22px; height: 22px; flex: none; border-radius: var(--radius-full);
102
+ display: grid; place-items: center; font: var(--font-weight-bold) 11px/1 var(--font-sans);
103
+ background: var(--surface-subtle); color: var(--text-muted); border: 1px solid var(--border-default);
104
+ }
105
+ .cc-step-dot.active { background: var(--accent); color: #fff; border-color: var(--accent); animation: cc-pulse 1.2s var(--ease-out) infinite; }
106
+ .cc-step-dot.done { background: var(--positive); color: #fff; border-color: var(--positive); }
107
+ @keyframes cc-pulse { 0%,100%{ opacity:1 } 50%{ opacity:.55 } }
108
+
109
+ /* ---- Lists (reports) ---- */
110
+ .cc-list { display: flex; flex-direction: column; gap: 4px; }
111
+ .cc-list-item {
112
+ display: flex; align-items: center; gap: var(--space-100); text-align: left;
113
+ padding: 9px 11px; border: none; background: none; border-radius: var(--radius-default);
114
+ cursor: pointer; font: var(--type-ui); color: var(--text-body);
115
+ transition: background var(--duration-fast) var(--ease-default);
116
+ }
117
+ .cc-list-item:hover { background: var(--surface-subtle); }
118
+ .cc-list-item.sel { background: var(--accent-subtle); color: var(--accent-text); }
119
+
120
+ /* ---- Radios (model) ---- */
121
+ .cc-radios { display: flex; flex-direction: column; gap: 8px; }
122
+ .cc-radio {
123
+ display: flex; align-items: center; gap: var(--space-150);
124
+ padding: 12px 14px; border: 1.5px solid var(--border-default); border-radius: var(--radius-default);
125
+ cursor: pointer; transition: var(--transition-colors); font: var(--type-ui); color: var(--text-body);
126
+ }
127
+ .cc-radio:hover { border-color: var(--text-secondary); }
128
+ .cc-radio.sel { border-color: var(--accent); background: var(--accent-subtle); }
129
+ .cc-radio-dot {
130
+ width: 16px; height: 16px; flex: none; border-radius: var(--radius-full);
131
+ border: 2px solid var(--border-field); position: relative;
132
+ }
133
+ .cc-radio.sel .cc-radio-dot { border-color: var(--accent); }
134
+ .cc-radio.sel .cc-radio-dot::after { content:''; position:absolute; inset:3px; border-radius:var(--radius-full); background:var(--accent); }
135
+ .cc-radio .s2-badge { margin-left: auto; }
136
+
137
+ .cc-quiet { padding: 9px 2px; }
138
+
139
+ /* ── live AI output panel (framed, neutral, auto-grows) ── */
140
+ .cc-llm-out { border:1px solid var(--border-hairline); border-radius: var(--radius-lg);
141
+ background: var(--surface-card); padding: 14px 16px; min-height: 120px;
142
+ font-size: 14px; line-height: 1.5; overflow:auto; }
143
+ .cc-md h1,.cc-md h2,.cc-md h3,.cc-md h4 { color: var(--accent); margin: 12px 0 6px; }
144
+ .cc-md h1 { font-size: 19px; } .cc-md h2 { font-size: 16px; } .cc-md h3 { font-size: 14px; }
145
+ .cc-md p { margin: 4px 0; } .cc-md ul { margin: 6px 0 6px 18px; }
146
+ .cc-md blockquote { border-left: 3px solid var(--accent); margin: 6px 0; padding: 2px 12px; color: var(--text-muted); }
147
+ .cc-md code { background: var(--surface-subtle); padding: 1px 5px; border-radius: 5px; font-family: var(--font-mono); font-size: 12.5px; }
148
+ .cc-md a { color: var(--accent); }
149
+ .cc-md-table { border-collapse: collapse; margin: 8px 0; font-size: 13px; width: 100%; }
150
+ .cc-md-table th { text-align: left; padding: 6px 10px; border-bottom: 2px solid var(--border-hairline); }
151
+ .cc-md-table td { padding: 6px 10px; border-bottom: 1px solid var(--border-hairline); }
ui_kits/chan-compass/app.jsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Chan Compass UI kit — app shell. Header + Tabs + routed content + footer. */
2
+ const _DS = window.ChanCompassSpectrum2DesignSystem_b52b37;
3
+ const { Tabs: CCTabs, StatusLight: CCStatus, Badge: CCBadge } = _DS;
4
+ const { useState: useS, useEffect: useE } = React;
5
+
6
+ function Glyph({ name, size = 18, color, style }) {
7
+ useE(() => { if (window.lucide) window.lucide.createIcons(); });
8
+ return <i data-lucide={name} style={{ width:size, height:size, color, display:'inline-flex', ...style }}></i>;
9
+ }
10
+
11
+ function App() {
12
+ const [tab, setTab] = useS('signals');
13
+ const items = [
14
+ { id:'signals', label:'Signals', icon:<Glyph name="trending-up" size={16}/> },
15
+ { id:'rotation', label:'Sector Rotation', icon:<Glyph name="refresh-cw" size={16}/> },
16
+ { id:'news', label:'Watchlist News', icon:<Glyph name="newspaper" size={16}/>, count:2 },
17
+ { id:'research', label:'Auto Research', icon:<Glyph name="flask-conical" size={16}/> },
18
+ { id:'automation', label:'Automation', icon:<Glyph name="clock" size={16}/> },
19
+ { id:'model', label:'Model', icon:<Glyph name="cpu" size={16}/> },
20
+ ];
21
+ const View = {
22
+ signals: window.SignalsView, rotation: window.RotationView,
23
+ news: window.NewsView, research: window.ResearchView,
24
+ automation: window.AutomationView, model: window.ModelView,
25
+ }[tab];
26
+
27
+ return (
28
+ <div className="cc-app">
29
+ <header className="cc-header">
30
+ <div className="cc-brand">
31
+ <div className="cc-mark"><Glyph name="compass" size={22} color="#fff"/></div>
32
+ <div className="cc-brand-text">
33
+ <div className="cc-wordmark">Chan Compass <span>· US Markets</span></div>
34
+ <div className="cc-tagline">Multi-timeframe 缠论 signal engine · fully local</div>
35
+ </div>
36
+ </div>
37
+ <div className="cc-header-right">
38
+ <CCBadge variant="accent"><Glyph name="cpu" size={12} style={{marginRight:4}}/> Local · llama.cpp</CCBadge>
39
+ <CCStatus variant="positive" live>Market open</CCStatus>
40
+ </div>
41
+ </header>
42
+
43
+ <div className="cc-navbar">
44
+ <CCTabs value={tab} onChange={setTab} items={items}/>
45
+ </div>
46
+
47
+ <main className="cc-main">
48
+ {View ? <View/> : null}
49
+ </main>
50
+
51
+ <footer className="cc-footer">
52
+ <span>Chan Compass · educational tool, not investment advice</span>
53
+ <span className="cc-foot-sep">·</span>
54
+ <span>data: Yahoo Finance</span>
55
+ <span className="cc-foot-sep">·</span>
56
+ <span>design language: Adobe Spectrum 2</span>
57
+ </footer>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
ui_kits/chan-compass/index.html ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- @dsCard group="Chan Compass" viewport="1280x900" name="Chan Compass — full app" subtitle="Interactive Spectrum 2 recreation · 6-tab signal engine" -->
2
+ <!-- @startingPoint section="Chan Compass" subtitle="Full interactive Spectrum 2 app — signals, rotation, research" viewport="1280x900" -->
3
+ <!DOCTYPE html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="utf-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <title>Chan Compass · US Markets</title>
9
+ <link rel="stylesheet" href="/styles.css">
10
+ <link rel="stylesheet" href="/app.css">
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script src="https://unpkg.com/react@18.3.1/umd/react.development.js" crossorigin="anonymous"></script>
15
+ <script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" crossorigin="anonymous"></script>
16
+ <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"></script>
17
+ <script src="https://unpkg.com/lucide@0.544.0/dist/umd/lucide.min.js"></script>
18
+ <script src="/_ds_bundle.js"></script>
19
+ <script src="/api.js"></script>
20
+ <script src="/md.js"></script>
21
+ <script type="text/babel" src="/views.jsx"></script>
22
+ <script type="text/babel" src="/app.jsx"></script>
23
+ <script>
24
+ // Re-render lucide icons after Babel mounts everything.
25
+ window.addEventListener('load', () => setTimeout(() => window.lucide && window.lucide.createIcons(), 400));
26
+ </script>
27
+ </body>
28
+ </html>
ui_kits/chan-compass/md.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* md.js — minimal markdown -> HTML for AI output (headings, tables, lists,
2
+ bold, code, links). Mirrors the email renderer so chat and email match. */
3
+ window.CCmd = function (text) {
4
+ if (!text) return "";
5
+ const esc = (s) => s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
6
+ const inline = (s) => esc(s)
7
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
8
+ .replace(/`(.+?)`/g, "<code>$1</code>")
9
+ .replace(/\[(.+?)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
10
+ const lines = text.split("\n");
11
+ let out = [], i = 0;
12
+ while (i < lines.length) {
13
+ let line = lines[i];
14
+ // table
15
+ if (line.includes("|") && i+1 < lines.length && /^\s*\|?[\s:\-|]+\|?\s*$/.test(lines[i+1])) {
16
+ const head = line.trim().replace(/^\||\|$/g,"").split("|").map(c=>c.trim());
17
+ i += 2; let rows = [];
18
+ while (i < lines.length && lines[i].includes("|")) {
19
+ rows.push(lines[i].trim().replace(/^\||\|$/g,"").split("|").map(c=>c.trim())); i++;
20
+ }
21
+ out.push('<table class="cc-md-table"><thead><tr>' +
22
+ head.map(h=>"<th>"+inline(h)+"</th>").join("") + "</tr></thead><tbody>" +
23
+ rows.map(r=>"<tr>"+r.map(c=>"<td>"+inline(c)+"</td>").join("")+"</tr>").join("") +
24
+ "</tbody></table>");
25
+ continue;
26
+ }
27
+ let m = line.match(/^(#{1,4})\s+(.*)$/);
28
+ if (m) { const lv=m[1].length; out.push(`<h${lv}>`+inline(m[2])+`</h${lv}>`); i++; continue; }
29
+ if (/^\s*[-*]\s+/.test(line)) {
30
+ let items=[];
31
+ while (i<lines.length && /^\s*[-*]\s+/.test(lines[i])) { items.push("<li>"+inline(lines[i].replace(/^\s*[-*]\s+/,""))+"</li>"); i++; }
32
+ out.push("<ul>"+items.join("")+"</ul>"); continue;
33
+ }
34
+ if (/^\s*>\s?/.test(line)) { out.push('<blockquote>'+inline(line.replace(/^\s*>\s?/,""))+"</blockquote>"); i++; continue; }
35
+ if (line.trim()==="---") { out.push("<hr/>"); i++; continue; }
36
+ if (line.trim()==="") { out.push("<div style='height:6px'></div>"); i++; continue; }
37
+ out.push("<p>"+inline(line)+"</p>"); i++;
38
+ }
39
+ return out.join("");
40
+ };
ui_kits/chan-compass/styles.css ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ Chan Compass · Spectrum 2 Design System
3
+ Global entry point — consumers link THIS one file.
4
+ Import order: fonts → primitives → semantics. Keep this file
5
+ as @import lines only.
6
+ ============================================================ */
7
+
8
+ @import url('tokens/fonts.css');
9
+ @import url('tokens/colors.css');
10
+ @import url('tokens/typography.css');
11
+ @import url('tokens/spacing.css');
12
+ @import url('tokens/elevation.css');
13
+ @import url('tokens/base.css');
ui_kits/chan-compass/views.jsx ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Chan Compass UI kit - view components, wired to the live Gradio Server backend
2
+ (window.CCApi). Spectrum 2 primitives from the design-system bundle. */
3
+ const DS = window.ChanCompassSpectrum2DesignSystem_b52b37;
4
+ const { Button, Field, Checkbox, Switch, Card, InlineAlert, Badge, StatusLight, SignalBadge, Tabs } = DS;
5
+ const { useState, useEffect, useRef } = React;
6
+ const API = window.CCApi;
7
+
8
+ function Icon({ name, size = 18, color, style }) {
9
+ useEffect(() => { if (window.lucide) window.lucide.createIcons(); });
10
+ return <i data-lucide={name} style={{ width:size, height:size, color, display:'inline-flex', ...style }}></i>;
11
+ }
12
+
13
+ const num = (v) => (typeof v === "number" ? v : parseFloat(v));
14
+ const dirColor = (v) => num(v) > 0 ? "var(--up)" : num(v) < 0 ? "var(--down)" : "var(--flat)";
15
+
16
+ function EmailRow({ getContent, tag, placeholder }) {
17
+ const [to, setTo] = useState("");
18
+ const [status, setStatus] = useState("");
19
+ const [busy, setBusy] = useState(false);
20
+ const send = async () => {
21
+ setBusy(true); setStatus("");
22
+ try { const r = await API.email(getContent(), to, tag); setStatus(r.status); }
23
+ catch (e) { setStatus("X " + e.message); }
24
+ setBusy(false);
25
+ };
26
+ return (
27
+ <div style={{marginTop:"var(--space-200)", display:"flex", flexDirection:"column", gap:6}}>
28
+ <div style={{display:"flex", gap:"var(--space-150)", alignItems:"flex-end"}}>
29
+ <Field label="Email this result to" value={to} onChange={e=>setTo(e.target.value)}
30
+ placeholder={placeholder || "name@example.com"} className="grow"/>
31
+ <Button variant="secondary" onClick={send} disabled={busy}>
32
+ <Icon name="mail" size={14}/> {busy ? "Sending..." : "Send Email"}
33
+ </Button>
34
+ </div>
35
+ {status && <div className="dim" style={{fontSize:13}}>{status}</div>}
36
+ </div>
37
+ );
38
+ }
39
+
40
+ function Kpi({ label, value, tone }) {
41
+ return (
42
+ <div style={{flex:1, padding:"14px 18px", background:"var(--surface-card)", border:"1px solid var(--border-hairline)",
43
+ borderRadius:"var(--radius-lg)", boxShadow:"var(--shadow-raised)"}}>
44
+ <div style={{font:"var(--font-weight-bold) 28px/1 var(--font-sans)", color:tone||"var(--text-heading)"}}>{value}</div>
45
+ <div style={{font:"var(--type-detail)", color:"var(--text-muted)", marginTop:4}}>{label}</div>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ function Markdown({ text }) {
51
+ if (!text) return null;
52
+ const html = window.CCmd ? window.CCmd(text) : text.replace(/\n/g,"<br/>");
53
+ return <div className="cc-md" dangerouslySetInnerHTML={{__html: html}}/>;
54
+ }
55
+
56
+ /* ─────────── Signals ─────────── */
57
+ function SignalsView() {
58
+ const [pool, setPool] = useState("AAPL, MSFT, NVDA, TSLA, AMZN, GOOGL, META, AMD");
59
+ const [force, setForce] = useState(false);
60
+ const [running, setRunning] = useState(false);
61
+ const [rows, setRows] = useState([]);
62
+ const [summary, setSummary] = useState("");
63
+ const [sel, setSel] = useState(null);
64
+ const [aiText, setAiText] = useState("");
65
+ const [aiBusy, setAiBusy] = useState(false);
66
+
67
+ const run = async () => {
68
+ setRunning(true); setAiText("");
69
+ try {
70
+ const r = await API.runSignals(pool, force);
71
+ setRows(r.rows || []); setSummary(r.summary || "");
72
+ setSel((r.rows && r.rows[0] && r.rows[0].Ticker) || null);
73
+ } catch (e) { setSummary("X " + e.message); }
74
+ setRunning(false);
75
+ };
76
+ const explain = async () => {
77
+ if (!sel) return;
78
+ setAiBusy(true); setAiText("");
79
+ await API.signalSummary(sel, (m) => setAiText(m.text || ""));
80
+ setAiBusy(false);
81
+ };
82
+ const counts = rows.reduce((a,r)=>{ const k=(r.Tomorrow||"").replace(/[^A-Z]/g,"");
83
+ a[k]=(a[k]||0)+1; return a; }, {});
84
+
85
+ return (
86
+ <div style={{display:"flex", flexDirection:"column", gap:"var(--space-300)"}}>
87
+ <div style={{display:"flex", gap:"var(--space-200)"}}>
88
+ <Kpi label="BUY" value={counts.BUY||0} tone="var(--up)"/>
89
+ <Kpi label="SELL" value={counts.SELL||0} tone="var(--down)"/>
90
+ <Kpi label="HOLD" value={counts.HOLD||0} tone="var(--accent)"/>
91
+ <Kpi label="WAIT" value={counts.WAIT||0}/>
92
+ </div>
93
+ <Card title="Tomorrow's plan" subtitle="Long-hold mode - the unchanged Chan engine runs over each ticker">
94
+ <div style={{display:"flex", gap:"var(--space-200)", alignItems:"flex-end", marginBottom:"var(--space-250)"}}>
95
+ <Field label="Ticker pool" mono value={pool} onChange={e=>setPool(e.target.value)} className="grow" help="Comma separated"/>
96
+ <Checkbox checked={force} onChange={e=>setForce(e.target.checked)}>Force fresh download</Checkbox>
97
+ <Button variant="accent" onClick={run} disabled={running}>
98
+ {running ? <><Icon name="loader" size={15}/> Running...</> : <><Icon name="play" size={15}/> Run analysis</>}
99
+ </Button>
100
+ </div>
101
+ {rows.length === 0 ? (
102
+ <div className="cc-empty"><Icon name="table" size={22} color="var(--text-disabled)"/>
103
+ <div>Run analysis to see tomorrow's plan.</div></div>
104
+ ) : (
105
+ <div className="cc-table">
106
+ <table>
107
+ <thead><tr>
108
+ <th>Ticker</th><th>Tomorrow</th><th>Buy point</th><th>Buy zone</th>
109
+ <th>Invalid below</th><th>Signal</th><th className="num">Conf.</th><th className="num">Close</th>
110
+ </tr></thead>
111
+ <tbody>
112
+ {rows.map(r=>(
113
+ <tr key={r.Ticker} className={r.Ticker===sel?"sel":""} onClick={()=>{setSel(r.Ticker); setAiText("");}}>
114
+ <td><span className="mono bold">{r.Ticker}</span></td>
115
+ <td>{r.Tomorrow}</td>
116
+ <td className="mono dim">{r["Buy point"]}</td>
117
+ <td className="mono dim">{r["Buy zone"]}</td>
118
+ <td className="mono dim">{r["Invalid below"]}</td>
119
+ <td className="dim">{r.Signal}</td>
120
+ <td className="num mono">{r.Confidence}</td>
121
+ <td className="num mono">{r.Close}</td>
122
+ </tr>
123
+ ))}
124
+ </tbody>
125
+ </table>
126
+ </div>
127
+ )}
128
+ {summary && <div className="dim" style={{marginTop:8, fontSize:13}}>{summary}</div>}
129
+ </Card>
130
+ <Card title={"AI Summary" + (sel?" - "+sel:"")} subtitle="Summary sub-agent - Chan-Tuned Qwen3-1.7B - references the multi-timeframe ruling chain">
131
+ <div style={{display:"flex", gap:"var(--space-150)", marginBottom:"var(--space-200)"}}>
132
+ <Button variant="accent" size="sm" onClick={explain} disabled={!sel || aiBusy}>
133
+ <Icon name="sparkles" size={14}/> {aiBusy ? "Summarizing..." : "AI summary (local LLM)"}
134
+ </Button>
135
+ </div>
136
+ <div className="cc-llm-out">
137
+ {aiText ? <Markdown text={aiText}/> : <span className="dim">AI output appears here. Pick a ticker, then AI summary.</span>}
138
+ </div>
139
+ <EmailRow getContent={()=>aiText} tag="Signals summary"/>
140
+ </Card>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ /* ─────────── Rotation ─────────── */
146
+ function RotationView() {
147
+ const [d1, setD1] = useState([]);
148
+ const [asof, setAsof] = useState("");
149
+ const [ai, setAi] = useState(""); const [busy, setBusy] = useState(false);
150
+ const [loading, setLoading] = useState(false);
151
+ const refresh = async () => {
152
+ setLoading(true);
153
+ try { const r = await API.rotation(); setD1(r.d1||[]); setAsof(r.asof||""); }
154
+ catch(e){ setAsof("X "+e.message); }
155
+ setLoading(false);
156
+ };
157
+ const narrate = async () => { setBusy(true); setAi(""); await API.rotationNarrative(m=>setAi(m.text||"")); setBusy(false); };
158
+ return (
159
+ <div style={{display:"flex", flexDirection:"column", gap:"var(--space-300)"}}>
160
+ <InlineAlert variant="informative" title="Where capital is flowing">
161
+ 11 SPDR sector ETFs (full S&amp;P 500). Flow proxy = change % x dollar volume; RS = return minus SPY.
162
+ True per-sector fund-flow feeds are paid data - this is the standard free proxy.
163
+ </InlineAlert>
164
+ <div style={{display:"flex", gap:"var(--space-150)"}}>
165
+ <Button variant="accent" onClick={refresh} disabled={loading}>
166
+ <Icon name="refresh-cw" size={15}/> {loading?"Refreshing...":"Refresh rotation (instant)"}
167
+ </Button>
168
+ <Button variant="secondary" onClick={narrate} disabled={busy || !d1.length}>
169
+ <Icon name="bot" size={15}/> {busy?"Writing...":"AI narrative (local LLM)"}
170
+ </Button>
171
+ {asof && <span style={{marginLeft:"auto"}}><StatusLight variant="neutral">{asof}</StatusLight></span>}
172
+ </div>
173
+ {d1.length>0 && (
174
+ <Card title="Sector rotation" subtitle="1-day flow proxy - sorted">
175
+ <div className="cc-table"><table>
176
+ <thead><tr>{Object.keys(d1[0]).map(k=><th key={k} className={(k!=="Sector"&&k!=="ETF")?"num":""}>{k}</th>)}</tr></thead>
177
+ <tbody>{d1.map((r,i)=>(
178
+ <tr key={i}>{Object.entries(r).map(([k,v])=>(
179
+ <td key={k} className={(k!=="Sector"&&k!=="ETF")?"num mono":""}
180
+ style={{color: typeof v==="string"&&v.includes("%")?dirColor(parseFloat(v)):undefined}}>{v}</td>
181
+ ))}</tr>
182
+ ))}</tbody>
183
+ </table></div>
184
+ </Card>
185
+ )}
186
+ {ai && <InlineAlert variant="ai" title="Narrator sub-agent - Qwen3-1.7B">{ai}</InlineAlert>}
187
+ <Card title="Email" subtitle="Send the current rotation narrative">
188
+ <EmailRow getContent={()=>ai} tag="Sector rotation"/>
189
+ </Card>
190
+ </div>
191
+ );
192
+ }
193
+
194
+ /* ─────────── News ─────────── */
195
+ function NewsView() {
196
+ const [hold, setHold] = useState("");
197
+ const [out, setOut] = useState("");
198
+ const [busy, setBusy] = useState(false);
199
+ useEffect(()=>{ API.holdings().then(r=>setHold((r.holdings||[]).join(", "))).catch(()=>{}); },[]);
200
+ const save = async () => { await API.saveHoldings(hold); };
201
+ const check = async () => { setBusy(true); setOut(""); await API.checkNews(m=>setOut(m.text||"")); setBusy(false); };
202
+ return (
203
+ <div style={{display:"flex", flexDirection:"column", gap:"var(--space-300)", maxWidth:820}}>
204
+ <Card title="Watchlist news" subtitle="For each holding, only today's news is checked - streamed as it arrives">
205
+ <div style={{display:"flex", gap:"var(--space-150)", alignItems:"flex-end", marginBottom:"var(--space-200)"}}>
206
+ <Field label="My holdings" mono value={hold} onChange={e=>setHold(e.target.value)} className="grow"/>
207
+ <Button variant="secondary" onClick={save}><Icon name="save" size={14}/> Save holdings</Button>
208
+ <Button variant="accent" onClick={check} disabled={busy}>
209
+ <Icon name="search" size={15}/> {busy?"Checking...":"Check today's news"}
210
+ </Button>
211
+ </div>
212
+ <div className="cc-llm-out" style={{minHeight:200}}>
213
+ {out ? <Markdown text={out}/> : <span className="dim">News briefs appear here.</span>}
214
+ </div>
215
+ <EmailRow getContent={()=>out} tag="Watchlist news"/>
216
+ </Card>
217
+ </div>
218
+ );
219
+ }
220
+
221
+ /* ─────────── Research ─────────── */
222
+ function ResearchView() {
223
+ const [tic, setTic] = useState("");
224
+ const [progress, setProgress] = useState("");
225
+ const [report, setReport] = useState("");
226
+ const [reports, setReports] = useState([]);
227
+ const [busy, setBusy] = useState(false);
228
+ const [sel, setSel] = useState(null);
229
+ useEffect(()=>{ API.reports().then(r=>setReports(r.reports||[])).catch(()=>{}); },[]);
230
+ const run = async () => {
231
+ if (!tic) return;
232
+ setBusy(true); setProgress(""); setReport("");
233
+ await API.runResearch(tic, (m)=>{
234
+ if (m.progress === "__done__") { setReports(m.reports||[]); setReport(m.report||""); }
235
+ else { setProgress(m.progress||""); setReport(m.report||""); }
236
+ });
237
+ setBusy(false);
238
+ };
239
+ const open = async (name) => { setSel(name); const r = await API.report(name); setReport(r.markdown||""); };
240
+ return (
241
+ <div style={{display:"grid", gridTemplateColumns:"1fr 300px", gap:"var(--space-300)", alignItems:"start"}}>
242
+ <div style={{display:"flex", flexDirection:"column", gap:"var(--space-300)"}}>
243
+ <Card title="Auto research agent" subtitle="Multi-agent - PLAN, 6 parallel tools, Analyst + Reporter write in parallel">
244
+ <div style={{display:"flex", gap:"var(--space-150)", alignItems:"flex-end", marginBottom:"var(--space-250)"}}>
245
+ <Field label="Ticker" placeholder="e.g. NVDA" value={tic} onChange={e=>setTic(e.target.value)} className="grow"/>
246
+ <Button variant="accent" onClick={run} disabled={busy}>
247
+ <Icon name="bot" size={15}/> {busy?"Running...":"Run research agent"}
248
+ </Button>
249
+ </div>
250
+ {progress && <div className="cc-llm-out" style={{marginBottom:12}}><Markdown text={progress}/></div>}
251
+ <div className="cc-llm-out" style={{minHeight:200}}>
252
+ {report ? <Markdown text={report}/> : <span className="dim">Agent trace + sectioned report appear here.</span>}
253
+ </div>
254
+ <EmailRow getContent={()=>report} tag="Research report"/>
255
+ </Card>
256
+ </div>
257
+ <Card title="Report library" subtitle="Auto + manual - /data" elevation="raised">
258
+ <div className="cc-list">
259
+ {reports.length===0 && <div className="dim" style={{padding:8}}>No reports yet.</div>}
260
+ {reports.map((r)=>(
261
+ <button key={r} className={"cc-list-item " + (r===sel?"sel":"")} onClick={()=>open(r)}>
262
+ <Icon name="file-text" size={15} color="var(--text-muted)"/>
263
+ <span className="mono">{r}</span>
264
+ </button>
265
+ ))}
266
+ </div>
267
+ </Card>
268
+ </div>
269
+ );
270
+ }
271
+
272
+ /* ─────────── Automation ─────────── */
273
+ function AutomationView() {
274
+ const [state, setState] = useState({log:[], schedule:"", traces:""});
275
+ const [repo, setRepo] = useState("ranranrunforit/chan-compass-agent-traces");
276
+ const [pubStatus, setPubStatus] = useState("");
277
+ const [running, setRunning] = useState(false);
278
+ useEffect(()=>{
279
+ const tick = ()=> API.autoState().then(setState).catch(()=>{});
280
+ tick(); const id=setInterval(tick, 2500); return ()=>clearInterval(id);
281
+ },[]);
282
+ const runNow = async () => { setRunning(true); await API.autoRun().catch(()=>{}); setRunning(false); };
283
+ const publish = async () => { setPubStatus("Publishing...");
284
+ try { const r = await API.publishTraces(repo); setPubStatus(r.status); }
285
+ catch(e){ setPubStatus("X "+e.message); } };
286
+ return (
287
+ <div style={{display:"flex", flexDirection:"column", gap:"var(--space-300)"}}>
288
+ <InlineAlert variant="informative" title="Daily pipeline">{state.schedule}</InlineAlert>
289
+ <Card title="Pipeline" subtitle="data, signals, rotation, news, auto-research"
290
+ action={<Button variant="accent" size="sm" onClick={runNow} disabled={running}>
291
+ <Icon name="zap" size={14}/> {running?"Running...":"Run now"}</Button>}>
292
+ <div className="cc-log" style={{maxHeight:300, overflow:"auto"}}>
293
+ {(state.log||[]).map((l,i)=><div key={i} className="mono" style={{fontSize:12}}>{l}</div>)}
294
+ {(!state.log || !state.log.length) && <span className="dim">(no log yet)</span>}
295
+ </div>
296
+ </Card>
297
+ <Card title="Share traces" subtitle="Publish every agent trace as a Hub dataset (uses your HF_TOKEN - no command line)">
298
+ <div style={{display:"flex", gap:"var(--space-150)", alignItems:"flex-end"}}>
299
+ <Field label="Dataset repo id" mono value={repo} onChange={e=>setRepo(e.target.value)} className="grow"/>
300
+ <Button variant="secondary" onClick={publish}><Icon name="upload-cloud" size={14}/> Publish traces as dataset</Button>
301
+ </div>
302
+ {pubStatus && <div className="dim" style={{marginTop:8, fontSize:13}}><Markdown text={pubStatus}/></div>}
303
+ <div className="cc-llm-out" style={{marginTop:12}}><Markdown text={state.traces||""}/></div>
304
+ </Card>
305
+ </div>
306
+ );
307
+ }
308
+
309
+ /* ─────────── Model ─────────── */
310
+ function ModelView() {
311
+ const [status, setStatus] = useState("");
312
+ const [workers, setWorkers] = useState({});
313
+ const [test, setTest] = useState("");
314
+ const [ft, setFt] = useState(""); const [dl, setDl] = useState("");
315
+ useEffect(()=>{
316
+ const tick = ()=> API.modelStatus().then(r=>{setStatus(r.status); setWorkers(r.workers||{});}).catch(()=>{});
317
+ tick(); const id=setInterval(tick, 3000); return ()=>clearInterval(id);
318
+ },[]);
319
+ useEffect(()=>{ API.finetuneStatus().then(r=>setFt(r.status)).catch(()=>{}); },[]);
320
+ const runTest = async () => { setTest("Testing..."); const r=await API.modelTest(); setTest(r.result); };
321
+ const exportDs = async () => { const r = await API.exportDataset();
322
+ setDl(r.path ? "Exported "+r.count+" pairs -> "+r.path : "No pairs captured yet."); };
323
+ return (
324
+ <div style={{display:"flex", flexDirection:"column", gap:"var(--space-300)", maxWidth:760}}>
325
+ <InlineAlert variant="informative" title="Everything runs locally through llama.cpp">
326
+ Qwen3 GGUF weights, all below the 32B cap. The Summary sub-agent uses a published fine-tune (Chan-Tuned Qwen3-1.7B).
327
+ </InlineAlert>
328
+ <Card title="Sub-agent pool" subtitle="Each sub-agent has its own lock - they run in parallel"
329
+ action={<Button variant="secondary" size="sm" onClick={runTest}><Icon name="zap" size={14}/> Test sub-agents now</Button>}>
330
+ <div className="cc-radios">
331
+ {Object.entries(workers).map(([k,w])=>(
332
+ <div key={k} className="cc-radio" style={{cursor:"default"}}>
333
+ <StatusLight variant={w.ready?"positive":"notice"} live={w.ready}>{k}</StatusLight>
334
+ <span className="mono dim" style={{marginLeft:8}}>{w.model}</span>
335
+ <span style={{marginLeft:"auto"}} className="dim">{w.ready?"ready":w.stage}</span>
336
+ </div>
337
+ ))}
338
+ </div>
339
+ {test && <div className="cc-llm-out" style={{marginTop:12}}><Markdown text={test}/></div>}
340
+ </Card>
341
+ <Card title="Fine-tuning dataset" subtitle="Every Signals AI summary is captured as a training pair on /data">
342
+ <div className="dim" style={{marginBottom:10}}><Markdown text={ft}/></div>
343
+ <Button variant="accent" onClick={exportDs}><Icon name="download" size={15}/> Export dataset (JSONL)</Button>
344
+ {dl && <div className="dim" style={{marginTop:8, fontSize:13}}>{dl}</div>}
345
+ </Card>
346
+ </div>
347
+ );
348
+ }
349
+
350
+ Object.assign(window, { SignalsView, RotationView, ResearchView, NewsView, AutomationView, ModelView });