@forgecharts/sdk 1.3.6 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import React7, { forwardRef, useRef, useState, useImperativeHandle, useEffect, useCallback, createContext, useMemo, useContext } from 'react';
1
+ import React11, { forwardRef, useRef, useState, useImperativeHandle, useEffect, useCallback, createContext, useMemo, useContext } from 'react';
2
2
  import { TextStyle, Application, Container, Graphics, Text, FillGradient } from 'pixi.js';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import ReactDOM from 'react-dom';
@@ -28018,6 +28018,1640 @@ function OrderTicket({
28018
28018
  }
28019
28019
  );
28020
28020
  }
28021
+ var COLUMN_TYPES = [
28022
+ { value: "number", label: "Number", hint: "Float (prices, scores, ratios)" },
28023
+ { value: "integer", label: "Integer", hint: "Whole number (volume, count)" },
28024
+ { value: "text", label: "Text", hint: "String (labels, symbols)" },
28025
+ { value: "boolean", label: "Boolean", hint: "True / False flag" },
28026
+ { value: "date", label: "Date", hint: "Calendar date (no time)" },
28027
+ { value: "timestamp", label: "Timestamp", hint: "Full datetime with timezone" },
28028
+ { value: "percentage", label: "Percentage", hint: "Float stored as 0\u2013100" },
28029
+ { value: "price", label: "Price", hint: "Float (currency value)" }
28030
+ ];
28031
+ function DatasetManagerDrawer({ scriptId, isOwner, apiBase, authToken: staticToken, getAuthToken }) {
28032
+ const resolveToken = React11.useCallback(async () => {
28033
+ if (staticToken) return staticToken;
28034
+ if (getAuthToken) {
28035
+ try {
28036
+ return await getAuthToken();
28037
+ } catch {
28038
+ return "";
28039
+ }
28040
+ }
28041
+ return "";
28042
+ }, [staticToken, getAuthToken]);
28043
+ const [view, setView] = useState("list");
28044
+ const [datasets, setDatasets] = useState([]);
28045
+ const [activeDataset, setActiveDataset] = useState(null);
28046
+ const [loading, setLoading] = useState(false);
28047
+ const [error, setError] = useState(null);
28048
+ const fetchDatasets = useCallback(async () => {
28049
+ if (!scriptId) return;
28050
+ setLoading(true);
28051
+ setError(null);
28052
+ try {
28053
+ const token = await resolveToken();
28054
+ const res = await fetch(`${apiBase}/api/scripts/${scriptId}/datasets`, {
28055
+ headers: { Authorization: `Bearer ${token}` }
28056
+ });
28057
+ if (!res.ok) throw new Error(await res.text());
28058
+ setDatasets(await res.json());
28059
+ } catch (e) {
28060
+ setError(String(e));
28061
+ } finally {
28062
+ setLoading(false);
28063
+ }
28064
+ }, [scriptId, apiBase, resolveToken]);
28065
+ useEffect(() => {
28066
+ void fetchDatasets();
28067
+ }, [fetchDatasets]);
28068
+ useEffect(() => {
28069
+ const processing = datasets.some((d) => d.ingest_status === "pending" || d.ingest_status === "processing");
28070
+ if (!processing) return;
28071
+ const timer = setInterval(() => {
28072
+ void fetchDatasets();
28073
+ }, 3e3);
28074
+ return () => clearInterval(timer);
28075
+ }, [datasets, fetchDatasets]);
28076
+ const handleDelete = async (dsId) => {
28077
+ if (!scriptId || !confirm("Delete this dataset and all its rows?")) return;
28078
+ const token = await resolveToken();
28079
+ await fetch(`${apiBase}/api/scripts/${scriptId}/datasets/${dsId}`, {
28080
+ method: "DELETE",
28081
+ headers: { Authorization: `Bearer ${token}` }
28082
+ });
28083
+ void fetchDatasets();
28084
+ };
28085
+ const handleViewUsage = (ds) => {
28086
+ setActiveDataset(ds);
28087
+ setView("usage");
28088
+ };
28089
+ const handleEdit = (ds) => {
28090
+ setActiveDataset(ds);
28091
+ setView("design");
28092
+ };
28093
+ if (!scriptId) {
28094
+ return /* @__PURE__ */ jsx("div", { className: "dsm-empty", children: /* @__PURE__ */ jsx("p", { children: "Save your script first to manage datasets." }) });
28095
+ }
28096
+ return /* @__PURE__ */ jsxs("div", { className: "dsm-root", children: [
28097
+ /* @__PURE__ */ jsxs("div", { className: "dsm-header", children: [
28098
+ view !== "list" && /* @__PURE__ */ jsx("button", { className: "dsm-back-btn", onClick: () => {
28099
+ setView("list");
28100
+ setActiveDataset(null);
28101
+ }, children: "\u2190 Back" }),
28102
+ /* @__PURE__ */ jsx("span", { className: "dsm-title", children: view === "list" ? "Datasets" : view === "design" ? activeDataset ? `Edit: ${activeDataset.name}` : "New Dataset" : view === "import" ? `Import: ${activeDataset?.name}` : `Usage: ${activeDataset?.name}` }),
28103
+ view === "list" && isOwner && /* @__PURE__ */ jsx("button", { className: "dsm-new-btn", onClick: () => {
28104
+ setActiveDataset(null);
28105
+ setView("design");
28106
+ }, children: "+ New Dataset" })
28107
+ ] }),
28108
+ error && /* @__PURE__ */ jsx("div", { className: "dsm-error", children: error }),
28109
+ view === "list" && /* @__PURE__ */ jsx(
28110
+ DatasetList,
28111
+ {
28112
+ datasets,
28113
+ loading,
28114
+ isOwner,
28115
+ onDelete: handleDelete,
28116
+ onViewUsage: handleViewUsage,
28117
+ onEdit: handleEdit,
28118
+ onImport: (ds) => {
28119
+ setActiveDataset(ds);
28120
+ setView("import");
28121
+ }
28122
+ }
28123
+ ),
28124
+ view === "design" && /* @__PURE__ */ jsx(
28125
+ DatasetDesigner,
28126
+ {
28127
+ scriptId,
28128
+ existing: activeDataset,
28129
+ apiBase,
28130
+ resolveToken,
28131
+ onSchemaSaved: (ds) => {
28132
+ setDatasets((prev) => {
28133
+ const idx = prev.findIndex((d) => d.id === ds.id);
28134
+ return idx >= 0 ? prev.map((d, i) => i === idx ? ds : d) : [...prev, ds];
28135
+ });
28136
+ setActiveDataset(ds);
28137
+ setView("import");
28138
+ },
28139
+ onError: setError
28140
+ }
28141
+ ),
28142
+ view === "import" && activeDataset && /* @__PURE__ */ jsx(
28143
+ DatasetImporter,
28144
+ {
28145
+ scriptId,
28146
+ dataset: activeDataset,
28147
+ apiBase,
28148
+ resolveToken,
28149
+ onImported: (ds) => {
28150
+ setDatasets((prev) => prev.map((d) => d.id === ds.id ? ds : d));
28151
+ setActiveDataset(ds);
28152
+ setView("usage");
28153
+ },
28154
+ onSkip: () => setView("usage"),
28155
+ onError: setError
28156
+ }
28157
+ ),
28158
+ view === "usage" && activeDataset && /* @__PURE__ */ jsx(DatasetUsage, { dataset: activeDataset })
28159
+ ] });
28160
+ }
28161
+ function DatasetList({ datasets, loading, isOwner, onDelete, onViewUsage, onEdit, onImport }) {
28162
+ if (loading && datasets.length === 0) {
28163
+ return /* @__PURE__ */ jsx("div", { className: "dsm-loading", children: "Loading datasets\u2026" });
28164
+ }
28165
+ if (datasets.length === 0) {
28166
+ return /* @__PURE__ */ jsxs("div", { className: "dsm-empty", children: [
28167
+ /* @__PURE__ */ jsx("p", { children: "No datasets yet." }),
28168
+ isOwner && /* @__PURE__ */ jsxs("p", { children: [
28169
+ "Click ",
28170
+ /* @__PURE__ */ jsx("strong", { children: "+ New Dataset" }),
28171
+ " to import your first dataset."
28172
+ ] })
28173
+ ] });
28174
+ }
28175
+ return /* @__PURE__ */ jsx("div", { className: "dsm-list", children: datasets.map((ds) => /* @__PURE__ */ jsxs("div", { className: "dsm-list-item", children: [
28176
+ /* @__PURE__ */ jsxs("div", { className: "dsm-list-item-header", children: [
28177
+ /* @__PURE__ */ jsx("span", { className: "dsm-list-item-name", children: ds.name }),
28178
+ /* @__PURE__ */ jsx("span", { className: `dsm-status dsm-status--${ds.ingest_status}`, children: ds.ingest_status === "ready" ? `${ds.row_count.toLocaleString()} rows` : ds.ingest_status })
28179
+ ] }),
28180
+ /* @__PURE__ */ jsxs("div", { className: "dsm-list-item-cols", children: [
28181
+ /* @__PURE__ */ jsx("span", { className: "dsm-col-tag dsm-col-tag--time", children: "time" }),
28182
+ ds.columns.map((c) => /* @__PURE__ */ jsx("span", { className: `dsm-col-tag dsm-col-tag--${c.type}`, title: c.type, children: c.name }, c.name))
28183
+ ] }),
28184
+ ds.ingest_error && /* @__PURE__ */ jsxs("div", { className: "dsm-ingest-error", children: [
28185
+ "\u26A0 ",
28186
+ ds.ingest_error
28187
+ ] }),
28188
+ /* @__PURE__ */ jsxs("div", { className: "dsm-list-item-actions", children: [
28189
+ ds.ingest_status === "ready" && /* @__PURE__ */ jsx("button", { className: "dsm-action-btn", onClick: () => onViewUsage(ds), children: "Usage" }),
28190
+ isOwner && /* @__PURE__ */ jsxs(Fragment, { children: [
28191
+ /* @__PURE__ */ jsx("button", { className: "dsm-action-btn dsm-action-btn--primary", onClick: () => onImport(ds), children: ds.ingest_status === "ready" ? "Re-import" : "Import Data" }),
28192
+ /* @__PURE__ */ jsx("button", { className: "dsm-action-btn", onClick: () => onEdit(ds), children: "Edit Schema" }),
28193
+ /* @__PURE__ */ jsx("button", { className: "dsm-action-btn dsm-action-btn--danger", onClick: () => onDelete(ds.id), children: "Delete" })
28194
+ ] })
28195
+ ] })
28196
+ ] }, ds.id)) });
28197
+ }
28198
+ function DatasetDesigner({ scriptId, existing, apiBase, resolveToken, onSchemaSaved, onError }) {
28199
+ const [name, setName] = useState(existing?.name ?? "");
28200
+ const [columns, setColumns] = useState(
28201
+ existing?.columns ?? [{ name: "", type: "number", nullable: true }]
28202
+ );
28203
+ const [saving, setSaving] = useState(false);
28204
+ const addColumn = () => setColumns((prev) => [...prev, { name: "", type: "number", nullable: true }]);
28205
+ const removeColumn = (i) => setColumns((prev) => prev.filter((_, idx) => idx !== i));
28206
+ const updateColumn = (i, field, value) => setColumns((prev) => prev.map((c, idx) => idx === i ? { ...c, [field]: value } : c));
28207
+ const handleSaveSchema = async () => {
28208
+ if (!name.trim()) {
28209
+ onError("Dataset name is required.");
28210
+ return;
28211
+ }
28212
+ if (columns.some((c) => !c.name.trim())) {
28213
+ onError("All columns must have a name.");
28214
+ return;
28215
+ }
28216
+ setSaving(true);
28217
+ try {
28218
+ const body = {
28219
+ name: name.trim(),
28220
+ columns: columns.map((c) => ({ ...c, name: c.name.trim() }))
28221
+ };
28222
+ const token = await resolveToken();
28223
+ const url = existing ? `${apiBase}/api/scripts/${scriptId}/datasets/${existing.id}` : `${apiBase}/api/scripts/${scriptId}/datasets`;
28224
+ const method = existing ? "PATCH" : "POST";
28225
+ const res = await fetch(url, {
28226
+ method,
28227
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
28228
+ body: JSON.stringify(body)
28229
+ });
28230
+ if (!res.ok) {
28231
+ const text = await res.text();
28232
+ let msg;
28233
+ try {
28234
+ msg = JSON.stringify(JSON.parse(text).error);
28235
+ } catch {
28236
+ msg = `HTTP ${res.status}`;
28237
+ }
28238
+ throw new Error(msg);
28239
+ }
28240
+ onSchemaSaved(await res.json());
28241
+ } catch (e) {
28242
+ onError(String(e));
28243
+ } finally {
28244
+ setSaving(false);
28245
+ }
28246
+ };
28247
+ return /* @__PURE__ */ jsxs("div", { className: "dsm-designer", children: [
28248
+ !existing && /* @__PURE__ */ jsxs("div", { className: "dsm-steps", children: [
28249
+ /* @__PURE__ */ jsx("span", { className: "dsm-step dsm-step--active", children: "1 \xB7 Define schema" }),
28250
+ /* @__PURE__ */ jsx("span", { className: "dsm-step-sep", children: "\u203A" }),
28251
+ /* @__PURE__ */ jsx("span", { className: "dsm-step dsm-step--upcoming", children: "2 \xB7 Import data" })
28252
+ ] }),
28253
+ /* @__PURE__ */ jsx("label", { className: "dsm-label", children: "Dataset Name" }),
28254
+ /* @__PURE__ */ jsx(
28255
+ "input",
28256
+ {
28257
+ className: "dsm-input",
28258
+ value: name,
28259
+ onChange: (e) => setName(e.target.value),
28260
+ placeholder: "e.g. sentiment",
28261
+ disabled: !!existing
28262
+ }
28263
+ ),
28264
+ /* @__PURE__ */ jsx("p", { className: "dsm-hint", children: "Lowercase letters, numbers, and underscores only." }),
28265
+ /* @__PURE__ */ jsx("label", { className: "dsm-label", children: "Columns" }),
28266
+ /* @__PURE__ */ jsxs("div", { className: "dsm-col-locked", children: [
28267
+ /* @__PURE__ */ jsx("span", { className: "dsm-col-tag dsm-col-tag--time", children: "time" }),
28268
+ /* @__PURE__ */ jsx("span", { className: "dsm-hint", children: "Required \u2014 always the first column" })
28269
+ ] }),
28270
+ columns.map((col, i) => /* @__PURE__ */ jsxs("div", { className: "dsm-col-row", children: [
28271
+ /* @__PURE__ */ jsx(
28272
+ "input",
28273
+ {
28274
+ className: "dsm-input dsm-col-name",
28275
+ value: col.name,
28276
+ onChange: (e) => updateColumn(i, "name", e.target.value),
28277
+ placeholder: "column_name"
28278
+ }
28279
+ ),
28280
+ /* @__PURE__ */ jsx(
28281
+ "select",
28282
+ {
28283
+ className: "dsm-select",
28284
+ value: col.type,
28285
+ onChange: (e) => updateColumn(i, "type", e.target.value),
28286
+ children: COLUMN_TYPES.map((t) => /* @__PURE__ */ jsx("option", { value: t.value, title: t.hint, children: t.label }, t.value))
28287
+ }
28288
+ ),
28289
+ /* @__PURE__ */ jsxs("label", { className: "dsm-nullable-label", children: [
28290
+ /* @__PURE__ */ jsx(
28291
+ "input",
28292
+ {
28293
+ type: "checkbox",
28294
+ checked: col.nullable,
28295
+ onChange: (e) => updateColumn(i, "nullable", e.target.checked)
28296
+ }
28297
+ ),
28298
+ "Nullable"
28299
+ ] }),
28300
+ /* @__PURE__ */ jsx("button", { className: "dsm-remove-col-btn", onClick: () => removeColumn(i), children: "\u2715" })
28301
+ ] }, i)),
28302
+ /* @__PURE__ */ jsx("button", { className: "dsm-add-col-btn", onClick: addColumn, children: "+ Add Column" }),
28303
+ /* @__PURE__ */ jsx("button", { className: "dsm-save-btn", onClick: handleSaveSchema, disabled: saving, children: saving ? "Saving\u2026" : existing ? "Save Schema \u2192" : "Create Dataset \u2192" })
28304
+ ] });
28305
+ }
28306
+ function buildCsvTemplate(dataset) {
28307
+ const headers = ["time", ...dataset.columns.map((c) => c.name)].join(",");
28308
+ const exampleRow = [
28309
+ "2024-01-01",
28310
+ ...dataset.columns.map((c) => {
28311
+ switch (c.type) {
28312
+ case "number":
28313
+ case "price":
28314
+ case "percentage":
28315
+ return "0.00";
28316
+ case "integer":
28317
+ return "0";
28318
+ case "boolean":
28319
+ return "false";
28320
+ case "date":
28321
+ return "2024-01-01";
28322
+ case "timestamp":
28323
+ return "2024-01-01T00:00:00Z";
28324
+ case "text":
28325
+ return "value";
28326
+ default:
28327
+ return "";
28328
+ }
28329
+ })
28330
+ ].join(",");
28331
+ return `${headers}
28332
+ ${exampleRow}
28333
+ `;
28334
+ }
28335
+ function DatasetImporter({ scriptId, dataset, apiBase, resolveToken, onImported, onSkip, onError }) {
28336
+ const [fileContent, setFileContent] = useState("");
28337
+ const [fileType, setFileType] = useState("csv");
28338
+ const [preview, setPreview] = useState([]);
28339
+ const [importing, setImporting] = useState(false);
28340
+ const [fileName, setFileName] = useState("");
28341
+ const fileRef = useRef(null);
28342
+ const allHeaders = ["time", ...dataset.columns.map((c) => c.name)];
28343
+ const downloadTemplate = () => {
28344
+ const csv = buildCsvTemplate(dataset);
28345
+ const blob = new Blob([csv], { type: "text/csv" });
28346
+ const url = URL.createObjectURL(blob);
28347
+ const a = document.createElement("a");
28348
+ a.href = url;
28349
+ a.download = `${dataset.name}_template.csv`;
28350
+ a.click();
28351
+ URL.revokeObjectURL(url);
28352
+ };
28353
+ const handleFile = (e) => {
28354
+ const file = e.target.files?.[0];
28355
+ if (!file) return;
28356
+ setFileName(file.name);
28357
+ const ft = file.name.endsWith(".json") ? "json" : "csv";
28358
+ setFileType(ft);
28359
+ const reader = new FileReader();
28360
+ reader.onload = (ev) => {
28361
+ const content = ev.target?.result;
28362
+ setFileContent(content);
28363
+ if (ft === "csv") {
28364
+ const lines = content.split(/\r?\n/).filter((l) => l.trim()).slice(0, 6);
28365
+ setPreview(lines.map((l) => l.split(",")));
28366
+ } else {
28367
+ try {
28368
+ const arr = JSON.parse(content);
28369
+ const first = arr.slice(0, 5);
28370
+ const keys = Object.keys(first[0] ?? {});
28371
+ setPreview([keys, ...first.map((r) => keys.map((k) => String(r[k] ?? "")))]);
28372
+ } catch {
28373
+ setPreview([]);
28374
+ }
28375
+ }
28376
+ };
28377
+ reader.readAsText(file);
28378
+ };
28379
+ const handleImport = async () => {
28380
+ if (!fileContent) {
28381
+ onError("Please choose a file to import.");
28382
+ return;
28383
+ }
28384
+ setImporting(true);
28385
+ try {
28386
+ const token = await resolveToken();
28387
+ const res = await fetch(`${apiBase}/api/scripts/${scriptId}/datasets/${dataset.id}`, {
28388
+ method: "PATCH",
28389
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
28390
+ body: JSON.stringify({ file_content: fileContent, file_type: fileType })
28391
+ });
28392
+ if (!res.ok) {
28393
+ const text = await res.text();
28394
+ let msg;
28395
+ try {
28396
+ msg = JSON.stringify(JSON.parse(text).error);
28397
+ } catch {
28398
+ msg = `HTTP ${res.status}`;
28399
+ }
28400
+ throw new Error(msg);
28401
+ }
28402
+ onImported(await res.json());
28403
+ } catch (e) {
28404
+ onError(String(e));
28405
+ } finally {
28406
+ setImporting(false);
28407
+ }
28408
+ };
28409
+ return /* @__PURE__ */ jsxs("div", { className: "dsm-importer", children: [
28410
+ /* @__PURE__ */ jsxs("div", { className: "dsm-steps", children: [
28411
+ /* @__PURE__ */ jsx("span", { className: "dsm-step dsm-step--done", children: "1 \xB7 Define schema \u2713" }),
28412
+ /* @__PURE__ */ jsx("span", { className: "dsm-step-sep", children: "\u203A" }),
28413
+ /* @__PURE__ */ jsx("span", { className: "dsm-step dsm-step--active", children: "2 \xB7 Import data" })
28414
+ ] }),
28415
+ /* @__PURE__ */ jsxs("div", { className: "dsm-schema-summary", children: [
28416
+ /* @__PURE__ */ jsx("span", { className: "dsm-schema-summary-label", children: "Expected columns:" }),
28417
+ allHeaders.map((h) => /* @__PURE__ */ jsx("span", { className: `dsm-col-tag dsm-col-tag--${h === "time" ? "time" : dataset.columns.find((c) => c.name === h)?.type ?? "number"}`, children: h }, h))
28418
+ ] }),
28419
+ /* @__PURE__ */ jsxs("div", { className: "dsm-template-section", children: [
28420
+ /* @__PURE__ */ jsxs("div", { className: "dsm-template-desc", children: [
28421
+ "Download a pre-filled CSV template with the correct column headers for ",
28422
+ /* @__PURE__ */ jsx("strong", { children: dataset.name }),
28423
+ ". Fill it with your data, then upload it below."
28424
+ ] }),
28425
+ /* @__PURE__ */ jsx("button", { className: "dsm-template-btn", onClick: downloadTemplate, children: "\u2193 Download CSV Template" })
28426
+ ] }),
28427
+ /* @__PURE__ */ jsx("label", { className: "dsm-label", children: "Upload Data (CSV or JSON)" }),
28428
+ /* @__PURE__ */ jsx("input", { ref: fileRef, type: "file", accept: ".csv,.json", className: "dsm-file-input", onChange: handleFile }),
28429
+ /* @__PURE__ */ jsx("button", { className: "dsm-upload-btn", onClick: () => fileRef.current?.click(), children: fileName ? `\u2713 ${fileName} \u2014 click to change` : "Choose File" }),
28430
+ preview.length > 0 && /* @__PURE__ */ jsxs("div", { className: "dsm-preview", children: [
28431
+ /* @__PURE__ */ jsx("div", { className: "dsm-preview-label", children: "Preview (first 5 rows)" }),
28432
+ /* @__PURE__ */ jsx("div", { className: "dsm-preview-table-wrap", children: /* @__PURE__ */ jsxs("table", { className: "dsm-preview-table", children: [
28433
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: preview[0].map((h, i) => /* @__PURE__ */ jsx("th", { className: allHeaders.includes(h.trim()) ? "" : "dsm-col-unknown", children: h }, i)) }) }),
28434
+ /* @__PURE__ */ jsx("tbody", { children: preview.slice(1).map((row, i) => /* @__PURE__ */ jsx("tr", { children: row.map((cell, j) => /* @__PURE__ */ jsx("td", { children: cell }, j)) }, i)) })
28435
+ ] }) })
28436
+ ] }),
28437
+ /* @__PURE__ */ jsxs("div", { className: "dsm-import-actions", children: [
28438
+ /* @__PURE__ */ jsx("button", { className: "dsm-save-btn", onClick: handleImport, disabled: importing || !fileContent, children: importing ? "Importing\u2026" : "Import Data" }),
28439
+ /* @__PURE__ */ jsx("button", { className: "dsm-skip-btn", onClick: onSkip, children: "Skip for now" })
28440
+ ] })
28441
+ ] });
28442
+ }
28443
+ function DatasetUsage({ dataset }) {
28444
+ const [copied, setCopied] = useState(false);
28445
+ const snippet = [
28446
+ `// Dataset: ${dataset.name} (${dataset.row_count.toLocaleString()} rows)`,
28447
+ ...dataset.columns.map((c) => `${c.name} = request.data("${dataset.name}", "${c.name}")`)
28448
+ ].join("\n");
28449
+ const copy = () => {
28450
+ void navigator.clipboard.writeText(snippet);
28451
+ setCopied(true);
28452
+ setTimeout(() => setCopied(false), 2e3);
28453
+ };
28454
+ return /* @__PURE__ */ jsxs("div", { className: "dsm-usage", children: [
28455
+ /* @__PURE__ */ jsxs("p", { className: "dsm-usage-intro", children: [
28456
+ "Copy this snippet into your ForgeScript indicator to access the ",
28457
+ /* @__PURE__ */ jsx("strong", { children: dataset.name }),
28458
+ " dataset."
28459
+ ] }),
28460
+ /* @__PURE__ */ jsxs("div", { className: "dsm-usage-code-wrap", children: [
28461
+ /* @__PURE__ */ jsx("pre", { className: "dsm-usage-code", children: snippet }),
28462
+ /* @__PURE__ */ jsx("button", { className: "dsm-copy-btn", onClick: copy, children: copied ? "\u2713 Copied" : "Copy" })
28463
+ ] }),
28464
+ /* @__PURE__ */ jsxs("div", { className: "dsm-usage-cols", children: [
28465
+ /* @__PURE__ */ jsx("div", { className: "dsm-usage-col-label", children: "Available columns:" }),
28466
+ /* @__PURE__ */ jsx("div", { className: "dsm-col-tag dsm-col-tag--time", children: "time" }),
28467
+ dataset.columns.map((c) => /* @__PURE__ */ jsxs("div", { className: `dsm-col-tag dsm-col-tag--${c.type}`, title: c.type, children: [
28468
+ c.name,
28469
+ " ",
28470
+ /* @__PURE__ */ jsxs("span", { className: "dsm-col-type", children: [
28471
+ "(",
28472
+ c.type,
28473
+ ")"
28474
+ ] })
28475
+ ] }, c.name))
28476
+ ] })
28477
+ ] });
28478
+ }
28479
+ var TEMPLATE = `## Overview
28480
+
28481
+ Describe what this indicator measures and why it is useful.
28482
+
28483
+ ## How It Works
28484
+
28485
+ Explain the calculation logic in plain language.
28486
+
28487
+ ## Inputs & Parameters
28488
+
28489
+ List each \`input.*\` variable the user can configure.
28490
+
28491
+ ## Interpreting Signals
28492
+
28493
+ Explain how to read the plots, shapes, or alerts this indicator produces.
28494
+
28495
+ ## Datasets Required
28496
+
28497
+ If this indicator uses \`request.data()\`, describe the bundled datasets and their columns.
28498
+
28499
+ ## Limitations & Caveats
28500
+
28501
+ Note any known edge cases, asset classes it does not work well on, or timeframe restrictions.
28502
+
28503
+ ## Changelog
28504
+
28505
+ - **v1** \u2014 Initial release
28506
+ `;
28507
+ function DocsEditor({ scriptId, isOwner, apiBase, authToken: staticToken, getAuthToken }) {
28508
+ const resolveToken = useCallback(async () => {
28509
+ if (getAuthToken) {
28510
+ try {
28511
+ return await getAuthToken();
28512
+ } catch {
28513
+ return "";
28514
+ }
28515
+ }
28516
+ return staticToken ?? "";
28517
+ }, [getAuthToken, staticToken]);
28518
+ const [content, setContent] = useState("");
28519
+ const [original, setOriginal] = useState("");
28520
+ const [loading, setLoading] = useState(false);
28521
+ const [saving, setSaving] = useState(false);
28522
+ const [saved, setSaved] = useState(false);
28523
+ const [error, setError] = useState(null);
28524
+ const [preview, setPreview] = useState(false);
28525
+ const textareaRef = useRef(null);
28526
+ const fetchDocs = useCallback(async () => {
28527
+ if (!scriptId) return;
28528
+ setLoading(true);
28529
+ setError(null);
28530
+ try {
28531
+ const tok = await resolveToken();
28532
+ const res = await fetch(`${apiBase}/api/scripts/${scriptId}/docs`, {
28533
+ headers: { Authorization: `Bearer ${tok}` }
28534
+ });
28535
+ if (!res.ok) throw new Error(await res.text());
28536
+ const data = await res.json();
28537
+ setContent(data.content || "");
28538
+ setOriginal(data.content || "");
28539
+ } catch (e) {
28540
+ setError(String(e));
28541
+ } finally {
28542
+ setLoading(false);
28543
+ }
28544
+ }, [scriptId, apiBase, resolveToken]);
28545
+ useEffect(() => {
28546
+ void fetchDocs();
28547
+ }, [fetchDocs]);
28548
+ const handleSave = async () => {
28549
+ if (!scriptId) return;
28550
+ setSaving(true);
28551
+ setError(null);
28552
+ try {
28553
+ const tok = await resolveToken();
28554
+ const res = await fetch(`${apiBase}/api/scripts/${scriptId}/docs`, {
28555
+ method: "PUT",
28556
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${tok}` },
28557
+ body: JSON.stringify({ content })
28558
+ });
28559
+ if (!res.ok) throw new Error(await res.text());
28560
+ setOriginal(content);
28561
+ setSaved(true);
28562
+ setTimeout(() => setSaved(false), 2500);
28563
+ } catch (e) {
28564
+ setError(String(e));
28565
+ } finally {
28566
+ setSaving(false);
28567
+ }
28568
+ };
28569
+ const isDirty = content !== original;
28570
+ if (!scriptId) {
28571
+ return /* @__PURE__ */ jsx("div", { className: "docs-empty", children: "Save your script first to add documentation." });
28572
+ }
28573
+ if (loading) {
28574
+ return /* @__PURE__ */ jsx("div", { className: "docs-loading", children: "Loading documentation\u2026" });
28575
+ }
28576
+ if (!isOwner) {
28577
+ return /* @__PURE__ */ jsx("div", { className: "docs-viewer", children: content ? /* @__PURE__ */ jsx("div", { className: "docs-rendered", dangerouslySetInnerHTML: { __html: renderMarkdown(content) } }) : /* @__PURE__ */ jsx("div", { className: "docs-empty", children: "The author has not added documentation yet." }) });
28578
+ }
28579
+ return /* @__PURE__ */ jsxs("div", { className: "docs-editor-root", children: [
28580
+ /* @__PURE__ */ jsxs("div", { className: "docs-toolbar", children: [
28581
+ /* @__PURE__ */ jsx(
28582
+ "button",
28583
+ {
28584
+ className: `docs-tab-btn${!preview ? " active" : ""}`,
28585
+ onClick: () => setPreview(false),
28586
+ children: "Edit"
28587
+ }
28588
+ ),
28589
+ /* @__PURE__ */ jsx(
28590
+ "button",
28591
+ {
28592
+ className: `docs-tab-btn${preview ? " active" : ""}`,
28593
+ onClick: () => setPreview(true),
28594
+ children: "Preview"
28595
+ }
28596
+ ),
28597
+ !content && /* @__PURE__ */ jsx("button", { className: "docs-template-btn", onClick: () => setContent(TEMPLATE), children: "Use Template" }),
28598
+ /* @__PURE__ */ jsx("div", { style: { flex: 1 } }),
28599
+ error && /* @__PURE__ */ jsx("span", { className: "docs-error", children: error }),
28600
+ saved && /* @__PURE__ */ jsx("span", { className: "docs-saved", children: "\u2713 Saved" }),
28601
+ /* @__PURE__ */ jsx(
28602
+ "button",
28603
+ {
28604
+ className: "docs-save-btn",
28605
+ onClick: handleSave,
28606
+ disabled: saving || !isDirty,
28607
+ children: saving ? "Saving\u2026" : "Save Docs"
28608
+ }
28609
+ )
28610
+ ] }),
28611
+ /* @__PURE__ */ jsx("div", { className: "docs-panes", children: !preview ? /* @__PURE__ */ jsx(
28612
+ "textarea",
28613
+ {
28614
+ ref: textareaRef,
28615
+ className: "docs-textarea",
28616
+ value: content,
28617
+ onChange: (e) => setContent(e.target.value),
28618
+ placeholder: "Write your indicator documentation in Markdown\u2026",
28619
+ spellCheck: true
28620
+ }
28621
+ ) : /* @__PURE__ */ jsx("div", { className: "docs-rendered", dangerouslySetInnerHTML: { __html: renderMarkdown(content) } }) })
28622
+ ] });
28623
+ }
28624
+ function renderMarkdown(md) {
28625
+ let html = escapeHtml(md);
28626
+ html = html.replace(
28627
+ /```[\w]*\n([\s\S]*?)```/g,
28628
+ (_m, code) => `<pre class="docs-code-block"><code>${code.trimEnd()}</code></pre>`
28629
+ );
28630
+ html = html.replace(/`([^`]+)`/g, '<code class="docs-inline-code">$1</code>');
28631
+ html = html.replace(/^######\s+(.+)$/gm, "<h6>$1</h6>");
28632
+ html = html.replace(/^#####\s+(.+)$/gm, "<h5>$1</h5>");
28633
+ html = html.replace(/^####\s+(.+)$/gm, "<h4>$1</h4>");
28634
+ html = html.replace(/^###\s+(.+)$/gm, "<h3>$1</h3>");
28635
+ html = html.replace(/^##\s+(.+)$/gm, "<h2>$1</h2>");
28636
+ html = html.replace(/^#\s+(.+)$/gm, "<h1>$1</h1>");
28637
+ html = html.replace(/\*\*\*(.+?)\*\*\*/g, "<strong><em>$1</em></strong>");
28638
+ html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
28639
+ html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
28640
+ html = html.replace(/((?:^[-*]\s.+\n?)+)/gm, (block) => {
28641
+ const items = block.trim().split("\n").map((l) => `<li>${l.replace(/^[-*]\s/, "")}</li>`);
28642
+ return `<ul>${items.join("")}</ul>`;
28643
+ });
28644
+ html = html.replace(/^---$/gm, "<hr>");
28645
+ html = html.replace(/^(?!<[hupoli]|<pre|<hr)(.+)$/gm, "<p>$1</p>");
28646
+ html = html.replace(/\n{2,}/g, "\n");
28647
+ return html;
28648
+ }
28649
+ function escapeHtml(s) {
28650
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
28651
+ }
28652
+ var VISIBILITY_OPTIONS = [
28653
+ {
28654
+ value: "private",
28655
+ label: "Private",
28656
+ desc: "Only you can see and run this indicator."
28657
+ },
28658
+ {
28659
+ value: "public",
28660
+ label: "Public",
28661
+ desc: "Any user can view and run this indicator for free."
28662
+ },
28663
+ {
28664
+ value: "for_sale",
28665
+ label: "For Sale",
28666
+ desc: "Any user can see a preview. Only purchasers can run it."
28667
+ }
28668
+ ];
28669
+ function ScriptSettings({ scriptId, isOwner, apiBase, authToken: staticToken, getAuthToken, onUpdated }) {
28670
+ const resolveToken = useCallback(async () => {
28671
+ if (getAuthToken) {
28672
+ try {
28673
+ return await getAuthToken();
28674
+ } catch {
28675
+ return "";
28676
+ }
28677
+ }
28678
+ return staticToken ?? "";
28679
+ }, [getAuthToken, staticToken]);
28680
+ const [meta, setMeta] = useState(null);
28681
+ const [name, setName] = useState("");
28682
+ const [visibility, setVis] = useState("private");
28683
+ const [salePrice, setSalePrice] = useState("");
28684
+ const [currency, setCurrency] = useState("USD");
28685
+ const [previewPct, setPreviewPct] = useState(0);
28686
+ const [loading, setLoading] = useState(false);
28687
+ const [saving, setSaving] = useState(false);
28688
+ const [saved, setSaved] = useState(false);
28689
+ const [error, setError] = useState(null);
28690
+ useEffect(() => {
28691
+ if (!scriptId) return;
28692
+ setLoading(true);
28693
+ resolveToken().then(
28694
+ (tok) => fetch(`${apiBase}/api/scripts/${scriptId}`, {
28695
+ headers: { Authorization: `Bearer ${tok}` }
28696
+ })
28697
+ ).then((r) => r.json()).then((data) => {
28698
+ setMeta(data);
28699
+ setName(data.name);
28700
+ setVis(data.visibility);
28701
+ setSalePrice(data.sale_price ?? "");
28702
+ setCurrency(data.sale_currency);
28703
+ setPreviewPct(data.preview_pct);
28704
+ }).catch((e) => setError(String(e))).finally(() => setLoading(false));
28705
+ }, [scriptId, apiBase]);
28706
+ const handleSave = async () => {
28707
+ if (!scriptId) return;
28708
+ if (visibility === "for_sale" && (!salePrice || Number(salePrice) < 1)) {
28709
+ setError("Sale price must be at least $1.00 when set to For Sale.");
28710
+ return;
28711
+ }
28712
+ setSaving(true);
28713
+ setError(null);
28714
+ try {
28715
+ const tok = await resolveToken();
28716
+ const body = {
28717
+ name: name.trim() || meta?.name,
28718
+ visibility,
28719
+ sale_currency: currency,
28720
+ preview_pct: previewPct
28721
+ };
28722
+ if (visibility === "for_sale") {
28723
+ body["sale_price"] = Number(salePrice);
28724
+ }
28725
+ const res = await fetch(`${apiBase}/api/scripts/${scriptId}`, {
28726
+ method: "PATCH",
28727
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${tok}` },
28728
+ body: JSON.stringify(body)
28729
+ });
28730
+ if (!res.ok) throw new Error(await res.text());
28731
+ const updated = await res.json();
28732
+ setMeta(updated);
28733
+ setSaved(true);
28734
+ setTimeout(() => setSaved(false), 2500);
28735
+ onUpdated?.(updated);
28736
+ } catch (e) {
28737
+ setError(String(e));
28738
+ } finally {
28739
+ setSaving(false);
28740
+ }
28741
+ };
28742
+ if (!scriptId) {
28743
+ return /* @__PURE__ */ jsx("div", { className: "ss-empty", children: "Save your script first to configure settings." });
28744
+ }
28745
+ if (loading) {
28746
+ return /* @__PURE__ */ jsx("div", { className: "ss-loading", children: "Loading settings\u2026" });
28747
+ }
28748
+ if (!isOwner) {
28749
+ return /* @__PURE__ */ jsxs("div", { className: "ss-viewer", children: [
28750
+ /* @__PURE__ */ jsxs("div", { className: "ss-row", children: [
28751
+ /* @__PURE__ */ jsx("span", { className: "ss-label", children: "Visibility" }),
28752
+ /* @__PURE__ */ jsx("span", { className: `ss-badge ss-badge--${meta?.visibility}`, children: meta?.visibility?.replace("_", " ") })
28753
+ ] }),
28754
+ meta?.visibility === "for_sale" && /* @__PURE__ */ jsxs("div", { className: "ss-row", children: [
28755
+ /* @__PURE__ */ jsx("span", { className: "ss-label", children: "Price" }),
28756
+ /* @__PURE__ */ jsxs("span", { className: "ss-value", children: [
28757
+ meta.sale_currency,
28758
+ " ",
28759
+ meta.sale_price
28760
+ ] })
28761
+ ] })
28762
+ ] });
28763
+ }
28764
+ return /* @__PURE__ */ jsxs("div", { className: "ss-root", children: [
28765
+ /* @__PURE__ */ jsxs("div", { className: "ss-section", children: [
28766
+ /* @__PURE__ */ jsx("label", { className: "ss-section-label", children: "Indicator Name" }),
28767
+ /* @__PURE__ */ jsx(
28768
+ "input",
28769
+ {
28770
+ className: "ss-input",
28771
+ value: name,
28772
+ onChange: (e) => setName(e.target.value),
28773
+ placeholder: "My Indicator"
28774
+ }
28775
+ )
28776
+ ] }),
28777
+ /* @__PURE__ */ jsxs("div", { className: "ss-section", children: [
28778
+ /* @__PURE__ */ jsx("label", { className: "ss-section-label", children: "Visibility" }),
28779
+ /* @__PURE__ */ jsx("div", { className: "ss-vis-group", children: VISIBILITY_OPTIONS.map((opt) => /* @__PURE__ */ jsx(
28780
+ "button",
28781
+ {
28782
+ className: `ss-vis-btn${visibility === opt.value ? " active" : ""}`,
28783
+ onClick: () => setVis(opt.value),
28784
+ children: opt.label
28785
+ },
28786
+ opt.value
28787
+ )) }),
28788
+ /* @__PURE__ */ jsx("p", { className: "ss-vis-desc", children: VISIBILITY_OPTIONS.find((o) => o.value === visibility)?.desc })
28789
+ ] }),
28790
+ visibility === "for_sale" && /* @__PURE__ */ jsxs("div", { className: "ss-section", children: [
28791
+ /* @__PURE__ */ jsx("label", { className: "ss-section-label", children: "Sale Price" }),
28792
+ /* @__PURE__ */ jsxs("div", { className: "ss-price-row", children: [
28793
+ /* @__PURE__ */ jsxs(
28794
+ "select",
28795
+ {
28796
+ className: "ss-currency-select",
28797
+ value: currency,
28798
+ onChange: (e) => setCurrency(e.target.value),
28799
+ children: [
28800
+ /* @__PURE__ */ jsx("option", { value: "USD", children: "USD" }),
28801
+ /* @__PURE__ */ jsx("option", { value: "EUR", children: "EUR" }),
28802
+ /* @__PURE__ */ jsx("option", { value: "GBP", children: "GBP" })
28803
+ ]
28804
+ }
28805
+ ),
28806
+ /* @__PURE__ */ jsx(
28807
+ "input",
28808
+ {
28809
+ className: "ss-price-input",
28810
+ type: "number",
28811
+ min: "1",
28812
+ step: "0.01",
28813
+ value: salePrice,
28814
+ onChange: (e) => setSalePrice(e.target.value),
28815
+ placeholder: "9.99"
28816
+ }
28817
+ )
28818
+ ] }),
28819
+ /* @__PURE__ */ jsx("p", { className: "ss-hint", children: "Minimum price: $1.00. Purchasers retain access even if you change visibility later." }),
28820
+ /* @__PURE__ */ jsxs("label", { className: "ss-section-label", style: { marginTop: "1rem" }, children: [
28821
+ "Source Preview for Non-Purchasers: ",
28822
+ /* @__PURE__ */ jsxs("strong", { children: [
28823
+ previewPct,
28824
+ "%"
28825
+ ] })
28826
+ ] }),
28827
+ /* @__PURE__ */ jsx(
28828
+ "input",
28829
+ {
28830
+ className: "ss-slider",
28831
+ type: "range",
28832
+ min: "0",
28833
+ max: "100",
28834
+ step: "5",
28835
+ value: previewPct,
28836
+ onChange: (e) => setPreviewPct(Number(e.target.value))
28837
+ }
28838
+ ),
28839
+ /* @__PURE__ */ jsx("p", { className: "ss-hint", children: previewPct === 0 ? "Non-purchasers see no source code." : previewPct === 100 ? "Non-purchasers see the full source code." : `Non-purchasers see the first ${previewPct}% of the source code.` })
28840
+ ] }),
28841
+ /* @__PURE__ */ jsxs("div", { className: "ss-footer", children: [
28842
+ error && /* @__PURE__ */ jsx("span", { className: "ss-error", children: error }),
28843
+ saved && /* @__PURE__ */ jsx("span", { className: "ss-saved", children: "\u2713 Settings saved" }),
28844
+ /* @__PURE__ */ jsx("button", { className: "ss-save-btn", onClick: handleSave, disabled: saving, children: saving ? "Saving\u2026" : "Save Settings" })
28845
+ ] })
28846
+ ] });
28847
+ }
28848
+ var TA_FUNCTIONS = [
28849
+ // Moving averages
28850
+ { name: "sma(src, len)", desc: "Simple moving average" },
28851
+ { name: "ema(src, len)", desc: "Exponential moving average" },
28852
+ { name: "wma(src, len)", desc: "Weighted moving average" },
28853
+ { name: "rma(src, len)", desc: "Wilder (RMA) moving average" },
28854
+ { name: "swma(src)", desc: "Symmetrically weighted MA (period 4)" },
28855
+ { name: "vwma(src, len)", desc: "Volume-weighted moving average" },
28856
+ // Oscillators & indicators
28857
+ { name: "rsi(src, len)", desc: "Relative Strength Index [0\u2013100]" },
28858
+ { name: "macd(src, fast, slow, sig)", desc: "MACD line (stores __macd_signal, __macd_hist)" },
28859
+ { name: "stoch(src, high, low, len)", desc: "Stochastic %K" },
28860
+ { name: "obv()", desc: "On-Balance Volume" },
28861
+ // Bands & volatility
28862
+ { name: "bb(src, len, mult)", desc: "Bollinger middle band (stores __bb_upper, __bb_lower)" },
28863
+ { name: "atr(len)", desc: "Average true range" },
28864
+ { name: "tr()", desc: "True range (single bar)" },
28865
+ { name: "stdev(src, len)", desc: "Standard deviation" },
28866
+ { name: "dev(src, len)", desc: "Mean absolute deviation" },
28867
+ { name: "variance(src, len)", desc: "Rolling variance" },
28868
+ // Crossover & direction
28869
+ { name: "crossover(a, b)", desc: "True when a crosses above b" },
28870
+ { name: "crossunder(a, b)", desc: "True when a crosses below b" },
28871
+ { name: "cross(a, b)", desc: "True on any cross (either direction)" },
28872
+ { name: "rising(src, len)", desc: "True if source rose for len bars" },
28873
+ { name: "falling(src, len)", desc: "True if source fell for len bars" },
28874
+ // Rolling aggregates
28875
+ { name: "highest(src, len)", desc: "Highest value over N bars" },
28876
+ { name: "lowest(src, len)", desc: "Lowest value over N bars" },
28877
+ { name: "sum(src, len)", desc: "Rolling sum over N bars" },
28878
+ { name: "cum(src)", desc: "Cumulative sum from bar 0" },
28879
+ // Change & momentum
28880
+ { name: "change(src, len?)", desc: "Difference from len bars ago (default 1)" },
28881
+ { name: "mom(src, len)", desc: "Momentum (alias for change)" },
28882
+ // Lookback
28883
+ { name: "barssince(cond)", desc: "Bars since condition was true" },
28884
+ { name: "valuewhen(cond, src, occ?)", desc: "Value at Nth true condition" },
28885
+ { name: "pivothigh(src, l, r)", desc: "Pivot high detection" },
28886
+ { name: "pivotlow(src, l, r)", desc: "Pivot low detection" },
28887
+ // Statistical
28888
+ { name: "correlation(s1, s2, len)", desc: "Pearson correlation coefficient" }
28889
+ ];
28890
+ var BUILT_IN_SERIES = [
28891
+ "open",
28892
+ "high",
28893
+ "low",
28894
+ "close",
28895
+ "volume",
28896
+ "hl2",
28897
+ "hlc3",
28898
+ "ohlc4",
28899
+ "bar_index"
28900
+ ];
28901
+ var BARSTATE_PROPERTIES = [
28902
+ { name: "barstate.islast", desc: "True on the last bar" },
28903
+ { name: "barstate.isfirst", desc: "True on the first bar" },
28904
+ { name: "barstate.isconfirmed", desc: "True when the bar is confirmed (always true for historical)" },
28905
+ { name: "barstate.isnew", desc: "True on the first tick of a new bar" },
28906
+ { name: "barstate.ishistory", desc: "True when processing historical data" },
28907
+ { name: "barstate.isrealtime", desc: "True when processing realtime data (always false)" }
28908
+ ];
28909
+ var OUTPUT_FUNCTIONS = [
28910
+ { name: "plot(value, ...)", desc: "Plot a line on the chart" },
28911
+ { name: "hline(price, ...)", desc: "Horizontal reference line" },
28912
+ { name: "fill(id1, id2, ...)", desc: "Fill area between two references" },
28913
+ { name: "bgcolor(color)", desc: "Set per-bar background color" },
28914
+ { name: "barcolor(color)", desc: "Override candle color per bar" },
28915
+ { name: "plotshape(cond, ...)", desc: "Draw a marker shape" },
28916
+ { name: "plotchar(cond, ...)", desc: "Draw a character marker" },
28917
+ { name: "plotarrow(value, ...)", desc: "Draw up/down arrow" }
28918
+ ];
28919
+ var UTILITY_FUNCTIONS = [
28920
+ { name: 'indicator("Title")', desc: "Declare the indicator name" },
28921
+ { name: "input(default)", desc: "Create a numeric user parameter" },
28922
+ { name: "input.string(default)", desc: "Create a string user parameter" },
28923
+ { name: "input.text_area(default)", desc: "Create a multiline text parameter" },
28924
+ { name: "nz(val, rep?)", desc: "Replace NaN with rep (default 0)" },
28925
+ { name: "na(val)", desc: "Returns true if value is NaN" }
28926
+ ];
28927
+ var MATH_FUNCTIONS = [
28928
+ "abs",
28929
+ "max",
28930
+ "min",
28931
+ "round",
28932
+ "floor",
28933
+ "ceil",
28934
+ "sqrt",
28935
+ "log",
28936
+ "log10",
28937
+ "exp",
28938
+ "pow",
28939
+ "sign",
28940
+ "sin",
28941
+ "cos",
28942
+ "tan",
28943
+ "asin",
28944
+ "acos",
28945
+ "atan",
28946
+ "avg",
28947
+ "random"
28948
+ ];
28949
+ var COLOR_CONSTANTS = [
28950
+ "red",
28951
+ "green",
28952
+ "blue",
28953
+ "white",
28954
+ "black",
28955
+ "yellow",
28956
+ "orange",
28957
+ "purple",
28958
+ "aqua",
28959
+ "lime",
28960
+ "teal",
28961
+ "fuchsia",
28962
+ "silver",
28963
+ "gray",
28964
+ "olive",
28965
+ "maroon",
28966
+ "navy"
28967
+ ];
28968
+ var COLOR_FUNCTIONS = [
28969
+ { name: "color.new(base, transp)", desc: "Apply transparency (0\u2013100) to a color" },
28970
+ { name: "color.rgb(r, g, b, t?)", desc: "Create color from RGBA components" }
28971
+ ];
28972
+ var STRING_FUNCTIONS = [
28973
+ { name: "str.tostring(val)", desc: "Convert value to string" },
28974
+ { name: "str.tonumber(val)", desc: "Convert string to number" },
28975
+ { name: "str.format(tpl, ...)", desc: "Format string with {0}, {1}, \u2026 placeholders" },
28976
+ { name: "str.length(s)", desc: "String length" },
28977
+ { name: "str.trim(s)", desc: "Remove leading/trailing whitespace" },
28978
+ { name: "str.contains(s, sub)", desc: "True if s contains sub" },
28979
+ { name: "str.substring(s, st, en)", desc: "Extract substring" },
28980
+ { name: "str.replace_all(s, t, r)", desc: "Replace all occurrences of t with r" },
28981
+ { name: "str.upper(s)", desc: "Uppercase" },
28982
+ { name: "str.lower(s)", desc: "Lowercase" },
28983
+ { name: "str.split(s, sep)", desc: "Split string into array by separator" }
28984
+ ];
28985
+ var ARRAY_FUNCTIONS = [
28986
+ { name: "array.new_float(sz, val)", desc: "Create float array of size with initial value" },
28987
+ { name: "array.from(v1, v2, ...)", desc: "Create array from values" },
28988
+ { name: "array.size(arr)", desc: "Number of elements" },
28989
+ { name: "array.get(arr, i)", desc: "Get element at index" },
28990
+ { name: "array.set(arr, i, v)", desc: "Set element at index" },
28991
+ { name: "array.push(arr, v)", desc: "Append element" },
28992
+ { name: "array.pop(arr)", desc: "Remove and return last element" },
28993
+ { name: "array.remove(arr, i)", desc: "Remove element at index" },
28994
+ { name: "array.clear(arr)", desc: "Remove all elements" },
28995
+ { name: "array.includes(arr, v)", desc: "True if array contains value" },
28996
+ { name: "array.indexof(arr, v)", desc: "Index of first occurrence (-1 if none)" },
28997
+ { name: "array.slice(arr, s, e)", desc: "Sub-array from start to end" },
28998
+ { name: "array.join(arr, sep)", desc: "Join elements into string" },
28999
+ { name: "array.sort(arr)", desc: "Sort array in place (numeric)" },
29000
+ { name: "array.reverse(arr)", desc: "Reverse array in place" },
29001
+ { name: "array.avg(arr)", desc: "Average of elements" },
29002
+ { name: "array.sum(arr)", desc: "Sum of elements" },
29003
+ { name: "array.min(arr)", desc: "Minimum element" },
29004
+ { name: "array.max(arr)", desc: "Maximum element" }
29005
+ ];
29006
+ var TABLE_FUNCTIONS = [
29007
+ { name: "table.new(pos, cols, rows)", desc: "Create table (+ bgcolor=, border_color=, border_width=, frame_color=, frame_width=)" },
29008
+ { name: "table.cell(id, c, r, text)", desc: "Set cell (+ text_color=, bgcolor=, text_size=, text_halign=, text_valign=)" },
29009
+ { name: "table.clear(id, c, r)", desc: "Clear a single cell" },
29010
+ { name: "table.delete(id)", desc: "Delete the entire table" },
29011
+ { name: "text.align_left/center/right", desc: "Horizontal alignment constants" },
29012
+ { name: "text.align_top/center/bottom", desc: "Vertical alignment constants" }
29013
+ ];
29014
+ var TSCRIPT_TEMPLATE = `indicator("My Indicator")
29015
+
29016
+ // Parameters \u2014 shown in the UI
29017
+ length = input(14)
29018
+ fast = input(9)
29019
+ slow = input(21)
29020
+
29021
+ // Calculations using built-in TA functions
29022
+ fast_ma = ema(close, fast)
29023
+ slow_ma = ema(close, slow)
29024
+ signal = crossover(fast_ma, slow_ma)
29025
+
29026
+ // Plot results (overlay: true draws on price pane)
29027
+ plot(fast_ma)
29028
+ plot(slow_ma)
29029
+ `;
29030
+ function ScriptDrawer({ onClose, onAddIndicator, apiUrl, getAuthToken }) {
29031
+ const [activeTab, setActiveTab] = useState("code");
29032
+ const [isOwner, setIsOwner] = useState(true);
29033
+ const [code, setCode] = useState(TSCRIPT_TEMPLATE);
29034
+ const [overlay, setOverlay] = useState(false);
29035
+ useEffect(() => {
29036
+ const match = code.match(/indicator\s*\([^)]*overlay\s*=\s*(true|false)/i);
29037
+ if (match) {
29038
+ setOverlay(match[1].toLowerCase() === "true");
29039
+ }
29040
+ }, [code]);
29041
+ const [errors, setErrors] = useState([]);
29042
+ const [refOpen, setRefOpen] = useState(false);
29043
+ const [drawerWidth, setDrawerWidth] = useState(380);
29044
+ const [savedScripts, setSavedScripts] = useState([]);
29045
+ const [scriptsMenuOpen, setScriptsMenuOpen] = useState(false);
29046
+ const [saving, setSaving] = useState(false);
29047
+ const [activeScriptId, setActiveScriptId] = useState(null);
29048
+ const [saveNameInput, setSaveNameInput] = useState("");
29049
+ const [showSaveNamePrompt, setShowSaveNamePrompt] = useState(false);
29050
+ const scriptsMenuRef = useRef(null);
29051
+ const dragState = useRef(null);
29052
+ const [agentTyping, setAgentTyping] = useState(false);
29053
+ const agentCodeRef = useRef("");
29054
+ useAgentUIEvent("script:clear", () => {
29055
+ agentCodeRef.current = "";
29056
+ setCode("");
29057
+ setErrors([]);
29058
+ setAgentTyping(true);
29059
+ setActiveTab("code");
29060
+ });
29061
+ useAgentUIEvent("script:type", ({ chunk, done, scriptName }) => {
29062
+ agentCodeRef.current += chunk;
29063
+ setCode(agentCodeRef.current);
29064
+ if (scriptName) setSaveNameInput(scriptName);
29065
+ if (done) setAgentTyping(false);
29066
+ });
29067
+ useAgentUIEvent("script:reload", () => {
29068
+ fetchSavedScripts();
29069
+ });
29070
+ useAgentUIEvent("script:activate", ({ scriptId }) => {
29071
+ setActiveScriptId(scriptId);
29072
+ fetchSavedScripts();
29073
+ });
29074
+ const handleAddToChartRef = useRef(() => {
29075
+ });
29076
+ useAgentUIEvent("script:attach", () => {
29077
+ handleAddToChartRef.current();
29078
+ });
29079
+ useAgentUIEvent("script:ready", () => {
29080
+ setAgentTyping(false);
29081
+ setErrors([]);
29082
+ });
29083
+ const onResizeMouseDown = (e) => {
29084
+ e.preventDefault();
29085
+ dragState.current = { startX: e.clientX, startW: drawerWidth };
29086
+ const onMove = (ev) => {
29087
+ if (!dragState.current) return;
29088
+ const delta = dragState.current.startX - ev.clientX;
29089
+ setDrawerWidth(Math.min(Math.round(window.innerWidth * 0.7), Math.max(280, dragState.current.startW + delta)));
29090
+ };
29091
+ const onUp = () => {
29092
+ dragState.current = null;
29093
+ window.removeEventListener("mousemove", onMove);
29094
+ window.removeEventListener("mouseup", onUp);
29095
+ };
29096
+ window.addEventListener("mousemove", onMove);
29097
+ window.addEventListener("mouseup", onUp);
29098
+ };
29099
+ const _authHeaders = useCallback(async () => {
29100
+ if (!getAuthToken) return {};
29101
+ try {
29102
+ const tok = await getAuthToken();
29103
+ return tok ? { Authorization: `Bearer ${tok}` } : {};
29104
+ } catch {
29105
+ return {};
29106
+ }
29107
+ }, [getAuthToken]);
29108
+ const extractScriptName = useCallback((src) => {
29109
+ const m = src.match(/indicator\s*\(\s*["']([^"']+)["']/);
29110
+ return m?.[1] ?? "Untitled Script";
29111
+ }, []);
29112
+ const fetchSavedScripts = useCallback(async () => {
29113
+ const base = apiUrl ?? "";
29114
+ try {
29115
+ const auth = await _authHeaders();
29116
+ const res = await fetch(`${base}/api/scripts?mine=true`, { headers: { ...auth } });
29117
+ if (res.ok) {
29118
+ const data = await res.json();
29119
+ setSavedScripts(data);
29120
+ }
29121
+ } catch {
29122
+ }
29123
+ }, [apiUrl, _authHeaders]);
29124
+ useEffect(() => {
29125
+ fetchSavedScripts();
29126
+ }, [fetchSavedScripts]);
29127
+ useEffect(() => {
29128
+ if (!scriptsMenuOpen) return;
29129
+ const handler = (e) => {
29130
+ if (scriptsMenuRef.current && !scriptsMenuRef.current.contains(e.target)) {
29131
+ setScriptsMenuOpen(false);
29132
+ setShowSaveNamePrompt(false);
29133
+ }
29134
+ };
29135
+ document.addEventListener("mousedown", handler);
29136
+ return () => document.removeEventListener("mousedown", handler);
29137
+ }, [scriptsMenuOpen]);
29138
+ const handleSaveScript = useCallback(async (name) => {
29139
+ setSaving(true);
29140
+ const base = apiUrl ?? "";
29141
+ const auth = await _authHeaders();
29142
+ const scriptSource = code;
29143
+ const scriptLang = "forgescript";
29144
+ const scriptName = name ?? extractScriptName(scriptSource);
29145
+ try {
29146
+ if (activeScriptId) {
29147
+ await fetch(`${base}/api/scripts/${encodeURIComponent(activeScriptId)}/versions`, {
29148
+ method: "POST",
29149
+ headers: { "Content-Type": "application/json", ...auth },
29150
+ body: JSON.stringify({ source: scriptSource })
29151
+ });
29152
+ } else {
29153
+ const res = await fetch(`${base}/api/scripts`, {
29154
+ method: "POST",
29155
+ headers: { "Content-Type": "application/json", ...auth },
29156
+ body: JSON.stringify({ name: scriptName, language: scriptLang, source: scriptSource })
29157
+ });
29158
+ if (res.ok) {
29159
+ const created = await res.json();
29160
+ setActiveScriptId(created.id);
29161
+ }
29162
+ }
29163
+ await fetchSavedScripts();
29164
+ } catch {
29165
+ }
29166
+ setSaving(false);
29167
+ setShowSaveNamePrompt(false);
29168
+ }, [apiUrl, _authHeaders, code, activeScriptId, extractScriptName, fetchSavedScripts]);
29169
+ const handleSaveClick = useCallback(() => {
29170
+ if (activeScriptId) {
29171
+ handleSaveScript();
29172
+ } else {
29173
+ const autoName = extractScriptName(code);
29174
+ setSaveNameInput(autoName);
29175
+ setShowSaveNamePrompt(true);
29176
+ setScriptsMenuOpen(true);
29177
+ }
29178
+ }, [activeScriptId, handleSaveScript, code, extractScriptName]);
29179
+ const handleLoadScript = useCallback(async (script) => {
29180
+ const base = apiUrl ?? "";
29181
+ const auth = await _authHeaders();
29182
+ try {
29183
+ const res = await fetch(`${base}/api/scripts/${encodeURIComponent(script.id)}/versions`, {
29184
+ headers: { ...auth }
29185
+ });
29186
+ if (!res.ok) return;
29187
+ const versions = await res.json();
29188
+ if (versions.length === 0) return;
29189
+ const latest = versions[0];
29190
+ setActiveScriptId(script.id);
29191
+ setCode(latest.source);
29192
+ setErrors([]);
29193
+ } catch {
29194
+ }
29195
+ setScriptsMenuOpen(false);
29196
+ }, [apiUrl, _authHeaders]);
29197
+ const handleDeleteScript = useCallback(async (id) => {
29198
+ const base = apiUrl ?? "";
29199
+ const auth = await _authHeaders();
29200
+ try {
29201
+ await fetch(`${base}/api/scripts/${encodeURIComponent(id)}`, {
29202
+ method: "DELETE",
29203
+ headers: { ...auth }
29204
+ });
29205
+ if (activeScriptId === id) setActiveScriptId(null);
29206
+ await fetchSavedScripts();
29207
+ } catch {
29208
+ }
29209
+ }, [apiUrl, _authHeaders, activeScriptId, fetchSavedScripts]);
29210
+ const handleAddToChart = () => {
29211
+ setErrors([]);
29212
+ onAddIndicator({ type: "script", script: code, overlay });
29213
+ };
29214
+ handleAddToChartRef.current = handleAddToChart;
29215
+ return /* @__PURE__ */ jsxs("div", { className: "script-drawer", style: { width: drawerWidth }, children: [
29216
+ /* @__PURE__ */ jsx("div", { className: "script-drawer-resize", onMouseDown: onResizeMouseDown }),
29217
+ /* @__PURE__ */ jsxs("div", { className: "script-drawer-header", children: [
29218
+ /* @__PURE__ */ jsxs("span", { className: "script-drawer-title", children: [
29219
+ /* @__PURE__ */ jsxs(
29220
+ "svg",
29221
+ {
29222
+ viewBox: "0 0 16 16",
29223
+ width: "14",
29224
+ height: "14",
29225
+ fill: "none",
29226
+ stroke: "currentColor",
29227
+ strokeWidth: "1.6",
29228
+ strokeLinecap: "round",
29229
+ style: { flexShrink: 0 },
29230
+ children: [
29231
+ /* @__PURE__ */ jsx("polyline", { points: "4,6 2,8 4,10" }),
29232
+ /* @__PURE__ */ jsx("polyline", { points: "12,6 14,8 12,10" }),
29233
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "3", x2: "7", y2: "13" })
29234
+ ]
29235
+ }
29236
+ ),
29237
+ "Script Engine"
29238
+ ] }),
29239
+ /* @__PURE__ */ jsxs("div", { className: "script-header-actions", children: [
29240
+ /* @__PURE__ */ jsxs(
29241
+ "button",
29242
+ {
29243
+ className: "script-header-btn",
29244
+ onClick: handleSaveClick,
29245
+ disabled: saving,
29246
+ title: activeScriptId ? "Save changes" : "Save Script",
29247
+ children: [
29248
+ /* @__PURE__ */ jsxs(
29249
+ "svg",
29250
+ {
29251
+ viewBox: "0 0 14 14",
29252
+ width: "13",
29253
+ height: "13",
29254
+ fill: "none",
29255
+ stroke: "currentColor",
29256
+ strokeWidth: "1.5",
29257
+ strokeLinecap: "round",
29258
+ strokeLinejoin: "round",
29259
+ children: [
29260
+ /* @__PURE__ */ jsx("path", { d: "M11.5 13H2.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h7l3 3v8a1 1 0 0 1-1 1z" }),
29261
+ /* @__PURE__ */ jsx("path", { d: "M9.5 13V8h-5v5" }),
29262
+ /* @__PURE__ */ jsx("path", { d: "M4.5 1v3h4" })
29263
+ ]
29264
+ }
29265
+ ),
29266
+ saving ? "\u2026" : "Save"
29267
+ ]
29268
+ }
29269
+ ),
29270
+ /* @__PURE__ */ jsxs("div", { className: "script-my-scripts-wrap", ref: scriptsMenuRef, children: [
29271
+ /* @__PURE__ */ jsxs(
29272
+ "button",
29273
+ {
29274
+ className: "script-header-btn",
29275
+ onClick: () => {
29276
+ setScriptsMenuOpen((o) => !o);
29277
+ setShowSaveNamePrompt(false);
29278
+ },
29279
+ title: "My Scripts",
29280
+ children: [
29281
+ /* @__PURE__ */ jsx(
29282
+ "svg",
29283
+ {
29284
+ viewBox: "0 0 14 14",
29285
+ width: "13",
29286
+ height: "13",
29287
+ fill: "none",
29288
+ stroke: "currentColor",
29289
+ strokeWidth: "1.5",
29290
+ strokeLinecap: "round",
29291
+ strokeLinejoin: "round",
29292
+ children: /* @__PURE__ */ jsx("path", { d: "M1.5 3.5h11M1.5 7h11M1.5 10.5h11" })
29293
+ }
29294
+ ),
29295
+ "My Scripts"
29296
+ ]
29297
+ }
29298
+ ),
29299
+ scriptsMenuOpen && /* @__PURE__ */ jsxs("div", { className: "script-my-scripts-dropdown", children: [
29300
+ showSaveNamePrompt && /* @__PURE__ */ jsxs("div", { className: "script-save-name-prompt", children: [
29301
+ /* @__PURE__ */ jsx(
29302
+ "input",
29303
+ {
29304
+ className: "script-save-name-input",
29305
+ type: "text",
29306
+ placeholder: "Script name",
29307
+ value: saveNameInput,
29308
+ onChange: (e) => setSaveNameInput(e.target.value),
29309
+ onKeyDown: (e) => {
29310
+ if (e.key === "Enter" && saveNameInput.trim()) {
29311
+ handleSaveScript(saveNameInput.trim());
29312
+ }
29313
+ },
29314
+ autoFocus: true
29315
+ }
29316
+ ),
29317
+ /* @__PURE__ */ jsx(
29318
+ "button",
29319
+ {
29320
+ className: "script-save-name-confirm",
29321
+ onClick: () => saveNameInput.trim() && handleSaveScript(saveNameInput.trim()),
29322
+ disabled: !saveNameInput.trim() || saving,
29323
+ children: saving ? "\u2026" : "Save"
29324
+ }
29325
+ )
29326
+ ] }),
29327
+ savedScripts.length === 0 && !showSaveNamePrompt && /* @__PURE__ */ jsx("div", { className: "script-my-scripts-empty", children: "No saved scripts yet" }),
29328
+ savedScripts.map((s) => /* @__PURE__ */ jsxs("div", { className: "script-my-scripts-item", children: [
29329
+ /* @__PURE__ */ jsxs(
29330
+ "button",
29331
+ {
29332
+ className: "script-my-scripts-load",
29333
+ onClick: () => handleLoadScript(s),
29334
+ title: `Load "${s.name}"`,
29335
+ children: [
29336
+ /* @__PURE__ */ jsx("span", { className: "script-lang-badge forge", children: "ForgeScript" }),
29337
+ /* @__PURE__ */ jsx("span", { className: "script-my-scripts-name", children: s.name })
29338
+ ]
29339
+ }
29340
+ ),
29341
+ /* @__PURE__ */ jsx(
29342
+ "button",
29343
+ {
29344
+ className: "script-my-scripts-delete",
29345
+ onClick: (e) => {
29346
+ e.stopPropagation();
29347
+ handleDeleteScript(s.id);
29348
+ },
29349
+ title: "Delete",
29350
+ children: /* @__PURE__ */ jsxs(
29351
+ "svg",
29352
+ {
29353
+ viewBox: "0 0 10 10",
29354
+ width: "10",
29355
+ height: "10",
29356
+ stroke: "currentColor",
29357
+ strokeWidth: "1.5",
29358
+ strokeLinecap: "round",
29359
+ children: [
29360
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "8", y2: "8" }),
29361
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "2", x2: "2", y2: "8" })
29362
+ ]
29363
+ }
29364
+ )
29365
+ }
29366
+ )
29367
+ ] }, s.id))
29368
+ ] })
29369
+ ] }),
29370
+ /* @__PURE__ */ jsx("button", { className: "script-close-btn", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsxs(
29371
+ "svg",
29372
+ {
29373
+ viewBox: "0 0 12 12",
29374
+ width: "12",
29375
+ height: "12",
29376
+ stroke: "currentColor",
29377
+ strokeWidth: "1.8",
29378
+ strokeLinecap: "round",
29379
+ children: [
29380
+ /* @__PURE__ */ jsx("line", { x1: "1", y1: "1", x2: "11", y2: "11" }),
29381
+ /* @__PURE__ */ jsx("line", { x1: "11", y1: "1", x2: "1", y2: "11" })
29382
+ ]
29383
+ }
29384
+ ) })
29385
+ ] })
29386
+ ] }),
29387
+ /* @__PURE__ */ jsxs("div", { className: "script-main-tabs", children: [
29388
+ /* @__PURE__ */ jsx(
29389
+ "button",
29390
+ {
29391
+ className: `script-main-tab${activeTab === "code" ? " active" : ""}`,
29392
+ onClick: () => setActiveTab("code"),
29393
+ children: "Code"
29394
+ }
29395
+ ),
29396
+ /* @__PURE__ */ jsx(
29397
+ "button",
29398
+ {
29399
+ className: `script-main-tab${activeTab === "datasets" ? " active" : ""}`,
29400
+ onClick: () => setActiveTab("datasets"),
29401
+ children: "Datasets"
29402
+ }
29403
+ ),
29404
+ /* @__PURE__ */ jsx(
29405
+ "button",
29406
+ {
29407
+ className: `script-main-tab${activeTab === "docs" ? " active" : ""}`,
29408
+ onClick: () => setActiveTab("docs"),
29409
+ children: "Docs"
29410
+ }
29411
+ ),
29412
+ /* @__PURE__ */ jsx(
29413
+ "button",
29414
+ {
29415
+ className: `script-main-tab${activeTab === "settings" ? " active" : ""}`,
29416
+ onClick: () => setActiveTab("settings"),
29417
+ children: "Settings"
29418
+ }
29419
+ )
29420
+ ] }),
29421
+ activeTab === "datasets" && /* @__PURE__ */ jsx(
29422
+ DatasetManagerDrawer,
29423
+ {
29424
+ scriptId: activeScriptId,
29425
+ isOwner,
29426
+ apiBase: apiUrl ?? "",
29427
+ ...getAuthToken ? { getAuthToken } : {}
29428
+ }
29429
+ ),
29430
+ activeTab === "docs" && /* @__PURE__ */ jsx(
29431
+ DocsEditor,
29432
+ {
29433
+ scriptId: activeScriptId,
29434
+ isOwner,
29435
+ apiBase: apiUrl ?? "",
29436
+ ...getAuthToken ? { getAuthToken } : {}
29437
+ }
29438
+ ),
29439
+ activeTab === "settings" && /* @__PURE__ */ jsx(
29440
+ ScriptSettings,
29441
+ {
29442
+ scriptId: activeScriptId,
29443
+ isOwner,
29444
+ apiBase: apiUrl ?? "",
29445
+ ...getAuthToken ? { getAuthToken } : {},
29446
+ onUpdated: () => fetchSavedScripts()
29447
+ }
29448
+ ),
29449
+ activeTab !== "code" ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
29450
+ /* @__PURE__ */ jsxs("div", { className: "script-lang-tabs", children: [
29451
+ /* @__PURE__ */ jsx("button", { className: "script-lang-tab active", children: "ForgeScript" }),
29452
+ /* @__PURE__ */ jsxs(
29453
+ "button",
29454
+ {
29455
+ className: "script-lang-tab-help",
29456
+ title: "ForgeScript Manual",
29457
+ onClick: () => window.open("/forgescript-manual.html", "_blank", "noopener,noreferrer"),
29458
+ "aria-label": "Open ForgeScript Manual",
29459
+ children: [
29460
+ /* @__PURE__ */ jsxs(
29461
+ "svg",
29462
+ {
29463
+ viewBox: "0 0 16 16",
29464
+ width: "13",
29465
+ height: "13",
29466
+ fill: "none",
29467
+ stroke: "currentColor",
29468
+ strokeWidth: "1.5",
29469
+ strokeLinecap: "round",
29470
+ strokeLinejoin: "round",
29471
+ children: [
29472
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "8", r: "7" }),
29473
+ /* @__PURE__ */ jsx("path", { d: "M6 6c0-1.1.9-2 2-2s2 .9 2 2c0 1-.7 1.6-1.5 2-.4.2-.5.5-.5.8" }),
29474
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "12", r: "0.6", fill: "currentColor", stroke: "none" })
29475
+ ]
29476
+ }
29477
+ ),
29478
+ "Help"
29479
+ ]
29480
+ }
29481
+ )
29482
+ ] }),
29483
+ /* @__PURE__ */ jsxs("div", { className: "script-ref-section", children: [
29484
+ /* @__PURE__ */ jsxs(
29485
+ "button",
29486
+ {
29487
+ className: "script-ref-toggle",
29488
+ onClick: () => setRefOpen((o) => !o),
29489
+ children: [
29490
+ /* @__PURE__ */ jsx(
29491
+ "svg",
29492
+ {
29493
+ viewBox: "0 0 10 6",
29494
+ width: "8",
29495
+ height: "6",
29496
+ fill: "currentColor",
29497
+ style: { transform: refOpen ? "rotate(180deg)" : "none", transition: "transform 0.15s" },
29498
+ children: /* @__PURE__ */ jsx("path", { d: "M0 0l5 6 5-6z" })
29499
+ }
29500
+ ),
29501
+ "Built-in Reference"
29502
+ ]
29503
+ }
29504
+ ),
29505
+ refOpen && /* @__PURE__ */ jsxs("div", { className: "script-ref-body", children: [
29506
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Series" }),
29507
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: BUILT_IN_SERIES.map((s) => /* @__PURE__ */ jsx("span", { className: "script-feature-pill series", children: s }, s)) }),
29508
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Bar State" }),
29509
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: BARSTATE_PROPERTIES.map((f) => /* @__PURE__ */ jsx("span", { className: "script-feature-pill barstate", title: f.desc, children: f.name }, f.name)) }),
29510
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "TA Functions" }),
29511
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: TA_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill ta", title: f.desc, children: [
29512
+ f.name.split("(")[0],
29513
+ "()"
29514
+ ] }, f.name)) }),
29515
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Output" }),
29516
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: OUTPUT_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill output", title: f.desc, children: [
29517
+ f.name.split("(")[0],
29518
+ "()"
29519
+ ] }, f.name)) }),
29520
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Math" }),
29521
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: MATH_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill math", children: [
29522
+ f,
29523
+ "()"
29524
+ ] }, f)) }),
29525
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Color" }),
29526
+ /* @__PURE__ */ jsxs("div", { className: "script-feature-list", children: [
29527
+ COLOR_CONSTANTS.map((c) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill color", title: `color.${c}`, children: [
29528
+ "color.",
29529
+ c
29530
+ ] }, c)),
29531
+ COLOR_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill color", title: f.desc, children: [
29532
+ f.name.split("(")[0],
29533
+ "()"
29534
+ ] }, f.name))
29535
+ ] }),
29536
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "String" }),
29537
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: STRING_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill str", title: f.desc, children: [
29538
+ f.name.split("(")[0],
29539
+ "()"
29540
+ ] }, f.name)) }),
29541
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Array" }),
29542
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: ARRAY_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill array", title: f.desc, children: [
29543
+ f.name.split("(")[0],
29544
+ "()"
29545
+ ] }, f.name)) }),
29546
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Table" }),
29547
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: TABLE_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill table", title: f.desc, children: [
29548
+ f.name.split("(")[0],
29549
+ "()"
29550
+ ] }, f.name)) }),
29551
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Utility" }),
29552
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: UTILITY_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill util", title: f.desc, children: [
29553
+ f.name.split("(")[0],
29554
+ "()"
29555
+ ] }, f.name)) }),
29556
+ /* @__PURE__ */ jsx("div", { className: "script-ref-detail", children: [...BARSTATE_PROPERTIES, ...TA_FUNCTIONS, ...OUTPUT_FUNCTIONS, ...COLOR_FUNCTIONS, ...STRING_FUNCTIONS, ...ARRAY_FUNCTIONS, ...TABLE_FUNCTIONS, ...UTILITY_FUNCTIONS].map((f) => /* @__PURE__ */ jsxs("div", { className: "script-ref-row", children: [
29557
+ /* @__PURE__ */ jsx("code", { className: "script-ref-fn", children: f.name }),
29558
+ /* @__PURE__ */ jsx("span", { className: "script-ref-desc", children: f.desc })
29559
+ ] }, f.name)) })
29560
+ ] })
29561
+ ] }),
29562
+ /* @__PURE__ */ jsxs("div", { className: "script-editor-wrap", style: { position: "relative" }, children: [
29563
+ agentTyping && /* @__PURE__ */ jsxs("div", { style: {
29564
+ position: "absolute",
29565
+ top: 6,
29566
+ right: 10,
29567
+ zIndex: 10,
29568
+ display: "flex",
29569
+ alignItems: "center",
29570
+ gap: 5,
29571
+ fontSize: 11,
29572
+ color: "var(--primary, #8ab4f8)",
29573
+ background: "var(--surface, #1e1e1e)",
29574
+ padding: "2px 8px",
29575
+ borderRadius: 10,
29576
+ border: "1px solid rgba(138,180,248,0.3)",
29577
+ pointerEvents: "none"
29578
+ }, children: [
29579
+ /* @__PURE__ */ jsx("span", { style: {
29580
+ width: 6,
29581
+ height: 6,
29582
+ borderRadius: "50%",
29583
+ background: "var(--primary, #8ab4f8)",
29584
+ animation: "agent-pulse 1s ease-in-out infinite"
29585
+ } }),
29586
+ "Agent typing\u2026"
29587
+ ] }),
29588
+ /* @__PURE__ */ jsx(
29589
+ "textarea",
29590
+ {
29591
+ className: "script-editor",
29592
+ spellCheck: false,
29593
+ value: code,
29594
+ readOnly: agentTyping,
29595
+ onChange: (e) => {
29596
+ if (!agentTyping) {
29597
+ setCode(e.target.value);
29598
+ setErrors([]);
29599
+ }
29600
+ }
29601
+ }
29602
+ )
29603
+ ] }),
29604
+ errors.length > 0 && /* @__PURE__ */ jsx("div", { className: "script-errors", children: errors.map((msg, i) => /* @__PURE__ */ jsxs("div", { className: "script-error-line", children: [
29605
+ /* @__PURE__ */ jsxs(
29606
+ "svg",
29607
+ {
29608
+ viewBox: "0 0 12 12",
29609
+ width: "11",
29610
+ height: "11",
29611
+ fill: "none",
29612
+ stroke: "currentColor",
29613
+ strokeWidth: "1.6",
29614
+ strokeLinecap: "round",
29615
+ style: { flexShrink: 0, color: "var(--down, #ef5350)" },
29616
+ children: [
29617
+ /* @__PURE__ */ jsx("circle", { cx: "6", cy: "6", r: "5" }),
29618
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "3.5", x2: "6", y2: "6.5" }),
29619
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "8", x2: "6", y2: "8.5" })
29620
+ ]
29621
+ }
29622
+ ),
29623
+ msg
29624
+ ] }, i)) })
29625
+ ] }),
29626
+ activeTab === "code" && /* @__PURE__ */ jsxs("div", { className: "script-footer", children: [
29627
+ /* @__PURE__ */ jsxs("label", { className: "script-overlay-label", children: [
29628
+ /* @__PURE__ */ jsx(
29629
+ "input",
29630
+ {
29631
+ type: "checkbox",
29632
+ checked: overlay,
29633
+ onChange: (e) => setOverlay(e.target.checked)
29634
+ }
29635
+ ),
29636
+ "Overlay on price"
29637
+ ] }),
29638
+ /* @__PURE__ */ jsxs("button", { className: "script-run-btn", onClick: handleAddToChart, children: [
29639
+ /* @__PURE__ */ jsx(
29640
+ "svg",
29641
+ {
29642
+ viewBox: "0 0 12 12",
29643
+ width: "11",
29644
+ height: "11",
29645
+ fill: "currentColor",
29646
+ style: { flexShrink: 0 },
29647
+ children: /* @__PURE__ */ jsx("polygon", { points: "2,1 11,6 2,11" })
29648
+ }
29649
+ ),
29650
+ "Add to Chart"
29651
+ ] })
29652
+ ] })
29653
+ ] });
29654
+ }
28021
29655
  function ChartWorkspace({
28022
29656
  // TabBar
28023
29657
  tabs,
@@ -28093,6 +29727,8 @@ function ChartWorkspace({
28093
29727
  onSessionChange,
28094
29728
  scriptDrawerOpen,
28095
29729
  onToggleScriptDrawer,
29730
+ builtinScriptDrawer,
29731
+ scriptApiUrl,
28096
29732
  // Trading panel
28097
29733
  tradingPanel,
28098
29734
  tradingPanelOpen,
@@ -28117,13 +29753,17 @@ function ChartWorkspace({
28117
29753
  }) {
28118
29754
  const capabilities = useChartCapabilities();
28119
29755
  const autoTrading = tradingBridge !== void 0;
28120
- const [autoOrderEntryOpen, setAutoOrderEntryOpen] = React7.useState(false);
28121
- const [autoTradingPanelOpen, setAutoTradingPanelOpen] = React7.useState(false);
29756
+ const [autoOrderEntryOpen, setAutoOrderEntryOpen] = React11.useState(false);
29757
+ const [autoTradingPanelOpen, setAutoTradingPanelOpen] = React11.useState(false);
28122
29758
  const effOnToggleOrderEntry = onToggleOrderEntry ?? (autoTrading ? () => setAutoOrderEntryOpen((o) => !o) : void 0);
28123
29759
  const effOrderEntryOpen = onToggleOrderEntry ? orderEntryOpen ?? false : autoOrderEntryOpen;
28124
29760
  const effOnToggleTradeDrawer = onToggleTradeDrawer ?? (autoTrading ? () => setAutoOrderEntryOpen((o) => !o) : void 0);
28125
29761
  const effTradeDrawerOpen = onToggleTradeDrawer ? tradeDrawerOpen : autoOrderEntryOpen;
28126
29762
  const autoJournal = autoTrading && !tradingPanel && capabilities.tradingPanel;
29763
+ const [autoScriptOpen, setAutoScriptOpen] = React11.useState(false);
29764
+ const effOnToggleScriptDrawer = onToggleScriptDrawer ?? (() => setAutoScriptOpen((o) => !o));
29765
+ const effScriptDrawerOpen = onToggleScriptDrawer ? scriptDrawerOpen ?? false : autoScriptOpen;
29766
+ const closeScriptDrawer = onToggleScriptDrawer ?? (() => setAutoScriptOpen(false));
28127
29767
  const gate = (hostProp, licensed) => hostProp !== false && licensed;
28128
29768
  const effShowTradeButton = gate(showTradeButton, capabilities.orderEntry) && effOnToggleTradeDrawer !== void 0;
28129
29769
  const effShowOrderEntry = gate(showOrderEntry, capabilities.orderEntry) && effOnToggleOrderEntry !== void 0;
@@ -28287,6 +29927,15 @@ function ChartWorkspace({
28287
29927
  onSymbolChange
28288
29928
  }
28289
29929
  ),
29930
+ builtinScriptDrawer !== false && effScriptDrawerOpen && capabilities.forgeScript && /* @__PURE__ */ jsx(
29931
+ ScriptDrawer,
29932
+ {
29933
+ onClose: closeScriptDrawer,
29934
+ onAddIndicator,
29935
+ ...scriptApiUrl !== void 0 ? { apiUrl: scriptApiUrl } : {},
29936
+ ...getAuthToken !== void 0 ? { getAuthToken } : {}
29937
+ }
29938
+ ),
28290
29939
  leftDrawers,
28291
29940
  drawers,
28292
29941
  /* @__PURE__ */ jsx(
@@ -28340,8 +29989,8 @@ function ChartWorkspace({
28340
29989
  session,
28341
29990
  onSessionChange,
28342
29991
  showSessionSelector: capabilities.sessionHours,
28343
- onToggleScriptDrawer,
28344
- scriptDrawerOpen,
29992
+ onToggleScriptDrawer: effOnToggleScriptDrawer,
29993
+ scriptDrawerOpen: effScriptDrawerOpen,
28345
29994
  showScriptButton: effShowForgeScript,
28346
29995
  ...tradingPanelOpen !== void 0 ? { tradingPanelOpen } : {},
28347
29996
  ...onToggleTradingPanel !== void 0 && capabilities.tradingPanel ? { onToggleTradingPanel } : autoJournal ? { tradingPanelOpen: autoTradingPanelOpen, onToggleTradingPanel: () => setAutoTradingPanelOpen((o) => !o) } : {},
@@ -29373,7 +31022,7 @@ function fileToTextAttachment(file) {
29373
31022
  reader.readAsText(file);
29374
31023
  });
29375
31024
  }
29376
- function renderMarkdown(text) {
31025
+ function renderMarkdown2(text) {
29377
31026
  if (!text) return null;
29378
31027
  const parts = [];
29379
31028
  const codeBlockRegex = /```([\s\S]*?)```/g;
@@ -29597,7 +31246,7 @@ function AssistantPanel({ onClose, chartContext }) {
29597
31246
  /* @__PURE__ */ jsx("div", { children: "Ask the AI assistant anything" }),
29598
31247
  /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "var(--text-muted, #777)", marginTop: 4 }, children: "About your chart, indicators, or trading ideas." })
29599
31248
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
29600
- agent.messages.map((msg, idx) => /* @__PURE__ */ jsxs(React7.Fragment, { children: [
31249
+ agent.messages.map((msg, idx) => /* @__PURE__ */ jsxs(React11.Fragment, { children: [
29601
31250
  idx === agent.sessionStart && agent.sessionStart > 0 && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "8px 0", margin: "4px 0" }, children: [
29602
31251
  /* @__PURE__ */ jsx("div", { style: { flex: 1, height: 1, background: "var(--border, rgba(0,0,0,0.12))" } }),
29603
31252
  /* @__PURE__ */ jsx("span", { style: { fontSize: 10, color: "var(--text-muted, #888)", whiteSpace: "nowrap", textTransform: "uppercase", letterSpacing: "0.06em" }, children: "New session" }),
@@ -29620,7 +31269,7 @@ function AssistantPanel({ onClose, chartContext }) {
29620
31269
  },
29621
31270
  imgIdx
29622
31271
  )) }),
29623
- msg.role === "assistant" ? renderMarkdown(msg.content) : msg.content,
31272
+ msg.role === "assistant" ? renderMarkdown2(msg.content) : msg.content,
29624
31273
  msg.role === "assistant" && agent.isStreaming && msg === agent.messages[agent.messages.length - 1] && /* @__PURE__ */ jsx("span", { style: { marginLeft: 4, animation: "agent-blink 1s infinite" }, children: "\u258C" })
29625
31274
  ] }) }),
29626
31275
  msg.toolCalls && msg.toolCalls.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginTop: 4 }, children: msg.toolCalls.map((tool) => /* @__PURE__ */ jsxs("div", { children: [