@forgecharts/sdk 1.3.5 → 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.
- package/dist/react/index.js +1657 -8
- package/dist/react/index.js.map +1 -1
- package/dist/react/internal.js +3997 -3981
- package/dist/react/internal.js.map +1 -1
- package/dist/react/shell/ManagedAppShell.d.ts.map +1 -1
- package/dist/react/workspace/ChartWorkspace.d.ts +20 -3
- package/dist/react/workspace/ChartWorkspace.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/react/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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] =
|
|
28121
|
-
const [autoTradingPanelOpen, setAutoTradingPanelOpen] =
|
|
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
|
|
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(
|
|
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" ?
|
|
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: [
|