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 . < / d d >
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 . < / d d >
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 < / o p t i o n >
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 . < / d d >
110+ < dt > GoogleAI Config < span class = "property-type" > required</ span > </ dt >
111+ < dd > Choose a GoogleAI Config Node . < / d d >
112+ < dt > Model < span class = "property-type" > required</ span > </ dt >
113+ < dd > Choose a GoogleAI generate Model . < / d d >
114+ < dt > Interaction Mode < span class = "property-type" > required</ span > </ dt >
115+ < dd > Choose a Interaction Mode . < / d d >
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 . < / d d >
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 > t e m p l a t e s .< / 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