Template raw JSON view

This commit is contained in:
space-nuko
2023-05-25 16:43:59 -05:00
parent fecd06d3f4
commit e6b446079a
10 changed files with 300 additions and 51 deletions

View File

@@ -73,7 +73,6 @@
"@sveltejs/vite-plugin-svelte": "^2.1.1", "@sveltejs/vite-plugin-svelte": "^2.1.1",
"@tsconfig/svelte": "^4.0.1", "@tsconfig/svelte": "^4.0.1",
"@types/dompurify": "^3.0.2", "@types/dompurify": "^3.0.2",
"@zerodevx/svelte-json-view": "^1.0.5",
"canvas-to-svg": "^1.0.3", "canvas-to-svg": "^1.0.3",
"cm6-theme-basic-dark": "^0.2.0", "cm6-theme-basic-dark": "^0.2.0",
"cm6-theme-basic-light": "^0.2.0", "cm6-theme-basic-light": "^0.2.0",

View File

@@ -707,7 +707,7 @@
"buttonVariant": "primary", "buttonVariant": "primary",
"buttonSize": "large", "buttonSize": "large",
"tags": [], "tags": [],
"destroyChildOnCLose": false "destroyChildOnClose": false
} }
}, },
"children": [ "children": [
@@ -733,7 +733,7 @@
"buttonVariant": "primary", "buttonVariant": "primary",
"buttonSize": "large", "buttonSize": "large",
"tags": [], "tags": [],
"destroyChildOnCLose": false "destroyChildOnClose": false
} }
}, },
"children": [ "children": [
@@ -760,7 +760,7 @@
"buttonVariant": "primary", "buttonVariant": "primary",
"buttonSize": "large", "buttonSize": "large",
"tags": [], "tags": [],
"destroyChildOnCLose": false "destroyChildOnClose": false
} }
}, },
"children": [ "children": [
@@ -788,7 +788,7 @@
"buttonVariant": "primary", "buttonVariant": "primary",
"buttonSize": "large", "buttonSize": "large",
"tags": [], "tags": [],
"destroyChildOnCLose": false "destroyChildOnClose": false
} }
}, },
"children": [], "children": [],
@@ -813,7 +813,7 @@
"buttonVariant": "primary", "buttonVariant": "primary",
"buttonSize": "large", "buttonSize": "large",
"tags": [], "tags": [],
"destroyChildOnCLose": false "destroyChildOnClose": false
} }
}, },
"children": [], "children": [],
@@ -837,7 +837,7 @@
"buttonVariant": "primary", "buttonVariant": "primary",
"buttonSize": "large", "buttonSize": "large",
"tags": [], "tags": [],
"destroyChildOnCLose": false "destroyChildOnClose": false
} }
}, },
"children": [ "children": [
@@ -864,7 +864,7 @@
"buttonVariant": "primary", "buttonVariant": "primary",
"buttonSize": "large", "buttonSize": "large",
"tags": [], "tags": [],
"destroyChildOnCLose": false "destroyChildOnClose": false
} }
}, },
"children": [], "children": [],
@@ -888,7 +888,7 @@
"buttonVariant": "primary", "buttonVariant": "primary",
"buttonSize": "large", "buttonSize": "large",
"tags": [], "tags": [],
"destroyChildOnCLose": false "destroyChildOnClose": false
} }
}, },
"children": [ "children": [
@@ -915,7 +915,7 @@
"buttonVariant": "primary", "buttonVariant": "primary",
"buttonSize": "large", "buttonSize": "large",
"tags": [], "tags": [],
"destroyChildOnCLose": false "destroyChildOnClose": false
} }
}, },
"children": [], "children": [],

View File

