Render markdown in frontend

This commit is contained in:
space-nuko
2023-05-29 18:19:09 -05:00
parent 1a23039b60
commit 8b1c8ba9ee
7 changed files with 366 additions and 22 deletions

View File

@@ -68,7 +68,7 @@ function getConnectionPos(node: SerializedLGraphNode, is_input: boolean, slotNum
return out;
}
function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, node: SerializedLGraphNode, slotIndex: number, isInput: boolean, widgetNodeType: string, value: any): [ComfyWidgetNode, SerializedComfyWidgetNode] {
function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, widgetNodeType: string, value: any, node?: SerializedLGraphNode, slotIndex?: number, isInput?: boolean): [ComfyWidgetNode, SerializedComfyWidgetNode] {
const comfyWidgetNode = LiteGraph.createNode<ComfyWidgetNode>(widgetNodeType);
comfyWidgetNode.flags.collapsed = true;
const size: Vector2 = [0, 0];
@@ -85,12 +85,15 @@ function createSerializedWidgetNode(vanillaWorkflow: ComfyVanillaWorkflow, node:
const serWidgetNode = comfyWidgetNode.serialize() as SerializedComfyWidgetNode;
serWidgetNode.comfyValue = value;
serWidgetNode.shownOutputProperties = {};
getConnectionPos(node, isInput, slotIndex, serWidgetNode.pos);
if (isInput)
serWidgetNode.pos[0] -= size[0] - 20;
else
serWidgetNode.pos[0] += 20;
serWidgetNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2;
if (node != null) {
getConnectionPos(node, isInput, slotIndex, serWidgetNode.pos);
if (isInput)
serWidgetNode.pos[0] -= size[0] - 20;
else
serWidgetNode.pos[0] += 20;
serWidgetNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2;
}
if (widgetNodeType === "ui/text" && typeof value === "string" && value.indexOf("\n") != -1) {
const lineCount = countNewLines(value);
@@ -260,11 +263,12 @@ function convertPrimitiveNode(vanillaWorkflow: ComfyVanillaWorkflow, node: Seria
const [comfyWidgetNode, serWidgetNode] = createSerializedWidgetNode(
vanillaWorkflow,
widgetNodeType,
value,
node,
0, // first output on the PrimitiveNode
false, // this is an output slot index
widgetNodeType,
value);
false // this is an output slot index
);
// Set the UI node's min/max/step from the node def
configureWidgetNodeProperties(serWidgetNode, widgetOpts)
@@ -381,6 +385,20 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
removeSerializedNode(vanillaWorkflow, node);
continue
}
else if (node.type === "Note") {
const [comfyWidgetNode, serWidgetNode] = createSerializedWidgetNode(
vanillaWorkflow,
"ui/markdown",
node.widgets_values[0]
);
serWidgetNode.pos = [node.pos[0], node.pos[1]]
const group = layoutState.addContainer(left, { title: "" })
layoutState.addWidget(group, comfyWidgetNode)
removeSerializedNode(vanillaWorkflow, node);
continue
}
const def = ComfyApp.knownBackendNodes[node.type];
if (def == null) {
@@ -449,11 +467,12 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
const [comfyWidgetNode, serWidgetNode] = createSerializedWidgetNode(
vanillaWorkflow,
widgetNodeType,
value,
node,
connInputIndex,
true,
widgetNodeType,
value);
true
);
configureWidgetNodeProperties(serWidgetNode, inputOpts)
@@ -492,11 +511,12 @@ export default function convertVanillaWorkflow(vanillaWorkflow: ComfyVanillaWork
// Let's create a gallery for this output node and hook it up
const [comfyGalleryNode, serGalleryNode] = createSerializedWidgetNode(
vanillaWorkflow,
"ui/gallery",
[],
node,
connOutputIndex,
false,
"ui/gallery",
[]);
);
if (group == null)
group = layoutState.addContainer(isOutputNode ? right : left, { title: node.title || node.type })

View File

@@ -0,0 +1,57 @@
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout } from "@litegraph-ts/core";
import MarkdownWidget from "$lib/widgets/MarkdownWidget.svelte";
import ComfyWidgetNode, { type ComfyWidgetProperties } from "./ComfyWidgetNode";
export interface ComfyMarkdownProperties extends ComfyWidgetProperties {
}
export default class ComfyMarkdownNode extends ComfyWidgetNode<string> {
override properties: ComfyMarkdownProperties = {
tags: [],
defaultValue: false,
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "store", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "value", type: "string" },
{ name: "changed", type: BuiltInSlotType.EVENT },
]
}
override svelteComponentType = MarkdownWidget;
override defaultValue = "";
constructor(name?: string) {
super(name, "")
}
override createDisplayWidget(): ITextWidget {
const widget = this.addWidget<ITextWidget>(
"text",
"Value",
"",
(v: string) => {
if (v == null || v === this.getValue()) {
return;
}
this.setValue(v);
},
{
multiline: true,
inputStyle: { fontFamily: "monospace" }
}
)
return widget;
}
}
LiteGraph.registerNodeType({
class: ComfyMarkdownNode,
title: "UI.Markdown",
desc: "Displays Markdown in the UI",
type: "ui/markdown"
})

