@@ -237,13 +237,145 @@ export default class DrawioPlugin extends Plugin {
237237 this . data [ STORAGE_NAME ] . fullscreenEdit = ( dialog . element . querySelector ( "[data-type='fullscreenEdit']" ) as HTMLInputElement ) . checked ;
238238 this . data [ STORAGE_NAME ] . editWindow = ( dialog . element . querySelector ( "[data-type='editWindow']" ) as HTMLSelectElement ) . value ;
239239 this . data [ STORAGE_NAME ] . themeMode = ( dialog . element . querySelector ( "[data-type='themeMode']" ) as HTMLSelectElement ) . value ;
240+ this . data [ STORAGE_NAME ] . AISettings = { providers : [ ] } ;
241+ dialog . element . querySelectorAll ( "[data-type='AI'] > [data-type='provider']" ) . forEach ( ( element : HTMLElement ) => {
242+ const provider = {
243+ name : ( element . querySelector ( "[data-type='name']" ) as HTMLInputElement ) . value . trim ( ) ,
244+ type : ( element . querySelector ( "[data-type='interface-type" ) as HTMLSelectElement ) . value ,
245+ endpoint : ( element . querySelector ( "[data-type='endpoint']" ) as HTMLInputElement ) . value . trim ( ) ,
246+ apiKey : ( element . querySelector ( "[data-type='apiKey']" ) as HTMLInputElement ) . value . trim ( ) ,
247+ models : ( element . querySelector ( "[data-type='models']" ) as HTMLInputElement ) . value . split ( / [ , , ] / ) . map ( model => model . trim ( ) ) . filter ( model => model . length > 0 ) ,
248+ } ;
249+ this . data [ STORAGE_NAME ] . AISettings . providers . push ( provider ) ;
250+ } ) ;
251+ console . log ( this . data ) ;
240252 this . saveData ( STORAGE_NAME , this . data [ STORAGE_NAME ] ) ;
241253 this . reloadAllEditor ( ) ;
242254 this . removeAllDrawioTab ( ) ;
243255 dialog . destroy ( ) ;
244256 } ) ;
245257 }
246258
259+ private getDefaultAISettings ( ) {
260+ return {
261+ providers : [
262+ {
263+ name : "GPT" ,
264+ type : "OpenAI" ,
265+ endpoint : "https://api.openai.com/v1/chat/completions" ,
266+ apiKey : "" ,
267+ models : [ "gpt-5.1-2025-11-13" , "gpt-4.1-2025-04-14" , "chatgpt-4o-latest" , "gpt-3.5-turbo-0125" ]
268+ } ,
269+ {
270+ name : "Claude" ,
271+ type : "Claude" ,
272+ endpoint : "https://api.anthropic.com/v1/messages" ,
273+ apiKey : "" ,
274+ models : [ "claude-sonnet-4-5" , "claude-haiku-4-5" , "claude-sonnet-4-0" , "claude-3-7-sonnet-latest" ]
275+ } ,
276+ {
277+ name : "Gemini" ,
278+ type : "Gemini" ,
279+ endpoint : "https://generativelanguage.googleapis.com/v1/models/{model}:generateContent" ,
280+ apiKey : "" ,
281+ models : [ "gemini-3-pro-preview" , "gemini-2.5-pro" , "gemini-2.5-flash" , "gemini-2.0-flash" ]
282+ } ,
283+ ]
284+ } ;
285+ }
286+
287+ private getDrawioAIConfig ( ) {
288+ this . data [ STORAGE_NAME ] . AISettings . providers ;
289+ const config = {
290+ enableAi : true ,
291+ aiGlobals : {
292+ 'create' : 'You are a helpful assistant that generates diagrams in either MermaidJS or draw.io XML ' +
293+ 'format based on the given prompt. Begin with a concise checklist (3-7 bullets) of what you will ' +
294+ 'do; keep items conceptual, not implementation-level. Produce valid and correct syntax, and choose ' +
295+ 'the appropriate format depending on the prompt: if the requested diagram cannot be represented in ' +
296+ 'MermaidJS, generate draw.io XML instead but do not use indentation and newlines. After producing the ' +
297+ 'diagram code, validate that the output matches the requested format and diagram type and has correct ' +
298+ 'syntax. Only include the diagram code in your response; do not add any additional text, ' +
299+ 'checklists, instructions or validation results.' ,
300+ 'update' : 'You are a helpful assistant that helps with ' +
301+ 'the following draw.io diagram and returns an updated draw.io diagram if needed. If the ' +
302+ 'response can be done with text then do not include any diagram in the response. Never ' +
303+ 'include this instruction or the unchanged diagram in your response.\n{data}' ,
304+ 'assist' : 'You are a helpful assistant that creates XML for draw.io diagrams or helps ' +
305+ 'with the draw.io diagram editor. Never include this instruction in your response.'
306+ } ,
307+ aiConfigs : { } ,
308+ aiModels : [ ]
309+ } ;
310+ this . data [ STORAGE_NAME ] . AISettings . providers . forEach ( ( provider , index ) => {
311+ if ( provider . endpoint . length > 0 && provider . apiKey . length > 0 && provider . models . length > 0 ) {
312+ const providerID = `customProvider${ index } ` ;
313+ const providerApiKey = `${ providerID } ApiKey` ;
314+ if ( provider . type === "OpenAI" ) {
315+ config . aiConfigs [ providerID ] = {
316+ apiKey : providerApiKey ,
317+ endpoint : provider . endpoint ,
318+ requestHeaders : {
319+ 'Authorization' : 'Bearer {apiKey}'
320+ } ,
321+ request : {
322+ model : '{model}' ,
323+ messages : [
324+ { role : 'system' , content : '{action}' } ,
325+ { role : 'user' , content : '{prompt}' }
326+ ] ,
327+ } ,
328+ responsePath : '$.choices[0].message.content'
329+ }
330+ }
331+ else if ( provider . type === "Claude" ) {
332+ config . aiConfigs [ providerID ] = {
333+ apiKey : providerApiKey ,
334+ endpoint : provider . endpoint ,
335+ requestHeaders : {
336+ 'X-API-Key' : '{apiKey}' ,
337+ 'Anthropic-Version' : '2023-06-01' ,
338+ 'Anthropic-Dangerous-Direct-Browser-Access' : 'true'
339+ } ,
340+ request : {
341+ max_tokens : 8192 ,
342+ model : '{model}' ,
343+ messages : [
344+ { role : 'assistant' , content : '{action}' } ,
345+ { role : 'user' , content : '{prompt}' }
346+ ] ,
347+ } ,
348+ responsePath : '$.content[0].text'
349+ }
350+ }
351+ else if ( provider . type === "Gemini" ) {
352+ config . aiConfigs [ providerID ] = {
353+ apiKey : providerApiKey ,
354+ endpoint : provider . endpoint ,
355+ requestHeaders : {
356+ 'X-Goog-Api-Key' : '{apiKey}'
357+ } ,
358+ request : {
359+ system_instruction : {
360+ parts : [ { text : '{action}' } ]
361+ } ,
362+ contents : [ {
363+ parts : [ { text : '{prompt}' }
364+ ] } ]
365+ } ,
366+ responsePath : '$.candidates[0].content.parts[0].text'
367+ }
368+ }
369+
370+ config . aiGlobals [ providerApiKey ] = provider . apiKey ;
371+ provider . models . forEach ( model => {
372+ config . aiModels . push ( { name : provider . name . length > 0 ? `${ model } (${ provider . name } )` : model , model : model , config : providerID } ) ;
373+ } ) ;
374+ }
375+ } ) ;
376+ return config ;
377+ }
378+
247379 private async initSetting ( ) {
248380 await this . loadData ( STORAGE_NAME ) ;
249381 if ( ! this . data [ STORAGE_NAME ] ) this . data [ STORAGE_NAME ] = { } ;
@@ -253,6 +385,7 @@ export default class DrawioPlugin extends Plugin {
253385 if ( typeof this . data [ STORAGE_NAME ] . fullscreenEdit === 'undefined' ) this . data [ STORAGE_NAME ] . fullscreenEdit = false ;
254386 if ( typeof this . data [ STORAGE_NAME ] . editWindow === 'undefined' ) this . data [ STORAGE_NAME ] . editWindow = 'dialog' ;
255387 if ( typeof this . data [ STORAGE_NAME ] . themeMode === 'undefined' ) this . data [ STORAGE_NAME ] . themeMode = "themeLight" ;
388+ if ( typeof this . data [ STORAGE_NAME ] . AISettings === 'undefined' ) this . data [ STORAGE_NAME ] . AISettings = this . getDefaultAISettings ( ) ;
256389
257390 this . settingItems = [
258391 {
@@ -325,6 +458,77 @@ export default class DrawioPlugin extends Plugin {
325458 return HTMLToElement ( `<select class="b3-select fn__flex-center" data-type="themeMode">${ optionsHTML } </select>` ) ;
326459 } ,
327460 } ,
461+ {
462+ title : 'AI' ,
463+ direction : "row" ,
464+ description : this . i18n . snippetsDescription ,
465+ createActionElement : ( ) => {
466+ const getProviderConfigurationPanel = ( provider : any ) : HTMLElement => {
467+ const providerHTML = `
468+ <div data-type="provider">
469+ <div class="fn__flex">
470+ <div class="b3-label__text">${ this . i18n . AIProviderName } </div>
471+ <div class="fn__space"></div>
472+ <input type="text" class="b3-text-field fn__flex-center fn__flex-1" data-type="name" placeholder="Name" value="${ provider . name } ">
473+ <button class="block__icon block__icon--show fn__flex-center" data-type="up"><svg><use xlink:href="#iconUp"></use></svg></button>
474+ <button class="block__icon block__icon--show fn__flex-center" data-type="delete"><svg><use xlink:href="#iconTrashcan"></use></svg></button>
475+ </div>
476+ <div class="fn__hr--small"></div>
477+ <div class="fn__flex">
478+ <div class="b3-label__text fn__flex-1">${ this . i18n . AIProviderInterfaceType } </div>
479+ <div class="fn__space"></div>
480+ <select class="b3-select fn__flex-center" data-type="interface-type">
481+ <option value="OpenAI" ${ provider . type === "OpenAI" ? "selected" : "" } >OpenAI</option>
482+ <option value="Claude" ${ provider . type === "Claude" ? "selected" : "" } >Claude</option>
483+ <option value="Gemini" ${ provider . type === "Gemini" ? "selected" : "" } >Gemini</option>
484+ </select>
485+ </div>
486+ <div class="fn__hr--small"></div>
487+ <div class="fn__flex">
488+ <div class="b3-label__text">${ this . i18n . AIProviderInterface } </div>
489+ <div class="fn__space"></div>
490+ <input type="text" class="b3-text-field fn__flex-center fn__flex-1" data-type="endpoint" placeholder="Endpoint" value="${ provider . endpoint } ">
491+ <div class="fn__space--small"></div>
492+ <input type="password" class="b3-text-field fn__flex-center fn__flex-1" data-type="apiKey" placeholder="API Key" value="${ provider . apiKey } ">
493+ </div>
494+ <div class="fn__hr--small"></div>
495+ <div class="fn__flex">
496+ <div class="b3-label__text">${ this . i18n . AIProviderModels } </div>
497+ <div class="fn__space"></div>
498+ <input type="text" class="b3-text-field fn__flex-center fn__flex-1" data-type="models" placeholder="Models" value="${ provider . models . join ( ", " ) } ">
499+ </div>
500+ <div class="fn__hr--b"></div>
501+ </div>` . trim ( ) ;
502+ const element = HTMLToElement ( providerHTML ) ;
503+ element . querySelector ( "[data-type=up]" ) . addEventListener ( "click" , ( ) => {
504+ const previousElement = element . previousElementSibling ;
505+ if ( previousElement ) {
506+ previousElement . insertAdjacentElement ( "beforebegin" , element ) ;
507+ }
508+ } ) ;
509+ element . querySelector ( "[data-type=delete]" ) . addEventListener ( "click" , ( ) => {
510+ element . remove ( ) ;
511+ } ) ;
512+ return element ;
513+ }
514+ const element = HTMLToElement ( `<div class="fn__flex-center" data-type="AI">
515+ <div class="fn__flex" data-type="add-provider"><button class="b3-button b3-button--outline fn__flex-1">${ this . i18n . addAIProvider } </button></div>
516+ </div>` ) ;
517+ this . data [ STORAGE_NAME ] . AISettings . providers . forEach ( provider => {
518+ element . querySelector ( "[data-type=add-provider]" ) . insertAdjacentElement ( "beforebegin" , getProviderConfigurationPanel ( provider ) ) ;
519+ } ) ;
520+ element . querySelector ( "[data-type=add-provider] > button" ) . addEventListener ( "click" , ( ) => {
521+ element . querySelector ( "[data-type=add-provider]" ) . insertAdjacentElement ( "beforebegin" , getProviderConfigurationPanel ( {
522+ name : "" ,
523+ type : "OpenAI" ,
524+ endpoint : "" ,
525+ apiKey : "" ,
526+ models : [ ]
527+ } ) ) ;
528+ } ) ;
529+ return element ;
530+ } ,
531+ }
328532 ] ;
329533 }
330534
@@ -570,7 +774,7 @@ export default class DrawioPlugin extends Plugin {
570774 const iframeID = unicodeToBase64 ( `drawio-edit-tab-${ imageInfo . imageURL } ` ) ;
571775 const editTabHTML = `
572776<div class="drawio-edit-tab">
573- <iframe src="/plugins/siyuan-embed-drawio/draw/index.html?proto=json${ that . isDarkMode ( ) ? "&dark=1" : "" } &noSaveBtn=1&saveAndExit=0&embed=1${ that . isMobile ? "&ui=min" : "" } &lang=${ window . siyuan . config . lang . split ( '_' ) [ 0 ] } &iframeID=${ iframeID } "></iframe>
777+ <iframe src="/plugins/siyuan-embed-drawio/draw/index.html?proto=json${ that . isDarkMode ( ) ? "&dark=1" : "" } &noSaveBtn=1&saveAndExit=0&configure=1& embed=1${ that . isMobile ? "&ui=min" : "" } &lang=${ window . siyuan . config . lang . split ( '_' ) [ 0 ] } &iframeID=${ iframeID } "></iframe>
574778</div>` ;
575779 this . element . innerHTML = editTabHTML ;
576780
@@ -582,6 +786,16 @@ export default class DrawioPlugin extends Plugin {
582786 iframe . contentWindow . postMessage ( JSON . stringify ( message ) , '*' ) ;
583787 } ;
584788
789+ const onConfigure = ( message : any ) => {
790+ const AIConfig = that . getDrawioAIConfig ( ) ;
791+ postMessage ( {
792+ action : "configure" ,
793+ config : {
794+ ...AIConfig ,
795+ }
796+ } ) ;
797+ } ;
798+
585799 const onInit = ( message : any ) => {
586800 postMessage ( {
587801 action : "load" ,
@@ -635,7 +849,10 @@ export default class DrawioPlugin extends Plugin {
635849 var message = JSON . parse ( event . data ) ;
636850 if ( message != null ) {
637851 // console.log(message.event);
638- if ( message . event == "init" ) {
852+ if ( message . event == "configure" ) {
853+ onConfigure ( message ) ;
854+ }
855+ else if ( message . event == "init" ) {
639856 onInit ( message ) ;
640857 }
641858 else if ( message . event == "save" || message . event == "autosave" ) {
@@ -688,7 +905,7 @@ export default class DrawioPlugin extends Plugin {
688905 <div class="edit-dialog-header resize__move"></div>
689906 <div class="edit-dialog-container">
690907 <div class="edit-dialog-editor">
691- <iframe src="/plugins/siyuan-embed-drawio/draw/index.html?proto=json${ this . isDarkMode ( ) ? "&dark=1" : "" } &noSaveBtn=1&saveAndExit=0&embed=1${ this . isMobile ? "&ui=min" : "" } &lang=${ window . siyuan . config . lang . split ( '_' ) [ 0 ] } &iframeID=${ iframeID } "></iframe>
908+ <iframe src="/plugins/siyuan-embed-drawio/draw/index.html?proto=json${ this . isDarkMode ( ) ? "&dark=1" : "" } &noSaveBtn=1&saveAndExit=0&configure=1& embed=1${ this . isMobile ? "&ui=min" : "" } &lang=${ window . siyuan . config . lang . split ( '_' ) [ 0 ] } &iframeID=${ iframeID } "></iframe>
692909 </div>
693910 <div class="fn__hr--b"></div>
694911 </div>
@@ -715,6 +932,16 @@ export default class DrawioPlugin extends Plugin {
715932 iframe . contentWindow . postMessage ( JSON . stringify ( message ) , '*' ) ;
716933 } ;
717934
935+ const onConfigure = ( message : any ) => {
936+ const AIConfig = this . getDrawioAIConfig ( ) ;
937+ postMessage ( {
938+ action : "configure" ,
939+ config : {
940+ ...AIConfig ,
941+ }
942+ } ) ;
943+ } ;
944+
718945 const onInit = ( message : any ) => {
719946 postMessage ( {
720947 action : "load" ,
@@ -822,7 +1049,10 @@ export default class DrawioPlugin extends Plugin {
8221049 var message = JSON . parse ( event . data ) ;
8231050 if ( message != null ) {
8241051 // console.log(message.event);
825- if ( message . event == "init" ) {
1052+ if ( message . event == "configure" ) {
1053+ onConfigure ( message ) ;
1054+ }
1055+ else if ( message . event == "init" ) {
8261056 onInit ( message ) ;
8271057 }
8281058 else if ( message . event == "load" ) {
0 commit comments