Skip to content

Commit 390477e

Browse files
committed
feat: support AI generation
1 parent 9528898 commit 390477e

File tree

7 files changed

+256
-8
lines changed

7 files changed

+256
-8
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ This plugin serves as the third plugin in the **Embed Series**, aiming to provid
3434
- [x] Fullscreen edit
3535
- [x] Light/Dark mode
3636
- [x] Edit in Tab/Dialog
37+
- [x] AI Generation
3738

3839
> If you have additional feature requests or suggestions, feel free to [open an issue on GitHub](https://github.com/YuxinZhaozyx/siyuan-embed-drawio/issues) or [post in the SiYuan community](https://ld246.com/article/1762744532030) to request support for additional packages.
3940
@@ -81,6 +82,8 @@ The label of a draw.io image block can be configured in the plugin settings.
8182

8283
## Changelog
8384

85+
+ v0.7.0
86+
+ Feature: Support AI generation
8487
+ v0.6.7
8588
+ Update: update draw.io to v29.5.1
8689
+ Optimize: Only images starting with `drawio-` will show edit button and label to optimize document loading speed

README_zh_CN.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
> 重要提示:为提升文档加载速度,本插件只识别以 `drawio-` 开头的图片,如果您发现原有的draw.io图片不显示编辑按钮,请重命名图片为 `drawio-` 开头的文件名。
2020
21+
> 重大更新:v0.7.0版本起支持AI生成
22+
2123
> 「嵌入式系列」思源插件QQ交流群:1037356690
2224
> 赞助: https://yuxinzhao.net/sponsor
2325
@@ -53,6 +55,7 @@
5355
- [x] 全屏编辑
5456
- [x] 明暗模式
5557
- [x] Tab/Dialog窗口编辑
58+
- [x] AI生成
5659

5760
> 如有更多需求/建议欢迎[在GitHub仓库中提issue](https://github.com/YuxinZhaozyx/siyuan-embed-drawio/issues)[在思源笔记社区中发帖](https://ld246.com/article/1762744532030)
5861
@@ -75,6 +78,8 @@
7578

7679
## 更新日志
7780

81+
+ v0.7.0
82+
+ 新增功能:支持AI生成
7883
+ v0.6.7
7984
+ 更新:更新draw.io到v29.5.1
8085
+ 优化:只有命名以 `drawio-` 开头的图片才会显示编辑按钮和标签以优化文档加载速度

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "siyuan-embed-drawio",
3-
"version": "0.6.7",
3+
"version": "0.7.0",
44
"type": "module",
55
"description": "This is a plugin for siyuan",
66
"author": "",

plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "siyuan-embed-drawio",
33
"author": "Yuxin Zhao",
44
"url": "https://github.com/YuxinZhaozyx/siyuan-embed-drawio",
5-
"version": "0.6.7",
5+
"version": "0.7.0",
66
"minAppVersion": "3.0.0",
77
"disabledInPublish": true,
88
"backends": [

src/i18n/en_US.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,10 @@
1616
"themeMode": "Theme Mode",
1717
"themeModeDescription": "Theme of window",
1818
"zoom": "Zoom",
19-
"zoomDescription": "Scale Factor (make PNG image more clear)"
19+
"zoomDescription": "Scale Factor (make PNG image more clear)",
20+
"AIProviderName": "Provider Name",
21+
"AIProviderInterfaceType": "Interface Type",
22+
"AIProviderInterface": "Interface",
23+
"AIProviderModels": "Models",
24+
"addAIProvider": "Add Provider"
2025
}

src/i18n/zh_CN.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,10 @@
1616
"themeMode": "主题模式",
1717
"themeModeDescription": "窗口的主题",
1818
"zoom": "缩放",
19-
"zoomDescription": "缩放比例(让PNG图像更清晰)"
19+
"zoomDescription": "缩放比例(让PNG图像更清晰)",
20+
"AIProviderName": "提供商名称",
21+
"AIProviderInterfaceType": "接口类型",
22+
"AIProviderInterface": "接口",
23+
"AIProviderModels": "模型",
24+
"addAIProvider": "添加提供商"
2025
}

src/index.ts

Lines changed: 234 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)