From 7e4401a6250eda491413fb90b20358671d204a9b Mon Sep 17 00:00:00 2001 From: anandamarsh Date: Sun, 17 Sep 2023 20:14:36 -0700 Subject: [PATCH 1/3] Ready for PR (#22) * redy for PR * marked readonly --- src/prompt.less | 21 +++++++++-------- src/view/code.tsx | 57 ++++++++++++++++++++++++++--------------------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/prompt.less b/src/prompt.less index 36581b782..4c84e05cd 100644 --- a/src/prompt.less +++ b/src/prompt.less @@ -302,18 +302,10 @@ input[type="checkbox"] { .save-disabled:hover { background-color: #aaaea7; } - .close-enabled { + .close { color: white; background-color: #9e0000; } - .close-disabled { - color: rgb(52, 52, 52); - background-color: #aaaea7; - cursor: default !important; - } - .close-disabled:hover { - background-color: #aaaea7; - } .message { color: white; border-radius: 6px; @@ -321,6 +313,17 @@ input[type="checkbox"] { padding: 4px 1rem; max-width: 80vw; } + .readonly { + .mono-font(12px); + position: absolute; + top: calc(1.5rem + 3px); + right: 10rem; + border-radius: 5px; + background-color: @term-bright-red; + color: white; + z-index: 1; + padding: 0 6px 2px; + } } .renderer-container.json-renderer { diff --git a/src/view/code.tsx b/src/view/code.tsx index 518c60a26..339cfe180 100644 --- a/src/view/code.tsx +++ b/src/view/code.tsx @@ -1,13 +1,11 @@ import * as React from "react"; import { RendererContext, RendererOpts, LineStateType, RendererModelContainerApi } from "../types"; import Editor from "@monaco-editor/react"; -import cn from "classnames"; import { Markdown } from "../elements"; import { GlobalModel, GlobalCommandRunner } from "../model"; import Split from "react-split-it"; import "./split.css"; import loader from "@monaco-editor/loader"; -import { editor } from "monaco-editor"; loader.config({ paths: { vs: "./node_modules/monaco-editor/min/vs" } }); function renderCmdText(text: string): any { @@ -45,6 +43,7 @@ class SourceCodeRenderer extends React.Component< isPreviewerAvailable: boolean; showPreview: boolean; editorFraction: number; + showReadonly: boolean; } > { /** @@ -57,7 +56,7 @@ class SourceCodeRenderer extends React.Component< languagesWithPreviewer = ["markdown"]; filePath; cacheKey; - originalData; + originalCode; monacoEditor: any; // reference to mounted monaco editor. TODO need the correct type markdownRef; syncing; @@ -67,7 +66,7 @@ class SourceCodeRenderer extends React.Component< this.monacoEditor = null; const editorHeight = Math.max(props.savedHeight - 25, 0); // must subtract the padding/margin to get the real editorHeight this.markdownRef = React.createRef(); - this.syncing = false; // to avoid recursive calls between the two scroll listeners + this.syncing = false; this.state = { code: null, languages: [], @@ -79,6 +78,7 @@ class SourceCodeRenderer extends React.Component< isPreviewerAvailable: false, showPreview: this.props.lineState["showPreview"], editorFraction: this.props.lineState["editorFraction"] || 0.5, + showReadonly: false, }; } @@ -91,7 +91,7 @@ class SourceCodeRenderer extends React.Component< this.setState({ code, isClosed: this.props.lineState["prompt:closed"] }); } else { this.props.data.text().then((code) => { - this.originalData = code; + this.originalCode = code; this.setState({ code, isClosed: this.props.lineState["prompt:closed"] }); SourceCodeRenderer.codeCache.set(this.cacheKey, code); }); @@ -150,7 +150,7 @@ class SourceCodeRenderer extends React.Component< e.preventDefault(); this.doSave(); } - if (e.code === "KeyD" && (e.ctrlKey || e.metaKey) && !this.state.isSave) { + if (e.code === "KeyD" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); this.doClose(); } @@ -178,16 +178,13 @@ class SourceCodeRenderer extends React.Component< this.props.rendererApi.onFocusChanged(false); }); } + if (!this.getAllowEditing()) this.setState({ showReadonly: true }); }; handleEditorScrollChange(e) { - // Get the maximum scrollable height for the editor + if (!this.state.showPreview) return; const scrollableHeightEditor = this.monacoEditor.getScrollHeight() - this.monacoEditor.getLayoutInfo().height; - - // Calculate the scroll percentage const verticalScrollPercentage = e.scrollTop / scrollableHeightEditor; - - // Apply the same percentage to the markdown div const markdownDiv = this.markdownRef.current; if (markdownDiv) { const scrollableHeightMarkdown = markdownDiv.scrollHeight - markdownDiv.clientHeight; @@ -227,17 +224,20 @@ class SourceCodeRenderer extends React.Component< } }; - doSave = () => { + doSave = (onSave = () => {}) => { if (!this.state.isSave) return; const { screenId, lineId } = this.props.context; const encodedCode = new TextEncoder().encode(this.state.code); GlobalModel.writeRemoteFile(screenId, lineId, this.filePath, encodedCode, { useTemp: true }) .then(() => { - this.originalData = this.state.code; - this.setState({ - isSave: false, - message: { status: "success", text: `Saved to ${this.props.cwd}/${this.filePath}` }, - }); + this.originalCode = this.state.code; + this.setState( + { + isSave: false, + message: { status: "success", text: `Saved to ${this.props.cwd}/${this.filePath}` }, + }, + onSave + ); setTimeout(() => this.setState({ message: null }), 3000); }) .catch((e) => { @@ -247,13 +247,22 @@ class SourceCodeRenderer extends React.Component< }; doClose = () => { - if (this.state.isSave) return; + // if there is unsaved data + if (this.state.isSave) + return GlobalModel.showAlert({ + message: "Do you want to Save your changes before closing?", + confirm: true, + }).then((result) => { + if (result) return this.doSave(this.doClose); + this.setState({ code: this.originalCode, isSave: false }, this.doClose); + }); const { screenId, lineId } = this.props.context; GlobalCommandRunner.setLineState(screenId, lineId, { ...this.props.lineState, "prompt:closed": true }, false) .then(() => { this.setState({ isClosed: true, message: { status: "success", text: `Closed. This editor is now read-only` }, + showReadonly: true, }); setTimeout(() => { this.setEditorHeight(); @@ -278,7 +287,7 @@ class SourceCodeRenderer extends React.Component< SourceCodeRenderer.codeCache.set(this.cacheKey, code); this.setState({ code }, () => { this.setEditorHeight(); - this.props.data.text().then((originalCode) => this.setState({ isSave: code !== originalCode })); + this.setState({ isSave: code !== this.originalCode }); }); }; @@ -309,11 +318,12 @@ class SourceCodeRenderer extends React.Component< getCodeEditor = () => (
+ {this.state.showReadonly &&
{"read-only"}
} this.handleDivScroll()} > - +
); }; @@ -376,10 +386,7 @@ class SourceCodeRenderer extends React.Component< )} {allowEditing && (
-
+
{`close (`} {renderCmdText("D")} {`)`} From 4953550425937837642a6368856ef07973fab8cf Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 17 Sep 2023 20:31:55 -0700 Subject: [PATCH 2/3] bump version to v0.3.1 --- package.json | 2 +- version.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e4639e2ad..fbf746bb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Prompt", - "version": "0.3.0", + "version": "0.3.1", "main": "dist/emain.js", "license": "Proprietary", "dependencies": { diff --git a/version.js b/version.js index e0edbae94..2fb0bc030 100644 --- a/version.js +++ b/version.js @@ -1,2 +1,2 @@ -const VERSION = "v0.3.0"; +const VERSION = "v0.3.1"; module.exports = VERSION; From e30128d1828273bb024316252ef30f4092eb844d Mon Sep 17 00:00:00 2001 From: sawka Date: Sun, 17 Sep 2023 21:00:54 -0700 Subject: [PATCH 3/3] Mustache Template Renderer (#23) * experimental version of the mustache renderer * mustache error message / loading cleanup, add refresh button --- package.json | 1 + src/emain.ts | 14 ++- src/plugins.ts | 13 +++ src/prompt.less | 32 ++++++- src/prompt.ts | 2 + src/view/mustache.tsx | 218 ++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 5 + 7 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 src/view/mustache.tsx diff --git a/package.json b/package.json index fbf746bb4..c4f1c6b43 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "mobx": "^6.6.0", "mobx-react": "^7.5.0", "monaco-editor": "^0.41.0", + "mustache": "^4.2.0", "node-fetch": "^3.2.10", "react": "^18.1.0", "react-dom": "^18.1.0", diff --git a/src/emain.ts b/src/emain.ts index bdf9e179d..a99172b6e 100644 --- a/src/emain.ts +++ b/src/emain.ts @@ -169,8 +169,13 @@ function getMods(input: any) { } function shNavHandler(event: any, url: any) { - console.log("navigation canceled", url); event.preventDefault(); + if (url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://")) { + console.log("open external, shNav", url); + electron.shell.openExternal(url); + } else { + console.log("navigation canceled", url); + } } function shFrameNavHandler(event: any, url: any) { @@ -183,6 +188,7 @@ function shFrameNavHandler(event: any, url: any) { if (event.frame.name == "webview") { // "webview" links always open in new window // this will *not* effect the initial load because srcdoc does not count as an electron navigation + console.log("open external, frameNav", url); electron.shell.openExternal(url); return; } @@ -301,13 +307,19 @@ function createMainWindow(clientData) { }); win.webContents.setWindowOpenHandler(({ url, frameName }) => { if (url.startsWith("https://docs.getprompt.dev/")) { + console.log("openExternal docs", url); electron.shell.openExternal(url); } else if (url.startsWith("https://discord.gg/")) { + console.log("openExternal discord", url); electron.shell.openExternal(url); } else if (url.startsWith("https://extern/?")) { let qmark = url.indexOf("?"); let param = url.substr(qmark + 1); let newUrl = decodeURIComponent(param); + console.log("openExternal extern", newUrl); + electron.shell.openExternal(newUrl); + } else if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) { + console.log("openExternal fallback", url); electron.shell.openExternal(newUrl); } console.log("window-open denied", url); diff --git a/src/plugins.ts b/src/plugins.ts index 5b27a0695..09aea67ad 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -2,6 +2,7 @@ import { RendererPluginType } from "./types"; import { SimpleImageRenderer } from "./view/image"; import { SimpleMarkdownRenderer } from "./view/markdown"; import { SourceCodeRenderer } from "./view/code"; +import { SimpleMustacheRenderer } from "./view/mustache"; import { OpenAIRenderer, OpenAIRendererModel } from "./view/openai"; import { isBlank } from "./util"; import { sprintf } from "sprintf-js"; @@ -28,6 +29,17 @@ const MarkdownPlugin: RendererPluginType = { simpleComponent: SimpleMarkdownRenderer, }; +const MustachePlugin: RendererPluginType = { + name: "mustache", + rendererType: "simple", + heightType: "pixels", + dataType: "blob", + collapseType: "hide", + globalCss: null, + mimeTypes: ["text/plain"], + simpleComponent: SimpleMustacheRenderer, +}; + const CodePlugin: RendererPluginType = { name: "code", rendererType: "simple", @@ -87,6 +99,7 @@ if ((window as any).PluginModel == null) { PluginModel.registerRendererPlugin(MarkdownPlugin); PluginModel.registerRendererPlugin(CodePlugin); PluginModel.registerRendererPlugin(OpenAIPlugin); + PluginModel.registerRendererPlugin(MustachePlugin); (window as any).PluginModel = PluginModel; } diff --git a/src/prompt.less b/src/prompt.less index 4c84e05cd..26e0c2428 100644 --- a/src/prompt.less +++ b/src/prompt.less @@ -254,7 +254,7 @@ input[type="checkbox"] { padding-top: 10px; padding-bottom: 15px; } - + .monaco-editor .monaco-editor-background { background-color: rgba(255, 255, 255, 0.075) !important; } @@ -331,7 +331,26 @@ input[type="checkbox"] { font-size: 12px; } -.renderer-container .markdown { +.renderer-container.mustache-renderer { + color: @term-white; + .cmd-hints { + display: inline-block !important; + position: relative; + margin-right: 26px; + } + .hint-item { + border-radius: 4px 4px 0 0; + padding: 3px 9px 2px 8px; + line-height: 15px; + text-align: center; + } + .refresh-button { + color: rgb(52, 52, 52); + background-color: @term-white; + } +} + +.renderer-container .content { padding: 5px; line-height: 1.5; width: fit-content; @@ -350,6 +369,15 @@ input[type="checkbox"] { margin: 2px 10px 6px 10px; padding: 4px 4px 4px 6px; } + + h1, + h2, + h3, + h4, + h5, + h6 { + color: @term-white; + } } .openai-renderer { diff --git a/src/prompt.ts b/src/prompt.ts index 2eafd5ae8..cb9d4c376 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -7,6 +7,7 @@ import { Main } from "./main"; import { GlobalModel } from "./model"; import { v4 as uuidv4 } from "uuid"; import { loadFonts } from "./util"; +import * as DOMPurify from "dompurify"; // @ts-ignore let VERSION = __PROMPT_VERSION__; @@ -31,5 +32,6 @@ document.addEventListener("DOMContentLoaded", () => { (window as any).mobx = mobx; (window as any).sprintf = sprintf; +(window as any).DOMPurify = DOMPurify; console.log("PROMPT", VERSION, BUILD); diff --git a/src/view/mustache.tsx b/src/view/mustache.tsx new file mode 100644 index 000000000..c20fe36be --- /dev/null +++ b/src/view/mustache.tsx @@ -0,0 +1,218 @@ +import * as React from "react"; +import * as mobx from "mobx"; +import * as mobxReact from "mobx-react"; +import cn from "classnames"; +import { boundMethod } from "autobind-decorator"; +import { If, For, When, Otherwise, Choose } from "tsx-control-statements/components"; +import * as T from "../types"; +import { sprintf } from "sprintf-js"; +import { isBlank } from "../util"; +import mustache from "mustache"; +import * as DOMPurify from "dompurify"; +import { GlobalModel } from "../model"; + +type OV = mobx.IObservableValue; + +const MaxMustacheSize = 200000; + +@mobxReact.observer +class SimpleMustacheRenderer extends React.Component< + { data: Blob; context: T.RendererContext; opts: T.RendererOpts; savedHeight: number; lineState: T.LineStateType }, + {} +> { + templateLoading: OV = mobx.observable.box(true, { name: "templateLoading" }); + templateLoadError: OV = mobx.observable.box(null, { name: "templateLoadError" }); + dataLoading: OV = mobx.observable.box(true, { name: "dataLoading" }); + dataLoadError: OV = mobx.observable.box(null, { name: "dataLoadError" }); + mustacheTemplateText: OV = mobx.observable.box(null, { name: "mustacheTemplateText" }); + parsedData: OV = mobx.observable.box(null, { name: "parsedData" }); + + componentDidMount() { + this.reloadTemplate(); + this.reloadData(); + } + + reloadTemplate() { + if (isBlank(this.props.lineState.template)) { + mobx.action(() => { + this.templateLoading.set(false); + this.templateLoadError.set(`no 'template' specified`); + })(); + return; + } + mobx.action(() => { + this.templateLoading.set(true); + this.templateLoadError.set(null); + })(); + let context = this.props.context; + let lineState = this.props.lineState; + let quotedTemplateName = JSON.stringify(lineState.template); + let rtnp = GlobalModel.readRemoteFile(context.screenId, context.lineId, lineState.template); + rtnp.then((file) => { + if (file.notFound) { + this.trySetTemplateLoadError(`mustache template ${quotedTemplateName} not found`); + return null; + } + return file.text(); + }) + .then((text) => { + if (isBlank(text)) { + this.trySetTemplateLoadError(`blank mustache template ${quotedTemplateName}`); + return; + } + mobx.action(() => { + this.mustacheTemplateText.set(text); + this.templateLoading.set(false); + })(); + return; + }) + .catch((e) => { + this.trySetTemplateLoadError(`loading mustache template ${quotedTemplateName}: ${e}`); + }); + } + + reloadData() { + // load json content + let dataBlob = this.props.data; + if (dataBlob == null || dataBlob.notFound) { + mobx.action(() => { + this.dataLoading.set(false); + this.dataLoadError.set( + `file {dataBlob && dataBlob.name ? JSON.stringify(dataBlob.name) : ""} not found` + ); + })(); + return; + } + mobx.action(() => { + this.dataLoading.set(true); + this.dataLoadError.set(null); + })(); + let rtnp = dataBlob.text(); + let quotedDataName = dataBlob.name || '"terminal output"'; + rtnp.then((text) => { + mobx.action(() => { + try { + this.parsedData.set(JSON.parse(text)); + this.dataLoading.set(false); + } catch (e) { + this.trySetDataLoadError(`parsing json data from ${quotedDataName}: ${e}`); + } + })(); + }).catch((e) => { + this.trySetDataLoadError(`loading json data ${quotedDataName}: ${e}`); + }); + } + + trySetTemplateLoadError(msg: string) { + if (this.templateLoadError.get() != null) { + return; + } + mobx.action(() => { + this.templateLoadError.set(msg); + })(); + } + + trySetDataLoadError(msg: string) { + if (this.dataLoadError.get() != null) { + return; + } + mobx.action(() => { + this.dataLoadError.set(msg); + })(); + } + + @boundMethod + doRefresh() { + this.reloadTemplate(); + } + + renderCmdHints() { + return ( +
+
+
+ refresh +
+
+
+ ); + } + + render() { + let errorMessage = this.dataLoadError.get() ?? this.templateLoadError.get(); + if (errorMessage != null) { + return ( +
+
ERROR: {errorMessage}
+ {this.renderCmdHints()} +
+ ); + } + if (this.templateLoading.get() || this.dataLoading.get()) { + return ( +
+
+ loading content +
+ {this.renderCmdHints()} +
+ ); + } + let opts = this.props.opts; + let maxWidth = opts.maxSize.width; + let minWidth = opts.maxSize.width; + if (minWidth > 1000) { + minWidth = 1000; + } + let templateText = this.mustacheTemplateText.get(); + let templateData = this.parsedData.get() || {}; + let renderedText = null; + try { + renderedText = mustache.render(templateText, templateData); + renderedText = DOMPurify.sanitize(renderedText); + } catch (e) { + return ( +
+
ERROR running template: {e.message}
+ {this.renderCmdHints()} +
+ ); + } + // TODO non-term content font-size (default to 16) + return ( +
+
+
+
+ {this.renderCmdHints()} +
+ ); + } +} + +export { SimpleMustacheRenderer }; diff --git a/yarn.lock b/yarn.lock index 75f9a709d..12e29d8c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5628,6 +5628,11 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" +mustache@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" + integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== + nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"