@@ -338,6 +338,7 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
uiState.update(s => { s.forceSaveUserState = null; return s; }); uiState.update(s => { s.forceSaveUserState = null; return s; });
nodes = relocateNodes(nodes); nodes = relocateNodes(nodes);
nodes = removeTags(nodes);
[nodes, links] = pruneDetachedLinks(nodes, links); [nodes, links] = pruneDetachedLinks(nodes, links);
const svg = renderSvg(canvas, graph, TEMPLATE_SVG_PADDING); const svg = renderSvg(canvas, graph, TEMPLATE_SVG_PADDING);
@@ -357,22 +358,35 @@ export function serializeTemplate(canvas: ComfyGraphCanvas, template: ComfyBoxTe
return serTemplate; return serTemplate;
} }
/*
* Extract embedded workflow from desc tags
*/
export function extractTemplateJSONFromSVG(svg: string): string | null {
const descEnd = svg.lastIndexOf("</desc>");
if (descEnd !== -1) {
const descStart = svg.lastIndexOf("<desc>", descEnd);
if (descStart !== -1) {
const json = svg.substring(descStart + 6, descEnd);
return unescapeXml(json);
}
}
return null;
}
/*
* Credit goes to pythongosssss for this format
*/
export function deserializeTemplateFromSVG(file: File): Promise<SerializedComfyBoxTemplate> { export function deserializeTemplateFromSVG(file: File): Promise<SerializedComfyBoxTemplate> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async () => { reader.onload = async () => {
const svg = reader.result as string; const svg = reader.result as string;
let template = null; let template = null;
let templateJSON = extractTemplateJSONFromSVG(svg);
// Extract embedded workflow from desc tags if (templateJSON)
const descEnd = svg.lastIndexOf("</desc>"); template = JSON.parse(templateJSON);
if (descEnd !== -1) {
const descStart = svg.lastIndexOf("<desc>", descEnd);
if (descStart !== -1) {
const json = svg.substring(descStart + 6, descEnd);
template = JSON.parse(unescapeXml(json));
}
}
if (!isSerializedComfyBoxTemplate(template)) { if (!isSerializedComfyBoxTemplate(template)) {
reject("Invalid template format!") reject("Invalid template format!")

View File

@@ -143,7 +143,8 @@
{ {
name: "Delete", name: "Delete",
variant: "secondary", variant: "secondary",
onClick: deleteTemplate onClick: deleteTemplate,
disabled: layout.template.isBuiltIn
}, },
{ {
name: "Close", name: "Close",

View File

@@ -13,6 +13,9 @@
} }
function onButtonClicked(modal: ModalData, button: ModalButton, closeDialog: Function) { function onButtonClicked(modal: ModalData, button: ModalButton, closeDialog: Function) {
if (button.disabled)
return;
if (button.onClick(modal) === false) if (button.onClick(modal) === false)
return return
@@ -39,7 +42,7 @@
<div slot="buttons" class="buttons" let:closeDialog> <div slot="buttons" class="buttons" let:closeDialog>
{#if modal != null && modal.buttons?.length > 0} {#if modal != null && modal.buttons?.length > 0}
{#each modal.buttons as button} {#each modal.buttons as button}
<Button variant={button.variant} on:click={() => onButtonClicked(modal, button, closeDialog)}> <Button variant={button.variant} disabled={button.disabled} on:click={() => onButtonClicked(modal, button, closeDialog)}>
{button.name} {button.name}
</Button> </Button>
{/each} {/each}

View File

@@ -0,0 +1,141 @@
<script lang="ts">
/*
* Modified from @zerodevx/svelte-json-view
*/
/** @type {*} - object or array to display */
export let json: any
/** @type {number} - initial expansion depth */
export let depth: number = Infinity
export let collapseByDefault: boolean | ((v: any) => boolean) | null = false;
export let _cur: number = 0
export let _last: boolean = true
/** @type {*[]} */
let items: any[]
let isArray: boolean = false
let brackets: [string, string] = ['', '']
export let collapsed: boolean | null = null
/**
* @param {*} i
* @returns {string}
*/
function getType(i: any): string {
if (i === null) return 'null'
return typeof i
}
/**
* @param {*} i
* @returns {string}
*/
function format(i: any): string {
const t = getType(i)
if (t === 'string') return `"${i}"`
if (t === 'function') return 'f () {...}'
if (t === 'symbol') return i.toString()
return i
}
function clicked() {
collapsed = !collapsed
}
/**
* @param {Event} e
*/
function pressed(e) {
if (e instanceof KeyboardEvent && ['Enter', ' '].includes(e.key)) clicked()
}
$: {
items = getType(json) === 'object' ? Object.keys(json) : []
isArray = Array.isArray(json)
brackets = isArray ? ['[', ']'] : ['{', '}']
}
$: {
if (collapsed === null)
collapsed = depth < _cur;
}
function calcCollapsed(json: any): boolean | null {
if (typeof collapseByDefault === "function")
return collapseByDefault(json)
else if (typeof collapseByDefault === "boolean")
return collapseByDefault
return null;
}
</script>
{#if !items.length}
<span class="_jsonBkt empty">{brackets[0]}{brackets[1]}</span>{#if !_last}<span class="_jsonSep"
>,</span
>{/if}
{:else if collapsed}
<span class="_jsonBkt _jsonCollapsed" role="button" tabindex="0" on:click={clicked} on:keydown={pressed}
>{brackets[0]}...{brackets[1]}</span
>{#if !_last && collapsed}<span class="_jsonSep">,</span>{/if}
{:else}
<span class="_jsonBkt" role="button" tabindex="0" on:click={clicked} on:keydown={pressed}
>{brackets[0]}</span
>
<ul class="_jsonList">
{#each items as i, idx}
<li>
{#if !isArray}
<span class="_jsonKey">"{i}"</span><span class="_jsonSep">:</span>
{/if}
{#if getType(json[i]) === 'object'}
<svelte:self json={json[i]} {collapseByDefault} collapsed={calcCollapsed(json[i])} {depth} _cur={_cur + 1} _last={idx === items.length - 1} />
{:else}
<span class="_jsonVal {getType(json[i])}">{format(json[i])}</span
>{#if idx < items.length - 1}<span class="_jsonSep">,</span>{/if}
{/if}
</li>
{/each}
</ul>
<span class="_jsonBkt" role="button" tabindex="0" on:click={clicked} on:keydown={pressed}
>{brackets[1]}</span
>{#if !_last}<span class="_jsonSep">,</span>{/if}
{/if}
<style>
._jsonList {
list-style: none;
margin: 0;
padding: 0;
padding-left: var(--jsonPaddingLeft, 1rem);
border-left: var(--jsonBorderLeft, 1px dotted);
}
._jsonBkt {
color: var(--jsonBracketColor, currentcolor);
}
._jsonBkt:not(.empty):hover {
cursor: pointer;
background: var(--jsonBracketHoverBackground, #e5e7eb);
}
._jsonSep {
color: var(--jsonSeparatorColor, currentcolor);
}
._jsonKey {
color: var(--jsonKeyColor, currentcolor);
}
._jsonVal {
color: var(--jsonValColor, #9ca3af);
}
._jsonVal.string {
color: var(--jsonValStringColor, #059669);
}
._jsonVal.number {
color: var(--jsonValNumberColor, #d97706);
}
._jsonVal.boolean {
color: var(--jsonValBooleanColor, #2563eb);
}
._jsonCollapsed {
color: var(--jsonCollapsedColor, currentcolor);
background: var(--jsonCollapsedBackground, currentcolor);
}
</style>

View File

@@ -2,7 +2,7 @@
import type { A1111ParsedInfotext } from "$lib/parseA1111"; import type { A1111ParsedInfotext } from "$lib/parseA1111";
import { Block, BlockTitle } from "@gradio/atoms"; import { Block, BlockTitle } from "@gradio/atoms";
import { TextBox } from "@gradio/form"; import { TextBox } from "@gradio/form";
import { JsonView } from '@zerodevx/svelte-json-view' import JsonView from '$lib/components/JsonView.svelte'
import type { A1111PromptAndInfo } from "$lib/components/ComfyApp"; import type { A1111PromptAndInfo } from "$lib/components/ComfyApp";
import { StaticImage } from "$lib/components/gradio/image"; import { StaticImage } from "$lib/components/gradio/image";
@@ -65,17 +65,6 @@
height: 70vh; height: 70vh;
color: none; color: none;
--jsonPaddingLeft: 1rem;
--jsonBorderLeft: 1px dotted var(--neutral-600);
--jsonBracketColor: currentcolor;
--jsonBracketHoverBackground: var(--neutral-100);
--jsonSeparatorColor: currentcolor;
--jsonKeyColor: var(--body-text-color);
--jsonValColor: var(--body-text-color-subdued);
--jsonValStringColor: var(--color-green-500);
--jsonValNumberColor: var(--color-blue-500);
--jsonValBooleanColor: var(--color-red-500);
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
overflow-y: none; overflow-y: none;
@@ -87,7 +76,7 @@
overflow: auto; overflow: auto;
.json { .json {
font-family: monospace; @include json-view;
} }
.scroll-container { .scroll-container {

View File

@@ -2,6 +2,9 @@
import type { ComfyBoxTemplate, SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate"; import type { ComfyBoxTemplate, SerializedComfyBoxTemplate } from "$lib/ComfyBoxTemplate";
import type { SerializedDragEntry, SerializedLayoutState } from "$lib/stores/layoutStates"; import type { SerializedDragEntry, SerializedLayoutState } from "$lib/stores/layoutStates";
import { Block, BlockTitle } from "@gradio/atoms"; import { Block, BlockTitle } from "@gradio/atoms";
import { Tabs, TabItem } from "@gradio/tabs";
import { JSON as JSONIcon } from "@gradio/icons";
import JsonView from '$lib/components/JsonView.svelte'
import SerializedLayoutPreviewNode from "./SerializedLayoutPreviewNode.svelte"; import SerializedLayoutPreviewNode from "./SerializedLayoutPreviewNode.svelte";
import Row from "../gradio/app/Row.svelte"; import Row from "../gradio/app/Row.svelte";
import createDOMPurify from "dompurify" import createDOMPurify from "dompurify"
@@ -10,6 +13,7 @@
import Textbox from "@gradio/form/src/Textbox.svelte"; import Textbox from "@gradio/form/src/Textbox.svelte";
import type { ModalData } from "$lib/stores/modalState"; import type { ModalData } from "$lib/stores/modalState";
import { writable, type Writable } from "svelte/store"; import { writable, type Writable } from "svelte/store";
import { negmod } from "$lib/utils";
const DOMPurify = createDOMPurify(window); const DOMPurify = createDOMPurify(window);
export let templateAndSvg: SerializedComfyBoxTemplate; export let templateAndSvg: SerializedComfyBoxTemplate;
@@ -18,6 +22,45 @@
let layout: SerializedLayoutState | null let layout: SerializedLayoutState | null
let root: SerializedDragEntry | null let root: SerializedDragEntry | null
let state: Writable<any> = writable({}) let state: Writable<any> = writable({})
let rawTemplate: SerializedComfyBoxTemplate | null
let showJSON = false;
let showAllJSON: number = 0;
let createdAt = "";
$: {
rawTemplate = { ...templateAndSvg };
rawTemplate.svg = undefined;
}
function collapseByDefault(json: any): boolean {
switch (showAllJSON) {
case 0:
return typeof json["id"] === "string";
case 1:
return typeof json["nodes"] === "object"
case 2:
default:
return false;
}
}
function expandJSON() {
showAllJSON = negmod(showAllJSON + 1, 3)
}
$: {
let options: Intl.DateTimeFormatOptions = {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
};
const date = new Date(templateAndSvg.metadata.createdAt);
createdAt = date.toLocaleString('en-US', options);
}
$: { $: {
state = _modal.state; state = _modal.state;
@@ -58,6 +101,7 @@
<BlockTitle>Metadata</BlockTitle> <BlockTitle>Metadata</BlockTitle>
<div> <div>
<Textbox label="Name" disabled={!editable} bind:value={$state.name} lines={1} max_lines={1} /> <Textbox label="Name" disabled={!editable} bind:value={$state.name} lines={1} max_lines={1} />
<Textbox label="Created At" disabled={true} bind:value={createdAt} lines={1} max_lines={1} />
<Textbox label="Author" disabled={!editable} bind:value={$state.author} lines={1} max_lines={1} /> <Textbox label="Author" disabled={!editable} bind:value={$state.author} lines={1} max_lines={1} />
<Textbox label="Description" disabled={!editable} bind:value={$state.description} lines={5} max_lines={5} /> <Textbox label="Description" disabled={!editable} bind:value={$state.description} lines={5} max_lines={5} />
</div> </div>
@@ -76,6 +120,8 @@
{/if} {/if}
</Row> </Row>
<div class="template-graph-preview"> <div class="template-graph-preview">
<Tabs selected="graph">
<TabItem name="Graph" id="graph">
<Block> <Block>
<Accordion label="Graph"> <Accordion label="Graph">
<Block> <Block>
@@ -85,6 +131,20 @@
</Block> </Block>
</Accordion> </Accordion>
</Block> </Block>
</TabItem>
<TabItem name="Raw JSON" id="json" on:select={() => (showJSON = true)}>
{#key showAllJSON}
{#if showJSON}
<button class="json-button" on:click={expandJSON}>
<JSONIcon />
</button>
<div class="json">
<JsonView json={rawTemplate} {collapseByDefault} />
</div>
{/if}
{/key}
</TabItem>
</Tabs>
</div> </div>
</div> </div>
@@ -120,10 +180,34 @@
:global(> .block) { :global(> .block) {
background: var(--panel-background-fill); background: var(--panel-background-fill);
} }
.json {
@include json-view;
}
} }
.template-graph-wrapper { .template-graph-wrapper {
overflow: auto; overflow: auto;
margin: auto; margin: auto;
} }
.json-button {
display: flex;
position: absolute;
top: var(--block-label-margin);
right: var(--block-label-margin);
align-items: center;
box-shadow: var(--shadow-drop);
border: 1px solid var(--border-color-primary);
border-radius: var(--block-label-right-radius);
background: var(--block-label-background-fill);
padding: 5px;
width: 30px;
height: 30px;
overflow: hidden;
color: var(--block-label-text-color);
font: var(--font);
font-size: var(--button-small-text-size);
}
</style> </style>

View File

@@ -8,6 +8,7 @@ export type ModalButton = {
name: string, name: string,
variant: "primary" | "secondary", variant: "primary" | "secondary",
onClick: (state: ModalData) => boolean | void, onClick: (state: ModalData) => boolean | void,
disabled?: boolean,
closeOnClick?: boolean closeOnClick?: boolean
} }
export interface ModalData { export interface ModalData {

View File

@@ -146,6 +146,23 @@ body {
} }
} }
@mixin json-view {
--jsonPaddingLeft: 1rem;
--jsonBorderLeft: 1px dotted var(--neutral-600);
--jsonBracketColor: currentcolor;
--jsonBracketHoverBackground: var(--primary-200);
--jsonSeparatorColor: currentcolor;
--jsonKeyColor: var(--body-text-color);
--jsonValColor: var(--body-text-color-subdued);
--jsonValStringColor: var(--color-green-500);
--jsonValNumberColor: var(--color-blue-500);
--jsonValBooleanColor: var(--color-red-500);
--jsonCollapsedColor: var(--neutral-100);
--jsonCollapsedBackground: var(--primary-400);
font-family: monospace;
}
hr { hr {
color: var(--panel-border-color); color: var(--panel-border-color);
} }