Skip to content

Commit 033b086

Browse files
committed
add-googleai-node
1 parent fb2d9ff commit 033b086

File tree

4 files changed

+552
-2
lines changed

4 files changed

+552
-2
lines changed

nodes/501-GoogleAI.html

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
<style>
2+
.slider-input-container {
3+
display: flex;
4+
justify-content: space-between;
5+
align-items: center;
6+
width: 100%;
7+
margin-bottom: 12px;
8+
}
9+
10+
input[type=range] {
11+
cursor: pointer;
12+
background-color: #ECECF1;
13+
-webkit-appearance: none;
14+
height: 5px;
15+
width: 100%;
16+
border-radius: 2.5px;
17+
outline: none;
18+
}
19+
20+
input[type=range]::-webkit-slider-thumb {
21+
-webkit-appearance: none;
22+
appearance: none;
23+
width: 16px;
24+
height: 16px;
25+
background-color: white;
26+
cursor: pointer;
27+
border-radius: 50%;
28+
box-shadow: 0px 0px 5px gray;
29+
border: 2px solid #C5C5D2;
30+
margin-top: -6px;
31+
}
32+
33+
input[type=range]:focus::-webkit-slider-thumb {
34+
border: 2px solid #1f93ffad;
35+
}
36+
37+
input[type=range]::-webkit-slider-runnable-track {
38+
background: linear-gradient(to right, #C5C5D2 0%, #C5C5D2 var(--percentage), #ECECF1 var(--percentage), #ECECF1 100%);
39+
height: 5px;
40+
border-radius: 2.5px;
41+
}
42+
43+
.slider-input-container label {
44+
margin-top: 0rem;
45+
}
46+
</style>
47+
<script type="text/x-red" data-template-name="googleai-config">
48+
<div class="form-row">
49+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
50+
<input type="text" id="node-input-name">
51+
</div>
52+
<div class="form-row">
53+
<label for="node-input-secretKey"><i class="fa fa-user"></i> SecretKey</label>
54+
<input type="password" id="node-input-secretKey" list="secretKey_datalist" placeholder="GoogleAI SecretKey" style="width:calc(100% - 50px);" autocomplete="off">
55+
</div>
56+
</script>
57+
58+
<script type="text/x-red" data-help-name="googleai-config">
59+
<p>GoogleAI Config</p>
60+
<p>When AI GoogleAI nodes are invoked, GoogleAI Config node is required.</p>
61+
<h3>Properties</h3>
62+
<dl class="message-properties">
63+
<dt>Name</dt>
64+
<dd>You can tag or describe this googleai-config.</dd>
65+
<dt>SecretKey <span class="property-type">required</span></dt>
66+
<dd>Manage your access keys as securely as you do your user name and password. Do not provide your access keys to a third party, even to help find your canonical user ID. By doing this, you might give someone permanent access to your account.</dd>
67+
</dl>
68+
</script>
69+
70+
<script type="text/x-red" data-template-name="googleai-generate">
71+
<div class="form-row">
72+
<label for="node-input-name"><i class="fa fa-tag"></i>Name</label>
73+
<input type="text" id="node-input-name" placeholder="Name"></input>
74+
</div>
75+
<div class="form-row">
76+
<label for="node-input-config"><i class="fa fa-user"></i> GoogleAI Config</label>
77+
<select type="text" id="node-input-config"></select>
78+
</div>
79+
<div class="form-row">
80+
<label for="node-input-modelId"><i class="fa fa-cog"></i> Model</label>
81+
<select id="node-input-modelId"></select>
82+
</div>
83+
<div class="form-row">
84+
<label for="node-input-mode"><i class="fa fa-commenting"></i> Interaction Mode</label>
85+
<select type="text" id="node-input-mode">
86+
<option value="single">Single Prompt</option>
87+
<option value="chat">Multi-turn Chat</option>
88+
</select>
89+
</div>
90+
<div class="form-row">
91+
<label for="node-input-field"><i class="fa fa-edit"></i> Set property</span></label>
92+
<input type="text" id="node-input-field" placeholder="payload" style="width:100%">
93+
<input type="hidden" id="node-input-fieldType">
94+
</div>
95+
<div class="form-row">
96+
<label for="node-input-params"><i class="fa fa-file-code-o"></i> Prompt</label>
97+
<input type="hidden" id="node-input-params" value="{}"/>
98+
<input type="hidden" id="node-input-noerr"/>
99+
<div class="form-row node-text-editor-row"><div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-param-editor" ></div></div>
100+
</div>
101+
</script>
102+
103+
<script type="text/x-red" data-help-name="googleai-generate">
104+
<p>GoogleAI Generate</p>
105+
<p>You can extend this flow by connecting it to your GoogleAI using the GoogleAI api node. Refer to <a href="https://ai.google.dev/api" target="_blank">GoogleAI api documents.</a></p>
106+
<h3>Properties</h3>
107+
<dl class="message-properties">
108+
<dt>Name</dt>
109+
<dd>You can tag or describe this googleai-generate.</dd>
110+
<dt>GoogleAI Config <span class="property-type">required</span></dt>
111+
<dd>Choose a GoogleAI Config Node.</dd>
112+
<dt>Model <span class="property-type">required</span></dt>
113+
<dd>Choose a GoogleAI generate Model.</dd>
114+
<dt>Interaction Mode <span class="property-type">required</span></dt>
115+
<dd>Choose a Interaction Mode.</dd>
116+
<dt>Set property <span class="property-type">required</span></dt>
117+
<dd>a msg with a property set by populating the configured prompt with properties from the incoming msg.</dd>
118+
<dt>Prompt <span class="property-type">required</span></dt>
119+
<dd>Insert a prompt of method. refer to each method description on GoogleAI api documents. You can use <a href="http://mustache.github.io/mustache.5.html" target="_blank">mustache</a> templates.</dd>
120+
</dl>
121+
</script>
122+
123+
<script type="text/javascript">
124+
(function () {
125+
GoogleAIApi = {
126+
getModels: function (secretKey) {
127+
return new Promise((resolve, reject) => {
128+
$.ajax({
129+
url: `https://generativelanguage.googleapis.com/v1beta/models?key=${secretKey}&pageSize=100`,
130+
}).then(resolve, reject);
131+
});
132+
}
133+
}
134+
})();
135+
</script>
136+
137+
<script type="text/javascript">
138+
(function () {
139+
const ST_NODES_CATEGORY = 'Samsung AutomationStudio';
140+
const GOOGLEAI_CONFIG = 'googleai-config';
141+
const GOOGLEAI_GENERATE = 'googleai-generate';
142+
const GOOGLEAI_SUPPORT_MODELS = [
143+
'gemini-3-flash-preview',
144+
'gemini-3-pro',
145+
'gemini-2.5-pro',
146+
'gemini-2.5-flash',
147+
'gemini-2.5-flash-lite',
148+
'gemini-2.0-flash',
149+
'gemini-2.0-flash-lite',
150+
'gemini-2.0-flash-exp', // 실험적이지만 채팅 가능
151+
'gemma-3-27b',
152+
'gemma-3-12b',
153+
'gemma-3-4b',
154+
'gemma-3-2b',
155+
'gemma-3-1b'
156+
];
157+
158+
function _GoogleAIProfile() {
159+
var _mymodelIds = {};
160+
161+
this.getMyModelIds = (nodeId) => {
162+
if (_mymodelIds[nodeId]) {
163+
return _mymodelIds[nodeId];
164+
} else {
165+
return Promise.reject(null);
166+
}
167+
};
168+
169+
this.addMyGoogleModels = (nodeId, secretKey, isRefresh) => {
170+
if (!secretKey) {
171+
_mymodelIds[nodeId] = Promise.resolve(null);
172+
return Promise.reject(null);
173+
}
174+
if (!_mymodelIds[nodeId] || isRefresh) {
175+
return GoogleAIApi.getModels(secretKey)
176+
.then((models) => {
177+
_mymodelIds[nodeId] = Promise.resolve(models.models.map((d) => d.name.replace("models/", "")));
178+
return _mymodelIds[nodeId]
179+
})
180+
.catch((e) => {
181+
_mymodelIds[nodeId] = Promise.resolve(null);
182+
return Promise.reject(null);
183+
});
184+
} else if (_mymodelIds[nodeId]) {
185+
return _mymodelIds[nodeId];
186+
}
187+
}
188+
189+
this.clearMyModels = () => {
190+
_mymodelIds = {};
191+
};
192+
}
193+
194+
const GoogleAIProfile = new _GoogleAIProfile();
195+
196+
197+
RED.nodes.registerType(GOOGLEAI_CONFIG, {
198+
category: ST_NODES_CATEGORY,
199+
paletteLabel: 'GoogleAI Config',
200+
color: '#74AA9C',
201+
defaults: {
202+
name: { value: 'GoogleAI Config' }
203+
},
204+
credentials: {
205+
secretKey: { type: 'password', required: true },
206+
},
207+
inputs: 0,
208+
outputs: 0,
209+
icon: 'google-ai.png',
210+
label: function () {
211+
if (this.name) return this.name;
212+
return 'GoogleAI Config';
213+
},
214+
oneditprepare: function () {
215+
const NODE = this;
216+
// if (NODE.credentials?.secretKey) {
217+
// const stateEl = document.querySelector('.st_state');
218+
// stateEl.classList.add('st_state--loading');
219+
220+
// GoogleAIProfile.getMyModelIds(NODE.id)
221+
// .then((models) => {
222+
// if (models == null) {
223+
// stateEl.classList.remove('st_state--valid');
224+
// } else {
225+
// stateEl.classList.add('st_state--valid');
226+
// }
227+
// })
228+
// .catch((err) => {
229+
// stateEl.classList.remove('st_state--valid');
230+
// })
231+
// .finally(() => {
232+
// stateEl.classList.remove('st_state--loading');
233+
// });
234+
// }
235+
236+
// function secretKeyChangeEvent(e, isRefresh) {
237+
// const pat = document.querySelector('#node-input-secretKey').value;
238+
// const stateEl = document.querySelector('.st_state');
239+
// stateEl.classList.add('st_state--loading');
240+
241+
// GoogleAIProfile.addMyGoogleModels(NODE.id, pat, isRefresh)
242+
// .then((models) => {
243+
// if (models == null) {
244+
// stateEl.classList.remove('st_state--valid');
245+
// } else {
246+
// stateEl.classList.add('st_state--valid');
247+
// }
248+
// })
249+
// .catch((err) => {
250+
// stateEl.classList.remove('st_state--valid');
251+
// })
252+
// .finally(() => {
253+
// stateEl.classList.remove('st_state--loading');
254+
// });
255+
// }
256+
// document.querySelector('#node-input-secretKey').addEventListener('change', secretKeyChangeEvent);
257+
// document.querySelector('.st_state').addEventListener('click', (e) => {
258+
// secretKeyChangeEvent(e, true);
259+
// });
260+
}
261+
});
262+
263+
RED.nodes.registerType(GOOGLEAI_GENERATE, {
264+
category: ST_NODES_CATEGORY,
265+
paletteLabel: 'GoogleAI Generate',
266+
color: '#69CEAF',
267+
defaults: {
268+
name: { value: '' },
269+
field: { value: 'payload', validate: RED.validators.typedInput('fieldType') },
270+
fieldType: { value: 'msg' },
271+
modelId: { value: '', required: true },
272+
mode: { value: 'single', required: true },
273+
config: {
274+
value: '',
275+
required: true,
276+
validate: function (val) {
277+
var isValidConfig = false;
278+
RED.nodes.eachNode(function (node) {
279+
if (node.id == val) {
280+
isValidConfig = true;
281+
}
282+
});
283+
return !!val && isValidConfig;
284+
},
285+
},
286+
params: { value: 'This is the payload: {{payload}} !', required: true },
287+
},
288+
inputs: 1,
289+
outputs: 1,
290+
icon: 'google-ai.png',
291+
label: function () {
292+
if (this.name) return this.name;
293+
return 'GoogleAI Generate';
294+
},
295+
oneditprepare: function () {
296+
var NODE = this;
297+
$('#node-input-config').html('');
298+
299+
RED.nodes.eachNode(function (node) {
300+
if (node.type == 'googleai-config') {
301+
const opEl = document.createElement('option');
302+
opEl.value = node.id;
303+
opEl.type = node.type;
304+
opEl.dataset.configNodeId = node.id;
305+
opEl.innerText = node.name + ' (' + node.id + ')';
306+
$('#node-input-config').append(opEl);
307+
}
308+
});
309+
310+
if (NODE.mode == undefined) {
311+
$('#node-input-mode').val('single');
312+
}
313+
$('#node-input-config').val(NODE.config);
314+
$('#node-input-field').typedInput({
315+
default: 'msg',
316+
types: ['msg', 'flow', 'global'],
317+
typeField: $('#node-input-fieldType'),
318+
});
319+
320+
NODE.editor = RED.editor.createEditor({
321+
id: 'node-input-param-editor',
322+
mode: 'ace/mode/handlebars',
323+
value: $('#node-input-params').val(),
324+
globals: {
325+
msg: true,
326+
context: true,
327+
RED: true,
328+
util: true,
329+
flow: true,
330+
global: true,
331+
console: true,
332+
Buffer: true,
333+
setTimeout: true,
334+
clearTimeout: true,
335+
setInterval: true,
336+
clearInterval: true,
337+
},
338+
});
339+
this.editor.focus();
340+
341+
NODE.initModelSelect = function (configId, initModelId) {
342+
const modelIdSelect = $('#node-input-modelId')
343+
modelIdSelect.empty();
344+
345+
GOOGLEAI_SUPPORT_MODELS.forEach((modelId) => {
346+
const optionEl = document.createElement('option');
347+
optionEl.value = modelId;
348+
optionEl.innerText = modelId;
349+
modelIdSelect.append(optionEl);
350+
});
351+
352+
if (initModelId) {
353+
modelIdSelect.val(initModelId);
354+
}
355+
}
356+
357+
function configChangeEvent(e, isRefresh) {
358+
const configId = e.target.selectedOptions[0]?.dataset?.configNodeId;
359+
NODE.initModelSelect(configId, NODE.modelId);
360+
}
361+
362+
NODE.initModelSelect(NODE.config, NODE.modelId);
363+
document.querySelector('#node-input-config').addEventListener('change', configChangeEvent);
364+
},
365+
oneditsave: function () {
366+
var annot = this.editor.getSession().getAnnotations();
367+
this.noerr = 0;
368+
$('#node-input-noerr').val(0);
369+
for (var k = 0; k < annot.length; k++) {
370+
if (annot[k].type === 'error') {
371+
$('#node-input-noerr').val(annot.length);
372+
this.noerr = annot.length;
373+
}
374+
}
375+
$('#node-input-params').val(this.editor.getValue());
376+
this.editor.destroy();
377+
delete this.editor;
378+
},
379+
oneditcancel: function () {
380+
this.editor.destroy();
381+
delete this.editor;
382+
},
383+
oneditresize: function (size) {
384+
var rows = $('#dialog-form>div:not(.node-text-editor-row)');
385+
var height = $('#dialog-form').height();
386+
for (var i = 0; i < rows.size(); i++) {
387+
height -= $(rows[i]).outerHeight(true);
388+
}
389+
var editorRow = $('#dialog-form>div.node-text-editor-row');
390+
height -= parseInt(editorRow.css('marginTop')) + parseInt(editorRow.css('marginBottom'));
391+
$('.node-text-editor').css('height', height + 'px');
392+
this.editor.resize();
393+
}
394+
});
395+
})();
396+
</script>

0 commit comments

Comments
 (0)