View File

@@ -106,13 +106,18 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
this.value = writable(value)
this.color ||= color.color
this.bgColor ||= color.bgColor
this.displayWidget = this.addWidget<ITextWidget>(
this.displayWidget = this.createDisplayWidget();
this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this))
}
protected createDisplayWidget(): ITextWidget {
const widget = this.addWidget<ITextWidget>(
"text",
"Value",
""
);
this.displayWidget.disabled = true; // prevent editing
this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this))
)
widget.disabled = true; // prevent editing
return widget;
}
addPropertyAsOutput(propertyName: string, type: string) {

View File

@@ -9,3 +9,4 @@ export { default as ComfyRadioNode } from "./ComfyRadioNode"
export { default as ComfyNumberNode } from "./ComfyNumberNode"
export { default as ComfyTextNode } from "./ComfyTextNode"
export { default as ComfyMultiRegionNode } from "./ComfyMultiRegionNode"
export { default as ComfyMarkdownNode } from "./ComfyMarkdownNode"

View File

@@ -0,0 +1,227 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutStates";
import { get, type Writable, writable } from "svelte/store";
import { Block } from "@gradio/atoms";
import type { ComfyMarkdownNode } from "$lib/nodes/widgets";
import SvelteMarkdown from "svelte-markdown"
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyMarkdownNode | null = null;
let nodeValue: Writable<string> = writable("");
let attrsChanged: Writable<number> = writable(0);
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyMarkdownNode
nodeValue = node.value;
attrsChanged = widget.attrsChanged;
}
};
</script>
<div class="wrapper prose">
{#key $attrsChanged}
{#if widget !== null && node !== null}
<Block>
<SvelteMarkdown source={$nodeValue} />
</Block>
{/if}
{/key}
</div>
<style lang="scss">
.wrapper {
padding: 2px;
width: 100%;
height: 100%;
:global(> button) {
height: 100%;
}
:global(> .block) {
border-radius: 0 !important;
}
}
.prose {
font-weight: var(--prose-text-weight);
font-size: var(--text-md);
}
.prose * {
color: var(--body-text-color);
}
.prose p {
margin-bottom: var(--spacing-sm);
line-height: var(--line-lg);
}
/* headings
*/
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5 {
margin: var(--spacing-xxl) 0 var(--spacing-lg);
font-weight: var(--prose-header-text-weight);
line-height: 1.3;
}
.prose > *:first-child {
margin-top: 0;
}
.prose h1 {
margin-top: 0;
font-size: var(--text-xxl);
}
.prose h2 {
font-size: var(--text-xl);
}
.prose h3 {
font-size: var(--text-lg);
}
.prose h4 {
font-size: 1.1em;
}
.prose h5 {
font-size: 1.05em;
}
/* lists
*/
.prose ul {
list-style: circle inside;
}
.prose ol {
list-style: decimal inside;
}
.prose ul > p,
.prose li > p {
display: inline-block;
}
.prose ol,
.prose ul {
margin-top: 0;
padding-left: 0;
}
.prose ul ul,
.prose ul ol,
.prose ol ol,
.prose ol ul {
margin: 0.5em 0 0.5em 3em;
font-size: 90%;
}
.prose li {
margin-bottom: 0.5em;
}
/* code
*/
.prose code {
border: 1px solid var(--border-color-primary);
border-radius: var(--radius-sm);
background: var(--background-fill-secondary);
padding: 1px 3px;
font-size: 85%;
white-space: nowrap;
}
.prose pre > code {
display: block;
padding: 0.5em 0.7em;
/* font-size: 100%; */
white-space: pre;
}
/* tables
*/
.prose th,
.prose td {
border-bottom: 1px solid #e1e1e1;
padding: 12px 15px;
text-align: left;
}
.prose th:first-child,
.prose td:first-child {
padding-left: 0;
}
.prose th:last-child,
.prose td:last-child {
padding-right: 0;
}
/* spacing
*/
.prose button,
.prose .button {
margin-bottom: var(--spacing-sm);
}
.prose input,
.prose textarea,
.prose select,
.prose fieldset {
margin-bottom: var(--spacing-sm);
}
.prose pre,
.prose blockquote,
.prose dl,
.prose figure,
.prose table,
.prose p,
.prose ul,
.prose ol,
.prose form {
margin-bottom: var(--spacing-md);
}
/* links
*/
.prose a {
color: var(--link-text-color);
text-decoration: underline;
}
.prose a:visited {
color: var(--link-text-color-visited);
}
.prose a:hover {
color: var(--link-text-color-hover);
}
.prose a:active {
color: var(--link-text-color-active);
}
/* misc
*/
.prose hr {
margin-top: 3em;
margin-bottom: 3.5em;
border-width: 0;
border-top: 1px solid #e1e1e1;
}
.prose blockquote {
margin: var(--size-6) 0 !important;
border-left: 5px solid var(--border-color-primary);
padding-left: var(--size-2);
}
.prose :last-child {
margin-bottom: 0 !important;
}
</style>