Compare commits

...

8 Commits

Author SHA1 Message Date
zsviczian 353732f597 2.4.0-beta-9 2024-08-24 13:59:49 +02:00
zsviczian 5599d2507f update writing machine data 2024-08-23 12:43:27 +02:00
zsviczian 70cf6ffe70 Support embeddables 2024-08-23 12:41:33 +02:00
zsviczian 61c9277097 writing machine 2024-08-22 21:59:06 +02:00
zsviczian 401052efd3 svg icon take 2 2024-08-22 20:36:00 +02:00
zsviczian a57a0e797d update writing machine svg 2024-08-22 20:33:52 +02:00
zsviczian 8f48853e2c updated writing machine script 2024-08-22 17:58:42 +02:00
zsviczian a62148dc07 publish Excalidraw Writing Machine 2024-08-21 21:53:59 +02:00
14 changed files with 547 additions and 26 deletions
+242
View File
@@ -0,0 +1,242 @@
/*
Generates a hierarchical Markdown document out of a visual layout of an article.
Watch this video to understand how the script is intended to work:
![Excalidraw Writing Machine YouTube Video](https://youtu.be/zvRpCOZAUSs)
You can download the sample Obsidian Templater file from [here](https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9)
You can download the demo PDF document showcased in the video from [here](https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf)
```js*/
const selectedElements = ea.getViewSelectedElements();
if (selectedElements.length !== 1 || selectedElements[0].type === "arrow") {
new Notice("Select a single element that is not an arrow and not a frame");
return;
}
const visited = new Set(); // Avoiding recursive infinite loops
delete window.ewm;
await ea.targetView.save();
//------------------
// Load Settings
//------------------
let settings = ea.getScriptSettings();
//set default values on first run
if(!settings["Template path"]) {
settings = {
"Template path" : {
value: "",
description: "The template file path that will receive the concatenated text. If the file includes <<<REPLACE ME>>> then it will be replaced with the generated text, if <<<REPLACE ME>>> is not present in the file the hierarchical markdown generated from the diagram will be added to the end of the template."
},
"ZK '# Summary' section": {
value: "Summary",
description: "The section in your visual zettelkasten file that contains the short written summary of the idea. This is the text that will be included in the hierarchical markdown file if visual ZK cards are included in your flow"
},
"ZK '# Source' section": {
value: "Source",
description: "The section in your visual zettelkasten file that contains the reference to your source. If present in the file, this text will be included in the output file as a reference"
},
"Embed image links": {
value: true,
description: "Should the resulting markdown document include the ![[embedded images]]?"
}
};
await ea.setScriptSettings(settings);
}
const ZK_SOURCE = settings["ZK '# Source' section"].value;
const ZK_SECTION = settings["ZK '# Summary' section"].value;
const INCLUDE_IMG_LINK = settings["Embed image links"].value;
let templatePath = settings["Template path"].value;
//------------------
// Select template file
//------------------
const MSG = "Select another file"
let selection = MSG;
if(templatePath && app.vault.getAbstractFileByPath(templatePath)) {
selection = await utils.suggester([templatePath, MSG],[templatePath, MSG], "Use previous template or select another?");
if(!selection) {
new Notice("process aborted");
return;
}
}
if(selection === MSG) {
const files = app.vault.getMarkdownFiles().map(f=>f.path);
selection = await utils.suggester(files,files,"Select the template to use. ESC to not use a tempalte");
}
if(selection && selection !== templatePath) {
settings["Template path"].value = selection;
await ea.setScriptSettings(settings);
}
templatePath = selection;
//------------------
// supporting functions
//------------------
function getNextElementFollowingArrow(el, arrow) {
if (arrow.startBinding?.elementId === el.id) {
return ea.getViewElements().find(x => x.id === arrow.endBinding?.elementId);
}
if (arrow.endBinding?.elementId === el.id) {
return ea.getViewElements().find(x => x.id === arrow.startBinding?.elementId);
}
return null;
}
function getImageLink(f) {
return `![${f.name}](${encodeURI(f.path)})`;
}
function getBoundText(el) {
const textId = el.boundElements?.find(x => x.type === "text")?.id;
const text = ea.getViewElements().find(x => x.id === textId)?.originalText;
return text ? text + "\n" : "";
}
async function getSectionText(file, section) {
const content = await app.vault.cachedRead(file);
const metadata = app.metadataCache.getFileCache(file);
if (!metadata || !metadata.headings) {
return null;
}
const targetHeading = metadata.headings.find(h => h.heading === section);
if (!targetHeading) {
return null;
}
const startPos = targetHeading.position.start.offset;
let endPos = content.length;
const nextHeading = metadata.headings.find(h => h.position.start.offset > startPos);
if (nextHeading) {
endPos = nextHeading.position.start.offset;
}
let sectionContent = content.slice(startPos, endPos).trim();
sectionContent = sectionContent.substring(sectionContent.indexOf('\n') + 1).trim();
// Remove Markdown comments enclosed in %%
sectionContent = sectionContent.replace(/%%[\s\S]*?%%/g, '').trim();
return sectionContent;
}
async function getBlockText(file, blockref) {
const content = await app.vault.cachedRead(file);
const blockPattern = new RegExp(`\\^${blockref}\\b`, 'g');
let blockPosition = content.search(blockPattern);
if (blockPosition === -1) {
return "";
}
const startPos = content.lastIndexOf('\n', blockPosition) + 1;
let endPos = content.indexOf('\n', blockPosition);
if (endPos === -1) {
endPos = content.length;
} else {
const nextBlockOrHeading = content.slice(endPos).search(/(^# |^\^|\n)/gm);
if (nextBlockOrHeading !== -1) {
endPos += nextBlockOrHeading;
} else {
endPos = content.length;
}
}
let blockContent = content.slice(startPos, endPos).trim();
blockContent = blockContent.replace(blockPattern, '').trim();
blockContent = blockContent.replace(/%%[\s\S]*?%%/g, '').trim();
return blockContent;
}
async function getElementText(el) {
if (el.type === "text") {
return el.originalText;
}
if (el.type === "image") {
const f = ea.getViewFileForImageElement(el);
if(!ea.isExcalidrawFile(f)) return f.basename + (INCLUDE_IMG_LINK ? `\n${getImageLink(f)}\n` : "");
let source = await getSectionText(f, ZK_SOURCE);
source = source ? ` (source:: ${source})` : "";
const summary = await getSectionText(f, ZK_SECTION) ;
if(summary) return (INCLUDE_IMG_LINK ? `${getImageLink(f)}\n${summary + source}` : summary + source) + "\n";
return f.basename + (INCLUDE_IMG_LINK ? `\n${getImageLink(f)}\n` : "");
}
if (el.type === "embeddable") {
const linkWithRef = el.link.match(/\[\[([^\]]*)]]/)?.[1];
if(!linkWithRef) return "";
const path = linkWithRef.split("#")[0];
const f = app.metadataCache.getFirstLinkpathDest(path, ea.targetView.file.path);
if(!f) return "";
if(f.extension !== "md") return f.basename;
const ref = linkWithRef.split("#")[1];
if(!ref) return await app.vault.cachedRead(f);
if(ref.startsWith("^")) {
return await getBlockText(f, ref.substring(1));
} else {
return await getSectionText(f, ref);
}
}
return getBoundText(el);
}
//------------------
// Navigating the hierarchy
//------------------
async function crawl(el, level, isFirst = false) {
visited.add(el.id);
let result = await getElementText(el) + "\n";
// Process all arrows connected to this element
const boundElementsData = el.boundElements.filter(x => x.type === "arrow");
const isFork = boundElementsData.length > (isFirst ? 1 : 2);
if(isFork) level++;
for(const bindingData of boundElementsData) {
const arrow = ea.getViewElements().find(x=> x.id === bindingData.id);
const nextEl = getNextElementFollowingArrow(el, arrow);
if (nextEl && !visited.has(nextEl.id)) {
if(isFork) result += `\n${"#".repeat(level)} `;
const arrowLabel = getBoundText(arrow);
if (arrowLabel) {
// If the arrow has a label, add it as an additional level
result += arrowLabel + "\n";
result += await crawl(nextEl, level);
} else {
// If no label, continue to the next element
result += await crawl(nextEl, level);
}
}
};
return result;
}
window.ewm = "## " + await crawl(selectedElements[0], 2, true);
const outputPath = await ea.getAttachmentFilepath(`EWM - ${ea.targetView.file.basename}.md`);
let result = templatePath
? await app.vault.cachedRead(app.vault.getAbstractFileByPath(templatePath))
: "";
if(result.match("<<<REPLACE ME>>>")) {
result = result.replaceAll("<<<REPLACE ME>>>",window.ewm);
} else {
result += window.ewm;
}
const outfile = await app.vault.create(outputPath,result);
setTimeout(()=>{
ea.openFileInNewOrAdjacentLeaf(outfile);
}, 250);
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke="CurrentColor" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-keyboard">
<path stroke-width="2" d="M10 8h.01"/>
<path stroke-width="2" d="M12 12h.01"/>
<path stroke-width="2" d="M14 8h.01"/>
<path stroke-width="2" d="M16 12h.01"/>
<path stroke-width="2" d="M18 8h.01"/>
<path stroke-width="2" d="M6 8h.01"/>
<path stroke-width="2" d="M7 16h10"/>
<path stroke-width="2" d="M8 12h.01"/>
<path fill="none" stroke-width="2" d="M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

+40
View File
@@ -8,6 +8,46 @@ https://zsviczian.github.io/obsidian-excalidraw-plugin/ExcalidrawScriptsEngine.h
```javascript
*/
if(ea.verifyMinimumPluginVersion && ea.verifyMinimumPluginVersion("2.4.0")) {
const api = ea.getExcalidrawAPI();
let appState = api.getAppState();
let gridFrequency = appState.gridStep;;
const customControls = (container) => {
new ea.obsidian.Setting(container)
.setName(`Major grid frequency`)
.addDropdown(dropdown => {
[2,3,4,5,6,7,8,9,10].forEach(grid=>dropdown.addOption(grid,grid));
dropdown
.setValue(gridFrequency)
.onChange(value => {
gridFrequency = value;
})
})
}
const gridSize = parseInt(await utils.inputPrompt(
"Grid size?",
null,
appState.GridSize?.toString()??"20",
null,
1,
false,
customControls
));
if(isNaN(gridSize)) return; //this is to avoid passing an illegal value to Excalidraw
const gridStep = isNaN(parseInt(gridFrequency)) ? appState.gridStep : parseInt(gridFrequency);
api.updateScene({
appState : {gridSize, gridStep, gridModeEnabled:true},
commitToHistory:false
});
}
// ----------------
// old script
// ----------------
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.19")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
File diff suppressed because one or more lines are too long
+7
View File
@@ -119,6 +119,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Custom%20Zoom.svg"/></div>|[[#Custom Zoom]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/ExcaliAI.svg"/></div>|[[#ExcaliAI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Writing%20Machine.svg"/></div>|[[#Excalidraw Writing Machine]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.svg"/></div>|[[#GPT Draw-a-UI]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Hardware%20Eraser%20Support.svg"/></div>|[[#Hardware Eraser Support]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Palette%20loader.svg"/></div>|[[#Palette Loader]]|
@@ -389,6 +390,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/ExcaliAI.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Various AI features based on GPT Vision.<br><iframe width="400" height="225" src="https://www.youtube.com/embed/A1vrSGBbWgo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-draw-a-ui.jpg'></td></tr></table>
## Excalidraw Writing Machine
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Excalidraw%20Writing%20Machine.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Excalidraw%20Writing%20Machine.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Creates a hierarchical Markdown document out of a visual layout of an article that can be fed to Templater and converted into an article using AI for Templater.<br>Watch this video to understand how the script is intended to work:<br><iframe width="400" height="225" src="https://www.youtube.com/embed/zvRpCOZAUSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br>You can download the sample Obsidian Templater file from <a href="https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9">here</a>. You can download the demo PDF document showcased in the video from <a href="https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf">here</a>.</td></tr></table>
## GPT Draw-a-UI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.md
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.4.0-beta-8",
"version": "2.4.0-beta-9",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
+1 -1
View File
@@ -19,7 +19,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.1-obsidian-41",
"@zsviczian/excalidraw": "0.17.1-obsidian-42",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",
+31 -1
View File
@@ -3335,7 +3335,7 @@ export const search = async (view: ExcalidrawView) => {
const ea = view.plugin.ea;
ea.reset();
ea.setView(view);
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link);
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image");
if (elements.length === 0) {
return;
}
@@ -3455,6 +3455,36 @@ export const getElementsWithLinkMatchingQuery = (
}));
}
/**
*
* @param elements
* @param query
* @param exactMatch - when searching for section header exactMatch should be set to true
* @returns the elements matching the query
*/
export const getImagesMatchingQuery = (
elements: ExcalidrawElement[],
query: string[],
excalidrawData: ExcalidrawData,
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
): ExcalidrawElement[] => {
if (!elements || elements.length === 0 || !query || query.length === 0) {
return [];
}
return elements.filter((el: ExcalidrawElement) =>
el.type === "image" &&
query.some((q) => {
const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim();
const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim();
const text = filename ?? equation;
if(!text) return false;
return exactMatch
? (text === q.toLowerCase())
: text.match(q.toLowerCase());
}));
}
export const cloneElement = (el: ExcalidrawElement):any => {
const newEl = JSON.parse(JSON.stringify(el));
newEl.version = el.version + 1;
+13 -3
View File
@@ -1,9 +1,9 @@
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawFrameLikeElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metadata";
import { AppState, BinaryFiles, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
type EmbeddedLink =
@@ -27,6 +27,7 @@ declare namespace ExcalidrawLib {
appState?: Partial<Omit<AppState, "offsetTop" | "offsetLeft">>;
files: BinaryFiles | null;
maxWidthOrHeight?: number;
exportingFrame?: ExcalidrawFrameLikeElement | null;
getDimensions?: (
width: number,
height: number,
@@ -170,11 +171,20 @@ declare namespace ExcalidrawLib {
var WelcomeScreen: any;
var TTDDialogTrigger: any;
var TTDDialog: any;
var DiagramToCodePlugin: (props: {
generate: GenerateDiagramToCode;
}) => any;
function getDataURL(file: Blob | File): Promise<DataURL>;
function destroyObsidianUtils(): void;
function registerLocalFont(fontMetrics: FontMetadata, uri: string): void;
function getFontFamilies(): string[];
function registerFontsInCSS(): Promise<void>;
function getCSSFontDefinition(fontFamily: number): Promise<string>;
function getTextFromElements (
elements: readonly ExcalidrawElement[],
separator?: string,
): string;
function safelyParseJSON (json: string): Record<string, any> | null;
}
+92 -18
View File
@@ -16,6 +16,7 @@ import {
import {
ExcalidrawElement,
ExcalidrawImageElement,
ExcalidrawMagicFrameElement,
ExcalidrawTextElement,
FileId,
NonDeletedExcalidrawElement,
@@ -61,7 +62,8 @@ import {
getTextElementsMatchingQuery,
cloneElement,
getFrameElementsMatchingQuery,
getElementsWithLinkMatchingQuery
getElementsWithLinkMatchingQuery,
getImagesMatchingQuery
} from "./ExcalidrawAutomate";
import { t } from "./lang/helpers";
import {
@@ -136,11 +138,12 @@ import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal";
import { getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { nanoid } from "nanoid";
import { CustomMutationObserver, DEBUGGING, debug, log} from "./utils/DebugHelper";
import { extractCodeBlocks, postOpenAI } from "./utils/AIUtils";
import { errorHTML, extractCodeBlocks, postOpenAI } from "./utils/AIUtils";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { SelectCard } from "./dialogs/SelectCard";
import { Packages } from "./types/types";
import React from "react";
import { diagramToHTML } from "./utils/matic";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -1144,7 +1147,7 @@ export default class ExcalidrawView extends TextFileView {
if(container) {
linkText = container.link;
if(linkText.startsWith("#")) {
if(linkText?.startsWith("#")) {
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
}
@@ -1366,7 +1369,7 @@ export default class ExcalidrawView extends TextFileView {
//final fallback to prevent resizing when text element is in edit mode
//this is to prevent jumping text due to on-screen keyboard popup
if (api.getAppState()?.editingElement?.type === "text") {
if (api.getAppState()?.editingTextElement) {
return;
}
this.zoomToFit(false);
@@ -1678,8 +1681,8 @@ export default class ExcalidrawView extends TextFileView {
return;
}
const st = api.getAppState();
const isEditing = st.editingElement !== null;
const isDragging = st.newElement !== null;
const isEditingText = st.editingTextElement !== null;
const isEditingNewElement = st.newElement !== null;
//this will reset positioning of the cursor in case due to the popup keyboard,
//or the command palette, or some other unexpected reason the onResize would not fire...
this.refreshCanvasOffset();
@@ -1689,8 +1692,8 @@ export default class ExcalidrawView extends TextFileView {
!this.semaphores.forceSaving &&
!this.semaphores.autosaving &&
!this.semaphores.embeddableIsEditingSelf &&
!isEditing &&
!isDragging //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630
!isEditingText &&
!isEditingNewElement //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630
) {
//console.log("autosave");
this.autosaveTimer = null;
@@ -3297,6 +3300,7 @@ export default class ExcalidrawView extends TextFileView {
currentItemTextAlign: st.currentItemTextAlign,
currentItemStartArrowhead: st.currentItemStartArrowhead,
currentItemEndArrowhead: st.currentItemEndArrowhead,
currentItemArrowType: st.currentItemArrowType,
scrollX: st.scrollX,
scrollY: st.scrollY,
zoom: st.zoom,
@@ -3425,7 +3429,7 @@ export default class ExcalidrawView extends TextFileView {
//(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.showHoverPreview, "ExcalidrawView.showHoverPreview", linktext, element);
if(!this.lastMouseEvent) return;
const st = this.excalidrawAPI?.getAppState();
if(st?.editingElement || st?.newElement) return; //should not activate hover preview when element is being edited or dragged
if(st?.editingTextElement || st?.newElement) return; //should not activate hover preview when element is being edited or dragged
if(this.semaphores.wheelTimeout) return;
//if link text is not provided, try to get it from the element
if (!linktext) {
@@ -3746,7 +3750,7 @@ export default class ExcalidrawView extends TextFileView {
return;
}
if (
st.editingElement === null &&
st.editingTextElement === null &&
//Removed because of
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/565
/*st.resizingElement === null &&
@@ -5067,6 +5071,70 @@ export default class ExcalidrawView extends TextFileView {
);
};
private diagramToCode() {
return this.packages.react.createElement(
this.packages.excalidrawLib.DiagramToCodePlugin,
{
generate: async ({ frame, children }:
{frame: ExcalidrawMagicFrameElement, children: readonly ExcalidrawElement[]}) => {
const appState = this.excalidrawAPI.getAppState();
try {
const blob = await this.packages.excalidrawLib.exportToBlob({
elements: children,
appState: {
...appState,
exportBackground: true,
viewBackgroundColor: appState.viewBackgroundColor,
},
exportingFrame: frame,
files: this.excalidrawAPI.getFiles(),
mimeType: "image/jpeg",
});
const dataURL = await this.packages.excalidrawLib.getDataURL(blob);
const textFromFrameChildren = this.packages.excalidrawLib.getTextFromElements(children);
const response = await diagramToHTML ({
image:dataURL,
apiKey: this.plugin.settings.openAIAPIToken,
text: textFromFrameChildren,
theme: appState.theme,
});
if (!response.ok) {
const json = await response.json();
const text = json.error?.message || "Unknown error during generation";
return {
html: errorHTML(text),
};
}
const json = await response.json();
if(json.choices[0].message.content == null) {
return {
html: errorHTML("Nothing generated"),
};
}
const message = json.choices[0].message.content;
const html = message.slice(
message.indexOf("<!DOCTYPE html>"),
message.indexOf("</html>") + "</html>".length,
);
return { html };
} catch (err: any) {
return {
html: errorHTML("Request failed"),
};
}
},
}
);
}
private ttdDialogTrigger() {
return this.packages.react.createElement(
this.packages.excalidrawLib.TTDDialogTrigger,
@@ -5315,14 +5383,14 @@ export default class ExcalidrawView extends TextFileView {
//...again, just aweful, but works.
const st = api.getAppState();
//isEventOnSameElement attempts to solve https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729
//the issue is that when the user hides the keyboard with the keyboard hide button and not tapping on the screen, then editingElement is not null
const isEventOnSameElement = this.editingTextElementId === st.editingElement?.id;
const isKeyboardOutEvent:Boolean = st.editingElement && st.editingElement.type === "text" && !isEventOnSameElement;
//the issue is that when the user hides the keyboard with the keyboard hide button and not tapping on the screen, then editingTextElement is not null
const isEventOnSameElement = this.editingTextElementId === st.editingTextElement?.id;
const isKeyboardOutEvent:Boolean = st.editingTextElement && !isEventOnSameElement;
const isKeyboardBackEvent:Boolean = (this.semaphores.isEditingText || isEventOnSameElement) && !isKeyboardOutEvent;
this.editingTextElementId = isKeyboardOutEvent ? st.editingElement.id : null;
this.editingTextElementId = isKeyboardOutEvent ? st.editingTextElement.id : null;
if(isKeyboardOutEvent) {
const appToolHeight = (this.contentEl.querySelector(".Island.App-toolbar") as HTMLElement)?.clientHeight ?? 0;
const editingElViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:st.editingElement.y}, st).y;
const editingElViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:st.editingTextElement.y}, st).y;
const scrollViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:-st.scrollY}, st).y;
const delta = editingElViewY - scrollViewY;
const isElementAboveKeyboard = height > (delta + appToolHeight*2)
@@ -5513,6 +5581,7 @@ export default class ExcalidrawView extends TextFileView {
this.renderCustomActionsMenu(),
this.renderWelcomeScreen(),
this.ttdDialog(),
this.diagramToCode(),
this.ttdDialogTrigger(),
),
this.renderToolsPanel(observer),
@@ -5693,15 +5762,20 @@ export default class ExcalidrawView extends TextFileView {
let match = getTextElementsMatchingQuery(
elements.filter((el: ExcalidrawElement) => el.type === "text"),
query,
exactMatch
exactMatch,
).concat(getFrameElementsMatchingQuery(
elements.filter((el: ExcalidrawElement) => el.type === "frame"),
query,
exactMatch
exactMatch,
)).concat(getElementsWithLinkMatchingQuery(
elements.filter((el: ExcalidrawElement) => el.link),
query,
exactMatch
exactMatch,
)).concat(getImagesMatchingQuery(
elements,
query,
this.excalidrawData,
exactMatch,
));
if (match.length === 0) {
+5
View File
@@ -9,6 +9,11 @@ export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
export const setExcalidrawPlugin = (plugin: ExcalidrawPlugin) => {
EXCALIDRAW_PLUGIN = plugin;
};
export const THEME = {
LIGHT: "light",
DARK: "dark",
} as const;
const MD_EXCALIDRAW = "# Excalidraw Data";
const MD_TEXTELEMENTS = "## Text Elements";
const MD_ELEMENTLINKS = "## Element Links";
+12 -1
View File
@@ -249,4 +249,15 @@ export const extractCodeBlocks = (markdown: string): { data: string, type: strin
}
return result;
}
}
export const errorHTML = (message: string) => `<html>
<body style="margin: 0; text-align: center">
<div style="display: flex; align-items: center; justify-content: center; flex-direction: column; height: 100vh; padding: 0 60px">
<div style="color:red">There was an error during generation</div>
</br>
</br>
<div>${message}</div>
</div>
</body>
</html>`;
+1
View File
@@ -74,6 +74,7 @@ export const setDynamicStyle = (
const str = (cm: ColorMaster) => cm.stringHEX({alpha:false});
const styleObject:{[x: string]: string;} = {
['backgroundColor']: str(cmBG()),
[`--color-primary`]: str(accent()),
[`--color-surface-low`]: str(gray1()),
[`--color-surface-mid`]: str(gray1()),
+90
View File
@@ -0,0 +1,90 @@
import { THEME } from "../constants/constants";
import type { Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import type { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import type { OpenAIInput, OpenAIOutput } from "@zsviczian/excalidraw/types/excalidraw/data/ai/types";
export type MagicCacheData =
| {
status: "pending";
}
| { status: "done"; html: string }
| {
status: "error";
message?: string;
code: "ERR_GENERATION_INTERRUPTED" | string;
};
const SYSTEM_PROMPT = `You are a skilled front-end developer who builds interactive prototypes from wireframes, and is an expert at CSS Grid and Flex design.
Your role is to transform low-fidelity wireframes into working front-end HTML code.
YOU MUST FOLLOW FOLLOWING RULES:
- Use HTML, CSS, JavaScript to build a responsive, accessible, polished prototype
- Leverage Tailwind for styling and layout (import as script <script src="https://cdn.tailwindcss.com"></script>)
- Inline JavaScript when needed
- Fetch dependencies from CDNs when needed (using unpkg or skypack)
- Source images from Unsplash or create applicable placeholders
- Interpret annotations as intended vs literal UI
- Fill gaps using your expertise in UX and business logic
- generate primarily for desktop UI, but make it responsive.
- Use grid and flexbox wherever applicable.
- Convert the wireframe in its entirety, don't omit elements if possible.
If the wireframes, diagrams, or text is unclear or unreadable, refer to provided text for clarification.
Your goal is a production-ready prototype that brings the wireframes to life.
Please output JUST THE HTML file containing your best attempt at implementing the provided wireframes.`;
export async function diagramToHTML({
image,
apiKey,
text,
theme = THEME.LIGHT,
}: {
image: DataURL;
apiKey: string;
text: string;
theme?: Theme;
}) {
const body: OpenAIInput.ChatCompletionCreateParamsBase = {
model: "gpt-4-vision-preview",
// 4096 are max output tokens allowed for `gpt-4-vision-preview` currently
max_tokens: 4096,
temperature: 0.1,
messages: [
{
role: "system",
content: SYSTEM_PROMPT,
},
{
role: "user",
content: [
{
type: "image_url",
image_url: {
url: image,
detail: "high",
},
},
{
type: "text",
text: `Above is the reference wireframe. Please make a new website based on these and return just the HTML file. Also, please make it for the ${theme} theme. What follows are the wireframe's text annotations (if any)...`,
},
{
type: "text",
text,
},
],
},
],
};
let result:
| ({ ok: true } & OpenAIOutput.ChatCompletion)
| ({ ok: false } & OpenAIOutput.APIError);
return await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(body),
});
}