node-red-contrib-vectorprime 0.1.3 → 0.1.4
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/package.json +1 -1
- package/vectorprime.html +123 -92
- package/vectorprime.js +34 -16
package/package.json
CHANGED
package/vectorprime.html
CHANGED
|
@@ -28,87 +28,86 @@
|
|
|
28
28
|
</script>
|
|
29
29
|
|
|
30
30
|
<script type="text/javascript">
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Call backend directly from the editor
|
|
73
|
-
const resp = await fetch(`${baseUrl}/v1/keys/free`, {
|
|
74
|
-
method: "POST",
|
|
75
|
-
headers: { "Content-Type": "application/json" },
|
|
76
|
-
body: JSON.stringify({ source: "node-red" })
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const text = await resp.text();
|
|
80
|
-
let data;
|
|
81
|
-
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
82
|
-
|
|
83
|
-
if (!resp.ok) {
|
|
84
|
-
statusEl.textContent = `❌ Failed (${resp.status}): ${data.message || data.error || "unknown"}`;
|
|
85
|
-
statusEl.style.color = "red";
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!data.api_key) {
|
|
90
|
-
statusEl.textContent = "❌ Backend did not return api_key";
|
|
91
|
-
statusEl.style.color = "red";
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
apiKeyInput.value = data.api_key;
|
|
96
|
-
statusEl.textContent = "✅ Free key generated + saved";
|
|
97
|
-
statusEl.style.color = "green";
|
|
98
|
-
} catch (err) {
|
|
99
|
-
statusEl.textContent = `❌ Error: ${err.message}`;
|
|
100
|
-
statusEl.style.color = "red";
|
|
101
|
-
}
|
|
102
|
-
};
|
|
31
|
+
(function () {
|
|
32
|
+
function normalizeBaseUrl(url) {
|
|
33
|
+
url = (url || "").trim();
|
|
34
|
+
if (!url) return "";
|
|
35
|
+
if (url.endsWith("/")) url = url.slice(0, -1);
|
|
36
|
+
return url;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
RED.nodes.registerType("vectorprime-config", {
|
|
40
|
+
category: "config",
|
|
41
|
+
defaults: {
|
|
42
|
+
baseUrl: { value: "https://vectorprime-kernel-backend.onrender.com", required: true }
|
|
43
|
+
},
|
|
44
|
+
credentials: {
|
|
45
|
+
apiKey: { type: "password" }
|
|
46
|
+
},
|
|
47
|
+
label: function () {
|
|
48
|
+
return "VectorPrime Config";
|
|
49
|
+
},
|
|
50
|
+
oneditprepare: function () {
|
|
51
|
+
const statusEl = document.getElementById("vp-key-status");
|
|
52
|
+
const btn = document.getElementById("vp-get-free-key");
|
|
53
|
+
|
|
54
|
+
btn.onclick = async function (e) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
statusEl.textContent = "Requesting free key...";
|
|
57
|
+
statusEl.style.color = "#444";
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const baseUrlInput = document.getElementById("node-config-input-baseUrl");
|
|
61
|
+
const apiKeyInput = document.getElementById("node-config-input-apiKey");
|
|
62
|
+
|
|
63
|
+
const baseUrl = normalizeBaseUrl(baseUrlInput.value);
|
|
64
|
+
|
|
65
|
+
if (!baseUrl.startsWith("http")) {
|
|
66
|
+
statusEl.textContent = "❌ Base URL missing/invalid";
|
|
67
|
+
statusEl.style.color = "red";
|
|
68
|
+
return;
|
|
103
69
|
}
|
|
104
|
-
|
|
105
|
-
|
|
70
|
+
|
|
71
|
+
// Call backend directly from the editor
|
|
72
|
+
const resp = await fetch(`${baseUrl}/v1/keys/free`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({ source: "node-red" })
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const text = await resp.text();
|
|
79
|
+
let data;
|
|
80
|
+
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
81
|
+
|
|
82
|
+
if (!resp.ok) {
|
|
83
|
+
statusEl.textContent = `❌ Failed (${resp.status}): ${data.message || data.error || "unknown"}`;
|
|
84
|
+
statusEl.style.color = "red";
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!data.api_key) {
|
|
89
|
+
statusEl.textContent = "❌ Backend did not return api_key";
|
|
90
|
+
statusEl.style.color = "red";
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
apiKeyInput.value = data.api_key;
|
|
95
|
+
statusEl.textContent = "✅ Free key generated + saved";
|
|
96
|
+
statusEl.style.color = "green";
|
|
97
|
+
} catch (err) {
|
|
98
|
+
statusEl.textContent = `❌ Error: ${err.message}`;
|
|
99
|
+
statusEl.style.color = "red";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
})();
|
|
106
105
|
</script>
|
|
107
106
|
|
|
108
107
|
<script type="text/html" data-template-name="vectorprime-rank">
|
|
109
108
|
<div class="form-row">
|
|
110
109
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
111
|
-
<input type="text" id="node-input-name" placeholder="VectorPrime
|
|
110
|
+
<input type="text" id="node-input-name" placeholder="Rank Decision (VectorPrime)">
|
|
112
111
|
</div>
|
|
113
112
|
|
|
114
113
|
<div class="form-row">
|
|
@@ -123,25 +122,57 @@
|
|
|
123
122
|
|
|
124
123
|
<div class="form-row">
|
|
125
124
|
<div style="font-size:12px; color:#666;">
|
|
126
|
-
✅
|
|
125
|
+
✅ <b>1-second summary:</b> Send a decision prompt + options → get ranking + probabilities back.
|
|
127
126
|
</div>
|
|
128
127
|
</div>
|
|
129
128
|
</script>
|
|
130
129
|
|
|
131
130
|
<script type="text/javascript">
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
RED.nodes.registerType("vectorprime-rank", {
|
|
132
|
+
// ✅ REQUIRED: Change category to Network
|
|
133
|
+
category: "network",
|
|
134
|
+
|
|
135
|
+
// ✅ Easy to see, not black
|
|
136
|
+
color: "#2563EB",
|
|
137
|
+
|
|
138
|
+
defaults: {
|
|
139
|
+
name: { value: "" },
|
|
140
|
+
config: { type: "vectorprime-config", required: true },
|
|
141
|
+
endpoint: { value: "/v1/kernel/rank", required: true }
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
inputs: 1,
|
|
145
|
+
outputs: 1,
|
|
146
|
+
icon: "font-awesome/fa-bolt",
|
|
147
|
+
|
|
148
|
+
// ✅ REQUIRED: Make it instantly understandable in palette
|
|
149
|
+
paletteLabel: "Rank Decision (VectorPrime)",
|
|
150
|
+
|
|
151
|
+
label: function () {
|
|
152
|
+
return this.name || "Rank Decision (VectorPrime)";
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
outputLabels: ["ranking + probabilities"]
|
|
156
|
+
});
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<script type="text/markdown" data-help-name="vectorprime-rank">
|
|
160
|
+
# Rank Decision (VectorPrime)
|
|
161
|
+
|
|
162
|
+
**What it does (1 second):**
|
|
163
|
+
Send a decision prompt + options → VectorPrime returns **ranking + probabilities**.
|
|
164
|
+
|
|
165
|
+
## Input: `msg.payload`
|
|
166
|
+
Example:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"decision_id": "node-red-demo-001",
|
|
171
|
+
"prompt": "Pick one.",
|
|
172
|
+
"options": [
|
|
173
|
+
{ "id": "a", "label": "A" },
|
|
174
|
+
{ "id": "b", "label": "B" }
|
|
175
|
+
],
|
|
176
|
+
"engine": "classical",
|
|
177
|
+
"seed": 123
|
|
178
|
+
}
|
package/vectorprime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
|
+
// ✅ Works with node-fetch v2 or v3
|
|
2
3
|
const fetch = (...args) =>
|
|
3
|
-
import("node-fetch").then((
|
|
4
|
+
import("node-fetch").then((mod) => (mod.default ? mod.default(...args) : mod(...args)));
|
|
4
5
|
|
|
5
6
|
function normalizeBaseUrl(url) {
|
|
6
7
|
url = (url || "").trim();
|
|
@@ -11,6 +12,7 @@ module.exports = function (RED) {
|
|
|
11
12
|
|
|
12
13
|
function normalizeEndpoint(path) {
|
|
13
14
|
path = (path || "").trim();
|
|
15
|
+
if (!path) return "/v1/kernel/rank";
|
|
14
16
|
if (!path.startsWith("/")) path = "/" + path;
|
|
15
17
|
return path;
|
|
16
18
|
}
|
|
@@ -38,15 +40,19 @@ module.exports = function (RED) {
|
|
|
38
40
|
node.name = config.name;
|
|
39
41
|
node.endpoint = config.endpoint || "/v1/kernel/rank";
|
|
40
42
|
|
|
41
|
-
const cfg = RED.nodes.getNode(config.config);
|
|
42
|
-
|
|
43
43
|
node.on("input", async function (msg, send, done) {
|
|
44
|
+
send = send || function () { node.send.apply(node, arguments); };
|
|
45
|
+
|
|
44
46
|
try {
|
|
47
|
+
const cfg = RED.nodes.getNode(config.config);
|
|
45
48
|
if (!cfg) {
|
|
46
49
|
node.status({ fill: "red", shape: "ring", text: "missing config" });
|
|
47
|
-
|
|
48
|
-
"VectorPrime config missing. Open node settings and set Config."
|
|
49
|
-
|
|
50
|
+
msg.payload = {
|
|
51
|
+
error: "VectorPrime config missing. Open node settings and set Config.",
|
|
52
|
+
};
|
|
53
|
+
send(msg);
|
|
54
|
+
if (done) done();
|
|
55
|
+
return;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
const baseUrl = normalizeBaseUrl(cfg.baseUrl);
|
|
@@ -54,10 +60,16 @@ module.exports = function (RED) {
|
|
|
54
60
|
|
|
55
61
|
if (!baseUrl.startsWith("http")) {
|
|
56
62
|
node.status({ fill: "red", shape: "ring", text: "invalid base url" });
|
|
57
|
-
|
|
63
|
+
msg.payload = { error: `Base URL missing/invalid: ${baseUrl}` };
|
|
64
|
+
send(msg);
|
|
65
|
+
if (done) done();
|
|
66
|
+
return;
|
|
58
67
|
}
|
|
59
68
|
|
|
69
|
+
// ✅ Ensure headers object exists
|
|
60
70
|
msg.headers = msg.headers || {};
|
|
71
|
+
|
|
72
|
+
// ✅ Allow incoming Authorization override (advanced users)
|
|
61
73
|
const incomingAuth =
|
|
62
74
|
msg.headers.authorization ||
|
|
63
75
|
msg.headers.Authorization ||
|
|
@@ -68,17 +80,21 @@ module.exports = function (RED) {
|
|
|
68
80
|
? String(cfg.credentials.apiKey).trim()
|
|
69
81
|
: "";
|
|
70
82
|
|
|
71
|
-
// If user didn't provide Authorization, use stored key
|
|
83
|
+
// ✅ If user didn't provide Authorization, use stored key from config credentials
|
|
72
84
|
if (!incomingAuth) {
|
|
73
85
|
if (!storedKey) {
|
|
74
86
|
node.status({ fill: "red", shape: "ring", text: "missing api key" });
|
|
75
|
-
|
|
76
|
-
"No API key. Open VectorPrime Config and click 'Get Free Key'."
|
|
77
|
-
|
|
87
|
+
msg.payload = {
|
|
88
|
+
error: "No API key. Open VectorPrime Config and click 'Get Free Key'.",
|
|
89
|
+
};
|
|
90
|
+
send(msg);
|
|
91
|
+
if (done) done();
|
|
92
|
+
return;
|
|
78
93
|
}
|
|
79
94
|
msg.headers.Authorization = `Bearer ${storedKey}`;
|
|
80
95
|
}
|
|
81
96
|
|
|
97
|
+
// ✅ Force JSON
|
|
82
98
|
msg.headers["Content-Type"] = "application/json";
|
|
83
99
|
|
|
84
100
|
const payload = msg.payload || {};
|
|
@@ -99,7 +115,7 @@ module.exports = function (RED) {
|
|
|
99
115
|
data = { raw: text };
|
|
100
116
|
}
|
|
101
117
|
|
|
102
|
-
//
|
|
118
|
+
// ✅ Free tier limit hit → return upgrade url inside Node-RED
|
|
103
119
|
if (!resp.ok) {
|
|
104
120
|
const upgradeUrl = data.upgrade_url || data.checkout_url || null;
|
|
105
121
|
|
|
@@ -112,24 +128,26 @@ module.exports = function (RED) {
|
|
|
112
128
|
msg.payload = data;
|
|
113
129
|
msg.vectorprime = { upgrade_url: upgradeUrl };
|
|
114
130
|
send(msg);
|
|
115
|
-
done();
|
|
131
|
+
if (done) done();
|
|
116
132
|
return;
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
node.status({ fill: "red", shape: "ring", text: `error ${resp.status}` });
|
|
120
136
|
msg.payload = data;
|
|
121
137
|
send(msg);
|
|
122
|
-
done();
|
|
138
|
+
if (done) done();
|
|
123
139
|
return;
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
node.status({ fill: "green", shape: "dot", text: "ok" });
|
|
127
143
|
msg.payload = data;
|
|
128
144
|
send(msg);
|
|
129
|
-
done();
|
|
145
|
+
if (done) done();
|
|
130
146
|
} catch (err) {
|
|
131
147
|
node.status({ fill: "red", shape: "ring", text: "failed" });
|
|
132
|
-
|
|
148
|
+
msg.payload = { error: err.message || String(err) };
|
|
149
|
+
send(msg);
|
|
150
|
+
if (done) done(err);
|
|
133
151
|
}
|
|
134
152
|
});
|
|
135
153
|
}
|